9.12. UDP ソケット

Python における UDP トラフィックは、データグラムソケット上の 2 つのメソッドで送受信されます。選択した宛先にデータグラムを送り出す sendto() と、データグラムを受信してそれがどこから来たのかを知る recvfrom() です。各呼び出しは 1 つの自己完結したメッセージを移動させ、接続状態はありません。

9.12.1. データグラムの送信

最も単純な UDP 送信は、ソケットコンストラクタの上に書く 1 行の Python です:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.sendto(b"hello", ("192.168.1.20", 9000))
s.close()

これは 192.168.1.20 のポート 9000b"hello" を送信して、そのまま立ち去ります。MicroPython はエフェメラルな送信元ポートを選びます。スクリプトは何もバインドする必要がありません。

同じペイロードを多数の宛先に送信するのは、単なるループです。ソケットは送信間で再利用でき、セットアップする接続もありません:

targets = [
    ("192.168.1.20", 9000),
    ("192.168.1.21", 9000),
    ("192.168.1.22", 9000),
]

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
    for addr in targets:
        s.sendto(b"hello", addr)

9.12.2. データグラムの受信

データグラムを受信するには、ソケットが送信側が宛先として使用する既知のポートを確保する必要があります。それが bind() の呼び出しです:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("0.0.0.0", 9000))

while True:
    data, src = s.recvfrom(1024)
    print("from", src, "got", data)

"0.0.0.0" というアドレスは「カメラ上のすべての IPv4 インターフェース」を意味します。どの Wi-Fi または Ethernet インターフェースがパケットを取り込んでも、ポート 9000 はこのソケットに属します。

recvfrom() への 1024 という引数は、返されるバッファに読み込む最大バイト数です。このサイズを超える UDP データグラムは 切り詰められます 。アプリケーションが想定する最大のデータグラムに合わせて値を選んでください。

recvfrom()(data, src) を返します。受信したバイト列と、送信元のアドレスです。送信元のアドレスが返信先となるため、小さなリクエスト/レスポンスプロトコルを簡単に書くことができます:

while True:
    request, src = s.recvfrom(1024)
    if request == b"ping":
        s.sendto(b"pong", src)

デフォルトでは recvfrom() はデータグラムが届くまで ブロック します。ブロックさせないためのパターン(タイムアウト、ノンブロッキングソケット、asyncio)は asyncioを使ったソケット にあります。

9.12.3. リクエストとリプライ

2 つの短いスクリプトです。1 つはリクエストを送信し、もう 1 つは受信して返信します。

受信側:

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("0.0.0.0", 9000))

while True:
    req, src = s.recvfrom(64)
    print("got", req, "from", src)
    s.sendto(b"ack: " + req, src)

送信側:

import socket

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
    s.settimeout(2.0)                              # 2 s reply window
    s.sendto(b"ping", ("192.168.1.20", 9000))
    try:
        reply, _ = s.recvfrom(64)
        print("reply:", reply)
    except OSError:
        print("no reply in 2 s -- packet lost?")

送信側で注目すべき点がいくつかあります:

  • bind()connect() もありません。UDP クライアントはただ送信するだけです。

  • settimeout() は受信呼び出しに期限を設けます。2 秒以内に返信が届かない場合、呼び出しは永久にブロックする代わりに OSError を発生させます。これはパケットの損失を検出する自然な方法です。

  • with ブロックがソケットを自動的に閉じます。

9.12.4. データグラムのサイズ制限

UDP データグラムは理論上は約 64 KB まで可能ですが、実用上の制限はもっと小さくなります。送信側と受信側の間の経路上のすべてのリンクには 最大伝送単位(MTU)があります。これはそのリンクが 1 フレームで運べる最大の連続バイトブロックです。Ethernet と Wi-Fi はどちらもこれを約 1500 バイトに制限しており、ほぼすべてのインターネット経路はどこかでその制限にたどり着きます。

データグラムが横断しなければならないリンクの MTU を超えると、ネットワーク層はそれをより小さな フラグメント に分割し、宛先で再構成します。UDP 自体は分割を一切認識しませんが、フラグメントにはいくつかの厄介な性質があります:

  • いずれか 1 つのフラグメントが失われると、データグラム全体が受信側で破棄されます。フラグメントごとの再送はありません。損失の確率はフラグメント数とともに増大します。

  • 一部のネットワークやファイアウォールは、フラグメント化されたパケットを不審なものとみなして完全に破棄します。

  • 再構成は受信側でメモリを消費しますが、マイクロコントローラではメモリが不足しがちです。

カメラにおける実用的なルールは、UDP メッセージを 1500 バイトより十分に小さく保つことです。約 1400 バイトにしておくと、IP ヘッダーと UDP ヘッダー、経路が追加するトンネリングのオーバーヘッド、そして Ethernet、Wi-Fi、VPN リンク間の MTU の小さな変動のための余地が残ります。それ以上を送信する必要があるアプリケーションは、アプリケーション層でデータをチャンクに分けるか、分割と再構成を自動的に処理する TCP に切り替えるべきです。

9.12.5. よくある落とし穴

  • UDP がパケットを失う可能性があることを忘れること。 静かなローカルネットワークでは完璧に動作するコードが、より混雑した、あるいはより広いネットワークでは微妙に失敗することがあります。常にメッセージが届かなかった可能性を想定して設計してください。

  • 送信側が送信する前に受信側がバインドされていないこと。 誰もリッスンしていないポートに送信されたデータグラムは、黙って破棄されます。先に受信側を起動してください。

  • 経路の MTU より大きなデータグラムを送信すること。 前のセクションを参照してください。メッセージは約 1400 バイト未満に保ってください。

上記のパターンは、カメラが必要とするほぼすべての UDP の用途をカバーします。次のページでは TCP について同等の内容を扱います。