9.13. TCP soketleri

TCP soketleri, farklı görünen ancak aynı temel türü paylaşan iki biçimde gelir: uzak bir sunucuya connect() ile bağlanan istemci soketleri ve gelen bağlantıları bind(), listen() ve accept() eden sunucu soketleri. Her iki rol de Soket nesneleri sayfasında tanıtılan aynı socket sınıfını kullanır; yalnızca üzerlerinde çağrılan yöntemler farklılaşır.

9.13.1. Bir TCP istemcisi

En basit istemci bir bağlantı açar, bir istek gönderir, yanıtı okur ve kapatır:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("192.168.1.20", 9000))

s.send(b"hello\n")
reply = s.recv(1024)
print("reply:", reply)

s.close()

connect(), TCP – güvenilir bir bayt akışı sayfasında ele alınan üç yönlü el sıkışmayı çalıştırır ve bağlantı açıldığında geri döner. send() bağlantıya bayt yazar; recv() ondan belirli bir sayıya kadar bayt okur. Uygulama işini bitirdiğinde close() bağlantıyı kapatır.

Aynı betik, Soket nesneleri sayfasındaki with deyimi deyimiyle sarmalanmıştır; böylece bir şey istisna fırlatsa bile soket kapatılır:

import socket

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect(("192.168.1.20", 9000))
    s.send(b"hello\n")
    print(s.recv(1024))

9.13.1.1. Bitene kadar okuma

Tek bir recv() çağrısı, istenen bayt sayısına kadar döndürür – daha azını döndürebilir, çünkü TCP bir mesaj dizisi değil bir akıştır. Uygulamanın, tam yanıtı alana kadar okumaya devam etmesi gerekir:

chunks = []
while True:
    chunk = s.recv(1024)
    if not chunk:                  # empty bytes -> other side closed
        break
    chunks.append(chunk)
reply = b"".join(chunks)

Döngü, recv() boş bir bytes döndürdüğünde sona erer. Bu, karşı taraf kendi bağlantı yarısını düzgünce kapattığında gerçekleşir; uygulama bu tür protokollerde “akış sonu”nu “mesaj sonu” ile aynı şekilde okur.

9.13.1.2. Bitene kadar gönderme

Tersi uyarı send() için geçerlidir: istenenden daha az bayt gönderebilir ve gerçekten yazılan bayt sayısını döndürür. Büyük yükler için, gönderilmeyen kalanı yeniden deneyin:

payload = some_big_bytes
while payload:
    n = s.send(payload)
    payload = payload[n:]

sendall() döngüyü dahili olarak yapar, böylece çoğu kod yalnızca bunu çağırabilir ve manuel yeniden denemeyi atlayabilir:

s.sendall(some_big_bytes)

9.13.2. Bir TCP sunucusu

Sunucu tarafı dört adımdan oluşur: bir port talep et, soketi dinleme moduna geçir, bağlantıları tek tek kabul et, kabul edilen her sokette iletişim kur. Minimal bir yankı (echo) sunucusu:

import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("0.0.0.0", 9000))
server.listen(1)
print("listening on port 9000")

while True:
    conn, addr = server.accept()
    print("connection from", addr)

    while True:
        data = conn.recv(1024)
        if not data:
            break
        conn.send(data)            # echo back

    conn.close()

Adım adım:

  • bind() kamerada bir host ve port talep eder. "0.0.0.0" herhangi bir arabirimde kabul eder; bunu belirli bir IP ile değiştirmek dinleyiciyi yalnızca o arabirimle sınırlar.

  • listen() soketi normal bir soketten dinleyen bir sokete geçirir. Argüman backlog değeridir – uygulama meşgulken MicroPython’ın kuyruğa alacağı bekleyen bağlantı sayısı. Küçük bir sayı seçin; çoğu durumda 1 yeterlidir.

  • accept() bir istemci bağlanana kadar engeller, ardından (conn, addr) döndürür: bu tek bağlantıyı temsil eden yeni bir soket ve istemcinin adresi. Dinleyen soketin kendisi daha fazlasını kabul etmek için açık kalır.

  • Konuşmanın tüm baytları yeni soket olan conn üzerinden akar. Okumalar ve yazmalar, istemci tarafındaki ile aynı recv() / send() çağrılarını kullanır.

  • İstemci kapattığında recv() b"" döndürür; iç döngü sona erer ve sunucu close() ile kendi tarafını kapatır.

Dıştaki while True bir sonraki istemciyi beklemek için accept() çağrısına geri atlar. Sunucu bu biçimde aynı anda bir istemciyi ele alır; paralel olarak birden fazla istemci çalıştırmak ya iş parçacıkları ya da asyncio gerektirir. İkincisi bir sonraki sayfanın konusudur.

9.13.3. Sık karşılaşılan tuzaklar

  • recv()’i mesaj biçimli sanmak. Öyle değildir. İki send(b"hi") çağrısı tek bir recv(4) ile b"hihi" olarak gelebilir veya iki recv(2) olarak gelebilir. Mesaj sınırları önemliyse, uygulamanın çerçeveleme eklemesi gerekir – bir satır sonu, bir uzunluk öneki, her neyse.

  • Kısa gönderimlerde yeniden denemeyi unutmak. Birkaç yüz bayttan büyük her şey için sendall() kullanın.

  • Kabul edilen soketi kapatmayı unutmak. Her conn ayrı bir sokettir; dinleyen soketi kapatmak kabul edilen soketleri kapatmaz. Her ikisindeki with blokları bunu yanlış yapmayı zorlaştırır:

    while True:
        with server.accept()[0] as conn:
            # ... talk on conn ...
    
  • Hâlâ TIME_WAIT durumundaki bir porta yeniden bağlanmak. Bir sunucu kapanmasından birkaç saniye sonra yeniden başladığında, MicroPython hâlâ önceki bağlantı için portu tuttuğundan bind() “address in use” hatasıyla başarısız olabilir. bind() öncesinde server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) bunu temizler.

9.13.4. Sırada ne var

accept() üzerinde engellemek, sunucunun aynı anda yalnızca bir istemciye hizmet verebileceği anlamına gelir. recv() üzerinde engellemek, tek bir yavaş istemcinin tüm döngüyü askıya alması anlamına gelir. Kameradaki standart yanıt asyncio‘dur – her bağlantıyı kendi görevi olarak çalıştırın, olay döngüsünün aralarında görev dağıtmasına izin verin. Bir sonraki sayfa bu sayfadaki her şeyin asyncio sürümlerini ele alır.