Python'da asenkron proxy'ler, birden fazla eşzamanlı ağ isteğini verimli biçimde yönetmek için aiohttp ve httpx gibi kütüphaneleri kullanır ve I/O işlemlerinin ana yürütme thread'ini bloklamasını önler.
Proxy servisleri doğası gereği I/O bağımlıdır; çalışma sürelerinin büyük bölümünü upstream sunuculardan gelen ağ yanıtlarını veya istemci isteklerini beklemekle geçirir. Geleneksel senkron (bloklayan) I/O modelleri thread başına aynı anda tek bir isteği işler; bu da kaynakların verimsiz kullanılmasına ve sınırlı ölçeklenebilirliğe yol açar. Python'un asyncio çatısını kullanan asenkron I/O ise, I/O işlemlerinin tamamlanmasını beklerken bağlam değiştirerek tek bir thread'in çok sayıda eşzamanlı bağlantıyı yönetmesine olanak tanır. Bu mimari, bir proxy'nin işlem hacmini ve yanıt verme hızını belirgin biçimde artırır.
Temel asenkron kavramlar
Python'un asyncio kütüphanesi asenkron programlamanın temelini sağlar. Başlıca öğeler şunlardır:
- Event loop: coroutine'leri planlayıp çalıştıran, I/O olaylarını ve callback'leri işleyen merkezi bileşen.
- Coroutine'ler (
async def): duraklatılıp devam ettirilebilen fonksiyonlar.async defile tanımlanır veawaitile çalıştırılır. awaitanahtar sözcüğü: bir awaitable (başka bir coroutine, Future veya Task) tamamlanana kadar coroutine'in çalışmasını duraklatmak için kullanılır. Bu, kontrolü event loop'a geri verir.
Asenkron proxy servisleri için aiohttp
aiohttp, asyncio için asenkron bir HTTP istemci/sunucu çatısıdır. Bir proxy'nin hem gelen (sunucu) hem de giden (istemci) bileşenlerini kurmak için uygundur.
Proxy sunucusu olarak aiohttp
aiohttp.web, gelen istemci isteklerini dinleyen bir web sunucusu kurmak için gereken araçları sağlar.
import aiohttp.web
async def handle_request(request):
"""
Gelen istekler için örnek bir handler.
Gerçek bir proxy'de bu, isteği iletirdi.
"""
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) # Tüm rotaları yakalar
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) # Sunucuyu çalışır durumda tutar
if __name__ == '__main__':
import asyncio
asyncio.run(main())
Asenkron HTTP istemcisi olarak aiohttp
aiohttp.ClientSession, istemci isteklerini upstream sunuculara iletmek için kritik olan giden HTTP isteklerini yapmak üzere kullanılır. Bağlantı havuzunu ve çerezleri yönetir.
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 hatalarında istisna fırlatır
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())
Asenkron proxy servisleri için httpx
httpx, Python için modern ve tam donanımlı bir HTTP istemcisidir; hem senkron hem asenkron API sunar. Asenkron yetenekleri asyncio üzerine kuruludur.
Asenkron HTTP istemcisi olarak httpx
httpx.AsyncClient, asenkron istekler yapmak için birincil arayüzdür. requests'e benzer bir API sunar; bu da requests kütüphanesini bilen geliştiriciler için onu sezgisel kılar.
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 hatalarında istisna fırlatır
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 sunucu yetenekleri sunmaz; tamamen bir istemci kütüphanesidir.
aiohttp ile httpx istemci karşılaştırması
| Özellik | aiohttp.ClientSession |
httpx.AsyncClient |
|---|---|---|
| Amaç | Asenkron HTTP istemci ve sunucu çatısı. | Asenkron (ve senkron) HTTP istemcisi. |
| API tarzı | Daha alt seviye asyncio entegrasyonu, daha ayrıntılı. |
requests benzeri API, genelde daha derli toplu. |
| HTTP/2 desteği | Yerel HTTP/2 istemci desteği yok. | Yerel HTTP/2 istemci desteği var. |
| HTTP/3 (QUIC) desteği | Yok. | quic-go (Rust) üzerinden deneysel destek. |
| WebSocket istemcisi | Var. | Yok. |
| Bağımlılıklar | multidict, yarl, async_timeout, attrs. |
httpcore, idna, certifi, `sniffio (asgari). |
| Bağlantı havuzu | ClientSession tarafından yönetilir. |
AsyncClient tarafından yönetilir. |
| Yönlendirme yönetimi | Otomatik, yapılandırılabilir. | Otomatik, yapılandırılabilir. |
| Akışlı yanıtlar | Evet, response.content.read() ile. |
Evet, response.aiter_bytes() ile. |
| Proxy yapılandırması | ClientSession metotlarında doğrudan proxy parametresi. |
AsyncClient ve isteklerde doğrudan proxies parametresi. |
Bir proxy sunucusu kurmak için sunucu yetenekleri nedeniyle aiohttp gereklidir. Giden istemci bileşeni için ikisi de kullanılabilir. httpx çoğu zaman daha basit bir API ve yerleşik HTTP/2 desteği sunar; bu da avantaj sağlayabilir.
aiohttp (sunucu) ve httpx (istemci) ile asenkron proxy kurmak
Bu yaklaşım, gelen proxy isteklerini işlemek için aiohttp'yi, bunları hedef sunucuya iletmek için httpx'i kullanır. Bu birleşim genellikle sunucu kontrolü ile istemci basitliği/özellikleri arasında iyi bir denge sağlar.
import aiohttp.web
import httpx
import asyncio
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Bağlantı havuzu için httpx.AsyncClient bir kez oluşturulur
# Bu istemci tüm giden isteklerde kullanılacaktır
# Asılı kalan bağlantıları önlemek için varsayılan bir timeout belirleyin
OUTGOING_CLIENT = httpx.AsyncClient(timeout=30.0)
async def proxy_handler(request):
"""
Gelen istemci isteklerini işler, httpx ile iletir
ve yanıtı istemciye döndürür.
"""
target_url = str(request.url).lstrip('/') # Yoldaki baştaki eğik çizgiyi kaldırır
# Şema, host ve query parametrelerini koruyarak hedef URL'yi yeniden oluşturur
# Tipik bir forward proxy'de istemci tam URL gönderir (ör. GET http://example.com/path)
# Reverse proxy'de sunucu yalnızca yolu alabilir ve bir temel URL'ye ihtiyaç duyar.
# Bu örnek, tam URL'nin yolda bulunduğu bir forward proxy varsayar.
# Reverse proxy için sabit bir temel URL eklerdiniz:
# target_url = f"http://upstream.example.com{request.url.path_qs}"
# Hop-by-hop ve proxy'ye özgü başlıkları hariç tutarak başlıkları çıkarır
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']
}
# Yoksa X-Forwarded-For ekler
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:
# İsteği httpx ile iletir
proxy_response = await OUTGOING_CLIENT.request(
method=request_method,
url=target_url,
headers=headers,
content=request_body,
params=request.query # Query parametrelerini ayrıca iletir
)
proxy_response.raise_for_status() # 4xx/5xx yanıtlarında istisna fırlatır
# Yanıtı istemci için hazırlar
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 başlıklar
}
# Büyük yanıtları belleğe yüklememek için yanıt gövdesini akış hâlinde gönderir
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()
# Bu rota tüm metotları ve yolları karşılar
# Forward proxy'de istemci istekleri şöyle görünür: GET http://example.com/path
# aiohttp yolu '/http://example.com/path' olarak ayrıştırır
# Tam URL'yi elde etmek için proxy_handler içinde baştaki '/' karakterini kaldırıyoruz.
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")
# Sunucuyu süresiz çalışır durumda tutar
try:
while True:
await asyncio.sleep(3600)
finally:
await OUTGOING_CLIENT.aclose() # httpx istemcisinin kapatıldığından emin olur
await runner.cleanup()
if __name__ == '__main__':
asyncio.run(start_proxy_server())
Proxy servisleri için pratik hususlar
Başlık yönetimi
Proxy'ler HTTP başlıklarını dikkatle yönetmelidir.
* Hop-by-hop başlıklar (Connection, Keep-Alive, Proxy-Authenticate, Proxy-Authorization, TE, Trailer, Transfer-Encoding, Upgrade) iki düğüm arasındaki bağlantıya özgüdür ve iletilmemelidir.
* X-Forwarded-For / X-Real-IP: upstream sunucuyu asıl talep sahibi hakkında bilgilendirmek için istemcinin IP adresini bu başlıklara ekleyin veya mevcut değere iliştirin.
* Via başlığı: proxy'nin devrede olduğunu belirtmek için isteğe bağlı olarak bir Via başlığı ekleyin.
Gövde akışı (streaming)
Büyük istek veya yanıt gövdelerinde, veriyi tamamen belleğe yüklemek yerine akış hâlinde aktarmak kritiktir. Hem aiohttp (gelen için request.read(), giden için response.write() ile) hem de httpx (content parametresi ve response.aiter_bytes() ile) akışı destekler; bu da bellek tükenmesini önler ve gecikmeyi azaltır.
Bağlantı havuzu
Hem aiohttp.ClientSession hem de httpx.AsyncClient bağlantı havuzu uygular. Bu istemcileri bir kez oluşturup birden fazla istekte yeniden kullanmak (OUTGOING_CLIENT örneğinde gösterildiği gibi) performans için kritiktir. Bu, her istek için yeni TCP bağlantıları kurmanın maliyetini azaltır.
Timeout'lar
Proxy servisleri, upstream sunucu gecikmelerinden veya arızalarından etkilenir. Giden istekler için sıkı timeout'lar uygulamak, kaynakların tükenmesini önlemek ve hızlı yanıt veren bir servis sunmak için şarttır. httpx.AsyncClient ve aiohttp.ClientSession, bağlantı, okuma ve toplam timeout değerlerinin yapılandırılmasına izin verir.
Hata yönetimi ve yeniden denemeler
Ağ sorunları (ör. bağlantı reddedildi, DNS hataları) ve HTTP hataları (ör. upstream'den gelen 5xx yanıtlar) için sağlam bir hata yönetimi gereklidir. Güvenilirliği artırmak için geçici hatalarda üstel geri çekilmeli (exponential backoff) yeniden deneme mekanizmaları uygulayın.
WebSocket proxy'leme
aiohttp, sunucu tarafında WebSocket'ler için yerel destek sunar (aiohttp.web.WebSocketResponse). WebSocket proxy'lemek, Upgrade başlığının işlenmesini ve istemci, proxy ile hedef WebSocket sunucusu arasında çift yönlü bir veri akışı kurulmasını gerektirir. httpx, istemci tarafında WebSocket bağlantılarını desteklemez.
Performans ayarı
uvloop:aiohttpuygulamalarındauvloop(Cython ile yazılmış,asyncioevent loop'unun doğrudan yerine geçen bir alternatif) kurmak performansı belirgin biçimde artırabilir.- İşletim sistemi limitleri: yüksek eşzamanlılığa sahip proxy servislerinde işletim sistemindeki açık dosya tanıtıcısı limitlerini (
ulimit -n) yükseltmek çoğu zaman gereklidir. - Kaynak izleme: darboğazları belirlemek ve kaynak dağılımını iyileştirmek için CPU, bellek ve ağ I/O'sunu izleyin.
