PAYATHON 2026

Python asyncio WebSocket [WinError 121] 断网异常处理

数据小陈

结论

这是客户端断网引起的 WebSocket 连接异常关闭。服务器读取 socket 时,Windows 的 Proactor 事件循环可能返回 OSError: [WinError 121],随后 websockets 会把它包装成 ConnectionClosedError: no close frame received or sent。在连接处理函数里捕获 websockets.exceptions.ConnectionClosed,未处理异常就不会继续向上冒泡,也不会再打印 connection handler failed 堆栈。如果想更快发现半开连接,可以再设置 ping_interval 和 ping_timeout。

原因

  • 客户端断网时,可能没有发送 WebSocket close frame,也没有发出正常的 TCP FIN/RST。服务器只能等底层 socket 读失败,或者等心跳超时。
  • Windows 上,asyncio 默认使用 ProactorEventLoop。底层 overlapped I/O 失败时,会以 OSError: [WinError 121] 的形式暴露出来,websockets 接收流程再把它转换成 ConnectionClosedError。
  • async for message in websocket 内部会调用 recv()。如果关闭异常没有被捕获,它会从 echo 处理器里抛出来,服务器日志就会记录堆栈。

解决步骤

  1. 在 echo 处理器中,用 try/except 包住 async for 循环。
  2. 捕获 ConnectionClosed。正常关闭可以用 ConnectionClosedOK 记日志;异常断网通常表现为 ConnectionClosedError。
  3. 如果确实遇到底层 OSError 向外冒泡,可以额外捕获 OSError,并按连接断开处理。不要吞掉所有未知异常。
  4. 按业务需要配置 ping_interval 和 ping_timeout,让服务器更早发现客户端失联。这两个值只是示例,需要根据网络环境调整。

代码示例

#!/usr/bin/env python

import asyncio
import logging

from websockets.asyncio.server import serve
from websockets.exceptions import ConnectionClosed, ConnectionClosedError, ConnectionClosedOK

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

async def echo(websocket):
    try:
        async for message in websocket:
            await websocket.send(message)
    except ConnectionClosedOK:
        logger.info('客户端正常关闭连接: %s', websocket.remote_address)
    except ConnectionClosedError:
        logger.warning('客户端异常断开或断网: %s', websocket.remote_address)
    except ConnectionClosed:
        logger.warning('客户端连接已关闭: %s', websocket.remote_address)
    except OSError as exc:
        logger.warning('底层 socket 读取失败,按连接断开处理: %s', exc)
    except Exception:
        logger.exception('连接处理中出现未预期异常')

async def main():
    async with serve(
        echo,
        '0.0.0.0',
        8765,
        ping_interval=20,
        ping_timeout=20,
    ) as server:
        await server.serve_forever()

if __name__ == '__main__':
    asyncio.run(main())

注意事项

  • ConnectionClosedError 表示服务器已经检测到连接异常关闭,不是必须修复的服务器 bug。捕获并记录日志属于正常处理。
  • ping_interval 设为 None 会关闭自动心跳;设为正数时,失联检测时间会受 ping_interval、ping_timeout 和系统 TCP 超时共同影响。
  • 如果使用不同版本的 websockets,导入路径或参数可能不同,请以实际安装版本的 API 为准。

备注:内容仅供参考。