第23章 Python:asyncio 实现,适合调试与脚本
学习目标
- 用 asyncio 写出 HTTP/CONNECT 代理,体会 Python 写代理的"最易读"
- 理解 asyncio 事件循环、
StreamReader/StreamWriter,及 GIL 对代理的影响 - 明白 Python 代理的定位:调试、原型、脚本、控制面,而非高吞吐数据面
- 知道何时该用 Python(mitmproxy/proxy.py),何时该换 Go/Rust
前置知识
- 第21章 Go、第22章 Rust(对照)、第04章 HTTP 代理协议
- 一点 Python
async/await
原理
Python 写代理的定位:可读性与开发速度优先
Python 写代理不是为了快,而是为了快速写出来、改起来爽。它的杀手锏是可读性和生态:
- 几十行就能写出可用代理,逻辑一目了然
- 改包/脚本化极方便——mitmproxy(第18章)本身就是 Python 写的,
proxy.py也是 - 适合:调试工具、协议原型、Mock、控制面、中低流量场景
asyncio:Python 的异步 IO
asyncio 是 Python 内置的事件循环框架,用 async/await 写非阻塞 IO:
asyncio.start_server(handler, host, port):起一个异步 TCP 服务StreamReader/StreamWriter:高层流式读写(await reader.read()/writer.write()+await writer.drain())asyncio.gather(...):并发跑多个协程(用来做双向转发)
GIL 的影响:代理恰好不太受伤
Python 有 GIL(全局解释器锁),同一时刻只有一个线程执行字节码,CPU 密集任务无法多核并行。但好消息是:
代理是 IO 密集型,大部分时间在等网络。asyncio 单线程事件循环在等待 IO 时切换协程,CPU 几乎不闲——所以 GIL 对纯转发型代理影响有限。
真正的瓶颈是:①解析/加解密等 CPU 工作(如 TLS、改包)会被 GIL 限制;②要吃满多核得上多进程(SO_REUSEPORT 多 worker,类似 Nginx)。所以 Python 适合中低流量;高吞吐数据面仍属 Go/Rust/C。
代码:HTTP/CONNECT 代理(asyncio)
一个支持明文 HTTP 转发 + CONNECT 隧道的正向代理,对照 第21章 的 Go 版:
#!/usr/bin/env python3
# http_proxy.py —— python3 http_proxy.py,监听 :8080
import asyncio
from urllib.parse import urlsplit
async def pipe(reader, writer):
"""单向转发:把 reader 的数据搬到 writer,直到 EOF"""
try:
while data := await reader.read(65536):
writer.write(data)
await writer.drain()
except Exception:
pass
finally:
writer.close()
async def handle(creader, cwriter):
# 读请求行:METHOD target VERSION
line = await creader.readline()
parts = line.decode("latin1").split()
if len(parts) < 3:
cwriter.close(); return
method, target = parts[0], parts[1]
# 读完剩余请求头
headers = b""
while True:
h = await creader.readline()
headers += h
if h in (b"\r\n", b"\n", b""):
break
if method == "CONNECT":
# 隧道:target 是 host:port(第04章 authority-form)
host, _, port = target.partition(":")
try:
sreader, swriter = await asyncio.open_connection(host, int(port or 443))
except Exception:
cwriter.write(b"HTTP/1.1 502 Bad Gateway\r\n\r\n")
await cwriter.drain(); cwriter.close(); return
cwriter.write(b"HTTP/1.1 200 Connection Established\r\n\r\n")
await cwriter.drain()
# 双向转发(对应 Go 的两个 io.Copy / Rust 的 copy_bidirectional)
await asyncio.gather(pipe(creader, swriter), pipe(sreader, cwriter))
else:
# 明文 HTTP:target 是 absolute-form(第04章),转成 origin-form 发后端
u = urlsplit(target)
host, port = u.hostname, u.port or 80
try:
sreader, swriter = await asyncio.open_connection(host, port)
except Exception:
cwriter.write(b"HTTP/1.1 502 Bad Gateway\r\n\r\n")
await cwriter.drain(); cwriter.close(); return
path = u.path or "/"
if u.query:
path += "?" + u.query
# 绝对 URI → 相对路径(第04章的代理转换)
swriter.write(f"{method} {path} HTTP/1.1\r\n".encode() + headers)
await swriter.drain()
await asyncio.gather(pipe(creader, swriter), pipe(sreader, cwriter))
async def main():
server = await asyncio.start_server(handle, "0.0.0.0", 8080)
print("HTTP 代理监听 127.0.0.1:8080")
async with server:
await server.serve_forever()
asyncio.run(main())
python3 http_proxy.py &
curl -x http://127.0.0.1:8080 http://httpbin.org/ip # 明文 HTTP → handle 的 else 分支
curl -x http://127.0.0.1:8080 https://example.com/ -s -o /dev/null -w "%{http_code}\n" # CONNECT → 200
对照三种语言:逻辑骨架完全一样(读请求→判 CONNECT→拨号→双向转发),Python 的
asyncio.gather(pipe, pipe)= Rust 的copy_bidirectional= Go 的两个go io.Copy。差别只在表达密度与运行时性能。
脚本化改包:Python 的主场
Python 的真正优势是"顺手改包"。给上面的 pipe 加几行就能篡改流量——这正是 mitmproxy 的能力来源(第18章):
async def pipe(reader, writer, transform=None):
while data := await reader.read(65536):
if transform:
data = transform(data) # 想改什么改什么,一行的事
writer.write(data)
await writer.drain()
writer.close()
灵活性与性能
| 维度 | Python + asyncio |
|---|---|
| 开发速度 | 最快,代码最短最易读 |
| 改包/脚本 | 最方便(mitmproxy/proxy.py 即证) |
| 吞吐/延迟 | 三种语言里最低(解释执行 + GIL) |
| 多核 | 需多进程(SO_REUSEPORT) |
| 适合 | 调试工具、原型、Mock、控制面、中低流量 |
| 不适合 | 高吞吐核心数据面 |
排错
| 现象 | 根因 | 解决 |
|---|---|---|
| 协程不跑 | 忘 await / 没进事件循环 | 用 asyncio.run 驱动、该 await 的 await |
| 整个服务卡死 | 在协程里调了阻塞函数(如同步 requests) | 用异步库,或 run_in_executor |
writer.write 不生效 | 没 await writer.drain() | 写后 drain,背压才正确 |
| 高负载下 CPU 单核打满 | GIL,单进程单核 | 多进程 + SO_REUSEPORT |
大量 ResourceWarning | writer 没 close | 转发结束成对 close |
本章小结
- Python + asyncio 写代理:最易读、最适合调试/原型/脚本,mitmproxy 即用它写就。
- 三语言骨架一致:
asyncio.gather(pipe,pipe)= Rustcopy_bidirectional= Go 双io.Copy。 - GIL 对纯转发型代理影响有限(IO 密集),但 CPU 工作受限、吃满多核要多进程。
- 定位:调试/控制面/中低流量用 Python,高吞吐数据面交给 Go/Rust/C。
下一章 第24章 C + epoll,下探到性能极限:用 C 裸写 epoll 事件循环 + splice 零拷贝,并对全篇四种语言做选型总结。