asyncio + aiohttp 不能直接做动态代理,因其是应用层http客户端,无法透传tls握手、丢弃原始请求头、不支持connect方法,必须用asyncio.open_connection实现tcp层字节级双向桥接。

为什么 asyncio + aiohttp 不能直接做动态代理
直接用 aiohttp.ClientSession 转发 HTTP 请求看似可行,但会丢掉原始请求头(比如 Host、Connection、Upgrade)、无法透传 TLS 握手、不支持 CONNECT 方法——这意味着 HTTPS 流量根本转不过去,浏览器会报 ERR_TUNNEL_CONNECTION_FAILED。动态代理不是“发个 GET 就完事”,它得在 TCP 层维持双向流,而 aiohttp 是应用层 HTTP 客户端,天生不处理底层 socket 透传。
必须用 asyncio.open_connection + socket-level relay
核心是绕过 HTTP 解析,把 client socket 和 upstream socket 做字节级双向桥接。关键点有三个:
- 收到客户端 CONNECT 请求后,先解析目标 host:port(如
example.com:443),再用asyncio.open_connection(host, port)建立上游连接 - 建立成功后返回
HTTP/1.1 200 Connection established,然后立即启动两个并发 task:asyncio.create_task(pipe(client_reader, upstream_writer))和asyncio.create_task(pipe(upstream_reader, client_writer)) -
pipe()函数必须用while True循环 +await reader.read(8192)拉取数据,不能用readuntil()或readline(),否则二进制流量(如 TLS record)会被截断
示例片段(简化版 pipe):
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, OSError):
pass
如何支持 HTTP/HTTPS + 自动协议识别
浏览器发起代理请求时,HTTP 明文走普通请求,HTTPS 先发 CONNECT。服务端不能假设所有连接都是 HTTPS,得根据首行判断:
SkillSub Pro - Python 题解与代码注释双功能技能功能概述SkillSub Pro - Python 题解与代码注释双功能技能是一项面向实际任务的技能,主要用于SkillSub Pro 是一个 Python 题解生成与代码注释的 双功能合体技能 ,专为学生、算法学习者和开发者设计;✅ 一个技能,两种用途 :;核心要点📝 题解模式 :输入题目/题号,自动生成完整 Python 题解(含详细注释、解题思路、复杂度分析);💬 注释模式 :输入 Python 代码,自动添加详细中。它将相关步骤、
- 若首行匹配
^CONNECT [^ ]+:[0-9]+ HTTP/→ 进入 CONNECT 处理流程 - 若首行匹配
^(GET|POST|HEAD|PUT|DELETE) .+ HTTP/→ 当作 HTTP 代理,需重写Host头并转发完整请求(含 body) - 其他情况(如 HTTP/2 preface、TLS ClientHello 前几个字节)→ 直接按 raw TCP 转发,不解析
注意:不要尝试读完整 HTTP header 再判断,因为 TLS ClientHello 可能紧贴在 CONNECT 后发送(尤其 Chrome),必须用 reader.read(1) 或 reader.peek(1) 配合超时做轻量试探。
实际部署时最常被忽略的坑
本地测试跑通不等于线上可用。真实环境里这几个点几乎必踩:
- Linux 默认每个进程最多 1024 个文件描述符,代理每条连接占 2 个 socket,100 并发就爆了 —— 必须调
ulimit -n 65536并在代码里设asyncio.get_event_loop().set_default_executor(ThreadPoolExecutor(max_workers=32)) - 客户端空闲连接不主动关闭,上游服务器可能发
FIN,但你的pipe()任务没监听reader.at_eof()就会卡住 —— 每次read()后都要检查if reader.at_eof(): break - 没有设置 socket timeout,某个坏连接 hang 死整个 task ——
asyncio.wait_for(reader.read(n), timeout=30)是底线 - Windows 上
ProactorEventLoop对socket.dup()支持差,建议显式用SelectorEventLoop
动态代理真正的复杂度不在“怎么转”,而在“怎么不崩”——连接生命周期管理、错误传播方向、buffer 大小与吞吐的平衡,这些细节没压测过就上线,扛不住真实浏览器行为。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










