python并发编程-IO多路复用select/poll/epoll实现多客户端通信
定义
同时监控多个IO事件,当哪个IO事件准备就绪就(已经到了必然要处理的步骤)执行哪个IO事件。以此形成可以同时处理多个IO的行为,避免一个IO阻塞造成其他IO均无法执行,提高了IO执行效率。
具体方案
select方法: Windows/Linux/Unix
poll方法: Linux/Unix
epoll方法: Linux
select方法
rs, ws, xs=select(rlist, wlist, xlist[, timeout])
功能:
监控IO事件,阻塞等待IO发生
参数:
rlist 列表 存放关注的等待发生的IO事件 比如:accept()等待客户端连接
wlist 列表 存放关注的要主动处理的IO事件 比如:sendto()发消息
xlist 列表 存放关注的出现异常要处理的IO 比如:IO发生异常
timeout 超时时间
返回值:
rs 列表 rlist中准备就绪的IO
ws 列表 wlist中准备就绪的IO
xs 列表 xlist中准备就绪的IO
这三个返回列表,至少有一个不为空
注意
wlist中如果存在IO事件,则select立即返回给ws处理IO过程中不要出现死循环占有服务端的情况IO多路复用消耗资源较少,效率较高同时监听1024个IO事件
select 实现tcp服务
【1】 将关注的IO放入对应的监控类别列表
【2】通过select函数进行监控
【3】遍历select返回值列表,确定就绪IO事件
【4】处理发生的IO事件
select实现多客户端通信
服务端代码
from socket import *
from select import select
# 设置套接字为关注IO
s = socket()
s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
s.bind(("0.0.0.0", 9548))
s.listen(5)
# 设置关注的IO
rlist = [s]
wlist = []
xlist = []
# 循环监控IO的发生
while True:
print("开始监控IO行为")
rs, ws, xs = select(rlist, wlist, xlist)
# 遍历三个返回值列表,进一步判断哪个IO发生
for r in rs:
# 如果是套接字就绪,就处理连接
if r is s:
c, addr = r.accept()
print("Connect from:", addr)
rlist.append(c) # 加入新的关注IO
else:
data = r.recv(1024)
if not data:
rlist.remove(r)
r.close()
continue
print(data.decode())
# r.send(b"OK")
# 希望我们主动处理这个IO
wlist.append(r)
for w in ws:
w.send(b"OK Thanks")
wlist.remove(w)
for x in xs:
pass
客户端代码
from socket import *
sockfd = socket(AF_INET, SOL_SOCKET)
sockfd.connect(("172.40.71.158", 9548))
while True:
msg = input("输入消息:")
sockfd.send(msg.encode())
data = sockfd.recv(1024)
if not data:
print("服务器已退出")
break
print("接收到的消息为:", data.decode())
sockfd.close()
poll方法
p = select.poll() #这个poll是创建对象
功能 :
创建poll对象
返回值:
poll对象
p.register(fd,event)
功能:
注册关注的IO事件
参数:
fd 要关注的IO
event 要关注的IO事件类型
监测的事件类型按照按位或进行操作,第一个参数为监测事件IO,第二个参数为需要监测事件类型
常用类型:
POLLIN 读IO事件(rlist)
POLLOUT 写IO事件 (wlist)
POLLERR 异常IO (xlist)
POLLHUP 断开连接
e.g. p.register(sockfd,POLLIN|POLLERR)
p.unregister(fd)
功能:
取消对IO的关注
参数:
IO对象或者IO对象的fileno
events = p.poll() #这个poll()是阻塞监控
功能:
阻塞等待监控的IO事件发生
返回值:
返回发生的IO
events格式 [(fileno,event),()…]
每个元组为一个就绪IO,元组第一项是该IO的fileno,第二项为该IO就绪的事件类型。只能根据文件描述符回推那个阻塞对象。建立查找地图
poll实现tcp服务
【1】 创建套接字
【2】 将套接字register
【3】 建立查找地图,创建查找字典,并维护
【4】 循环监控IO发生
【5】 处理发生的IO
poll实现多客户端通信
服务端代码
from select import *
from socket import *
sockfd = socket()
sockfd.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
sockfd.bind(("0.0.0.0", 9548))
sockfd.listen(5)
# 创建poll对象
p = poll()
# 建立一个查找字典(地图){fileno:io_obj}
fdmap = {sockfd.fileno(): sockfd}
# 设置关注IO
p.register(sockfd, POLLIN | POLLERR)
# 循环监控IO时间发生
while True:
events = p.poll() # 阻塞等待IO发生
# 遍历列表处理IO
for fd, event in events: # fd为文件描述符,event为IO就绪事件类型
if fd == sockfd.fileno():
c, addr = fdmap[fd].accept()
print("Connect from:", addr)
# 添加新的关注事件
p.register(c, POLLIN | POLLHUP)
fdmap[c.fileno()] = c
elif event & POLLIN: # 客户端发消息
data = fdmap[fd].recv(1024)
if not data:
print("客户端退出")
p.unregister(fd) # 取消关注事件
fdmap[fd].close()
del fdmap[fd] # 从字典删除
continue
print(data.decode())
fdmap[fd].send(b"OK")
客户端代码同select客户端代码
epoll方法
使用方法
基本与poll相同
生成对象改为 epoll()
将所有事件类型改为EPOLL类型
epoll特点(效率是最高的)
epoll 效率比select poll要高
select poll将需要监听的IO事件交给内核,当有可以执行的IO事件发生时,内核会将所有的IO事件交给应用层,包括未准备就绪的IO事件,应用层需要遍历拿到准备就绪的IO事件,影响效率.而epoll是在内核开辟一块空间,哪个IO事件准备就绪,就将哪个IO事件交给应用层,效率得以提升.epoll 监控IO数量比select poll要多
select poll同时监听1024个IO事件,而epoll在1GB内存的机器上大约是10万左右.epoll 的触发方式比poll要多 (EPOLLET边缘触发)epoll默认是水平触发
水平触发,当一个IO事件准备就绪,应用层未去做处理,它会一直通知你,而边缘触发不会,第一次未处理它不会再去通知,当再次接收到消息时,第二次会将第一次通知的消息一起带过去.
epoll实现多客户端通信
服务端代码
from select import *
from socket import *
sockfd = socket()
sockfd.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
sockfd.bind(("0.0.0.0", 9549))
sockfd.listen(5)
# 创建epoll对象
ep = epoll()
# 建立一个查找字典(地图){fileno:io_obj}
fdmap = {sockfd.fileno(): sockfd}
# 设置关注IO
ep.register(sockfd, EPOLLIN | EPOLLERR)
# 循环监控IO时间发生
while True:
events = ep.poll() # 阻塞等待IO发生
# 遍历列表处理IO
for fd, event in events: # fd为文件描述符,event为IO就绪事件类型
if fd == sockfd.fileno():
c, addr = fdmap[fd].accept()
print("Connect from:", addr)
# 添加新的关注事件
ep.register(c, EPOLLIN | EPOLLHUP)
fdmap[c.fileno()] = c
elif event & EPOLLIN: # 客户端发消息
data = fdmap[fd].recv(1024)
if not data:
print("客户端退出")
ep.unregister(fd) # 取消关注事件
fdmap[fd].close()
del fdmap[fd] # 从字典删除
continue
print(data.decode())
fdmap[fd].send(b"OK")
客户端代码同select客户端代码