Python 中的异步代理使用 aiohttp、httpx 等库高效管理多个并发网络请求,避免 I/O 操作阻塞主执行线程。
代理服务本质上是 I/O 密集型的,大部分运行时间都在等待上游服务器的网络响应或客户端请求。传统的同步(阻塞)I/O 模型每个线程一次只处理一个请求,导致资源利用率低下、可扩展性受限。基于 Python asyncio 框架的异步 I/O 则允许单个线程在等待 I/O 操作完成时切换上下文,从而管理大量并发连接。这种架构显著提升了代理的吞吐量和响应能力。
异步核心概念
Python 的 asyncio 库为异步编程提供了基础。关键要素包括:
- 事件循环(Event Loop): 核心组件,负责调度和执行协程,处理 I/O 事件和回调。
- 协程(
async def): 可以暂停和恢复的函数。使用async def定义,通过await执行。 await关键字: 用于暂停协程的执行,直到某个可等待对象(另一个协程、Future 或 Task)完成。这会把控制权交还给事件循环。
用 aiohttp 构建异步代理服务
aiohttp 是面向 asyncio 的异步 HTTP 客户端/服务器框架。它非常适合构建代理的入站(服务器)和出站(客户端)两个组件。
将 aiohttp 用作代理服务器
aiohttp.web 提供了构建 Web 服务器所需的工具,用于监听传入的客户端请求。
import aiohttp.web
async def handle_request(request):
"""
处理传入请求的示例 handler。
在真实的代理中,这里会转发请求。
"""
return aiohttp.web.Response(text=f"Received: {request.method} {request.url}")
async def main():
app = aiohttp.web.Application()
app.router.add_route('*', '/{path:.*}', handle_request) # 捕获所有路由
runner = aiohttp.web.AppRunner(app)
await runner.setup()
site = aiohttp.web.TCPSite(runner, '0.0.0.0', 8080)
await site.start()
print("aiohttp proxy server started on port 8080")
while True:
await asyncio.sleep(3600) # 保持服务器运行
if __name__ == '__main__':
import asyncio
asyncio.run(main())
将 aiohttp 用作异步 HTTP 客户端
aiohttp.ClientSession 用于发起出站 HTTP 请求,这是把客户端请求转发到上游服务器的关键。它负责管理连接池和 cookie。
import aiohttp
import asyncio
async def fetch_url(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
response.raise_for_status() # HTTP 错误时抛出异常
return await response.text()
async def example_client_usage():
content = await fetch_url('http://httpbin.org/get')
print(f"Fetched content: {content[:100]}...")
if __name__ == '__main__':
asyncio.run(example_client_usage())
用 httpx 构建异步代理服务
httpx 是一个现代、功能完备的 Python HTTP 客户端,同时提供同步和异步 API。其异步能力构建在 asyncio 之上。
将 httpx 用作异步 HTTP 客户端
httpx.AsyncClient 是发起异步请求的主要接口。它提供类似 requests 的 API,对熟悉 requests 库的开发者非常直观。
import httpx
import asyncio
async def fetch_url_httpx(url):
async with httpx.AsyncClient() as client:
response = await client.get(url)
response.raise_for_status() # HTTP 错误时抛出异常
return response.text
async def example_httpx_client_usage():
content = await fetch_url_httpx('http://httpbin.org/get')
print(f"Fetched content (httpx): {content[:100]}...")
if __name__ == '__main__':
asyncio.run(example_httpx_client_usage())
httpx 不提供服务器功能,它纯粹是一个客户端库。
aiohttp 与 httpx 客户端对比
| 特性 | aiohttp.ClientSession |
httpx.AsyncClient |
|---|---|---|
| 用途 | 异步 HTTP 客户端与服务器框架。 | 异步(及同步)HTTP 客户端。 |
| API 风格 | 更底层的 asyncio 集成,代码更冗长。 |
类 requests 的 API,通常更简洁。 |
| HTTP/2 支持 | 客户端无原生 HTTP/2 支持。 | 客户端原生支持 HTTP/2。 |
| HTTP/3(QUIC)支持 | 无。 | 通过 quic-go(Rust)提供实验性支持。 |
| WebSocket 客户端 | 支持。 | 不支持。 |
| 依赖 | multidict、yarl、async_timeout、attrs。 |
httpcore、idna、certifi、`sniffio(依赖极少)。 |
| 连接池 | 由 ClientSession 管理。 |
由 AsyncClient 管理。 |
| 重定向处理 | 自动,可配置。 | 自动,可配置。 |
| 流式响应 | 支持,使用 response.content.read()。 |
支持,使用 response.aiter_bytes()。 |
| 代理配置 | ClientSession 方法中直接使用 proxy 参数。 |
AsyncClient 和请求中直接使用 proxies 参数。 |
构建代理服务器时,需要 aiohttp 的服务器能力。至于出站客户端组件,两者都可行。httpx 通常提供更简单的 API 和内置的 HTTP/2 支持,这可能更有优势。
用 aiohttp(服务器)和 httpx(客户端)构建异步代理
该方案用 aiohttp 处理传入的代理请求,用 httpx 将其转发到目标服务器。这种组合通常能在服务器控制力与客户端简洁性/功能之间取得良好平衡。
import aiohttp.web
import httpx
import asyncio
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 只初始化一次 httpx.AsyncClient 以复用连接池
# 该客户端将用于所有出站请求
# 设置默认超时以避免连接挂起
OUTGOING_CLIENT = httpx.AsyncClient(timeout=30.0)
async def proxy_handler(request):
"""
处理传入的客户端请求,使用 httpx 转发,
并将响应返回给客户端。
"""
target_url = str(request.url).lstrip('/') # 去掉路径开头的斜杠
# 重建目标 URL,保留协议、主机和查询参数
# 在典型的正向代理中,客户端发送完整 URL(例如 GET http://example.com/path)
# 在反向代理中,服务器可能只拿到路径,因此需要一个基础 URL。
# 本示例假设为正向代理,完整 URL 位于路径中。
# 若为反向代理,则应在前面拼接固定的基础 URL:
# target_url = f"http://upstream.example.com{request.url.path_qs}"
# 提取请求头,排除 hop-by-hop 头和代理专用头
headers = {
k: v for k, v in request.headers.items()
if k.lower() not in ['host', 'connection', 'keep-alive', 'proxy-authenticate',
'proxy-authorization', 'te', 'trailers', 'transfer-encoding',
'upgrade', 'via', 'x-forwarded-for', 'x-real-ip']
}
# 如果尚未存在,则添加 X-Forwarded-For
client_ip = request.remote
if client_ip:
headers['X-Forwarded-For'] = headers.get('X-Forwarded-For', '') + (', ' if headers.get('X-Forwarded-For') else '') + client_ip
request_method = request.method
request_body = await request.read() if request_method in ('POST', 'PUT', 'PATCH') else None
logger.info(f"Proxying {request_method} {target_url} from {request.remote}")
try:
# 使用 httpx 转发请求
proxy_response = await OUTGOING_CLIENT.request(
method=request_method,
url=target_url,
headers=headers,
content=request_body,
params=request.query # 单独传递查询参数
)
proxy_response.raise_for_status() # 4xx/5xx 响应时抛出异常
# 为客户端准备响应
response_headers = {
k: v for k, v in proxy_response.headers.items()
if k.lower() not in ['content-encoding', 'transfer-encoding', 'connection'] # hop-by-hop 头
}
# 以流式方式回传响应体,避免把大响应全部载入内存
response = aiohttp.web.StreamResponse(status=proxy_response.status, headers=response_headers)
await response.prepare(request)
async for chunk in proxy_response.aiter_bytes():
await response.write(chunk)
await response.write_eof()
logger.info(f"Forwarded {request_method} {target_url} with status {proxy_response.status}")
return response
except httpx.HTTPStatusError as e:
logger.error(f"HTTP error proxying {target_url}: {e.response.status_code} - {e.response.text}")
return aiohttp.web.Response(
status=e.response.status_code,
text=f"Upstream HTTP Error: {e.response.status_code}\n{e.response.text}",
content_type="text/plain"
)
except httpx.RequestError as e:
logger.error(f"Network error proxying {target_url}: {e}")
return aiohttp.web.Response(
status=502, # Bad Gateway
text=f"Proxy Network Error: {e}",
content_type="text/plain"
)
except Exception as e:
logger.exception(f"Unexpected error in proxy_handler for {target_url}")
return aiohttp.web.Response(
status=500,
text=f"Proxy Internal Error: {e}",
content_type="text/plain"
)
async def start_proxy_server():
app = aiohttp.web.Application()
# 该路由处理所有方法和路径
# 在正向代理中,客户端请求形如:GET http://example.com/path
# aiohttp 会把路径解析为 '/http://example.com/path'
# 我们在 proxy_handler 中去掉开头的 '/' 以得到完整 URL。
app.router.add_route('*', '/{path:.*}', proxy_handler)
runner = aiohttp.web.AppRunner(app)
await runner.setup()
site = aiohttp.web.TCPSite(runner, '0.0.0.0', 8080)
await site.start()
logger.info("Asynchronous proxy server started on http://0.0.0.0:8080")
# 让服务器持续运行
try:
while True:
await asyncio.sleep(3600)
finally:
await OUTGOING_CLIENT.aclose() # 确保关闭 httpx 客户端
await runner.cleanup()
if __name__ == '__main__':
asyncio.run(start_proxy_server())
代理服务的实践要点
请求头管理
代理必须谨慎处理 HTTP 头。
* hop-by-hop 头(Connection、Keep-Alive、Proxy-Authenticate、Proxy-Authorization、TE、Trailer、Transfer-Encoding、Upgrade)仅适用于两个节点之间的连接,不应转发。
* X-Forwarded-For / X-Real-IP:把客户端的 IP 地址添加或追加到这些头中,以便上游服务器了解原始请求方。
* Via 头:可选择添加 Via 头,表明代理参与了转发。
请求体流式传输
对于较大的请求体或响应体,务必以流式方式传输数据,而不是全部载入内存。aiohttp(入站用 request.read()、出站用 response.write())和 httpx(通过 content 参数和 response.aiter_bytes())都支持流式传输,可避免内存耗尽并降低延迟。
连接池
aiohttp.ClientSession 和 httpx.AsyncClient 都实现了连接池。只实例化一次这些客户端并在多次请求间复用(如 OUTGOING_CLIENT 所示)对性能至关重要,这可以减少每次请求都建立新 TCP 连接的开销。
超时
代理服务容易受到上游服务器延迟或故障的影响。为出站请求设置严格的超时是避免资源枯竭、保持服务响应能力的关键。httpx.AsyncClient 和 aiohttp.ClientSession 都支持配置连接超时、读取超时和总超时。
错误处理与重试
必须对网络问题(例如连接被拒绝、DNS 错误)和 HTTP 错误(例如上游返回 5xx)做健壮的错误处理。针对瞬时错误实现带指数退避的重试机制,以提升可靠性。
WebSocket 代理
aiohttp 在服务器端原生支持 WebSocket(aiohttp.web.WebSocketResponse)。代理 WebSocket 需要处理 Upgrade 头,并在客户端、代理和目标 WebSocket 服务器之间建立双向数据流。httpx 不支持 WebSocket 客户端连接。
性能调优
uvloop:对于aiohttp应用,安装uvloop(用 Cython 编写、可直接替换asyncio事件循环)可显著提升性能。- 操作系统限制:高并发代理服务通常需要调整操作系统的打开文件描述符上限(
ulimit -n)。 - 资源监控:监控 CPU、内存和网络 I/O,以定位瓶颈并优化资源分配。
