Tüm eğitimler
Rehber Network · WebSocket · asyncio websockets

WebSocket —
HTTP Upgrade'den Wire Frame'e.

HTTP polling'i geride bırak — full-duplex bağlantı, frame anatomisi ve asyncio ile Python server.

00 WebSocket nedir

WebSocket, tek bir TCP bağlantısı üzerinde tarayıcı ile sunucu arasında full-duplex iletişim sağlayan, IETF tarafından RFC 6455 ile 2011'de standartlaştırılmış bir protokoldür.

HTTP, istek-yanıt modeli üzerine kurulmuştur: client bir şey ister, server yanıt verir, bağlantı kapanır. Bu model statik web sayfaları için mükemmeldir; ancak gerçek zamanlı veri gerektiren uygulamalarda ciddi sorunlar yaratır. Sunucunun kendi inisiyatifiyle client'a mesaj gönderemeyişi, uzun yıllar boyunca çeşitli geçici çözümlerin doğmasına yol açtı.

WebSocket bu sorunu kökten çözdü: HTTP üzerinde başlayan ancak hemen TCP stream'e dönüşen, her iki tarafın da istediği zaman mesaj gönderebildiği kalıcı bir kanal sağlar. Tek bir handshake, sürekli bir bağlantı.

Hangi uygulamalar WebSocket ister

Real-time dashboardSunucu tarafındaki metrikler (CPU, bellek, ağ) anlık olarak tarayıcıya gönderilir; client polling gerekmez.
Chat uygulamasıHer kullanıcı mesajı tüm oturum sahiplerine anında iletilir; gecikme yoktur.
Live sensor feedGömülü sistem, sıcaklık veya titreşim verisini 100 ms aralıklarla göndermek için kalıcı bağlantıyı kullanır.
Game serverOyuncu pozisyonları, skoru ve olayları düşük gecikmeyle senkronize edilir.
Collaborative editingBirden fazla kullanıcının aynı belge üzerinde çalışması için anlık değişiklik senkronizasyonu.

HTTP polling vs WebSocket karşılaştırması

YöntemNasıl çalışırGecikmeSunucu yüküSunucudan push
HTTP PollingClient düzenli aralıklarla GET atarYüksek (interval kadar)Çok yüksek (boş yanıtlar)Hayır
HTTP Long-PollingClient bekler, server veri olunca yanıt verir; client tekrar isterOrtaYüksek (yarı açık bağlantılar)Dolaylı
SSE (EventSource)Server tek yönlü event stream açarDüşükOrtaEvet — tek yön
WebSocketTek TCP bağlantısı, iki yönlüÇok düşükDüşük (kalıcı bağlantı)Evet — iki yön

URL şeması

ws://Şifresiz WebSocket. Varsayılan port 80. Sadece geliştirme ortamında kullanın.
wss://TLS üzerinden WebSocket. Varsayılan port 443. Üretim için zorunlu.
ws://example.com/socket        → TCP:80, şifresiz
wss://example.com/socket       → TCP:443, TLS ile şifreli
ws://localhost:8765/           → yerel geliştirme
wss://api.example.com:8443/ws  → özel port, TLS
NOT

WebSocket, RFC 6455 ile 2011'de standartlaştırıldı. Tüm modern tarayıcılar ve popüler sunucu framework'leri destekler. HTTP/1.1 üzerinde kurulur; HTTP/2 üzerinden WebSocket için RFC 8441 ayrı bir mekanizma tanımlar ancak nadiren kullanılır.

Bu bölümde

  • HTTP request-response modelinin gerçek zamanlı senaryolardaki yetersizliği
  • Polling, long-polling, SSE ve WebSocket arasındaki farklar
  • ws:// ve wss:// URL şemaları
  • RFC 6455, 2011

01 HTTP Upgrade handshake

WebSocket bağlantısı, sıradan bir HTTP/1.1 isteğiyle başlar; sunucu 101 Switching Protocols yanıtını verdiği andan itibaren TCP soket WebSocket protokolüne devredilir.

