9.13. TCP sockets

TCP socket hadir dalam dua bentuk yang tampak berbeda namun berbagi tipe dasar yang sama: socket klien yang connect() ke server jarak jauh, dan socket server yang bind(), listen(), serta accept() koneksi yang masuk. Kedua peran menggunakan kelas socket yang sama yang diperkenalkan pada Objek socket; hanya metode yang dipanggil pada keduanya yang berbeda.

9.13.1. TCP klien

Klien paling sederhana membuka koneksi, mengirim permintaan, membaca balasan, dan menutup:

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() menjalankan three-way handshake yang dibahas pada TCP -- aliran byte yang andal dan kembali ketika koneksi sudah terbuka. send() menulis byte ke koneksi; recv() membaca hingga sejumlah byte darinya. Setelah aplikasi selesai, close() menutup koneksi.

Skrip yang sama dibungkus dalam idiom pernyataan with dari Objek socket, sehingga socket ditutup meskipun ada sesuatu yang menyebabkan pengecualian:

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. Membaca hingga selesai

Satu recv() mengembalikan hingga jumlah byte yang diminta -- bisa mengembalikan lebih sedikit, karena TCP adalah stream bukan urutan pesan. Aplikasi harus terus membaca hingga mendapatkan balasan lengkap:

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

Loop berakhir ketika recv() mengembalikan bytes kosong. Hal itu terjadi ketika sisi lain telah menutup setengah koneksinya dengan bersih; aplikasi membaca "akhir stream" sebagai sama dengan "akhir pesan" dalam gaya protokol ini.

9.13.1.2. Mengirim hingga selesai

Peringatan sebaliknya berlaku untuk send(): mungkin mengirim lebih sedikit byte dari yang diminta, mengembalikan jumlah byte yang sebenarnya ditulis. Untuk payload besar, coba ulang sisa yang belum terkirim:

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

sendall() melakukan loop secara internal, sehingga sebagian besar kode cukup memanggil itu dan menghindari percobaan ulang manual:

s.sendall(some_big_bytes)

9.13.2. TCP server

Sisi server terdiri dari empat langkah: klaim port, alihkan socket ke mode mendengarkan, terima koneksi satu per satu, berkomunikasi pada setiap socket yang diterima. Server echo minimal:

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()

Langkah demi langkah:

  • bind() mengklaim host dan port pada kamera. "0.0.0.0" menerima pada antarmuka apa pun; menggantinya dengan IP tertentu membatasi pendengar ke antarmuka tersebut.

  • listen() mengalihkan socket dari socket normal ke socket mendengarkan. Argumennya adalah backlog -- berapa banyak koneksi yang tertunda yang akan diantrekan MicroPython saat aplikasi sibuk. Pilih angka kecil; 1 cukup untuk sebagian besar kasus.

  • accept() memblokir hingga klien terhubung, lalu mengembalikan (conn, addr): socket baru yang mewakili satu koneksi ini, dan alamat klien. Socket mendengarkan itu sendiri tetap terbuka untuk menerima lebih banyak.

  • Semua byte untuk percakapan mengalir melalui conn, socket baru. Pembacaan dan penulisan menggunakan panggilan recv() / send() yang sama seperti pada sisi klien.

  • Ketika klien menutup, recv() mengembalikan b""; loop dalam berakhir dan server menutup ujungnya dengan close().

while True luar melompat kembali ke accept() untuk menunggu klien berikutnya. Server menangani satu klien pada satu waktu dalam bentuk ini; menjalankan beberapa klien secara paralel membutuhkan thread atau asyncio. Yang terakhir adalah subjek halaman berikutnya.

9.13.3. Jebakan umum

  • Memperlakukan recv() sebagai berbentuk pesan. Tidak demikian. Dua panggilan send(b"hi") mungkin tiba sebagai satu recv(4) dari b"hihi", atau sebagai dua recv(2)s. Aplikasi harus menambahkan framing jika batas pesan penting -- baris baru, awalan panjang, apa pun.

  • Lupa mencoba ulang pada pengiriman pendek. Gunakan sendall() untuk apa pun yang lebih dari beberapa ratus byte.

  • Lupa menutup socket yang diterima. Setiap conn adalah socket terpisah; menutup socket mendengarkan tidak menutup yang diterima. Blok with pada keduanya membuat hal ini sulit untuk salah:

    while True:
        with server.accept()[0] as conn:
            # ... talk on conn ...
    
  • Mengikat ulang ke port yang masih dalam TIME_WAIT. Ketika server dimulai ulang dalam beberapa detik setelah ditutup, bind() mungkin gagal dengan "address in use" karena MicroPython masih memegang port untuk koneksi sebelumnya. server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sebelum bind() membersihkan ini.

9.13.4. Apa selanjutnya

Memblokir pada accept() berarti server hanya dapat melayani satu klien pada satu waktu. Memblokir pada recv() berarti satu klien yang lambat menghentikan seluruh loop. Jawaban standar pada kamera adalah asyncio -- jalankan setiap koneksi sebagai tugasnya sendiri, biarkan event loop mendistribusikan di antara mereka. Halaman berikutnya mencakup versi asyncio dari semua yang ada di halaman ini.