こんにちは!技術メディアの案内人を務めるセキュリティスペシャリストのトモです。
ネットワークやインフラの世界に一歩踏み出すと、必ず出会うのが「TCP」と「UDP」という2つのプロトコルですよね。「TCPは信頼性が高いけれど遅い」「UDPは速いけれど信頼性がない(パケットが消えるかもしれない)」と教科書で習った方も多いのではないでしょうか。
でも、現場のリアルな開発では、「UDPの圧倒的なスピードは欲しいけれど、データが消えたり順番がバラバラになったりするのは困る!」というワガママ(だけど非常に合理的な)課題に直面することがよくあります。オンラインゲームやリアルタイムの動画配信、さらには最新のWebプロトコルである HTTP/3(その裏で動く QUIC)などがまさにそれです。
今回は、信頼性のないUDPの上で、アプリケーション側(私たちの書くコード)の工夫によって「パケットロス」と「順序逆転」を解決する設計指針について、身近な例えを交えながら一歩ずつ丁寧に紐解いていきましょう!
—
そもそもUDPの「パケットロス」と「順序逆転」ってなに?
まずは、UDPというプロトコルがネットワークの中でどんな動きをしているのか、現実世界の「郵便配達」に例えてイメージしてみましょう。
1. UDPは「ハガキのバラ送り」
TCPが「相手と電話を繋いで、一言ずつ『聞こえた?』と確認しながら進める会話」だとすれば、UDPは「宛先を書いてポストに投函するハガキ」です。
あなたは、1冊の長編小説を、1ページずつハガキに書き写して友達に送ることにしました。
- ハガキをポストに入れたら、あとは郵便局(ルーターやスイッチなどのネットワーク機器)にお任せです。
- 郵便局は「届ける努力(ベストエフォート)」はしますが、「絶対に届ける」という保証はしてくれません。
ここで2つの問題が発生します。
2. パケットロス(ハガキの紛失)
途中の郵便局が台風(ネットワークの輻輳・大混雑)でパンクしてしまい、あなたのハガキの束から「3ページ目」がどこかに消えてしまいました。UDPには「届いたかどうかを確認する仕組み」がありません。送信側は送りっぱなし、受信側は3ページ目が消えたことすら気づけません。これがパケットロスです。
3. 順序逆転(ハガキの追い越し)
郵便局では、ハガキを色々なルートで運びます。あるハガキは飛行機で、あるハガキはトラックで運ばれるかもしれません。
その結果、あなたが「1ページ目」「2ページ目」の順番で出したのに、友達の家には「2ページ目」が先に届き、翌日に「1ページ目」が届くということが起こります。これが順序逆転です。
—
アプリケーション層で「信頼性」をDIYする設計指針
「じゃあ、届かなかったり順番が狂ったりしたら、小説が読めないじゃないか!」
その通りです。だからこそ、届いたハガキを処理する「友達(アプリケーション)」の側で、ちょっとしたルールを決めておく必要があります。これがアプリケーション層での信頼性確保です。
具体的には、以下の3つのアイデアをハガキ(データ)に盛り込みます。
1. シーケンス番号(通し番号)を振る
- ハガキの隅に「これは1番目のハガキ」「これは2番目のハガキ」と大きく番号を書いておきます。
2. 受信側で並び替える(バッファリング)
- 友達は、ハガキが届いてもすぐに読まず、まずは机の上に番号順に並べます。
3. 届いていないものを「もう一度送って!」と頼む(再送制御)
- 「1番」「2番」「4番」が届いた時点で、友達は「3番が抜けている!」と気づきます。そこで、あなたに「3番をもう一回送って」と手紙(ACK/NACKや再送要求)を出します。
—
【Pythonで実践】UDP上で「順序並び替え」と「再送」を再現してみよう!
それでは、この仕組みをシンプルなPythonコードで実装してみましょう。
ここでは理解しやすいように、送信側(クライアント)と受信側(サーバー)の簡単なやり取りをシミュレーションします。
送信側(クライアント)のコード
送信データに「シーケンス番号」を付与して送信し、受信側からの「届いたよ(ACK)」を待ちます。一定時間待っても返事が来なければ、再送(リトライ)します。
import socket
import time
# 接続先の設定(ローカルホスト)
SERVER_IP = "127.0.0.1"
SERVER_PORT = 5005
TIMEOUT_SEC = 1.0 # 返事が来なかったら再送する秒数
# UDPソケットの作成
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(TIMEOUT_SEC)
# 送信したいデータリスト
messages = ["こんにちは!", "ネットワークの世界へ", "ようこそ!", "一緒に学びましょう。"]
for seq_num, message in enumerate(messages):
# パケットの構造: "シーケンス番号:メッセージ内容"
# 例: "0:こんにちは!"
packet = f"{seq_num}:{message}".encode('utf-8')
acked = False
attempts = 0
while not acked and attempts < 3:
try:
print(f"[送信] パケット {seq_num} を送ります... (試行 {attempts + 1})")
sock.sendto(packet, (SERVER_IP, SERVER_PORT))
# 受信側からのACK(確認応答)を待つ
ack_data, _ = sock.recvfrom(1024)
ack_msg = ack_data.decode('utf-8')
# ACKに正しいシーケンス番号が含まれているか確認
if ack_msg == f"ACK:{seq_num}":
print(f"[成功] パケット {seq_num} の受信確認が取れました!\n")
acked = True
except socket.timeout:
# タイムアウト(時間切れ)になったら再送ループへ
print(f"[警告] パケット {seq_num} の返事がありません。再送します...")
attempts += 1
print("すべてのメッセージの送信が完了しました!")
sock.close()
受信側(サーバー)のコード
受信したパケットの「シーケンス番号」を確認し、順番がバラバラであっても正しく並び替えて保持します。また、届いたら必ず「届いたよ」という返事(ACK)を送信側に送り返します。
import socket
# 待ち受け設定
LISTEN_IP = "127.0.0.1"
LISTEN_PORT = 5005
# UDPソケットの作成とバインド
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((LISTEN_IP, LISTEN_PORT))
print(f"サーバーが起動しました。ポート {LISTEN_PORT} でパケットを待っています...\n")
# 受信したデータを整理して格納するバッファ(辞書型)
received_buffer = {}
expected_seq = 0 # 次に受け取るべき理想のシーケンス番号
while True:
try:
# パケットの受信
data, addr = sock.recvfrom(1024)
message_str = data.decode('utf-8')
# パケットを「シーケンス番号」と「データ」に分解
seq_str, content = message_str.split(":", 1)
seq_num = int(seq_str)
print(f"[受信] パケット {seq_num} が届きました: '{content}'")
# 受信したパケットをバッファに保存(これで順序が逆転しても後で並び替え可能)
received_buffer[seq_num] = content
# 送信元へ「無事に届いたよ」とACKを返す
ack_packet = f"ACK:{seq_num}".encode('utf-8')
sock.sendto(ack_packet, addr)
# 期待している順番通りにデータが揃っているか確認して出力
while expected_seq in received_buffer:
print(f"-> [アプリ層での処理] {expected_seq}: {received_buffer[expected_seq]}")
# 処理が終わったデータはバッファから消しても良い(今回は簡易化のためそのまま)
expected_seq += 1
print(f"(次に待っているパケット番号: {expected_seq})\n")
except KeyboardInterrupt:
print("\nサーバーを停止します。")
break
sock.close()
—
現場で使われている「洗練された」既存の技術たち
「なるほど、こうやって自分で作ればUDPでも安全に通信できるんだ!」と実感していただけたでしょうか。
実務においては、これらをゼロから自作するだけでなく、すでに世界の天才エンジニアたちが開発した「信頼性付きUDP(Reliable UDP)」のライブラリやプロトコルを使うことがほとんどです。
- QUIC (HTTP/3): Googleが開発し、現在のWeb標準となっているプロトコルです。UDPをベースにしながら、TCPのような接続管理や暗号化、パケットロスへの対処を極めて高い次元で実現しています。
- SRT (Secure Reliable Transport): 主に高画質なライブ映像配信の現場で使われるプロトコルです。パケットロスが発生しやすい不安定なインターネット回線でも、映像が乱れないように瞬時に再送制御を行います。
- WebRTC: ブラウザ同士でリアルタイムにビデオ通話や音声通話を行うための規格です。ここでも裏側ではUDPが使われ、状況に応じてデータの再送や間引きを賢く行っています。
—
まとめ:一歩ずつ理解を深めていきましょう!
今回のポイントを整理してみましょう。
1. UDPは「送りっぱなし」のプロトコル。だから速いけれど、途中で消えたり(パケットロス)、順番が変わったり(順序逆転)する。
2. 解決するには、アプリケーション層で「シーケンス番号(通し番号)」を付け、受信側で一時的に「バッファ(机の上)」に並べてから処理する。
3. 届かなかったものは、送信側が「タイムアウト」を検知して「再送」する。
ネットワークの世界は、一見すると複雑な英単語や仕様書だらけに見えますが、その本質は「現実世界の郵便や会話のルールを、いかにコンピュータに真似させるか」という、とても人間味あふれる知恵の結晶です。
まずは「そうか、ハガキに番号を書いておけばいいんだな!」というイメージを持つところから、第一歩をスタートしてみてください。あなたのインフラ・ネットワーク学習を、これからも全力で応援しています!
コメント