WebSocket, sıfırdan yeni bir ağ protokolü olarak tasarlanmadı. Mevcut HTTP altyapısından — proxy'ler, firewall'lar, 80/443 portları — yararlanmak için HTTP üzerinde yükseltme (upgrade) mekanizmasını kullanır. Bu sayede bir WebSocket bağlantısı, standart bir HTTP isteğiyle başlayarak ağ cihazlarında herhangi bir özel konfigürasyon gerektirmez.

Client'tan gelen Upgrade isteği

HTTP request (client → server)
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: http://example.com
Upgrade: websocketProtokol yükseltme talebi. HTTP'den WebSocket'e geçmek istediğimizi bildirir.
Connection: UpgradeBu bağlantıda Upgrade başlığının uygulanmasını ister.
Sec-WebSocket-KeyRastgele üretilmiş 16 byte'ın base64 kodlaması. Sunucu bunu GUID ile birleştirip SHA-1 hash'ler ve geri gönderir — cevabın gerçek olduğunu kanıtlamak için.
Sec-WebSocket-Version: 13Kullanılan WebSocket protokol sürümü. RFC 6455 ile tanımlanan tek geçerli sürüm 13'tür.

Sunucudan gelen 101 yanıtı

HTTP response (server → client)
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Sec-WebSocket-Accept hesabı

Sunucu, Sec-WebSocket-Key değerini RFC 6455'te tanımlı sabit bir GUID ile birleştirir, SHA-1 hash alır ve base64'e dönüştürür. Bu mekanizma, sunucunun gerçekten WebSocket'i desteklediğini kanıtlar.

accept_key.py
import hashlib
import base64

# RFC 6455 Section 1.3 — sabit magic UUID
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"

def compute_accept(sec_websocket_key: str) -> str:
    combined = sec_websocket_key + GUID
    sha1_hash = hashlib.sha1(combined.encode("utf-8")).digest()
    return base64.b64encode(sha1_hash).decode("utf-8")

# Örnek: tarayıcının gönderdiği key
key = "dGhlIHNhbXBsZSBub25jZQ=="
accept = compute_accept(key)
print(f"Sec-WebSocket-Accept: {accept}")
# → s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
client → GET /ws HTTP/1.1 + Upgrade: websocket + Sec-WebSocket-Key
server → 101 Switching Protocols + Sec-WebSocket-Accept
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
HTTP bitti ───▶ TCP soket artık WebSocket protocol taşıyor
DİKKAT

Handshake tamamlandıktan sonra HTTP protokolü sona erer. Aynı TCP bağlantısı artık WebSocket frame'leri taşır. HTTP cookie'leri ve header'lar yalnızca handshake sırasında geçerlidir — kimlik doğrulama bu aşamada yapılmalıdır.

Bu bölümde

  • Upgrade ve Connection header'larının rolü
  • Sec-WebSocket-Key → SHA-1 → base64 → Sec-WebSocket-Accept akışı
  • 101 Switching Protocols sonrası HTTP'nin sona ermesi

02 Frame formatı

WebSocket verisi, her biri kontrol bitleri, opcode, uzunluk alanı ve isteğe bağlı maskeleme anahtarı içeren frame'lere bölünür.

Handshake tamamlanınca TCP bağlantısı WebSocket frame'leri taşımaya başlar. Her frame, ilk iki zorunlu byte ile başlar; kalan alanlar frame içeriğine göre değişir. Wire üzerinde gereksiz byte bulunmaz: küçük mesajlar 2-6 byte header ile gönderilir.

Frame bit diyagramı

RFC 6455 Frame Layout
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)    |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - -+
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - -+-------------------------------+
|  Masking-key (4 bytes, if MASK=1)                             |
+---------------------------------------------------------------+
|                      Payload Data                             |
+---------------------------------------------------------------+

Alan açıklamaları

