1、为什么选择 select技术处理高并发❓
采用select轮询技术处理高并发请求的优点
服务器处理并发网络请求场景中,如果使用线程池,或多进程进行并发处理,会占用过多的系统资源。而采用操作系统的 select 技术来处理并发请求,占用资源少,处理速度快。
select 能同时监听很多socket文件描述符,一旦监听到某个socoket产生了事件,即会自动执行相应处理函数,如从socket读取字节流,或向socket发送字节流。
Select技术的适用限制
select 非常适合存在高并发请求的项目,如物联网应用场景。
如果同时连接到soket服务的客户数量在1024以下时,使用select是很合适的。并发1024,实际能接受的请求/秒通常为数万,甚至更高,但如果连接的客户数量过多,由于select采用的是轮询模型,服务器响应效率不高。这种情况下,可考虑多进程+select 编程, Linux系统可以采用epoll模式, 当然也可以增加物理资源来解决。 Select应付不了的高并发场景,不建议使用Asyncio异步网络编程,同样会遇到1024文件描述符限制问题,而且处理速度不如select快,可靠性也不如select.
Python提供Select编程的两个内置模块
Python中有2个模块:select模块与selector模块,selector是高阶API,建议使用它。
两个模块提供了:select、poll、epoll三个方法,分别调用系统的select,poll,epoll 从而实现I/O多路复用。但注意,只有Linux支持epoll .
- Windows Python:提供: select
- Mac Python:提供: select
- Linux Python:提供: select、poll、epoll
2、Select 模块的使用
Python 的select 模块是基于操作系统的event事件来实现的。
对于socket来说,它可以监控socket连接的readable, writable, error事件,对应地,select监视3个列表(read, write, error)。
当所监控的某个socket connection有事件发生,会添加到相应监控列表。比如,对于write list,当监控到某个连接的writable 事件后,即向该连接发送(write)数据.
服务端代码示例
import select
import socket
def start_server(port):
HOST = '0.0.0.0'
PORT = port
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind((HOST, PORT)) # 套接字绑定的IP与端口
server.listen(10) # 开始TCP监听
inputs = [server] # 存放需要被检测可读的socket
outputs = [] # 存放需要被检测可写的socket
while inputs:
readable, writable, exceptional = select.select(inputs, outputs, inputs)
# 检查可读的端口
for s in readable:
if s is server: # 可读的是server,说明有连接进入
connection, client_address = s.accept()
inputs.append(connection) # 将新建立的soket加入input列表
else: # 客户请求socket
data = s.recv(1024) # 故意设置的这么小
if data:
# 从这个socket当中收到数据,
# 如果要发Response, 将其加入到outputs中, select模型将检查它是否可写
print(data.decode(encoding='utf-8'))
if s not in outputs:
outputs.append(s)
else:
# 收到为空的数据,意味着对方已经断开连接,需要做清理工作
if s in outputs:
outputs.remove(s)
inputs.remove(s)
s.close()
# 可写
for w in writable:
w.send('收到数据'.encode(encoding='utf-8'))
outputs.remove(w)
# 异常
for s in exceptional:
inputs.remove(s)
if s in outputs:
outputs.remove(s)
s.close()
if __name__ == '__main__':
start_server(8801)
客户端实现代码一
import os
import time
import socket
def start_client(addr, port):
PLC_ADDR = addr
PLC_PORT = port
s = socket.socket()
s.connect((PLC_ADDR, PLC_PORT))
count = 0
while True:
msg = '进程{pid}发送数据'.format(pid=os.getpid())
msg = msg.encode(encoding='utf-8')
s.send(msg)
recv_data = s.recv(1024)
print(recv_data.decode(encoding='utf-8'))
time.sleep(3)
count += 1
if count > 20:
break
s.close()
if __name__ == '__main__':
start_client('127.0.0.1', 8801)
客户端实现代码 – 线程池高并发请求
现在客户端使用ThreadPoolExecutor 异步线程池,同时发起10000个请求。
import os
import time
import socket
from concurrent.futures import ThreadPoolExecutor,as_completed, wait, ALL_COMPLETED
def start_client(addr: str="127.0.0.1", port:int=8801,n:int=0) -> None:
PLC_ADDR = addr
PLC_PORT = port
s = socket.socket()
s.connect((PLC_ADDR, PLC_PORT))
msg: str = '进程{pid}发送数据{d}'.format(pid=os.getpid(), d=n )
msg = msg.encode(encoding='utf-8')
s.send(msg)
recv_data = s.recv(1024)
#print(n, ": ", recv_data.decode(encoding='utf-8'))
s.close()
if __name__ == '__main__':
# 用5000个线程同时发起socket请求
t1 = time.time()
with ThreadPoolExecutor(max_workers=10000) as executor:
wait_for = [executor.submit(start_client,'127.0.0.1', 8801,i) for i in range(10000)]
wait(wait_for, return_when=ALL_COMPLETED)
t2 = time.time()
print(f"Total time: {t2-t1:.3f} seconds")
执行结果:
Total time: 2.912 seconds
可以看到,客户端在2.9秒内向服务器发起了10000次请求,在2.912秒内收到了全部响应,而服务器只使用了1个线程+select编程,就实现了对大量并发请求的处理。做为服务器的测试PC是台老旧电脑。
由此例可看出,Python select 在处理网络 I/O 的效率远高于用线程来处理socket 多连接,而且资源占用也非常低。
3、selectors 模块编程
selectors 模块是在 select 模块原型的基础上进行封装,建议使用此模块。
该模块提供了对 select() 和 poll() 函数的访问,这两个函数对大多数操作系统中是可用的。只有Linux版本支持epoll(). windows上只支持 select()方法
1) selector 数据主要数据结构
selector类的结构
BaseSelector
- SelectSelector
- PollSelector
- EpollSelector
- DevpollSelector
- KqueueSelector
BaseSelector类,支持在多个文件对象上等待 I/O 事件就绪。支持文件流(socket也是file stream)注册、注销,以及在这些流上等待 I/O 事件。但它是一个抽象基类,因此不能被实例化。实例化时请改用 DefaultSelector,或者 SelectSelector, KqueueSelector 等。
基本方法
- register(fileobj, events, data=None)
- unregister(fileobj)
- modify(fileobj, events, data=None)
- select(timeout=None)
等待直到有已注册的文件对象就绪. 返回由 (key, events) 元组构成的列表,每项各表示一个就绪的文件对象。
key的类型selectors.SelectorKey.
events只有两个可选值:两个event
- EVENT_READ 可读
- EVENT_WRITE 可写
其它类:
class selectors.SelectorKey
selector 用其返回文件对象等。属性有:
- fileobj 已注册的文件对象。
- fd 下层的文件描述符。
- events 必须在此文件对象上被等待的事件。
- data 可选的关联到此文件对象的不透明数据:例如,这可被用来存储各个客户端的会话 ID
class selectors.SelectSelector
基于 select.select() 的选择器。
class selectors.PollSelector
基于 select.poll() 的选择器。
2) Selector编程步骤
(1)创建socket:
host, port = sys.argv[1], int(sys.argv[2])
lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
lsock.bind((host, port))
lsock.listen()
print(f"Listening on {(host, port)}")
(2) socket必须是non-blocking mode. 以便允许其它连接读写
lsock.setblocking(False)
(3) 生成1个select对象,将socket注册到selector对象,
sel = selectors.DefaultSelector()
sel.register(lsock, selectors.EVENT_READ, data=None)
参数说明:
• fileobj : windows 只接受socket stream
• events : 只有EVENT_READ, 或 EVENT_WRITE
• data, 是传给select()方法结果里的 key.data
socket第1次register时,触发只读事件,向select()方法传递 data 参数
(4) 服务器进入监听循环
循环体内,首先用用 select()方法收集socket事件。
events = sel.select(timeout=None) 返回的是 (key, events) 类型的list,每个元素对应1个 ready状态的Socket Object.
key的类型为selectors.SelectorKey, 主要属性:
• fileobj
• fd 文件描述符
• data 这个数据是由register()中的参数data传入
while True:
events = sel.select() # 收集socket状态事件
for key, mask in events:
callback = key.data # data是register()传过来的
callback(key.fileobj, mask) # 实际为 accept(), 或read()
(5) 编写对读,写处理的函数(略)
简单示例1
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)
# 注册新进来的连接,状态为READ, data 为read.
sel.register(conn, selectors.EVENT_READ, read)
def read(conn, mask):
data = conn.recv(1000) # 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.close()
sock = socket.socket()
sock.bind(('localhost', 1234))
sock.listen(100)
sock.setblocking(False)
sel.register(sock, selectors.EVENT_READ, accept)
while True:
events = sel.select()
for key, mask in events:
callback = key.data
callback(key.fileobj, mask)
当然也可以不用callback
import selectors
import socket
# Set up the server
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("localhost", 7342))
server.listen()
# Set up the selectors "bag of sockets"
selector = selectors.DefaultSelector()
selector.register(server, selectors.EVENT_READ)
while True:
events = selector.select()
for key, _ in events:
sock = key.fileobj
print("About to accept.")
client, _ = sock.accept()
print("Accepted.")