Proxies assíncronos em Python utilizam bibliotecas como aiohttp e httpx para gerenciar várias requisições de rede simultâneas de forma eficiente, evitando que operações de I/O bloqueiem a thread principal de execução.
Serviços de proxy são, por natureza, limitados por I/O: passam a maior parte do tempo de operação esperando respostas de rede de servidores upstream ou requisições de clientes. Os modelos tradicionais de I/O síncrono (bloqueante) atendem uma requisição por vez em cada thread, o que gera uso ineficiente de recursos e escalabilidade limitada. O I/O assíncrono, com o framework asyncio do Python, permite que uma única thread gerencie inúmeras conexões simultâneas trocando de contexto enquanto aguarda a conclusão das operações de I/O. Essa arquitetura aumenta significativamente a vazão e a capacidade de resposta de um proxy.
Conceitos assíncronos fundamentais
A biblioteca asyncio do Python fornece a base para a programação assíncrona. Os elementos principais são:
- Event loop: o componente central que agenda e executa corrotinas, tratando eventos de I/O e callbacks.
- Corrotinas (
async def): funções que podem ser pausadas e retomadas. São definidas comasync defe executadas comawait. - Palavra-chave
await: usada para pausar a execução de uma corrotina até que um awaitable (outra corrotina, um Future ou uma Task) seja concluído. Isso devolve o controle ao event loop.
aiohttp para serviços de proxy assíncronos
aiohttp é um framework HTTP cliente/servidor assíncrono para asyncio. Ele é adequado para construir tanto o componente de entrada (servidor) quanto o de saída (cliente) de um proxy.
aiohttp como servidor proxy
aiohttp.web fornece as ferramentas necessárias para construir um servidor web que escuta requisições de clientes.
import aiohttp.web
async def handle_request(request):
"""
Um handler de exemplo para requisições recebidas.
Em um proxy real, isso encaminharia a requisição.
"""
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) # Captura todas as rotas
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) # Mantém o servidor em execução
if __name__ == '__main__':
import asyncio
asyncio.run(main())
aiohttp como cliente HTTP assíncrono
aiohttp.ClientSession é usado para fazer requisições HTTP de saída, essenciais para encaminhar as requisições dos clientes aos servidores upstream. Ele gerencia o pool de conexões e os cookies.
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() # Lança uma exceção em caso de erro 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 para serviços de proxy assíncronos
httpx é um cliente HTTP moderno e completo para Python que oferece APIs síncronas e assíncronas. Seus recursos assíncronos são construídos sobre asyncio.
httpx como cliente HTTP assíncrono
httpx.AsyncClient é a interface principal para fazer requisições assíncronas. Ele oferece uma API parecida com a do requests, o que a torna intuitiva para quem já conhece a biblioteca 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() # Lança uma exceção em caso de erro 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 não oferece recursos de servidor; é puramente uma biblioteca cliente.
Comparação entre os clientes aiohttp e httpx
| Recurso | aiohttp.ClientSession |
httpx.AsyncClient |
|---|---|---|
| Finalidade | Framework HTTP assíncrono cliente e servidor. | Cliente HTTP assíncrono (e síncrono). |
| Estilo de API | Integração asyncio de baixo nível, mais verbosa. |
API parecida com requests, geralmente mais concisa. |
| Suporte a HTTP/2 | Sem suporte nativo a HTTP/2 no cliente. | Suporte nativo a HTTP/2 no cliente. |
| Suporte a HTTP/3 (QUIC) | Não. | Suporte experimental via quic-go (Rust). |
| Cliente WebSocket | Sim. | Não. |
| Dependências | multidict, yarl, async_timeout, attrs. |
httpcore, idna, certifi, `sniffio (mínimas). |
| Pool de conexões | Gerenciado por ClientSession. |
Gerenciado por AsyncClient. |
| Tratamento de redirects | Automático, configurável. | Automático, configurável. |
| Respostas em streaming | Sim, usando response.content.read(). |
Sim, usando response.aiter_bytes(). |
| Configuração de proxy | Parâmetro proxy direto nos métodos de ClientSession. |
Parâmetro proxies direto em AsyncClient e requisições. |
Para construir um servidor proxy, aiohttp é necessário por seus recursos de servidor. Para o componente cliente de saída, ambos são viáveis. httpx costuma oferecer uma API mais simples e suporte nativo a HTTP/2, o que pode ser vantajoso.
Construindo um proxy assíncrono com aiohttp (servidor) e httpx (cliente)
Essa abordagem usa aiohttp para tratar as requisições de proxy recebidas e httpx para encaminhá-las ao servidor de destino. Essa combinação costuma oferecer um bom equilíbrio entre controle do servidor e simplicidade/recursos do cliente.
import aiohttp.web
import httpx
import asyncio
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Inicializa httpx.AsyncClient uma única vez para o pool de conexões
# Este cliente será usado em todas as requisições de saída
# Defina um timeout padrão para evitar conexões travadas
OUTGOING_CLIENT = httpx.AsyncClient(timeout=30.0)
async def proxy_handler(request):
"""
Trata as requisições recebidas dos clientes, encaminha usando httpx
e devolve a resposta ao cliente.
"""
target_url = str(request.url).lstrip('/') # Remove a barra inicial do caminho
# Reconstrói a URL de destino, preservando esquema, host e parâmetros de query
# Em um forward proxy típico, o cliente envia URLs completas (ex.: GET http://example.com/path)
# Em um reverse proxy, o servidor pode receber apenas o caminho e precisa de uma URL base.
# Este exemplo assume um forward proxy, em que a URL completa está no caminho.
# Para um reverse proxy, você anteporia uma URL base fixa:
# target_url = f"http://upstream.example.com{request.url.path_qs}"
# Extrai os cabeçalhos, excluindo os hop-by-hop e os específicos de proxy
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']
}
# Adiciona X-Forwarded-For se ainda não estiver presente
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:
# Encaminha a requisição usando httpx
proxy_response = await OUTGOING_CLIENT.request(
method=request_method,
url=target_url,
headers=headers,
content=request_body,
params=request.query # Passa os parâmetros de query separadamente
)
proxy_response.raise_for_status() # Lança exceção para respostas 4xx/5xx
# Prepara a resposta para o cliente
response_headers = {
k: v for k, v in proxy_response.headers.items()
if k.lower() not in ['content-encoding', 'transfer-encoding', 'connection'] # Cabeçalhos hop-by-hop
}
# Envia o corpo da resposta em streaming para não carregar respostas grandes na memória
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()
# Esta rota trata todos os métodos e caminhos
# Em um forward proxy, as requisições do cliente são assim: GET http://example.com/path
# o aiohttp interpreta o caminho como '/http://example.com/path'
# Removemos a '/' inicial em proxy_handler para obter a URL completa.
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")
# Mantém o servidor rodando indefinidamente
try:
while True:
await asyncio.sleep(3600)
finally:
await OUTGOING_CLIENT.aclose() # Garante que o cliente httpx seja fechado
await runner.cleanup()
if __name__ == '__main__':
asyncio.run(start_proxy_server())
Considerações práticas para serviços de proxy
Gerenciamento de cabeçalhos
Proxies precisam gerenciar os cabeçalhos HTTP com cuidado.
* Cabeçalhos hop-by-hop (Connection, Keep-Alive, Proxy-Authenticate, Proxy-Authorization, TE, Trailer, Transfer-Encoding, Upgrade) são específicos da conexão entre dois nós e não devem ser encaminhados.
* X-Forwarded-For / X-Real-IP: adicione ou acrescente o endereço IP do cliente a esses cabeçalhos para informar ao servidor upstream quem foi o solicitante original.
* Cabeçalho Via: opcionalmente, adicione um cabeçalho Via para indicar a participação do proxy.
Streaming do corpo
Para corpos grandes de requisição ou resposta, é fundamental transmitir os dados em streaming em vez de carregá-los inteiramente na memória. Tanto aiohttp (via request.read() na entrada e response.write() na saída) quanto httpx (via parâmetro content e response.aiter_bytes()) suportam streaming, o que evita o esgotamento de memória e reduz a latência.
Pool de conexões
Tanto aiohttp.ClientSession quanto httpx.AsyncClient implementam pool de conexões. Instanciar esses clientes uma única vez e reutilizá-los em várias requisições (como mostrado com OUTGOING_CLIENT) é crucial para o desempenho. Isso reduz o custo de estabelecer novas conexões TCP a cada requisição.
Timeouts
Serviços de proxy são vulneráveis a lentidão ou falhas dos servidores upstream. Implementar timeouts rígidos para as requisições de saída é essencial para evitar esgotamento de recursos e manter um serviço responsivo. httpx.AsyncClient e aiohttp.ClientSession permitem configurar timeouts de conexão, de leitura e total.
Tratamento de erros e retentativas
É necessário um tratamento robusto de erros de rede (por exemplo, conexão recusada, erros de DNS) e de erros HTTP (por exemplo, respostas 5xx do upstream). Implemente mecanismos de retentativa com backoff exponencial para erros transitórios, a fim de melhorar a confiabilidade.
Proxy de WebSocket
aiohttp oferece suporte nativo a WebSockets no lado do servidor (aiohttp.web.WebSocketResponse). Fazer proxy de WebSockets exige tratar o cabeçalho Upgrade e estabelecer um fluxo de dados bidirecional entre cliente, proxy e servidor WebSocket de destino. httpx não suporta conexões WebSocket no cliente.
Ajuste de desempenho
uvloop: em aplicaçõesaiohttp, instalar ouvloop(um substituto direto do event loop doasyncio, escrito em Cython) pode aumentar significativamente o desempenho.- Limites do sistema operacional: ajustar o limite de descritores de arquivo abertos (
ulimit -n) no sistema operacional costuma ser necessário em serviços de proxy de alta concorrência. - Monitoramento de recursos: monitore CPU, memória e I/O de rede para identificar gargalos e otimizar a alocação de recursos.