FIN (1 bit)1 = bu frame mesajın son (veya tek) parçası. 0 = fragmentation devam ediyor.
RSV1/RSV2/RSV3 (1 bit)Extension reserved. Uzantı anlaşması olmadan her biri 0 olmalı. Örneğin permessage-deflate sıkıştırması RSV1'i kullanır.
Opcode (4 bit)Frame türünü belirtir. Tanımsız bir opcode gelmesi durumunda bağlantı kapatılmalıdır.
MASK (1 bit)1 = payload maskelenmiş. Client → Server yönünde zorunlu; Server → Client yönünde yasak.
Payload len (7 bit)0–125: gerçek uzunluk. 126: sonraki 2 byte unsigned 16-bit uzunluk. 127: sonraki 8 byte unsigned 64-bit uzunluk.
Masking-key (4 byte)MASK=1 ise bu dört byte XOR anahtarı olarak payload'a uygulanır. Her frame için rastgele seçilmeli.

Opcode tablosu

OpcodeİsimAçıklama
0x0ContinuationÖnceki fragmente edilmiş mesajın devamı
0x1TextUTF-8 metin mesajı
0x2BinaryHam binary veri
0x3–0x7Gelecekteki data frame'ler için rezerve
0x8CloseBağlantı kapatma isteği
0x9PingHeartbeat / canlılık kontrolü
0xAPongPing'e yanıt
0xB–0xFGelecekteki control frame'ler için rezerve

Maskeleme: XOR uygulaması

masking.py
def apply_mask(payload: bytes, masking_key: bytes) -> bytes:
    """RFC 6455 Section 5.3 — XOR masking/unmasking (aynı işlem)."""
    return bytes(
        b ^ masking_key[i % 4]
        for i, b in enumerate(payload)
    )

# Client "Hello" metnini sunucuya gönderecek
payload  = b"Hello"
mask_key = b"\x37\xfa\x21\x3d"  # rastgele 4 byte

masked   = apply_mask(payload, mask_key)
unmasked = apply_mask(masked, mask_key)   # aynı fonksiyon ile geri al
print(unmasked)  # b'Hello'
NOT

Maskeleme, güvenlik amacıyla değil, önbellek zehirlenmesi saldırılarına (cache poisoning) karşı tasarlanmıştır. Proxy'lerin WebSocket frame'lerini HTTP yanıtı sanıp önbelleğe almasını engeller. Server-to-client mesajlar maskelenmez.

Bu bölümde

  • FIN, RSV, MASK bitlerinin anlamı
  • 7-bit payload length ve extended length kodlamaları
  • Opcode tablosu: 0x0–0xA
  • XOR maskeleme mekanizması ve amacı

03 Control frames: ping, pong, close

Control frame'ler (ping, pong, close) bağlantı yönetimine hizmet eder; veri frame'lerinin aksine fragmente edilemez ve payload'ları 125 byte ile sınırlıdır.

Ping / Pong — heartbeat

Ping frame'i (opcode 0x9), bir tarafın karşı tarafın hâlâ bağlı olup olmadığını sorgulamak için gönderdiği kontrol mesajıdır. Ping alan taraf, aynı payload ile bir pong frame'i (opcode 0xA) göndermek zorundadır.

Unsolicited PongPing beklemeden gönderilen pong frame'i de geçerlidir. Tek yönlü "ben hayattayım" bildirimi olarak kullanılabilir.
Ping intervalBest practice: 30–60 saniye. Çok sık ping, gereksiz trafik ve CPU yükü yaratır. Çok seyrek ping, kopan bağlantıların geç fark edilmesine yol açar.
ping_example.py
import asyncio
import websockets

async def server_with_ping(websocket):
    try:
        async for message in websocket:
            await websocket.send(f"echo: {message}")
    except websockets.ConnectionClosed:
        print("Bağlantı kapandı")

# ping_interval ve ping_timeout websockets.serve() parametresi olarak verilir
async def main():
    async with websockets.serve(
        server_with_ping,
        "0.0.0.0",
        8765,
        ping_interval=30,   # her 30 saniyede ping gönder
        ping_timeout=10,    # 10 saniye içinde pong gelmezse bağlantıyı kes
    ):
        await asyncio.get_event_loop().run_until_complete(asyncio.Future())

