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处理器里抛出来,服务器日志就会记录堆栈。
解决步骤
- 在
echo处理器中,用try/except包住async for循环。 - 捕获
ConnectionClosed。正常关闭可以用ConnectionClosedOK记日志;异常断网通常表现为ConnectionClosedError。 - 如果确实遇到底层
OSError向外冒泡,可以额外捕获OSError,并按连接断开处理。不要吞掉所有未知异常。 - 按业务需要配置
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 为准。
备注:内容仅供参考。
