一、必备知识
1.socket客户端:
阻塞式客户端:
import socket
#实例化socket对象
sock = socket.socket()
#向服务器发起连接
sock.connect(("43.226.160.17",80)) #连接会阻塞
# 仿HTTP发送请求
data = b"GET / HTTP/1.0\r\nhost: dig.chouti.com\r\n\r\n"
sock.sendall(data)
#接收服务器响应信息
response = sock.recv(8096) #等待数据回收时也会阻塞
print(response)
#关闭socket连接
sock.close()
伪非阻塞式:
import socket
#实例化socket对象
sock = socket.socket()
#设置非阻塞,不等待连接成功直接执行后面的代码,由于连接的操作不可能立即完成所以
#会报一个BlockingIOError:无法立即完成一个非阻止性套接字操 的错误,因此需要处理异常
sock.setblocking(False)
#向服务器发起连接
try:
sock.connect(("43.226.160.17",80)) #连接会阻塞
except BlockingIOError as e:
print(e)
import time
#虽然会报错,但连接的请求已经发出去,所以连接请求依然生效,
#我们可以使用这段时间进行其它的工作,达到资源的最大利用,这里用sleep代指
time.sleep(5)
# 仿HTTP发送请求
data = b"GET / HTTP/1.0\r\nhost: dig.chouti.com\r\n\r\n"
#由于没有建立连接,所以这里还发不出消息
try:
sock.sendall(data)
#接收服务器响应信息
response = sock.recv(8096) #等待数据回收时也会阻塞
except Exception as e:
print(e)
print(response)
#关闭socket连接
sock.close()
2.IO多路复用加socket实现非阻塞
伪代码:
import socket
import select
socket_list = []
for url in ["www.baidu.com","www.bing.com"]:
client = socket.socket()
client.setblocking(False)
try:
client.connect((url,80))
except BlockingIOError as e:
print(e)
socket_list.append(client)
#事件循环
while True:
r,w,e = select.select(socket_list,socket_list,[],0.05)
#w代指每一个连接成功的client
for obj in w:
obj.send("GET / HTTP/1.0\r\nhost: baidu.com\r\n\r\n") #这里host写死,下面的代码会解决
#r代指client有改变,可以收数据了
for obj in r:
response = obj.recv(1024)
print(response)
上边代码中,当对多个网站进行连接或爬取时,每一个单独的Url都不会阻塞,再通过select监测数据的变化,可以及时接收数据,又不会挡住后边Url的爬取工作,实现了简单的非阻塞的目标。
但这只是一段伪代码,接下来根据这个思路实现一个真正的异步非阻塞模块。
二、简单的异步非阻塞模块。
import socket
import select
class Request(object):
"""
封装socket对象,使每次循环时创建的socket对象能对应它的req_info字典,
方便其利用字典拿到对应的host信息等
"""
def __init__(self, sock, info):
self.sock = sock
self.info = info
def fileno(self):
return self.sock.fileno()
class AsyncRequest(object):
def __init__(self):
self.sock_list = []
self.conns = []
def add_request(self, req_info):
"""
创建请求
req_info: {'host': 'www.baidu.com', 'port': 80, 'path': '/'},
:return:
"""
sock = socket.socket()
sock.setblocking(False)
try:
sock.connect((req_info['host'], req_info['port']))
except BlockingIOError as e:
pass
obj = Request(sock, req_info)
self.sock_list.append(obj)
self.conns.append(obj)
def run(self):
"""
开始事件循环,检测:连接成功?数据是否返回?
:return:
"""
while True:
# select.select([socket对象,])
# 可是任何对象,对象一定要有fileno方法,实际上执行的是对象.fileno()
# select.select([request对象,])
r, w, e = select.select(self.sock_list, self.conns, [], 0.05)
# w,是否连接成功
for obj in w:
# 检查obj:request对象
# socket, {'host': 'www.baidu.com', 'port': 80, 'path': '/'},
data = "GET %s http/1.1\r\nhost:%s\r\n\r\n" % (obj.info['path'], obj.info['host'])
obj.sock.send(data.encode('utf-8'))
#连接成功后从列表中删除此obj对象,避免重复连接
self.conns.remove(obj)
# 数据返回,接收到数据
for obj in r:
response = obj.sock.recv(8096)
#函数名加括号运行对应的回调函数
obj.info['callback'](response)
#相应的为避免重复接收移除已经接收成功的对象
self.sock_list.remove(obj)
# 所有请求已经返回
if not self.sock_list:
break
if __name__ == '__main__':
#指定回调函数,可以在屏幕输出,也可以写入文件、数据库等
def callback_fun1(response):
print(response)
def callback_fun2(response):
pass
# with open ......
url_list = [
{'host': 'www.baidu.com', 'port': 80, 'path': '/', 'callback': callback_fun1},
{'host': 'www.cnblogs.com', 'port': 80, 'path': '/index.html', 'callback': callback_fun2},
{'host': 'www.bing.com', 'port': 80, 'path': '/', 'callback': callback_fun1},
]
obj = AsyncRequest()
for item in url_list:
obj.add_request(item)
obj.run()