Асинхронні проксі в Python використовують такі бібліотеки, як aiohttp та httpx, для ефективного керування численними одночасними мережевими запитами, запобігаючи блокуванню основного потоку виконання операціями вводу/виводу.
Проксі-сервіси за своєю суттю є I/O-bound, витрачаючи більшу частину свого операційного часу на очікування мережевих відповідей від вищестоящих серверів або клієнтських запитів. Традиційні синхронні (блокуючі) моделі вводу/виводу обробляють один запит за раз на потік, що призводить до неефективного використання ресурсів та обмеженої масштабованості. Асинхронний ввід/вивід, використовуючи фреймворк Python asyncio, дозволяє одному потоку керувати численними одночасними з'єднаннями, перемикаючи контекст під час очікування завершення операцій вводу/виводу. Ця архітектура значно підвищує пропускну здатність та чутливість проксі.
Основні асинхронні концепції
Бібліотека Python asyncio забезпечує основу для асинхронного програмування. Ключові елементи включають:
- Цикл подій (Event Loop): Центральний компонент, який планує та виконує корутини, обробляючи події вводу/виводу та зворотні виклики.
- Корутини (
async def): Функції, які можуть бути призупинені та відновлені. Вони визначаються за допомогоюasync defта виконуються за допомогоюawait. - Ключове слово
await: Використовується для призупинення виконання корутини до завершення awaitable об'єкта (іншої корутини, Future або Task). Це повертає керування циклу подій.
aiohttp для асинхронних проксі-сервісів
aiohttp — це асинхронний HTTP-клієнт/серверний фреймворк для asyncio. Він добре підходить для створення як вхідних (серверних), так і вихідних (клієнтських) компонентів проксі.
aiohttp як проксі-сервер
aiohttp.web надає необхідні інструменти для створення веб-сервера, який прослуховує вхідні клієнтські запити.
import aiohttp.web
async def handle_request(request):
"""
A placeholder handler for incoming requests.
In a real proxy, this would forward the request.
"""
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) # Catch all routes
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) # Keep the server running
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() # Raise an exception for HTTP errors
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 — це сучасний, повнофункціональний HTTP-клієнт для Python, який надає як синхронні, так і асинхронні API. Його асинхронні можливості побудовані на asyncio.
httpx як асинхронний HTTP-клієнт
httpx.AsyncClient є основним інтерфейсом для виконання асинхронних запитів. Він пропонує API, схожий на requests, що робить його інтуїтивно зрозумілим для розробників, знайомих з бібліотекою 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() # Raise an exception for HTTP errors
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, більш багатослівний. |
API, схожий на requests, зазвичай більш лаконічний. |
| Підтримка 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(). |
| Конфігурація проксі | Прямий параметр proxy для методів ClientSession. |
Прямий параметр proxies для AsyncClient та запитів. |
Для створення проксі-сервера 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__)
# Initialize httpx.AsyncClient once for connection pooling
# This client will be used for all outgoing requests
# Set a default timeout to prevent hanging connections
OUTGOING_CLIENT = httpx.AsyncClient(timeout=30.0)
async def proxy_handler(request):
"""
Handles incoming client requests, forwards them using httpx,
and returns the response to the client.
"""
target_url = str(request.url).lstrip('/') # Remove leading slash from path
# Reconstruct target URL, preserving scheme, host, and query parameters
# For a typical forward proxy, the client sends full URLs (e.g., GET http://example.com/path)
# For a reverse proxy, the server might only get the path, and needs a base URL.
# This example assumes a forward proxy where the full URL is in the path.
# For a reverse proxy, you'd prepend a fixed base URL:
# target_url = f"http://upstream.example.com{request.url.path_qs}"
# Extract headers, excluding hop-by-hop headers and proxy-specific headers
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']
}
# Add X-Forwarded-For if not already present
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:
# Forward the request using httpx
proxy_response = await OUTGOING_CLIENT.request(
method=request_method,
url=target_url,
headers=headers,
content=request_body,
params=request.query # Pass query parameters separately
)
proxy_response.raise_for_status() # Raise for 4xx/5xx responses
# Prepare response for the client
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 headers
}
# Stream response body to avoid loading large responses into memory
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()
# This route handles all methods and paths
# For a forward proxy, client requests look like: GET http://example.com/path
# aiohttp parses the path as '/http://example.com/path'
# We strip the leading '/' in proxy_handler to get the full 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")
# Keep the server running indefinitely
try:
while True:
await asyncio.sleep(3600)
finally:
await OUTGOING_CLIENT.aclose() # Ensure httpx client is closed
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 надає вбудовану підтримку WebSockets на стороні сервера (aiohttp.web.WebSocketResponse). Проксіювання WebSockets вимагає обробки заголовка Upgrade та встановлення двонаправленого потоку даних між клієнтом, проксі та цільовим сервером WebSocket. httpx не підтримує клієнтські з'єднання WebSocket.
Налаштування продуктивності
uvloop: Для додатківaiohttpвстановленняuvloop(замінник циклу подійasyncio, написаний на Cython) може значно підвищити продуктивність.- Обмеження операційної системи: Часто потрібно налаштувати обмеження на кількість відкритих файлових дескрипторів (
ulimit -n) в операційній системі для висококонкурентних проксі-сервісів. - Моніторинг ресурсів: Моніторинг ЦП, пам'яті та мережевого вводу/виводу для виявлення вузьких місць та оптимізації розподілу ресурсів.