非阻塞IO实现HTTP请求_回调之痛

本文探讨了通过非阻塞IO和IO多路复用(如select)实现HTTP请求的方法。非阻塞IO通过while True循环检查连接状态,但效率并不比直接阻塞高。而IO多路复用利用DefaultSelector进行事件监听,提高了并发处理能力。然而,这种回调机制带来了代码割裂、共享状态管理和异常处理的挑战,降低了代码可读性和维护性。文章列举了回调编程的痛点,并指出其在多层回调和异常处理方面的困难。
摘要由CSDN通过智能技术生成

通过非阻塞IO实现HTTP请求

这个案例并未提高访问效率,无非是将阻塞式IO换成了用while True轮询状态的过程,还不如直接阻塞
import socket
from urllib.parse import urlparse

def get_url(url):
    # 通过socket请求html
    url = urlparse(url)
    host = url.netloc
    path = url.path
    if path == "":
        path = "/"
    # 建立socket连接
    client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    # 由于使用非阻塞IO,所以需要不停询问连接是否建立好,while 循环不停地检查状态
    socket.setblocking(False)
    try:
        client.connect((host,80))
    # BlockingIOError是正常报错,实际上连接请求已经发送        
    except BlockingIOError as e:
        pass
    # 不停询问连接是否建立好,需要while 循环不停地检查状态
    # 做计算任务或者再次发起其他的连接请求
    while True:
        try:
            client.send("GET{}HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(path, host).encode("utf8"))
        except OSError as e:
            pass
    data = b""
    while True:
        try:
            d = client.recv(1024)
        except BlockingIOError as e:
            continue
        if d:
            data+=d
        else:
            break
    # 获取从客户端发送的数据
    data= data.decode("utf8")
    html_data = data.split("\r\n\r\n")[1]
    print(html_data)
    client.close()
if __name__ == "__main__":
    get_url("http://www.baidu.com")
使用select完成http请求_目前都是单线程,可使用IO多路复用,实现聊天群(作业)
DefaultSelector包装了select,会更好用,且会自动根据平台去选择用epoll还是poll

而且提供了注册的机子

from selectors import DefaultSelector,EVENT_WRITE,EVENT_READ
# 创建一个select实例
select= DefaultSelector()

# 为解决windows下的select传参小问题,需要如下操作
urls = ["http://www.baidu.com"]
stop = False
class Fetcher:
    def connected(self,key):
        # 在send之前取消掉监控的事件
        selector.unregister(key.fd)
        self.client.send("GET{}HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(path, host).encode("utf8"))
        # 接受数据,要先注册
        selector.register(self.client.fileno(),EVENT_READ,self.readable)
    def readable(self,key):
        # 但要注意:当数据可读时,并不代表内核空间的数据都准备好了,因此直接循环调用self.client.recv是会出现BlockingIOError的
        # 为了避免报错,可直接将while True去掉,因为loop事件回调函数中,只要有可读状态,即可调用回调函数readable,
        # 但接收数据就不能使用局部变量了,而应该放到实例属性变量中
        
        d = self.client.recv(1024)

        if d:
            self.data+=d
        else:
            # 当读取的数据为空,则直接取消注册
            selector.unregister(key.fd)
            # 获取从客户端发送的数据
            data= self.data.decode("utf8")
            html_data = data.split("\r\n\r\n")[1]
            print(html_data)
            self.client.close()
            
            # windows下的特殊调整
            urls.remove(self.spider_url)
            if not urls:
                global stop
                stop = True
                
    def get_url(self,url):
        # 这个spider_url是用于爬取后列表删除的
        self.spider_url = url
        url = urlparse(url)
        self.host = url.netloc
        self.path = url.path
        self.data = b""
        if self.path == "":
            self.path = "/"
        # 建立socket连接
        self.client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
        # 由于使用非阻塞IO,所以需要不停询问连接是否建立好,while 循环不停地检查状态
        self.socket.setblocking(False)
        try:
            self.client.connect((self.host,80))
        # BlockingIOError是正常报错,实际上连接请求已经发送        
        except BlockingIOError as e:
            pass
    
        # 注册:需要调用register(fileobj,events,data=None),fileobj是socket的文件描述符,events是事件,data是回调函数
        # data回调函数很重要
        # 在注册里有select的事件:EVENT_READ\EVENT_WRITE(send就是个写事件)
        # 注册时的self.client.fileno()有个返回值fd
        selector.register(self.client.fileno(),EVENT_WRITE,self.connected)
register注册后的回调函数是由循环loop函数来执行的,因此也叫做事件循环,
不停地请求socket的状态,并调用对应的回调函数
事件循环在IO多路复用中都会存在:回调+事件循环+select(poll\epoll)
def loop():
    import socket
    # 自己去调用,select本身是不支持register模式的,   
    # socket状态变化后的回调是由程序员完成的
    # 会返回
    while not stop: # windows下的模式,linux可以直接用while True
        
        # select()会返回存放(key,events&key.events)元组的列表,元组中的key是在调用register时的key,这个key里是一个namedtuple('SelectorKey',['fileobj','fd','events','data'])
        ready = slector.select()
        # ready里有nametuple,使用for循环进行拆包
        for key,mask in ready:
            # 回调函数是放在data中
            call_back = key.data
            call_back(key)   
        
if __name__ == "__main__":
    import time
    start_time = time.time()
    for url in range(20):
        url="http://shop.projectsedu.com/goods/{}/".format(url)
        urls.append(url)
        fetcher = Fetcher()
        fetcher.get_url(url)
    loop()
    print("所耗费时间{}".format(time.time()-start_time))
    

回调之痛!!!!

1、代码割裂(维护困难)——connect成功后注册EVENT_SEND的selector,回调函数是connected,进入connected后的send操作,也要注册回调,并在send后注册EVENT_READ的selector,回调函数是readable。并设计一个loop事件循环函数,不停地获取可返回状态的回调函数进行运行

痛点:

#1、如果回调函数执行不正常该如何?(因为是在loop函数中抛异常,就定位不到异常具体位置,很头疼!)
#2、如果回调里面还要嵌套回调怎么办?要嵌套很多层怎么办?(get_url->connected->readable,回调里嵌套回调,很头疼!)
#3、如果嵌套了很多层,其中某个环节出错了会造成什么后果?
#4、如果有个数据需要被每个回调都处理怎么办?(用类实例属性的方式,尽量少用全局变量来维护,但实例变量就很多)
#5、怎么使用当前函数中的局部变量   
"""总结:
    1、可读性差
    2、共享状态管理困难
    3、异常处理困难
"""
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值