在并发场景中,如果使用线程池,或多进程进行并发处理,会占用过多的系统资源。这时可以考虑采用操作系统的 select 技术来处理。
select 能监听的socket文件描述符,监听的socoket产生事件,即会自动执行相应处理函数,如接送或发送。 当soket的客户端数量在1024以下,使用select是很合适的。但如果链接客户端过多,select采用的是轮询模型,服务器响应效率不高。应该采用epoll,或者用asyncio异步编程。
Python中有2个模块:select模块与selector模块,selector是高阶API,建议使用它。
两个模块提供了:select、poll、epoll三个方法,分别调用系统的select,poll,epoll 从而实现I/O多路复用。但注意,只有Linux支持epoll
Python 的select 模块是基于操作系统的event事件来实现的。
对于socket来说,它可以监控socket连接的readable, writable, error事件,对应地,select监视3个列表(read, write, error),当所监控的某个socket connection有事件发生,会添加到相应监控列表。比如,对于write list,当监控到某个连接的writable 事件后,即向该连接发送(write)数据.
python还提供了1个selectors 模块,其基于select模块,提供高阶编程接口。
服务端示例
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)
selectors 模块是在 select 模块原型的基础上进行封装,建议使用此模块。
该模块提供了对 select() 和 poll() 函数的访问,这两个函数对大多数操作系统中是可用的。只有Linux版本支持epoll(). windows上只支持 select()方法
selector类的结构
BaseSelector
BaseSelector类,支持在多个文件对象上等待 I/O 事件就绪。支持文件流(socket也是file stream)注册、注销,以及在这些流上等待 I/O 事件。但它是一个抽象基类,因此不能被实例化。实例化时请改用 DefaultSelector,或者 SelectSelector, KqueueSelector 等。
基本方法
等待直到有已注册的文件对象就绪. 返回由 (key, events) 元组构成的列表,每项各表示一个就绪的文件对象。
key的类型selectors.SelectorKey.
events只有两个可选值:两个event
其它类:
class selectors.SelectorKey
selector 用其返回文件对象等。属性有:
class selectors.SelectSelector
基于 select.select() 的选择器。
class selectors.PollSelector
基于 select.poll() 的选择器。
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 参数
while True:
events = sel.select() # 收集socket状态事件
for key, mask in events:
callback = key.data # data是register()传过来的
callback(key.fileobj, mask) # 实际为 accept(), 或read()
简单示例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.")