9.13. TCP ソケット

TCP ソケットには、見た目は異なるものの同じ基盤となる型を共有する 2 つの形態があります。リモートサーバーへ connect() する クライアント ソケットと、bind()listen() を行い、着信接続を accept() する サーバー ソケットです。どちらの役割も ソケットオブジェクト で紹介した同じ socket クラスを使用し、違うのはそれらに対して呼び出すメソッドだけです。

9.13.1. TCP クライアント

最も単純なクライアントは、接続を開き、リクエストを送信し、応答を読み取り、そして閉じます:

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 -- 信頼性の高いバイトストリーム で扱った 3 ウェイハンドシェイクを実行し、接続が確立されると戻ります。send() は接続にバイト列を書き込み、recv() は接続から指定したバイト数までを読み取ります。アプリケーションが処理を終えたら、close() で接続を切断します。

ソケットオブジェクト で紹介した with 文のイディオムで同じスクリプトをラップしたもので、何かが例外を発生させた場合でもソケットが閉じられます:

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. 完了するまで読み取る

1 回の recv() は要求したバイト数 まで を返します。TCP はメッセージの列ではなくストリームであるため、それより少ない数を返すこともあります。アプリケーションは応答全体を取得するまで読み取りを続ける必要があります:

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

ループは recv() が空の bytes を返したときに終了します。これは相手側が接続の自分の側を正常にクローズしたときに発生します。この種のプロトコルでは、アプリケーションは「ストリームの終端」を「メッセージの終端」と同じものとして読み取ります。

9.13.1.2. 完了するまで送信する

逆の注意点が send() にも当てはまります。要求したバイト数より 少なく 送信することがあり、実際に書き込まれたバイト数を返します。大きなペイロードの場合は、送信されなかった残りを再試行してください:

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

sendall() は内部でループを実行するため、ほとんどのコードは単にこれを呼び出すだけで手動での再試行を避けられます:

s.sendall(some_big_bytes)

9.13.2. TCP サーバー

サーバー側は 4 つのステップで構成されます。ポートを確保し、ソケットをリスニングモードに切り替え、接続を 1 つずつ受け入れ、受け入れた各ソケット上でやり取りします。最小限のエコーサーバーは次のとおりです:

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

ステップごとに見ていきます:

  • bind() はカメラ上のホストとポートを確保します。"0.0.0.0" は任意のインターフェースで受け入れます。特定の IP に置き換えると、リスナーはそのインターフェースに限定されます。

  • listen() はソケットを通常のソケットから リスニング ソケットへ切り替えます。引数は バックログ で、アプリケーションがビジー状態のときに MicroPython が保留中の接続をいくつキューに入れるかを指定します。小さな数を選んでください。ほとんどの場合 1 で問題ありません。

  • accept() はクライアントが接続するまでブロックし、その後 (conn, addr) を返します。これはこの 1 つの接続を表す 新しい ソケットと、クライアントのアドレスです。リスニングソケット自体はさらに接続を受け入れるために開いたままになります。

  • やり取りのすべてのバイトは新しいソケットである conn を通じて流れます。読み取りと書き込みには、クライアント側と同じ recv() / send() の呼び出しを使用します。

  • クライアントが接続を閉じると、recv()b"" を返します。内側のループが終了し、サーバーは close() で自分側を閉じます。

外側の while Trueaccept() に戻り、次のクライアントを待ちます。この形ではサーバーは一度に 1 つのクライアントを処理します。複数のクライアントを並行して実行するには、スレッドか asyncio のどちらかが必要です。後者は次のページのテーマです。

9.13.3. よくある落とし穴

  • recv() をメッセージ形式として扱うこと。 そうではありません。2 回の send(b"hi") 呼び出しは、1 回の recv(4)b"hihi" として届くこともあれば、2 回の recv(2) として届くこともあります。メッセージの境界が重要な場合、アプリケーションはフレーミング(改行、長さプレフィックスなど何でも)を追加する必要があります。

  • 短い送信時の再試行を忘れること。 数百バイトを超えるものには sendall() を使用してください。

  • 受け入れたソケットを閉じ忘れること。 それぞれの conn は別個のソケットです。リスニングソケットを閉じても、受け入れたソケットは閉じられません。両方に with ブロックを使うと、これを間違えにくくなります:

    while True:
        with server.accept()[0] as conn:
            # ... talk on conn ...
    
  • まだ TIME_WAIT 状態にあるポートへ再バインドすること。 サーバーがクローズから数秒以内に再起動すると、MicroPython が前の接続のためにまだポートを保持しているため、bind() が「address in use」で失敗することがあります。bind() の前に server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) を実行すると、これが解消されます。

9.13.4. 次のステップ

accept() でのブロックは、サーバーが一度に 1 つのクライアントしか処理できないことを意味します。recv() でのブロックは、1 つの遅いクライアントがループ全体を止めてしまうことを意味します。カメラにおける標準的な答えは asyncio です。各接続をそれ自体のタスクとして実行し、イベントループにそれらの間でディスパッチさせます。次のページでは、このページのすべての asyncio バージョンを扱います。