select只适合小规模连接,因其fd_setsize限制(通常1024)、每次调用需全量拷贝fd集合且时间复杂度o(n),无法区分活跃与闲置fd,仅适用于百级以下并发场景。

select 为什么只适合小规模连接
因为 select 的文件描述符数量上限由系统 FD_SETSIZE 宏决定(通常为 1024),且每次调用都要把整个 fd 集合从用户态拷贝到内核态,时间复杂度是 O(n)。它不区分「活跃 fd」和「闲置 fd」,哪怕只有一个 socket 就绪,也要遍历全部检查。
所以别指望用 select 支撑几千并发——不是写不对,是设计上就扛不住。它适合教学、嵌入式或内部工具这类连接数稳定在百级以下的场景。
如何正确构造 read_fds、write_fds、except_fds 三个集合
这三个参数不是随便传个 list 就行:select.select() 要求传入可迭代对象,但内部会把它转成底层的 fd_set 结构,因此必须确保所有元素是**有效的、未关闭的整数型文件描述符**(比如 sock.fileno() 返回值)。
- 监听 socket 和已建立的 client socket 都要放进
read_fds,否则无法收到新连接或数据 - 只有当上一次
send()返回EAGAIN或EWOULDBLOCK时,才需要把该 socket 加入write_fds等待可写事件(否则默认不监听可写) -
except_fds实际很少用,TCP 场景下基本可传空列表[] - 每次调用前都得重新构建这三个集合——
select会修改它们,原集合不能复用
为什么不能直接 recv() 所有就绪 socket 而不检查返回值
就绪 ≠ 有数据可读。比如对端 close() 后,socket 会立即出现在 read_fds 中,此时 recv() 返回空字节 b'',必须主动 close() 并从监控集合中移除,否则下次循环还会被报告就绪,陷入忙等。
同样,如果 send() 没发完(比如缓冲区满),必须缓存剩余数据,并持续监听该 socket 的可写事件,直到全部发出——否则会丢数据。
常见错误现象:OSError: [Errno 9] Bad file descriptor,往往是因为 socket 已被 close 却还留在 fd 集合里;BlockingIOError 则常因忘记设 socket.setblocking(False)。
一个最小可行的 select 服务器骨架长什么样
核心逻辑就三步:初始化监听 socket → 进入循环调用 select.select() → 分别处理就绪的监听 fd 和 client fd。下面这段去掉注释就是能跑的主干:
import socket
import select
server = socket.socket()
server.setblocking(False)
server.bind(('localhost', 8080))
server.listen(5)
inputs = [server]
outputs = []
message_queues = {}
while inputs:
readable, writable, exceptional = select.select(inputs, outputs, inputs)
for s in readable:
if s is server:
conn, addr = server.accept()
conn.setblocking(False)
inputs.append(conn)
message_queues[conn] = []
else:
data = s.recv(1024)
if data:
message_queues[s].append(data)
if s not in outputs:
outputs.append(s)
else:
inputs.remove(s)
if s in outputs:
outputs.remove(s)
s.close()
del message_queues[s]
for s in writable:
if message_queues[s]:
sent = s.send(message_queues[s].pop(0))
if not message_queues[s]:
outputs.remove(s)
for s in exceptional:
inputs.remove(s)
if s in outputs:
outputs.remove(s)
s.close()
del message_queues[s]
注意 message_queues 是必需的——因为 select 不提供消息边界,也不缓存发送数据,应用层必须自己管好收发状态。这点容易被忽略,一上来就用 print() 代替队列,结果发一半卡住就再也恢复不了。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