asyncio.run(main())

Close frame ve kapanış el sıkışması

Bağlantıyı düzgün şekilde kapatmak için her iki tarafın da close frame göndermesi gerekir. İlk close frame'i gönderen taraf kapanışı başlatır; diğer taraf aynı status code ile yanıt vermelidir. Ardından TCP FIN ile bağlantı sonlandırılır.

Client → Close frame (code: 1000, reason: "Normal closure")
Server ← Close frame (code: 1000)   ← server echo
Server → TCP FIN
Client ← TCP FIN ACK

Close status kodları

KodİsimKullanım
1000Normal ClosureBağlantı amacına ulaştı, normal kapanış
1001Going AwaySunucu kapatılıyor veya tarayıcı sayfadan ayrılıyor
1002Protocol ErrorProtokol ihlali tespit edildi
1003Unsupported DataDesteklenmeyen veri türü alındı
1008Policy ViolationMesaj politikayı ihlal etti
1009Message Too BigMesaj izin verilen boyutu aştı
1011Internal ErrorSunucu tarafında beklenmedik hata
1012Service RestartSunucu yeniden başlıyor, tekrar bağlanabilirsiniz
DİKKAT

Aniden TCP bağlantısını kesmek (FIN göndermeden) yerine close frame ile düzgün kapanış yapın. Özellikle sunucu yeniden başlarken 1012 kodu göndermek, client'ların akıllıca reconnect yapmasını sağlar.

Bu bölümde

  • Ping (0x9) ve pong (0xA) opcode'larının heartbeat işlevi
  • 30–60 saniye ping interval best practice
  • Close frame (0x8) ile iki taraflı kapanış el sıkışması
  • 1000, 1001, 1011, 1012 close status kodları

04 Python websockets kütüphanesi — server

websockets kütüphanesi, asyncio tabanlı WebSocket sunucusu ve client'ı yazmak için Python'ın en olgun ve RFC uyumlu seçeneğidir.

Kurulum

bash
pip install websockets          # en güncel stabil sürüm
python -c "import websockets; print(websockets.__version__)"

Handler fonksiyonu imzası

Her WebSocket bağlantısı için bir coroutine handler çağrılır. Handler tamamlandığında bağlantı kapatılır.

handler_signature.py
import asyncio
import websockets
from websockets.server import WebSocketServerProtocol

async def handler(websocket: WebSocketServerProtocol) -> None:
    # websocket.remote_address → (ip, port) tuple
    # websocket.path          → "/chat" gibi URL path
    # websocket.request_headers → HTTP upgrade header'ları
    remote = websocket.remote_address
    print(f"Yeni bağlantı: {remote[0]}:{remote[1]} — path: {websocket.path}")
    try:
        async for message in websocket:
            print(f"[{remote}] aldı: {message}")
            await websocket.send(f"echo: {message}")
    except websockets.ConnectionClosedOK:
        print(f"[{remote}] normal kapandı")
    except websockets.ConnectionClosedError as e:
        print(f"[{remote}] hata: {e.code} {e.reason}")

Echo server — tam örnek

echo_server.py
import asyncio
import websockets

async def echo(websocket):
    async for message in websocket:
        await websocket.send(message)

async def main():
    async with websockets.serve(echo, "0.0.0.0", 8765) as server:
        print("Echo server başlatıldı → ws://localhost:8765")
        await server.wait_closed()

asyncio.run(main())

Broadcast: tüm client'lara gönderme

broadcast_server.py
import asyncio
import websockets

# Bağlı tüm client'ları takip eden set
CONNECTED: set = set()

async def handler(websocket):
    CONNECTED.add(websocket)
    try:
        async for message in websocket:
            # Gönderici hariç herkese ilet
            recipients = CONNECTED - {websocket}
            if recipients:
                await asyncio.gather(
                    *[r.send(message) for r in recipients]
                )
    finally:
        CONNECTED.discard(websocket)

