select、epoll、poll、udp、tcp。IO多路复用
select方法 : windows linux unix
poll方法: linux unix
epoll方法: linux
select IO多路复用
IO多路复用的目的即好处:
同时监控多个IO事件,当哪个IO事件准备就绪就执行哪个IO事件。以此形成可以同时处理多个IO的行为,避免一个IO阻塞造成其他IO均无法执行,提高了IO执行效率。
select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符从而进行后续的读写操作。
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。
select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。
另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。
它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。
总结
1、单个进程所打开的FD(文件描述符, File Descriptor)是有限制的,通过FD_SETSIZE设置,默认1024.
2、每次调用select,都需要把DF集合从用户态拷贝到内核态,这个开销在FD很多时会很大。
3、对socket扫描时是线性扫描,采用轮询的方法,效率较低(高并发情况下)
示例代码,也不知道从哪里弄的,这里是对select模块的使用在步骤上的理解。具体根据自己的需求来扩展。
服务端
# coding: utf-8
import select
import socket
import Queue
from time import sleep
# Create a TCP/IP
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setblocking(False)
# Bind the socket to the port
server_address = ('localhost', 8090)
print ('starting up on %s port %s' % server_address)
server.bind(server_address)
# Listen for incoming connections
server.listen(5)
# Sockets from which we expect to read
inputs = [server]
# Sockets to which we expect to write
# 处理要发送的消息
outputs = []
# Outgoing message queues (socket: Queue)
message_queues = {}
while inputs:
# Wait for at least one of the sockets to be ready for processing
print ('waiting for the next event')
# 开始select 监听, 对input_list 中的服务器端server 进行监听 # 一旦调用socket的send, recv函数,将会再次调用此模块
readable, writable, exceptional = select.select(inputs, outputs, inputs)
# Handle inputs
# 循环判断是否有客户端连接进来, 当有客户端连接进来时select 将触发
for s in readable:
# 判断当前触发的是不是服务端对象, 当触发的对象是服务端对象时,说明有新客户端连接进来了
# 表示有新用户来连接
if s is server:
# A "readable" socket is ready to accept a connection
connection, client_address = s.accept()
print ('connection from', client_address)
# this is connection not server
connection.setblocking(0)
# 将客户端对象也加入到监听的列表中, 当客户端发送消息时 select 将触发
inputs.append(connection)
# Give the connection a queue for data we want to send
# 为连接的客户端单独创建一个消息队列,用来保存客户端发送的消息
message_queues[connection] = Queue.Queue()
else:
# 有老用户发消息, 处理接受
# 由于客户端连接进来时服务端接收客户端连接请求,将客户端加入到了监听列表中(input_list), 客户端发送消息将触发
# 所以判断是否是客户端对象触发
data = s.recv(1024)
# 客户端未断开
if data != '':
# A readable client socket has data
print ('received "%s" from %s' % (data, s.getpeername()))
# 将收到的消息放入到相对应的socket客户端的消息队列中
message_queues[s].put(data)
# Add output channel for response
# 将需要进行回复操作socket放到output 列表中, 让select监听
if s not in outputs:
outputs.append(s)
else:
# 客户端断开了连接, 将客户端的监听从input列表中移除
# Interpret empty result as closed connection
print ('closing', client_address)
# Stop listening for input on the connection
if s in outputs:
outputs.remove(s)
inputs.remove(s)
s.close()
# Remove message queue
# 移除对应socket客户端对象的消息队列
del message_queues[s]
# Handle outputs
# 如果现在没有客户端请求, 也没有客户端发送消息时, 开始对发送消息列表进行处理, 是否需要发送消息
# 存储哪个客户端发送过消息
for s in writable:
try:
# 如果消息队列中有消息,从消息队列中获取要发送的消息
message_queue = message_queues.get(s)
send_data = ''
if message_queue is not None:
send_data = message_queue.get_nowait()
else:
# 客户端连接断开了
print "has closed "
except Queue.Empty:
# 客户端连接断开了
print "%s" % (s.getpeername())
outputs.remove(s)
else:
# print "sending %s to %s " % (send_data, s.getpeername)
# print "send something"
if message_queue is not None:
s.send(send_data)
else:
print "has closed "
# del message_queues[s]
# writable.remove(s)
# print "Client %s disconnected" % (client_address)
# # Handle "exceptional conditions"
# 处理异常的情况
for s in exceptional:
print ('exception condition on', s.getpeername())
# Stop listening for input on the connection
inputs.remove(s)
if s in outputs:
outputs.remove(s)
s.close()
# Remove message queue
del message_queues[s]
sleep(1)
客户端
# coding: utf-8
import socket
messages = ['This is the message ', 'It will be sent ', 'in parts ', ]
server_address = ('localhost', 8090)
# Create aTCP/IP socket
socks = [socket.socket(socket.AF_INET, socket.SOCK_STREAM), socket.socket(socket.AF_INET, socket.SOCK_STREAM), ]
# Connect thesocket to the port where the server is listening
print ('connecting to %s port %s' % server_address)
# 连接到服务器
for s in socks:
s.connect(server_address)
for index, message in enumerate(messages):
# Send messages on both sockets
for s in socks:
print ('%s: sending "%s"' % (s.getsockname(), message + str(index)))
s.send(bytes(message + str(index)).decode('utf-8'))
# Read responses on both sockets
for s in socks:
data = s.recv(1024)
print ('%s: received "%s"' % (s.getsockname(), data))
if data != "":
print ('closingsocket', s.getsockname())
s.close()
自己的代码示例
创建可以并发操作的服务端,来接收udp、tcp双工
#服务端
from socket import *
import select
server_tcp = socket(AF_INET, SOCK_STREAM)
server_tcp.bind(('127.0.0.1',8093))
server_tcp.listen(5)
# 设置为非阻塞
server_tcp.setblocking(False)
server_udp = socket(AF_INET, SOCK_DGRAM)
server_udp.bind(("127.0.0.1", 5095))
# 初始化将服务端socket对象加入监听列表,后面还要动态添加一些conn连接对象,当accept的时候sk就有感应,当recv的时候conn就有动静
rlist=[server_tcp, server_udp]
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_tcp:
# 接收客户端的连接, 获取客户端对象和客户端地址信息
conn,addr=sock.accept()
#把新的客户端连接加入到监听列表中,当客户端的连接有接收消息的时候,select将被触发,会知道这个连接有动静,有消息,那么返回给rl这个返回值列表里面。
rlist.append(conn)
# elif sock == server_udp:
# data, addr = socket.recvfrom(1024)
else:
# 由于客户端连接进来时socket接收客户端连接请求,将客户端连接加入到了监听列表中(rlist),客户端发送消息的时候这个连接将触发
# 所以判断是否是客户端连接对象触发
try:
data, ad = "", ""
if sock == server_tcp:
data=sock.recv(1024)
else:
data, ad = sock.recvfrom(1024)
#没有数据的时候,我们将这个连接关闭掉,并从监听列表中移除
if not data:
sock.close()
rlist.remove(sock)
continue
print("received {0} from client {1}".format(data.decode(), sock))
#将接受到的客户端的消息保存下来
# rdata[sock] = data.decode()
# rdata["addr"] = ad if sock == server_udp else ""
#将客户端连接对象和这个对象接收到的消息加工成返回消息,并添加到wdata这个字典里面
wdata[sock]=[data.upper(), ad if sock == server_udp else ""]
#需要给这个客户端回复消息的时候,我们将这个连接添加到wlist写监听列表中
wlist.append(sock)
#如果这个连接出错了,客户端暴力断开了(注意,我还没有接收他的消息,或者接收他的消息的过程中出错了)
except Exception:
#关闭这个连接
sock.close()
#在监听列表中将他移除,因为不管什么原因,它毕竟是断开了,没必要再监听它了
rlist.remove(sock)
# 如果现在没有客户端请求连接,也没有客户端发送消息时,开始对发送消息列表进行处理,是否需要发送消息
for sock in wl:
if sock.type == SOCK_STREAM:
length = sock.send(wdata[sock][0])
# print(f"tcp 连接长度 {length}")
wlist.remove(sock)
wdata.pop(sock)
else:
sock.sendto(wdata[sock][0], wdata[sock][1])
wlist.remove(sock)
wdata.pop(sock)
# #将一次select监听列表中有接收数据的conn对象所接收到的消息打印一下
# for k,v in rdata.items():
# print(k,'发来的消息是:',v)
# #清空接收到的消息
# rdata.clear()
udp客户端双工
import random
import socket
import time
address = ("127.0.0.1", 5095) # 服务端地址和端口
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# s.bind(address)
while True:
trigger = ''.join(random.sample(
['z', 'y', 'x', 'w', 'v', 'u', 't', 's', 'r', 'q', 'p', 'o', 'n', 'm', 'l', 'k', 'j', 'i', 'h', 'g', 'f', 'e',
'd', 'c', 'b', 'a'], 15))
s.sendto(trigger.encode(), address)
print(trigger)
data, addr = s.recvfrom(1024) # 返回数据和接入连接的(服务端)地址
data = data.decode()
print('[Recieved]', data)
# if trigger == '###': # 自定义结束字符串
# break
time.sleep(1)
s.close()
tcp客户端双工
import random
import time
from socket import *
client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8093))
while True:
msg=''.join(random.sample(
['z', 'y', 'x', 'w', 'v', 'u', 't', 's', 'r', 'q', 'p', 'o', 'n', 'm', 'l', 'k', 'j', 'i', 'h', 'g', 'f', 'e',
'd', 'c', 'b', 'a'], 20))
if not msg:continue
client.send(msg.encode('utf-8'))
data=client.recv(1024)
print('[Recieved]', data.decode('utf-8'))
time.sleep(0.3)
client.close()
poll模式
poll在1986年诞生于System V Release 3,它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。
poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
另外,select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()和poll()的时候将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。
总结
poll和select相比,只是没有FD的限制,其他基本一样
epoll模式
直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll,它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
epoll可以同时支持水平触发(Level Triggered)和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发,注意边缘触发并不是将事件丢弃,而是在下一次新的事件到来时将旧的未处理的事件一并带过来,大概理解为啥叫边缘触发了),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。
在水平触发下,如果我们没有采取行动,再次调用sellect、poll、epoll时它们会直接返回,因为未处理事件依然会上报。
epoll LT和ET模式的区别
1、epoll有EPOLLLT和EPOLLET两种触发模式,LT(水平触发)默认模式,ET(边缘触发)是"高速"模式
2、LT模式下,只要这个FD还有数据可读,每次epoll_wait都会返回他的事件,提醒用户程序去操作
3、ET模式下,他只会提示一次,知道下次再有数据流入之前都不会再提示,无论FD中是否还有数据可读。所以在ET模式下,read一个FD的时候一定要把它的buffer读完,或者遇到EAGAIN错误
从哪里弄的代码,自己验证,笔者没有自己写过
#!/usr/bin/env python
import select
import socket
response = b''
serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serversocket.bind(('0.0.0.0', 8080))
serversocket.listen(1)
# 因为socket默认是阻塞的,所以需要使用非阻塞(异步)模式。
serversocket.setblocking(0)
# 创建一个epoll对象
epoll = select.epoll()
# 在服务端socket上面注册对读event的关注。一个读event随时会触发服务端socket去接收一个socket连接
epoll.register(serversocket.fileno(), select.EPOLLIN)
try:
# 字典connections映射文件描述符(整数)到其相应的网络连接对象
connections = {}
requests = {}
responses = {}
while True:
# 查询epoll对象,看是否有任何关注的event被触发。参数“1”表示,我们会等待1秒来看是否有event发生。
# 如果有任何我们感兴趣的event发生在这次查询之前,这个查询就会带着这些event的列表立即返回
events = epoll.poll(1)
# event作为一个序列(fileno,event code)的元组返回。fileno是文件描述符的代名词,始终是一个整数。
for fileno, event in events:
# 如果是服务端产生event,表示有一个新的连接进来
if fileno == serversocket.fileno():
connection, address = serversocket.accept()
print('client connected:', address)
# 设置新的socket为非阻塞模式
connection.setblocking(0)
# 为新的socket注册对读(EPOLLIN)event的关注
epoll.register(connection.fileno(), select.EPOLLIN)
connections[connection.fileno()] = connection
# 初始化接收的数据
requests[connection.fileno()] = b''
# 如果发生一个读event,就读取从客户端发送过来的新数据
elif event & select.EPOLLIN:
print("------recvdata---------")
# 接收客户端发送过来的数据
requests[fileno] += connections[fileno].recv(1024)
# 如果客户端退出,关闭客户端连接,取消所有的读和写监听
if not requests[fileno]:
connections[fileno].close()
# 删除connections字典中的监听对象
del connections[fileno]
# 删除接收数据字典对应的句柄对象
del requests[connections[fileno]]
print(connections, requests)
epoll.modify(fileno, 0)
else:
# 一旦完成请求已收到,就注销对读event的关注,注册对写(EPOLLOUT)event的关注。写event发生的时候,会回复数据给客户端
epoll.modify(fileno, select.EPOLLOUT)
# 打印完整的请求,证明虽然与客户端的通信是交错进行的,但数据可以作为一个整体来组装和处理
print('-' * 40 + '\n' + requests[fileno].decode())
# 如果一个写event在一个客户端socket上面发生,它会接受新的数据以便发送到客户端
elif event & select.EPOLLOUT:
print("-------send data---------")
# 每次发送一部分响应数据,直到完整的响应数据都已经发送给操作系统等待传输给客户端
byteswritten = connections[fileno].send(requests[fileno])
requests[fileno] = requests[fileno][byteswritten:]
if len(requests[fileno]) == 0:
# 一旦完整的响应数据发送完成,就不再关注写event
epoll.modify(fileno, select.EPOLLIN)
# HUP(挂起)event表明客户端socket已经断开(即关闭),所以服务端也需要关闭。
# 没有必要注册对HUP event的关注。在socket上面,它们总是会被epoll对象注册
elif event & select.EPOLLHUP:
print("end hup------")
# 注销对此socket连接的关注
epoll.unregister(fileno)
# 关闭socket连接
connections[fileno].close()
del connections[fileno]
finally:
# 打开的socket连接不需要关闭,因为Python会在程序结束的时候关闭。这里显式关闭是一个好的代码习惯
epoll.unregister(serversocket.fileno())
epoll.close()
serversocket.close()
select poll epoll的区别
图例 嘻嘻嘻
select | poll | epoll | |
---|---|---|---|
数据结构 | 数组 | 链表 | 红黑树 |
最大连接数 | 1024 | 无上限 | 无上限 |
fd拷贝 | 每次调用select拷贝 | 每次调用poll拷贝 | df首次调用epoll_ctl拷贝,每次调用epoll_wait不拷贝 |
工作效率 | 轮询O(n) | 轮询O(n) | 回调 O(1) |
from _socket import *
from select import *
from socket import socket
sc = socket(family=AF_INET, type=SOCK_STREAM, proto=0)
sc.bind(("127.0.0.1",8888))
sc.listen(5)
rlist=[sc]
while True:
rs,ws,xs=select(rlist,[],[])
print(rs)
c,addr = sc.accept() #把处理套接字的accept注释掉试试,每次循环调用到select时都会直接返回,因未处理事件一直在上报(rs里面一直有数据)。
# print("Connect from",addr)
epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。
socketserver 实现tcp、udp
socketserver实现UDP服务端,支持并发
#udp服务端多线程
import socketserver
class MyThreadingUDPServer(socketserver.ThreadingUDPServer):
# allow_reuse_address = True
def __init__(self, *args, **kwargs):
self.allow_reuse_address = True #端口复用,即多个socket可以使用同一个端口
super(MyThreadingUDPServer, self).__init__(*args, **kwargs)
class MyUdphandler(socketserver.DatagramRequestHandler):
def handle(self):
data,sock=self.request
print(data, self.client_address)
sock.sendto(data.upper(),self.client_address)
if __name__ == '__main__':
server=socketserver.ThreadingUDPServer(('',8081),MyUdphandler)
# server=MyThreadingUDPServer(('',8081),MyUdphandler) 端口复用,即多个socket可以使用同一个端口,重写父类的init方法
server.serve_forever()
"""
这里要注意的是:
一旦在项目中使用此服务,
server.server_foreve()就会阻塞他之后的程序的运行
比如说在其后print(12312)
那么真正运行起来不会打印,因为被阻塞了,解决此问题,可以将其放到线程里面启动
code:
class MyThread(threading.Thread):
def __init__(self, port=None):
self.port = port
super(MyThread, self).__init__()
def run(self):
try:
server = socketserver.ThreadingUDPServer(('', self.port), MyUDPHandler)
server.serve_forever()
except Exception as e:
sys_logger.info(traceback.format_exc())
"""
socket UDP客户端
import socket, random, time
updclient =socket.socket(socket.AF_INET,socket.SOCK_DGRAM) #买手机
server_ip_port =('192.168.220.255',8081) #找到服务端软件
while True:#通信循环
msg = ''.join(random.sample(
['z', 'y', 'x', 'w', 'v', 'u', 't', 's', 'r', 'q', 'p', 'o', 'n', 'm', 'l', 'k', 'j', 'i', 'h', 'g', 'f', 'e',
'd', 'c', 'b', 'a'], 15))
updclient.sendto(msg.encode('utf-8'),server_ip_port) #发送信息,信息量,服务端地址
data,server_ip =updclient.recvfrom(1024) # 接收信息
print(data.decode('utf-8'))
time.sleep(1)
socketserver实现TCP服务端,支持并发
import socketserver
class myTcp(socketserver.StreamRequestHandler):
def handle(self):
while True:
data = self.request.recv(1024)
print("接收到数据:" + data.decode("UTF-8"))
self.request.sendall(data.upper())
def setup(self):
print("before handle,连接建立:",self.client_address)
def finish(self):
print("finish run after handle")
if __name__ == "__main__":
server = socketserver.ThreadingTCPServer(("", 8081), myTcp)
server.serve_forever()
socket TCP客户端
import socket, random, time
sc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sc.connect(("192.168.220.62", 8081))
while True:
msg = ''.join(random.sample(
['z', 'y', 'x', 'w', 'v', 'u', 't', 's', 'r', 'q', 'p', 'o', 'n', 'm', 'l', 'k', 'j', 'i', 'h', 'g', 'f', 'e',
'd', 'c', 'b', 'a'], 15))
sc.sendall(msg.encode("UTF-8"))
msg = sc.recv(1024)
print("服务器回应:" + msg.decode("UTF-8"))
time.sleep(1)
最后我们在瞄一下源码:
class BaseRequestHandler:
"""Base class for request handler classes.
This class is instantiated for each request to be handled. The
constructor sets the instance variables request, client_address
and server, and then calls the handle() method. To implement a
specific service, all you need to do is to derive a class which
defines a handle() method.
The handle() method can find the request as self.request, the
client address as self.client_address, and the server (in case it
needs access to per-server information) as self.server. Since a
separate instance is created for each request, the handle() method
can define other arbitrary instance variables.
"""
def __init__(self, request, client_address, server):
self.request = request
self.client_address = client_address
self.server = server
self.setup()
try:
self.handle()
finally:
self.finish()
def setup(self):
pass
def handle(self):
pass
def finish(self):
pass
所有的和socketserver有关的的派生类全部都继承自BaseRequestHandler。所有的连接先经过init初始化。
重写handle方法:具体处理传输的数据
setup:是连接进来走handle方法之前调用的,源码理由自己瞄一眼啊。可以再走数据处理之前先处理一波关于连接的东西,具体自己看需求
finish:是连接进来走handle方法之后调用的,源码理由自己瞄一眼啊。可以再走数据处理之后先处理一波关于连接的东西,具体自己看需求 比如什么时候关掉连接的打印日志,方便纠错。
过程中碰到使用select模式或者socketserver存在内存泄漏的问题,让我们先看一下源码
socketserver.ThreadingUDPServer的源码:
class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass
继承ThreadingMixIn, UDPServer,在看UDPServer源码:
class UDPServer(TCPServer):
又是继承在看TCPServer:
class TCPServer(BaseServer):
在看BaseServer:
def serve_forever(self, poll_interval=0.5):
"""Handle one request at a time until shutdown.
Polls for shutdown every poll_interval seconds. Ignores
self.timeout. If you need to do periodic tasks, do them in
another thread.
"""
self.__is_shut_down.clear()
try:
# XXX: Consider using another file descriptor or connecting to the
# socket to wake this up instead of polling. Polling reduces our
# responsiveness to a shutdown request and wastes cpu at all other
# times.
with _ServerSelector() as selector:
selector.register(self, selectors.EVENT_READ)
while not self.__shutdown_request:
ready = selector.select(poll_interval)
# bpo-35017: shutdown() called during select(), exit immediately.
if self.__shutdown_request:
break
if ready:
self._handle_request_noblock()
self.service_actions()
finally:
self.__shutdown_request = False
self.__is_shut_down.set()
这个方法有点东西,可以看出还是每个udp会走select模式:
with _ServerSelector() as selector:
selector.register(self, selectors.EVENT_READ)
if hasattr(selectors, 'PollSelector'):
_ServerSelector = selectors.PollSelector
else:
_ServerSelector = selectors.SelectSelector
根据系统选择是走select(windows)还是epoll(unix)
但是select多路复用会出现内存泄漏的问题,具体原因还没找到。
解决方案:重写udp连接,不用select模式
重写BaseServer类继承