朕喜技术(ZhenxiJS)
2026.09.08
1 分钟阅读

Python 单线程阻塞式 TCP 服务器并发缺陷:Slowloris 式卡死与 selectors 修复

关键词:TCP服务器 / socket并发 / 阻塞recv / selectors多路复用 / Slowloris / 单线程服务器 / 慢连接 / 并发缺陷 / Python网络编程

问题背景

写 TCP 服务端时常见的一个坑:使用「单线程 + 阻塞 recv」结构,看起来能收能回,但一旦有客户端连上却不发送数据,整个服务器就被卡死在 recv 上,再也无法 accept 新的连接。

这类缺陷正是 Slowloris(慢连接攻击) 能轻松拖垮服务器的原理——攻击者只建立连接但不发送数据,让服务器的 recv() 永远阻塞,所有合法用户全部超时。

技术栈:Python 3, socket, selectors, TCP, 多线程, 非阻塞 I/O。

复现步骤

git checkout 2026-09-08-1600
cd scenario/2026-09-08-1600
docker compose up -d --build
docker compose exec app bash

# 容器内:
python3 server/single_thread_server.py --port 12345 &
python3 client/demo_client.py --port 12345 --hold-ms 1500 --timeout 2.0

复现现象:conn#2 发送 hello 后等待响应超时(2002ms),报 socket.timeout。

[demo] conn#2 等响应超时 (2002ms)  → 服务器被 conn#1 卡死,无法并发
[demo] 结果: FAIL (bug reproduced)

根本原因

single_thread_server.py 的主循环中:

while True:
    conn, addr = srv.accept()        # 阻塞等新连接
    data = conn.recv(4096)           # ← 阻塞读,读完才回到 accept
    conn.sendall(b'PONG:' + data)
    conn.close()

recv() 是阻塞调用,若客户端不发数据,它会一直阻塞。于是:

  • 服务器接受了 conn#1 后,就卡在 conn#1 的 recv() 上;
  • conn#2 虽然能 connect 成功(内核 backlog 先缓存),但永远等不到 accept 和响应;
  • 整个服务只有 1 个连接被服务,其余全部卡死/超时。

解决办法

三种方案任选其一,原理都是让服务器不要长时间阻塞在单个连接的 recv 上:

方案 B:selectors 多路复用(推荐,单线程高效)

import socket, selectors

srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.bind(('127.0.0.1', 12347))
srv.listen(128)
srv.setblocking(False)   # 非阻塞是关键

sel = selectors.DefaultSelector()
sel.register(srv, selectors.EVENT_READ, on_accept)

def on_accept(sel, srv):
    conn, addr = srv.accept()
    conn.setblocking(False)
    sel.register(conn, selectors.EVENT_READ, on_read)

def on_read(sel, conn):
    data = conn.recv(65536)
    if data:
        conn.sendall(b'PONG:' + data)
    else:
        sel.unregister(conn)
        conn.close()

while True:
    events = sel.select()           # 只等有可读事件,绝不空转
    for key, _ in events:
        key.data(sel, key.fileobj)

方案 A:多线程

concurrent_server.py 中对每个连接 threading.Thread(target=handle, args=(conn, addr), daemon=True).start()。

方案 C:recv 超时

conn.settimeout(3) 给 recv 设超时,超时后关闭该连接或回到 accept。

验证

用 selectors 版服务器(或官方多线程版)替换后运行同样的 demo_client.py:

python3 server/selectors_server.py --port 12347 &
python3 client/demo_client.py --port 12347 --hold-ms 1500 --timeout 2.0

输出:

[demo] conn#2 收到响应: b'PONG:hello'  耗时 0ms  → 服务器能并发处理
[demo] 结果: PASS (concurrent)

修复前后对比:

服务器conn#1 空占conn#2 表现
单线程阻塞1500ms超时(2002ms)
多线程/selectors1500ms0ms 秒回

经验与避坑

  1. "单线程 + 阻塞 recv"是新手写网络服务最常见的并发缺陷。只要代码里有 recv() 紧跟 accept() 且中间没有非阻塞/超时/多路复用,就存在被慢连接拖死的风险。
  2. connect 成功不代表服务器在处理你。内核 backlog 会先缓存连接,客户端 connect 能成功但后续 recv 等不到响应,说明服务器已被卡死。
  3. selectors 是 Python 标准库推荐的 I/O 多路复用方案。相比 select,接口更简洁,自动选择 epoll/kqueue。注意非阻塞 socket 的 recv 可能抛 BlockingIOError,需 try/except 捕获。
  4. Slowloris 攻击防护:在生产环境中应同时设置 SO_RCVTIMEO、最大连接数、最大 header/body 大小等限制,不依赖单线程阻塞模型。
  5. docker-compose 构建坑:dockerfile 路径是相对于 context 解析的,不要写相对仓库根的 ../.. 前缀。另外 node:22-bookworm-slim 的 pip 需要加 --break-system-packages 才能安装包。

版权声明

本文为博主原创文章,遵循 CC BY-NC-SA 4.0 许可协议。 转载请注明出处:http://localhost/blogs/2026-09-08-1600-tcp

分享这篇文章