1.阻塞IO模型
在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:
普通的socket通信就是阻塞IO,blocking IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了。
2.非阻塞IO
如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error,就代表NO data 。从用户进程角度讲 ,它发起一个call后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是用户就可以在本次到下次再发起read询问的时间间隔内做其他事情。
所以,在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有
#服务端
from socket import *
import time
s=socket(AF_INET,SOCK_STREAM)
s.bind(('127.0.0.1',8080))
s.listen(5)
s.setblocking(False) #设置socket的接口为非阻塞
conn_l=[]
del_l=[]
while True:
try:
conn,addr=s.accept() #因为是非阻塞的没有连接会报错
conn_l.append(conn)
except BlockingIOError:
for conn in conn_l: #没有连接的时候就是循环已经请求过来的conn
try:
data=conn.recv(1024) #因为非阻塞,如果也没有conn也会报错,如有有就继续一直循环连接池里是否有人发信息
if not data: #有人断开连接
del_l.append(conn) #因为发送的数据为空了,说明这个连接已经断了,下次要删除这个连接否则会报错
continue
conn.send(data.upper())
except BlockingIOError:
pass
except ConnectionResetError:
del_l.append(conn)
for conn in del_l: #循环需要删除conn的列表
conn_l.remove(conn) #从连接池里去除
conn.close() #close
del_l=[] #清空这个需要删除conn的列表,否则下去for 还会循环到
#客户端
from socket import *
c=socket(AF_INET,SOCK_STREAM)
c.connect(('127.0.0.1',8080))
while True:
msg=input('>>: ')
if not msg:continue
c.send(msg.encode('utf-8'))
data=c.recv(1024)
print(data.decode('utf-8'))
3.IO多路复用
判断非阻塞调用是否就绪如果 OS 能做,是不是应用程序就可以不用自己去等待和判断了,就可以利用这个空闲去做其他事情以提高效率。
select poll epoll的好处就在于单个process就可以同时处理多个网络连接的IO
所以OS将I/O状态的变化都封装成了事件,如可读事件、可写事件。并且提供了专门的系统模块让应用程序可以接收事件通知。这个模块就是select。让应用程序可以通过select注册文件描述符和回调函数。当文件描述符的状态发生变化时,select 就调用事先注册的回调函数。
select因其算法效率比较低,后来改进成了poll,再后来又有进一步改进,BSD内核改进成了kqueue模块,而Linux内核改进成了epoll模块。这四个模块的作用都相同,暴露给程序员使用的API也几乎一致,区别在于kqueue 和 epoll 在处理大量文件描述符时效率更高。
鉴于 Linux 服务器的普遍性,以及为了追求更高效率,所以我们常常听闻被探讨的模块都是 epoll 。
它的基本原理就是会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。它的流程如图:
1.如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
2. 在多路复用模型中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
结论: select的优势在于可以处理多个连接,不适用于单个连接
#服务端
from socket import *
import select
s=socket(AF_INET,SOCK_STREAM)
s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
s.bind(('127.0.0.1',8081))
s.listen(5)
s.setblocking(False) #设置socket的接口为非阻塞
read_l=[s,] #把服务端放入 一个列表 一会传给select
while True:
#select()方法接收并监控3个通信列表, 第一个是所有的输入的data,就是指外部发过来的数据,第2个是监控和接收所有要发出去的data(outgoing data),第3个监控错误信息
r_l,w_l,x_l=select.select(read_l,[],[]) #等待有人连接
for ready_obj in r_l:
if ready_obj == s:
conn,addr=ready_obj.accept() #此时的ready_obj等于s
read_l.append(conn)
else:
try:
data=ready_obj.recv(1024) #此时的ready_obj等于conn
if not data:
ready_obj.close()
read_l.remove(ready_obj)
continue
ready_obj.send(data.upper())
except ConnectionResetError: #当接受的数据没有完成时,客服端断开了触发error
ready_obj.close()
read_l.remove(ready_obj)
#客户端
from socket import *
c=socket(AF_INET,SOCK_STREAM)
c.connect(('127.0.0.1',8081))
while True:
msg=input('>>: ')
if not msg:continue
c.send(msg.encode('utf-8'))
data=c.recv(1024)
print(data.decode('utf-8'))
epoll适用于linux,好处在于能接受的文件描述符要更多,epoll采用的是事件通知机制,而不再是以轮询的方式挨个询问每个文件描述符的状态,节省cpu时间。
mac os 使用 kqueue 同epoll
selectors,它的功能与linux的epoll,还是select模块,poll等类似;实现高效的I/O multiplexing, 常用于非阻塞的socket的编程中,根据平台选出最佳的IO多路机制,比如在win的系统上他默认的是select模式而在linux上它默认的epoll。
模块定义了一个 BaseSelector的抽象基类, 以及它的子类,包括:SelectSelector, PollSelector, EpollSelector, DevpollSelector, KqueueSelector.
另外还有一个DefaultSelector类,它其实是以上其中一个子类的别名而已,它自动选择为当前环境中最有效的Selector,所以平时用 DefaultSelector类就可以了,其它用不着。
import selectors
import socket
sel = selectors.DefaultSelector()
def accept(sock, mask):
conn, addr = sock.accept() # Should be ready
print('accepted', conn, 'from', addr)
conn.setblocking(False)
sel.register(conn, selectors.EVENT_READ, read)
def read(conn, mask):
data = conn.recv(1024) # Should be ready
if data:
print('echoing', repr(data), 'to', conn)
conn.send(data) # Hope it won't block
else:
print('closing', conn)
sel.unregister(conn) #取消某一个注册对象比如 conn
conn.close()
sock = socket.socket()
sock.bind(('localhost', 10000))
sock.listen(100)
sock.setblocking(False)
sel.register(sock, selectors.EVENT_READ, accept)
while True:
events = sel.select() #m默认阻塞,有活动连接就返回活动连接列表
for key, mask in events:
print(key) #SelectorKey,包含了fileobj注册的对象也就是socket对象,fd文件描述符,events 1 或者0 可读还是可写,data就是注册对象的的回调函数 accept
print(key.data) #accept 函数
print(mask) # 1 2 代表的读 写
print(key.fileobj) #及时绑定的sock对象
callback = key.data #accept
callback(key.fileobj, mask)
如果不清楚以后用在什么操作系统下,尽量选择selectors,会自动选择是匹配最合适的epoll 还是select