async def main():
    async with websockets.serve(handler, "0.0.0.0", 8765):
        await asyncio.Future()  # sonsuza kadar çalış

asyncio.run(main())

Path-based routing

router_server.py
async def handler(websocket):
    path = websocket.path
    if path == "/chat":
        await chat_handler(websocket)
    elif path == "/metrics":
        await metrics_handler(websocket)
    else:
        await websocket.close(1008, "Unknown endpoint")

Bu bölümde

  • async def handler(websocket) imzası ve yaşam döngüsü
  • recv(), send(), close() metodları
  • async with websockets.serve() ile server başlatma
  • CONNECTED set pattern ile broadcast
  • websocket.path ile URL tabanlı routing

05 asyncio ile client

websockets client'ı, asyncio context manager sözdizimi ile bağlanır; hem tek seferlik sorgular hem de sürekli mesaj akışı için kullanılabilir.

Temel bağlantı

basic_client.py
import asyncio
import websockets

async def main():
    uri = "ws://localhost:8765"
    async with websockets.connect(uri) as ws:
        await ws.send("Merhaba sunucu!")
        response = await ws.recv()
        print(f"Yanıt: {response}")
        # context manager bloğu sonunda bağlantı kapatılır

asyncio.run(main())

Async generator ile mesaj stream

stream_client.py
import asyncio
import websockets

async def listen():
    uri = "ws://localhost:8765"
    async with websockets.connect(uri) as ws:
        # async for: sunucu close frame gönderene kadar döngü sürer
        async for message in ws:
            print(f"Gelen: {message}")
            # binary veriyse: isinstance(message, bytes)

asyncio.run(listen())

Ping/pong ve bağlantı durumu

status_client.py
import asyncio
import websockets

async def check_connection():
    async with websockets.connect("ws://localhost:8765") as ws:
        # Manuel ping — latency ölçümü
        latency = await ws.ping()
        print(f"Round-trip latency: {latency:.3f}s")

        # Bağlantı durumu
        print(f"Kapalı mı: {ws.closed}")          # False
        print(f"Close code: {ws.close_code}")    # None (henüz kapalı değil)

        await ws.send("test")
        msg = await ws.recv()

    # Context manager'dan çıkıldıktan sonra
    print(f"Close code: {ws.close_code}")   # 1000

asyncio.run(check_connection())
ws.closedBoolean — bağlantı kapatılmış mı. Close frame alındıktan veya TCP kopunca True olur.
ws.close_codeInteger — bağlantı kapanış kodu (ör. 1000). Bağlantı açıkken None.
ws.latencyFloat — son ping/pong round-trip süresi saniye cinsinden. Bağlantı kalitesini izlemek için kullanılır.
ws.ping()Ping frame gönderir, pong alınınca tamamlanan bir awaitable döner. Dönüş değeri round-trip süresidir.

Bu bölümde

  • async with websockets.connect() context manager kullanımı
  • async for message in ws ile sürekli okuma
  • ws.ping() ile manuel latency ölçümü
  • ws.closed, ws.close_code durum alanları

06 JSON mesajlaşma ve routing

WebSocket, ham byte akışı sağlar; uygulama katmanında mesaj türü ve yük ayrımını sağlamak için JSON tabanlı envelope pattern kullanılır.

Mesaj zarfı (envelope) şeması

Ham string mesajlar yönetilemez hale gelince her mesajı bir JSON zarfı içinde gönderin. type alanı mesajı doğru handler'a yönlendirir; payload ise uygulama verisini taşır.

message_schema.json
// Komut mesajı
{
  "type": "command",
  "payload": { "action": "start", "target": "motor_1" }
}

// Sensör verisi
{
  "type": "sensor",
  "payload": { "temp": 42.5, "unit": "C", "ts": 1712345678 }
}

// Hata bildirimi
{
  "type": "error",
  "payload": { "code": 404, "message": "Unknown command" }
}

Dispatch pattern: type → handler

