直接用 asyncio.start_server 做端口转发容易丢数据,因 streamreader.read() 不保证读完所有可用字节,tcp 流式特性导致粘包或截断;需大缓冲读取、await drain()、双向并发转发及严格 eof 处理。

为什么直接用 asyncio.start_server 做端口转发容易丢数据?
因为默认的 asyncio.StreamReader.read() 不保证一次读完所有可用字节,而 TCP 是流式协议——对端可能分多次发来一个完整包,或一次发多个包。如果转发逻辑里没做缓冲和边界处理,就会出现粘包、截断,甚至连接意外关闭。
实操建议:
- 永远用
reader.read(65536)(大缓冲)而非readline()或readexactly(n),除非你明确控制了协议格式 - 转发时用
await writer.drain()确保写入完成,否则背压会导致内存暴涨或任务被取消 - 必须同时处理两端的 EOF:一端 close,另一端也要主动
writer.close()并await writer.wait_closed()
如何正确中继两个 asyncio.StreamWriter 之间的数据?
不能简单地 while True: data = await reader.read(...); writer.write(data) —— 这会阻塞另一端的读取,且无法并发处理双向流量。
实操建议:
- 用
asyncio.create_task()启动两个协程,分别负责 client→target 和 target→client 的单向转发 - 每个协程内部用
try/except捕获ConnectionResetError和BrokenPipeError,并主动关闭对端 writer - 在任一方向出错后,用
asyncio.shield()保护另一方向的 cleanup,避免被 cancel 中断
示例核心片段:
async def pipe(reader, writer):
try:
while True:
data = await reader.read(8192)
if not data:
break
writer.write(data)
await writer.drain()
except (ConnectionResetError, BrokenPipeError):
pass
finally:
writer.close()
await writer.wait_closed()
asyncio.open_connection 连接超时和重试怎么设才合理?
默认没有超时,open_connection 会无限等待 DNS 解析和 TCP 握手,导致整个转发任务卡死。
实操建议:
- 用
asyncio.wait_for(asyncio.open_connection(...), timeout=5.0)包裹连接调用 - 不要在
start_server的 handler 里裸写重试逻辑;应把重试封装成独立协程,并限制最大次数(如 3 次)和退避间隔(如指数增长) - 注意:重试时要重新创建 reader/writer,不能复用已失败的 socket 对象
为什么本地测试时 localhost 转发失败但 127.0.0.1 可以?
某些系统(尤其是 macOS 和部分 Linux 发行版)对 localhost 域名做了 IPv6 优先解析,而目标服务只监听 IPv4,导致连接被拒绝或超时。
实操建议:
- 始终在
open_connection中显式指定host='127.0.0.1',而不是依赖 DNS 解析 - 若需支持 IPv6,应单独判断地址族,或使用
family=socket.AF_INET强制 IPv4 - 调试时加一行
print(f"Connecting to {host}:{port} ({type(writer.get_extra_info('socket')).__name__}")确认实际使用的协议栈
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











