bigywater.tech
Flask · 78

Flask-SocketIO 长连接

Flask-SocketIO 的长连接到底是怎么建立的?服务端怎么知道客户端掉线了?多 worker 部署时广播为什么会丢?这篇用真实的 gunicorn、python-socketio 客户端和自己起的 redis,把握手包、断线时间和广播范围逐项打印出来。

1.87 s
客户端不再回应 ping 后(ping_interval 与 ping_timeout 各 1 秒),服务端触发 disconnect 的时间
5 → 7
2 个 worker 下广播收到的客户端数(最多 7):无 message_queue 与 redis message_queue
0 / 40 / 42 / 43
Engine.IO 与 Socket.IO 的 OPEN、CONNECT、EVENT、ACK 包类型

握手与数据包

实测 · 本机执行输出
  GET /socket.io/?EIO=4&transport=polling → 200 text/plain; charset=UTF-8
  正文: 0{"sid":"NQT5ExlZvCmWUVbuAAAA","upgrades":["websocket"],"pingTimeout":20000,"pingInterval":25000,"maxPayload":1000000}
  POST 40(Socket.IO 的 CONNECT 包)→ 200 'OK'
  GET 轮询取回 →  200 40{"sid":"htnndKup-mtkXqI1AAAB"}
  发带 ack 编号的事件 421["echo","hi"] 后,轮询依次取回: ['431[{"pid":63933,"echo":"hi"}]']
  用不存在的 sid 轮询 → 400 "Invalid session nope"

上一篇:Flask-Mail;下一篇:Flask-CORS。

先轮询再升级

实测 · 本机执行输出
  connect() 返回时(+103 ms)传输方式:websocket;升级后(+103 ms):websocket
  服务端事件日志: ['connect sid=eCieyrwTzJztgxPEAAAB auth=None transport=polling']
  只允许 websocket 时 transport = websocket
  只允许 polling 时 transport = polling (1.5 秒后仍是)

事件、ack 与拒绝连接

实测 · 本机执行输出
  call("echo", {a:1}) 的返回值(来自服务端处理函数的 return,即 ack): {'pid': 64066, 'echo': {'a': 1}}
  服务端 emit 回来的事件 joined: [{'room': 'r1', 'pid': 64066}]
  connect 处理函数 return False 时客户端: ConnectionError: One or more namespaces failed to connect: /
  服务端日志: ["connect sid=0Wz0BXaI7Lqg039wAAAB auth={'token': 'good'} transport=websocket", "connect sid=3IomF-AtMqiqGgcrAAAD auth={'token': 'bad'} transport=websocket"]

房间与广播

实测 · 本机执行输出
  A 向 r1 发 m1(include_self=True): {'A': ['m1'], 'B': ['m1'], 'C': []}
  A 向 r1 发 m2(include_self=False): {'A': [], 'B': ['m2'], 'C': []}
  A 广播 bcast(include_self=False): {'A': [], 'B': [('bcast', 'hello all')], 'C': [('bcast', 'hello all')]}

断线检测

实测 · 本机执行输出
  客户端主动 disconnect():服务端 disconnect 的 reason = ['transport close']
  kill -9 客户端进程(系统关掉 TCP 连接):服务端 0.01 秒后触发 disconnect,reason=transport close
  SIGSTOP 冻住客户端(进程还在、TCP 连接没关,只是不回应 ping):服务端 1.87 秒后触发 disconnect,reason=ping timeout

reason 的取值依赖库版本;这里只记录了 transport close 与 ping timeout 两种在本次实验里观察到的值。

多 worker 与 message_queue

实测 · 本机执行输出
  无 message_queue                            8 个客户端分布在 2 个 worker([2, 6]),A 广播(不含自己)后收到的客户端数:5(最多 7)
  message_queue=redis://127.0.0.1:9652       8 个客户端分布在 2 个 worker([1, 7]),A 广播(不含自己)后收到的客户端数:7(最多 7)

客户端用的都是 websocket-only 传输。轮询传输在多 worker 下需要粘性会话,这一点本篇没有验证。

polling 与 websocket 的延迟

实测 · 本机执行输出
  polling    中位数 1.30 ms,最大 1.8 ms
  websocket  中位数 0.28 ms,最大 2.1 ms

分步演示

Flask-SocketIO 长连接
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.7 / Flask 3.1.3 / Werkzeug 3.1.9 / Jinja2 3.1.6 / SQLAlchemy 2.1.1 / gunicorn 26.2.0(都装在自己的虚拟环境里);大多数实验用 Flask 的 test_client(直接调用 WSGI 应用,不经过网络),需要真实 HTTP 时另起 gunicorn 或开发服务器(端口 9600 以上,本机 127.0.0.1);计时用 time.perf_counter,数字每次运行会有小幅抖动。Flask-SocketIO 5.6.1、python-socketio 5.17.0(服务端与客户端)、python-engineio 4.14.0、simple-websocket 1.1.0、gunicorn gthread worker、自己起的 redis
    • 没有验证:eventlet/gevent 异步模式、轮询传输下多 worker 的粘性会话问题、nginx 反向代理下的 WebSocket 升级配置(见部署篇)、大量并发连接的资源消耗、浏览器端 socket.io-client、命名空间与房间的持久化
    京ICP备2026067502号-1京公网安备11010502063487号