router_server.py
import asyncio
import json
import websockets

async def handle_command(websocket, payload: dict):
    action = payload.get("action")
    target = payload.get("target")
    print(f"Komut: {action} → {target}")
    await websocket.send(json.dumps({
        "type": "ack",
        "payload": {"action": action, "status": "ok"}
    }))

async def handle_subscribe(websocket, payload: dict):
    topic = payload.get("topic", "all")
    print(f"Subscribe: {topic}")
    await websocket.send(json.dumps({
        "type": "subscribed",
        "payload": {"topic": topic}
    }))

# Tip → coroutine eşlemesi
DISPATCH = {
    "command": handle_command,
    "subscribe": handle_subscribe,
}

async def handler(websocket):
    async for raw in websocket:
        try:
            msg = json.loads(raw)
            msg_type = msg.get("type")
            payload  = msg.get("payload", {})
        except json.JSONDecodeError:
            await websocket.send(json.dumps({
                "type": "error",
                "payload": {"message": "Invalid JSON"}
            }))
            continue

        handler_fn = DISPATCH.get(msg_type)
        if handler_fn:
            await handler_fn(websocket, payload)
        else:
            await websocket.send(json.dumps({
                "type": "error",
                "payload": {"message": f"Unknown type: {msg_type}"}
            }))

Binary mesajlar: bytes gönderme

binary_send.py
import struct

# Sensör verisi: 4 byte float (sıcaklık) + 4 byte float (nem) + 8 byte int (timestamp)
packet = struct.pack("!ff q", 42.5, 68.3, 1712345678901)

# Binary websocket mesajı olarak gönder
await websocket.send(packet)

# Alıcı tarafta
raw = await websocket.recv()
if isinstance(raw, bytes):
    temp, humidity, ts = struct.unpack("!ff q", raw)
    print(f"Sıcaklık: {temp:.1f}°C, Nem: {humidity:.1f}%")

Rate limiting: per-client sayaç

rate_limit.py
import asyncio
import time

async def handler(websocket):
    window_start = time.monotonic()
    msg_count    = 0
    MAX_PER_SEC  = 20

    async for raw in websocket:
        now = time.monotonic()
        if now - window_start >= 1.0:
            window_start = now
            msg_count    = 0
        msg_count += 1
        if msg_count > MAX_PER_SEC:
            await websocket.close(1008, "Rate limit exceeded")
            return
        # normal işleme...

Bu bölümde

  • JSON envelope: type + payload şeması
  • DISPATCH dict ile type-based routing
  • JSON parse hatası yönetimi ve hata mesajı gönderme
  • struct.pack ile binary sensör verisi gönderme
  • Per-client rate limiting (sliding window)

07 Reconnect ve exponential backoff

Gerçek dünyada bağlantılar kopar; iyi bir client, akıllı bir bekleme stratejisiyle sunucuyu bunaltmadan yeniden bağlanır.

Bağlantı kopma senaryoları

Server restartSunucu yeniden başlarken close frame göndermeden TCP'yi keser. Client ConnectionClosed alır.
Network glitchAğ kesintisi TCP bağlantısını sessizce öldürür. Sonraki send/recv başarısız olur.
Ping timeoutSunucu ping'e yanıt vermezse websockets kütüphanesi bağlantıyı otomatik kapatır.
Idle timeoutBazı proxy ve firewall'lar uzun süre veri geçmeyen bağlantıları keser (tipik: 60–300s).

websockets.connect() parametreleri

ParametreTipAçıklama
additional_headersdictHandshake sırasında eklenecek özel HTTP header'ları (ör. Authorization)
open_timeoutfloatHandshake tamamlanma zaman aşımı (saniye). None = sınırsız.
close_timeoutfloatClose frame el sıkışması için beklenecek süre.
ping_intervalfloatOtomatik ping gönderme aralığı (saniye). None = devre dışı.
ping_timeoutfloatPing'e yanıt bekleme süresi. Aşılırsa bağlantı kesilir.

Exponential backoff with jitter

