IO多路复用 + 基于IO多路复用和socket并发请求 + 协程
IO多路复用
模块: import select.select()
1. IO多路复用的作用?
检测监听多个socket是否已经发生了变化 (是否已经连接成功/获取数据/可读/可写等).
操作系统检测socket是否发生变化,有三种模式:
select: 最多监听1024个socket, 循环检测.
poll:不限制监听socket个数;循环去检测(水平触发)。
epoll:不限制监听socket个数;回调方式(边缘触发)。
在python模块中有2个: select epoll
示例 1:
向百度发送一次请求, 有两种方式. 第一种是通过 requests , 另一种是通过 socket 对象.
socket 发送一次请求 单线程示例 2:
若向百度发送3个关键字请求, 将请求封装到函数中, 将关键字放入列表中,通过循环执行请求的函数. 该方法也是单线程的.
虽然能够实现功能,但是如果想要发送的请求是100次,这种单线程的工作,只能是一个一个的处理,因为connect和recv会有阻塞,需要等待事情处理完,才能执行下一个请求.
单线程 发送三次关键字请求示例 3:
为了解决上述的问题,有人说可以用多线程来解决,是的多线程的确是可以提高效率,如果线程池中有5个线程,那么就可以5个请求5个请求的处理,可以提高一定的效率.但是线程的工作过程中,connect和recv依然会有阻塞,还是要等待,服务端返回数据,才会执行下一个请求. 将示例2中的循环执行请求,换成创建线程来处理.
import threading
key_list = ['alex','db','sb']
for item in key_list:
t = threading.Thread(target=get_data,args=(item,)) # get_data函数是发送请求,item为关键字
t.start()
总结: 出现以上的问题都是因为 socket 对象在connect 和 recv 会有阻塞,导致要等待.....
因此, 想到解除 socket 的阻塞问题,让线程可以做其他的事情,但是解除阻塞以后,
要考虑, 怎么知道请求是IO操作? 怎么知道连接成功了? 怎么知道结果什么时候返回来了?
IO多路复用 select.select() 就是帮助我们检测以上问题的.
IO多路复用+socket并发请求
模块: Twisted
原理:
1. 解除阻塞 -- 非阻塞
2. 监听什么时候发生变化回来, select.select()
示例1 :
IO多路复用+socket请求 实现 单线程并发
http协议: b'GET /web?query=fdf HTTP/1.0\r\nhost:www.sogou.com\r\n\r\n 重点
import socket
import select
# 创建了3个socket对象
client1 = socket.socket()
client1.setblocking(False) # 百度创建连接: 非阻塞 阻塞的地方需要捕获异常
try:
client1.connect(('www.baidu.com',80))
except BlockingIOError as e:
pass
client2 = socket.socket()
client2.setblocking(False) # 百度创建连接: 非阻塞
try:
client2.connect(('www.sogou.com',80))
except BlockingIOError as e:
pass
client3 = socket.socket()
client3.setblocking(False) # 百度创建连接: 非阻塞
try:
client3.connect(('www.oldboyedu.com',80))
except BlockingIOError as e:
pass
socket_list = [client1,client2,client3]
conn_list = [client1,client2,client3]
while True:
rlist,wlist,elist = select.select(socket_list,conn_list,[],0.005) # 进行检测,rlist放返回数据的socket对象
# wlist放连接成功的socket对象,elist放出现异常的对象
# wlist中表示已经连接成功的socket对象
for sk in wlist:
if sk == client1:
sk.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.baidu.com\r\n\r\n')
elif sk==client2:
sk.sendall(b'GET /web?query=fdf HTTP/1.0\r\nhost:www.sogou.com\r\n\r\n')
else:
sk.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.oldboyedu.com\r\n\r\n')
conn_list.remove(sk)
for sk in rlist:
chunk_list = []
while True:
try:
chunk = sk.recv(8096)
if not chunk:
break
chunk_list.append(chunk)
except BlockingIOError as e:
break
body = b''.join(chunk_list)
# print(body.decode('utf-8'))
print('------------>',body)
sk.close()
socket_list.remove(sk)
if not socket_list:
break
示例2 :
单线程的并发 -- 高级版本 (实际上是对Twisted模块的原理解析,以后的工作中,会用该模块来实现基于IO复用的单线程并发)
import socket
import select
class Req(object): # 进行封装
def __init__(self,sk,func):
self.sock = sk
self.func = func
def fileno(self):
return self.sock.fileno()
class Nb(object): # 进行封装
def __init__(self):
self.conn_list = []
self.socket_list = []
def add(self,url,func):
client = socket.socket()
client.setblocking(False) # 非阻塞
try:
client.connect((url, 80))
except BlockingIOError as e:
pass
obj = Req(client,func)
self.conn_list.append(obj)
self.socket_list.append(obj)
def run(self):
while True:
rlist,wlist,elist = select.select(self.socket_list,self.conn_list,[],0.005)
# wlist中表示已经连接成功的req对象
for sk in wlist:
# 发生变换的req对象
sk.sock.sendall(b'GET /s?wd=alex HTTP/1.0\r\nhost:www.baidu.com\r\n\r\n')
self.conn_list.remove(sk)
for sk in rlist:
chunk_list = []
while True:
try:
chunk = sk.sock.recv(8096)
if not chunk:
break
chunk_list.append(chunk)
except BlockingIOError as e:
break
body = b''.join(chunk_list)
# print(body.decode('utf-8'))
sk.func(body)
sk.sock.close()
self.socket_list.remove(sk)
if not self.socket_list:
break
def baidu_repsonse(body):
print('百度下载结果:',body)
def sogou_repsonse(body):
print('搜狗下载结果:', body)
def oldboyedu_repsonse(body):
print('老男孩下载结果:', body)
t1 = Nb()
t1.add('www.baidu.com',baidu_repsonse) # 函数的回调
t1.add('www.sogou.com',sogou_repsonse)
t1.add('www.oldboyedu.com',oldboyedu_repsonse)
t1.run()
示例3 : ♥♥♥♥♥
利用 Twisted 模块
from twisted.web.client import getPage, defer from twisted.internet import reactor def all_done(arg): reactor.stop() def callback(contents): print(contents) deferred_list = [] url_list = ['http://www.bing.com', 'http://www.baidu.com', ] for url in url_list: deferred = getPage(bytes(url, encoding='utf8')) deferred.addCallback(callback) deferred_list.append(deferred) dlist = defer.DeferredList(deferred_list) dlist.addBoth(all_done) reactor.run()
总结:
1. socket 默认是否是阻塞的?阻塞体现在哪里?
默认是阻塞的,体现在 connect 和 recv上
2. 如何让socket编程非阻塞?
sk = socket.socket()
sk.setblocking(False) #设置非阻塞, 需要捕获异常,否则会报错
try: .........
except BlockingIOError as e :
break
3. IO多路复用的作用?
检测多个socket是否发生变化。
操作系统检测socket是否发生变化,有三种模式:
select:最多1024个socket;循环去检测。
poll:不限制监听socket个数;循环去检测(水平触发)。
epoll:不限制监听socket个数;回调方式(边缘触发)。
Python模块:
select.select
select.epoll 但是 windows不支持, linux 支持.
4. 提高并发的方案?
多进程
多线程
异步非阻塞模块(Twisted) scrapy框架(单线程完成并发)
5. 什么是异步非阻塞?
非阻塞: 不等待,
比如创建socket对某个地址进行connect. 获取接收数据recv 时默认都会等待(连接成功或接收到数据),才执行后续操作.
如果设置setblocking(False),以上两个过程就不再等待了,但是会报 BlockingIOError 的错误,只要捕获即可.
异步:通知, 执行完成之后自动执行回调函数或自动执行某些操作(通知).
比如做爬虫中向某个地址baidu.com发送请求,当请求执行完成之后自动执行回调函数.
6. 什么是同步阻塞?
阻塞: 等
同步: 按照顺序执行
key_list = ['alex','db','sb']
for item in key_list:
ret = requests.get('https://www.baidu.com/s?wd=%s' %item)
print(ret.text)
协程
1. 什么是协程?
进程: 操作系统中存在的
线程: 操作系统中存在的
协程: 微线程,是由程序员创造出来的一个不是真实存在的东西.
2. 协程的作用及编写?
作用:对一个线程进行分片,使得线程在代码块之间来回进行切换,而不是原来的逐行执行.
需要导入模块 import greenlet
该模块需要在cmd安装: pip3 install greenlet
示例 1 :
单纯的协程是无用的.
import greenlet def f1(): print(11) gr2.switch() print(22) gr2.switch() def f2(): print(33) gr1.switch() print(44) # 协程 gr1 gr1 = greenlet.greenlet(f1) # 只是创建协程并未运行 # 协程 gr2 gr2 = greenlet.greenlet(f2) gr1.switch() # 运行f1 # 11 # 33 # 22 # 44
示例 2: ♥♥♥♥♥
单纯的协程是无用的,但是当 协程 + 遇到IO就切换 ==>> 实现单线程的并发
需要安装模块 pip3 install gevent
导入 from gevent import monkey
monkey.patch_all() #非常重要,以后代码中遇到IO都会自动执行greenlet的switch进行切换
协程_IO切换示例:
from gevent import monkey monkey.patch_all() # 以后代码中遇到IO都会自动执行greenlet的switch进行切换 重点 import requests import gevent def get_page1(url): ret = requests.get(url) print(url,ret.content) def get_page2(url): ret = requests.get(url) print(url,ret.content) def get_page3(url): ret = requests.get(url) print(url,ret.content) gevent.joinall([ gevent.spawn(get_page1, 'https://www.python.org/'), # 协程1 gevent.spawn(get_page2, 'https://www.yahoo.com/'), # 协程2 gevent.spawn(get_page3, 'https://github.com/'), # 协程3 ])
示例3 :
手动实现协程, 基于yield 生成器
def f1(): print(11) yield print(22) yield print(33) def f2(): print(55) yield print(66) yield print(77) v1 = f1() v2 = f2() next(v1) # v1.send(None) next(v2) # v1.send(None) next(v1) # v1.send(None) next(v2) # v1.send(None) next(v1) # v1.send(None) next(v2) # v1.send(None) # 报错可以抛出异常处理
总结:
1.什么是协程?
协程也可以称为“微线程”,就是开发者控制线程执行流程,控制先执行某段代码然后再切换到另外函执行代码...来回切换。
2. 协程可以提高并发吗?
协程本身是无法实现并发的,(甚至性能会降低)
协程 + IO切换,就可以实现并发,提高性能.
3. 进程,线程,协程的区别?
重点
4. 单线程的并发(2种)
1) 协程 + IO切换: gevent
2) 基于事件循环的 异步非阻塞框架: Twisted


浙公网安备 33010602011771号