对于IO多路复用的理解
- 首先是网络请求到达,需要资源来处理,如果是一个线程来处理,那么这个线程就会被这一个请求阻塞,时刻监听这个请求的变化,是否有数据写入等等。线程的开启和关闭也需要耗费服务器的资源。当请求很多时,服务器需要很多的线程来处理,随着请求增多,线程不断地创建、销毁,服务器的响应性能会线性下降,到达某一个程度直接宕机。
- 对上面的模式进行优化,增加线程池、进程池等,池子会解决频繁创建、销毁线程的开销,当服务高峰期,一时涌进来很多的连接时,这种线程阻塞式的依然会导致宕机。
- 再优化,在一切皆文件的unix下,可以接收数据的对象或者连接,都叫做文件描述符fd,可以理解为系统会为每一个连接创建一个文件描述符,文件描述符里记录着连接信息和系统需要加载的信息,而系统也是通过循环检测文件描述符的变化来检测是否有连接发生变化的。那么就可以用一个线程检测多个文件描述符,也就是检测多个连接的变化,哪个发生了变化,就将发生变化的返回,调用相关的处理,每隔固定的时间就循环检测一次其下所有的文件描述符,找到发生变化的文件描述符,这大概可以认为是select做的事情,其真实情况是select会阻塞,当有变化时,会停止阻塞,开始轮训。
- 继续优化,一个线程调用select,有检测文件描述符数量的上限即1024个,这个跟内核的操作有关,那么连接数量多的时候依然会触发创建很多线程,虽然相对前面的已经有了不超过1024倍的提升,依然有瓶颈,这时,poll可以检测不限数量的文件描述符,相当于又是一个进步
- 再想想,
有些概念需要区分一下,当有IO流的时候,本来的操作是IO流直接触发流数据的读取,当有了select、poll、epoll,数据发生变化后会通过select、poll、epoll来触发流数据读取,所以可以select、poll、epoll理解为是IO流操作的代理。
I/O 多路复用( IO multiplexing)【事件驱动IO(event driven IO)】
IO多路复用其实就是我们说的select,poll,epoll,select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这几个函数会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。即select【以select举例】会轮询检测所有的连接,或者 IO。哪个有数据,就返回.我们就可以拿到了。【其理论事件驱动模型】
select poll epoll IO多路复用介绍
首先列一下,sellect、poll、epoll三者的区别
select
select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。
select目前几乎在所有的平台上支持
select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。
另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。
poll
它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。
一般也不用它,相当于过渡阶段
epoll
直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll。被公认为Linux2.6下性能最好的多路I/O就绪通知方法。windows不支持
没有最大文件描述符数量的限制。
比如100个连接,有两个活跃了,epoll会告诉用户这两个两个活跃了,直接取就ok了,而select是循环一遍。
(了解)epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。
另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。
所以市面上上见到的所谓的异步IO,比如nginx、Tornado、等,我们叫它异步IO,实际上是IO多路复用。
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
强调
- 如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
- 在多路复用模型中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
select函数
import select
fd_r_list, fd_w_list, fd_e_list = select.select(rlist, wlist, xlist, [timeout])
参数: 可接受四个参数(前三个必须)
rlist: wait until ready for reading #等待读的对象,你需要监听的需要获取数据的对象列表
wlist: wait until ready for writing #等待写的对象,你需要写一些内容的时候,input等等,也就是说我会循环他看看是否有需要发送的消息,如果有我取出这个对象的消息并发送出去,一般用不到,这里我们也给一个[]。
xlist: wait for an “exceptional condition” #等待异常的对象,一些额外的情况,一般用不到,但是必须传,那么我们就给他一个[]。
timeout: 超时时间
当超时时间 = n(正整数)时,那么如果监听的句柄均无任何变化,则select会阻塞n秒,之后返回三个空列表,如果监听的句柄有变化,则直接执行。
返回值:三个列表与上面的三个参数列表是对应的
select方法用来监视文件描述符(当文件描述符条件不满足时,select会阻塞),当某个文件描述符状态改变后,会返回三个列表
1、当参数1 序列中的fd满足“可读”条件时,则获取发生变化的fd并添加到fd_r_list中
2、当参数2 序列中含有fd时,则将该序列中所有的fd添加到 fd_w_list中
3、当参数3 序列中的fd发生错误时,则将该发生错误的fd添加到 fd_e_list中
4、当超时时间为空,则select会一直阻塞,直到监听的句柄发生变化
即:select()方法接收并监控3个通信列表, 第一个是所有的输入的data,就是指外部发过来的数据,第2个是监控和接收所有要发出去的data(outgoing data),第3个监控错误信息。
select是同时处理多个链接,不适用单个连接,或者说相对于线程阻塞处理一个连接,使用select会是效率变低。
示例
#服务端
from socket import *
import select
server = socket(AF_INET, SOCK_STREAM)
server.bind(('127.0.0.1',8093))
server.listen(5)
# 设置为非阻塞
server.setblocking(False)
# 初始化将服务端socket对象加入监听列表,后面还要动态添加一些conn连接对象,当accept的时候sk就有感应,当recv的时候conn就有动静
rlist=[server,]
rdata = {} #存放客户端发送过来的消息
wlist=[] #等待写对象
wdata={} #存放要返回给客户端的消息
print('预备!监听!!!')
count = 0 #写着计数用的,为了看实验效果用的,没用
while True:
# 开始 select 监听,对rlist中的服务端server进行监听,select函数阻塞进程,直到rlist中的套接字被触发(在此例中,套接字接收到客户端发来的握手信号,从而变得可读,满足select函数的“可读”条件),被触发的(有动静的)套接字(服务器套接字)返回给了rl这个返回值里面;
rl,wl,xl=select.select(rlist,wlist,[],0.5)
print('%s 次数>>'%(count),wl)
count = count + 1
# 对rl进行循环判断是否有客户端连接进来,当有客户端连接进来时select将触发
for sock in rl:
# 判断当前触发的是不是socket对象, 当触发的对象是socket对象时,说明有新客户端accept连接进来了
if sock == server:
# 接收客户端的连接, 获取客户端对象和客户端地址信息
conn,addr=sock.accept()
#把新的客户端连接加入到监听列表中,当客户端的连接有接收消息的时候,select将被触发,会知道这个连接有动静,有消息,那么返回给rl这个返回值列表里面。
rlist.append(conn)
else:
# 由于客户端连接进来时socket接收客户端连接请求,将客户端连接加入到了监听列表中(rlist),客户端发送消息的时候这个连接将触发
# 所以判断是否是客户端连接对象触发
try:
data=sock.recv(1024)
#没有数据的时候,我们将这个连接关闭掉,并从监听列表中移除
if not data:
sock.close()
rlist.remove(sock)
continue
print("received {0} from client {1}".format(data.decode(), sock))
#将接受到的客户端的消息保存下来
rdata[sock] = data.decode()
#将客户端连接对象和这个对象接收到的消息加工成返回消息,并添加到wdata这个字典里面
wdata[sock]=data.upper()
#需要给这个客户端回复消息的时候,我们将这个连接添加到wlist写监听列表中
wlist.append(sock)
#如果这个连接出错了,客户端暴力断开了(注意,我还没有接收他的消息,或者接收他的消息的过程中出错了)
except Exception:
#关闭这个连接
sock.close()
#在监听列表中将他移除,因为不管什么原因,它毕竟是断开了,没必要再监听它了
rlist.remove(sock)
# 如果现在没有客户端请求连接,也没有客户端发送消息时,开始对发送消息列表进行处理,是否需要发送消息
for sock in wl:
sock.send(wdata[sock])
wlist.remove(sock)
wdata.pop(sock)
# #将一次select监听列表中有接收数据的conn对象所接收到的消息打印一下
# for k,v in rdata.items():
# print(k,'发来的消息是:',v)
# #清空接收到的消息
# rdata.clear()
#---------------------------------------
#客户端
from socket import *
client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8093))
while True:
msg=input('>>: ').strip()
if not msg:continue
client.send(msg.encode('utf-8'))
data=client.recv(1024)
print(data.decode('utf-8'))
client.close()