Her başarısız denemeden sonra bekleme süresini ikiye katla; üzerine rastgele bir değer ekle (jitter). Bu yöntem, birden fazla client'ın aynı anda yeniden bağlanmasını (thundering herd) önler.

reconnect_client.py
import asyncio
import random
import websockets

URI       = "ws://localhost:8765"
MAX_DELAY = 60.0   # saniye — maksimum bekleme

async def connect_with_backoff():
    attempt = 0
    while True:
        try:
            async with websockets.connect(
                URI,
                open_timeout=10,
                ping_interval=20,
                ping_timeout=10,
            ) as ws:
                attempt = 0  # başarıyla bağlandık, sayacı sıfırla
                print("Bağlandı")
                await on_connected(ws)  # subscribe yenile, işleme başla

        except (
            websockets.ConnectionClosed,
            OSError,
            asyncio.TimeoutError,
        ) as exc:
            delay = min(2 ** attempt + random.uniform(0, 1), MAX_DELAY)
            print(f"Bağlantı kesildi ({exc}). {delay:.1f}s sonra tekrar dene.")
            await asyncio.sleep(delay)
            attempt += 1

async def on_connected(ws):
    # Bağlanıldığında subscribe mesajı gönder
    import json
    await ws.send(json.dumps({"type": "subscribe", "payload": {"topic": "sensors"}}))
    async for msg in ws:
        print(f"Mesaj: {msg}")

asyncio.run(connect_with_backoff())

Backoff değerleri (örnek)

Deneme2^attemptJitter (0–1)Toplam bekleme
01s~0.4s~1.4s
12s~0.7s~2.7s
24s~0.2s~4.2s
38s~0.9s~8.9s
532s~0.5s~32.5s
6+60s (max)~0.x s≤60s
NOT

Circuit breaker pattern: belirli sayıda ardışık başarısız denemeden sonra bağlantıyı "açık" (open) duruma al ve yeniden denemeden önce daha uzun süre bekle. Bu, tamamen erişilmez bir sunucuya sürekli bağlanma girişimini önler. Üretim sistemlerinde tenacity veya stamina gibi kütüphaneler bu pattern'i hazır sunar.

Bu bölümde

  • Server restart, network glitch, ping timeout senaryoları
  • open_timeout, ping_interval, ping_timeout parametreleri
  • min(2^attempt + random(0,1), max_delay) formülü
  • Reconnect sonrası subscribe yenileme
  • Circuit breaker pattern özeti

08 Pratik: minimal chat server

Bu bölümde WebSocket ile gerçek bir chat sistemi inşa ediyoruz: broadcast server, nickname yönetimi ve asyncio ile eşzamanlı gönderme/alma.

Mesaj formatı

message_types.json
// Kullanıcı odaya katıldı
{ "type": "join",  "nick": "ahmet", "text": "" }

// Normal mesaj
{ "type": "chat",  "nick": "ahmet", "text": "Merhaba!" }

// Kullanıcı ayrıldı (server üretir)
{ "type": "leave", "nick": "ahmet", "text": "" }

Server

chat_server.py
import asyncio
import json
import websockets

# nick → websocket eşlemesi
USERS: dict = {}

async def broadcast(msg: dict, exclude=None):
    """Tüm bağlı kullanıcılara mesaj gönder."""
    data = json.dumps(msg, ensure_ascii=False)
    targets = [ws for ws in USERS.values() if ws is not exclude]
    if targets:
        await asyncio.gather(*[ws.send(data) for ws in targets])

