
本文详解如何解决基于python原生socket实现的http服务器中请求“卡住不读取”的问题,核心在于将阻塞式accept()改为非阻塞i/o模型,避免后续请求在操作系统队列中长时间滞留。
本文详解如何解决基于python原生socket实现的http服务器中请求“卡住不读取”的问题,核心在于将阻塞式accept()改为非阻塞i/o模型,避免后续请求在操作系统队列中长时间滞留。
在使用 Python socket 模块手写简易 HTTP 服务器时,一个常见但容易被忽视的问题是:多个并发请求(如浏览器加载 HTML、CSS、favicon.ico)无法被及时响应,部分请求看似“丢失”,实则被阻塞在系统连接队列中,直到新连接触发 accept() 才被取出处理。
根本原因在于原始代码中 serverSocket.accept() 是完全阻塞调用——服务器一次只能处理一个连接,且必须等当前请求完整处理完毕、handle_request() 返回后,才会回到 accept() 等待下一个连接。而现代浏览器(如 Firefox)会复用 TCP 连接或并行发起多个短连接(HTTP/1.1 默认流水线受限,但资源加载天然并发),导致:
- 第 1 个连接(
/)被接受 → 处理 → 返回; - 第 2 个连接(
/assets/css/main.css)被接受 → 处理 → 返回; - 此时第 3 个连接(
/assets/favicon.ico)已由内核接收并放入已完成连接队列(completed connection queue),但服务器尚未调用accept(),因此该连接“静默等待”; - 直到你手动执行
curl触发一次新accept(),队列中的 favicon 请求才被取出处理。
这并非 socket 数据丢失,而是单线程同步模型的固有瓶颈。
✅ 正确解法:使用 select 实现非阻塞多路复用
通过 select.select(),服务器可同时监听服务端 socket(等待新连接)和已建立的客户端 socket(等待数据到达),无需阻塞等待任一事件:
import socket
import select
def handle_request(client_socket):
# 注意:此处需适配非阻塞 socket —— 不再使用 makefile()
# 因为 makefile() 在非阻塞模式下可能抛出 BlockingIOError
try:
# 一次性尝试读取最多 4096 字节(实际应循环读取直至双换行)
data = client_socket.recv(4096)
if not data:
return False
# 简化示例:仅解析首行(真实场景需完整解析 HTTP 报文)
request_line = data.split(b'\r\n')[0].decode('utf-8') if b'\r\n' in data else ''
print(f"Request: {request_line}")
# 构造简单响应
response = b"HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World!"
client_socket.sendall(response)
return True
except (ConnectionResetError, BrokenPipeError, OSError) as e:
print(f"Client error: {e}")
return False
except Exception as e:
print(f"Unexpected error: {e}")
return False
def run_server(port=9999):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind(("localhost", port))
server_socket.listen(5)
server_socket.setblocking(False) # 关键:设为非阻塞
connections = [] # 存储活跃客户端 socket
print(f"Server listening on http://localhost:{port}")
while True:
# select 监听 server_socket(新连接)和所有已连接 client_socket(数据到达)
readable, _, _ = select.select([server_socket] + connections, [], [], 1.0)
for sock in readable:
if sock is server_socket:
# 接受新连接
try:
client_socket, addr = server_socket.accept()
client_socket.setblocking(False)
connections.append(client_socket)
print(f"New connection from {addr}")
except OSError as e:
# 队列为空或资源不足时 accept 可能失败(非阻塞模式下正常)
continue
else:
# 处理客户端请求
if not handle_request(sock):
# 客户端断开或处理失败,清理连接
try:
sock.close()
except:
pass
if sock in connections:
connections.remove(sock)
⚠️ 重要注意事项
-
makefile()不适用于非阻塞 socket:原代码中clientSocket.makefile()依赖阻塞 I/O 行为,在非阻塞模式下会频繁抛出BlockingIOError。应改用recv()并自行处理缓冲与协议解析。 -
HTTP 解析需健壮性:真实场景中必须按 RFC 7230 完整解析请求行、头部、空行及消息体长度(
Content-Length或Transfer-Encoding: chunked),不能仅靠换行计数。 -
连接管理需异常防护:客户端可能意外断开,
recv()返回空字节表示关闭,sendall()可能触发BrokenPipeError,务必捕获并清理连接。 -
性能与扩展性:
select适用于百级连接;万级连接建议升级至epoll(Linux)或kqueue(macOS/BSD),或直接使用asyncio。
✅ 总结
请求“延迟出现”本质是单线程阻塞模型与并发客户端之间的不匹配。通过 select + 非阻塞 socket,服务器从“串行等待”转变为“事件驱动轮询”,真正实现对多个连接的公平、及时响应。这是构建可靠网络服务的基础一步,也为后续引入线程池、异步框架(如 aiohttp)打下坚实理解基础。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