async def handler(websocket):
    nick = None
    try:
        # İlk mesaj: join frame
        raw = await websocket.recv()
        data = json.loads(raw)
        nick = data.get("nick", "anonim").strip()[:20]

        if nick in USERS:
            await websocket.send(json.dumps({
                "type": "error", "nick": "server",
                "text": f"'{nick}' takma adı zaten kullanımda."
            }))
            await websocket.close(1008, "Nick taken")
            return

        USERS[nick] = websocket
        print(f"[+] {nick} katıldı ({len(USERS)} kullanıcı)")
        await broadcast({"type": "join", "nick": nick, "text": ""}, exclude=websocket)
        await websocket.send(json.dumps({
            "type": "info", "nick": "server",
            "text": f"Hoşgeldin {nick}! Odada {len(USERS)} kişi var."
        }))

        async for raw in websocket:
            msg = json.loads(raw)
            if msg.get("type") == "chat":
                text = str(msg.get("text", ""))[:500]
                await broadcast({"type": "chat", "nick": nick, "text": text})

    except (websockets.ConnectionClosed, json.JSONDecodeError):
        pass
    finally:
        if nick and nick in USERS:
            del USERS[nick]
            print(f"[-] {nick} ayrıldı ({len(USERS)} kullanıcı)")
            await broadcast({"type": "leave", "nick": nick, "text": ""})

async def main():
    async with websockets.serve(handler, "0.0.0.0", 8765):
        print("Chat server → ws://localhost:8765")
        await asyncio.Future()

asyncio.run(main())

Client — eşzamanlı gönderme ve alma

chat_client.py
import asyncio
import json
import sys
import websockets

URI = "ws://localhost:8765"

async def receive_messages(ws):
    async for raw in ws:
        msg = json.loads(raw)
        t, nick, text = msg["type"], msg["nick"], msg.get("text", "")
        if t == "chat":
            print(f"\r[{nick}] {text}")
        elif t == "join":
            print(f"\r*** {nick} odaya katıldı ***")
        elif t == "leave":
            print(f"\r*** {nick} ayrıldı ***")
        elif t in ("info", "error"):
            print(f"\r[server] {text}")

async def send_messages(ws, nick: str):
    loop = asyncio.get_event_loop()
    while True:
        # stdin'i asyncio ile oku (blocking olmadan)
        line = await loop.run_in_executor(None, sys.stdin.readline)
        line = line.rstrip("\n")
        if line.lower() == "/quit":
            await ws.close()
            break
        await ws.send(json.dumps({
            "type": "chat", "nick": nick, "text": line
        }))

async def main():
    nick = input("Nick: ").strip()
    async with websockets.connect(URI) as ws:
        # Join mesajı gönder
        await ws.send(json.dumps({"type": "join", "nick": nick, "text": ""}))
        # Gönderme ve alma'yı eşzamanlı çalıştır
        await asyncio.gather(
            receive_messages(ws),
            send_messages(ws, nick),
        )

asyncio.run(main())

Çalıştırma

bash
# Terminal 1: server
python chat_server.py

# Terminal 2: birinci client
python chat_client.py
# Nick: ahmet

# Terminal 3: ikinci client
python chat_client.py
# Nick: fatma

# Mesaj gönder:
# ahmet terminali: Merhaba!
# fatma terminali: *** ahmet odaya katıldı *** → [ahmet] Merhaba!

TLS ile wss://: SSLContext

tls_server.py
import asyncio
import ssl
import websockets

def create_ssl_context() -> ssl.SSLContext:
    ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    ctx.load_cert_chain(
        certfile="/etc/ssl/certs/server.crt",
        keyfile="/etc/ssl/private/server.key",
    )
    return ctx

async def main():
    ssl_ctx = create_ssl_context()
    async with websockets.serve(
        handler, "0.0.0.0", 8443, ssl=ssl_ctx
    ):
        print("TLS chat server → wss://localhost:8443")
        await asyncio.Future()

asyncio.run(main())
NOT

Self-signed sertifika için: openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -nodes. Client'ta ssl=True (sistem CA kullan) veya özel CA için ssl.create_default_context() ile custom CA yükleyin.

Bu bölümde

  • Chat server: USERS dict, broadcast, join/leave bildirimleri
  • Nick çakışması kontrolü ve 1008 ile bağlantı reddi
  • asyncio.gather ile eşzamanlı gönderme + alma
  • loop.run_in_executor ile async stdin okuma
  • ssl.SSLContext ile wss:// TLS desteği