【Python】プロセス間通信の基本(ソケット編:TCPとUDPを比較)
概要
以前の記事では、プロセス間通信の手段として ソケット(TCP) の基本的な使い方を紹介しました。
今回はその派生として、TCPとUDPの違いを理解しつつ、実際にPythonで両者を体験してみます。通信方式の違いをより深く理解する機会になれば良いなと思います。
TCPとUDPの違い
TCPとUDPは、ネットワーク通信におけるトランスポート層(レイヤー4)で使われる代表的なプロトコルです。どちらも「データをアプリケーション間でやり取りするためのルール」ですが、通信の信頼性を確保する方法や使われ方に違いがあります。
TCPとは
TCP(Transmission Control Protocol) は、通信の信頼性を重視したプロトコルです。通信前に、スリーウェイハンドシェイクと呼ばれる手順で接続を確立し、その後にデータを送信します。
TCPはこのような特性に加えて、通信中に次のような信頼性を確保する仕組みを持っています。
- データの順序保証(送信した順番通りに相手に届く)
- 再送制御(パケットが失われた場合、自動的に再送される)
- 重複排除(同じデータを何度も受信しないように管理される)
これらは、シーケンス番号、確認応答(ACK)、ウィンドウ制御などの仕組みを用いて実現されています。
TCPは信頼性の高い通信を実現できますが、その分、確認応答や制御のオーバーヘッドがあり、UDPと比較すると通信効率が劣ります。
UDPとは
UDP(User Datagram Protocol) は、TCPとは対照的に、コネクションレスなプロトコルです。事前の接続確立などは行わず、送りたいときにすぐデータを送信する、軽量で高速な通信方式です。
ただし、UDPは次のような点で信頼性が保証されません。
- 順番が前後する可能性がある
- 届く保証がない(パケットロスしても自動で再送されない)
- 相手に届いたかどうか確認しない
TCPに比べて欠点が多いように見えますが、この軽さと速さはUDP最大のメリットです。特に、以下のような用途で使われます。
-
リアルタイム性が重要なアプリケーション(ビデオ通話、オンラインゲームなど)
→ データが1パケット欠けても止まるよりは流れ続ける方が優先 -
ブロードキャスト通信(例:ネットワーク内の一斉通知)
→ 多数の相手に送る場合、TCPのように1台1台と接続を確立していたら非効率
ソケットを使ったTCP通信のサンプル
PythonでTCP通信をするには、ソケットを作成する際にSOCK_STREAM
を指定します。
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
サンプルは、ソケット通信のサンプルコード(TCP)にありますので、よろしければご覧ください。
ソケットを使ったUDP通信のサンプル
UDP通信では、ソケットの作成時にSOCK_DGRAM
を指定します。
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
以下は、UDPを使ったサーバとクライアント間でメッセージをやり取りする簡単なサンプルコードです。
サーバ側
クライアントから送信されたメッセージを受信して内容を表示し、"メッセージを返却しました"という定型メッセージをそのまま返信する、シンプルなサーバです。
ソケットは 0.0.0.0
でバインドしているため、自身のどのIPアドレス宛てに届いた通信でも受け付けるようになっています。while True
により、ctrl + c
で明示的に終了するまで、メッセージを受け取り続けます。
import socket
HOST = '0.0.0.0'
PORT = 12345
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.bind((HOST, PORT))
print(f"UDPサーバを起動しました: {HOST}:{PORT}")
while True:
data, client_addr = sock.recvfrom(1024)
message = data.decode('utf-8')
print(f"[受信] {client_addr} から: {message}")
response = "メッセージを返却しました"
sock.sendto(response.encode('utf-8'), client_addr)
print(f"[送信] {client_addr} へメッセージを返却しました")
クライアント側
メッセージをサーバに送信し、サーバからの返答を表示します。end
を入力するまで何度もメッセージを送信できます。また、サーバが応答しない場合は5秒でタイムアウトし、エラーメッセージを表示するようになっています。
import socket
SERVER_IP = input("サーバのIPアドレスを入力してください: ") or '127.0.0.1'
SERVER_PORT = 12345
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.settimeout(5.0)
while True:
message = input("送信メッセージを入力してください(endで終了): ")
if message == 'end':
print("通信を終了します。")
break
sock.sendto(message.encode('utf-8'), (SERVER_IP, SERVER_PORT))
try:
data, _ = sock.recvfrom(1024)
print(f"[サーバからの応答] {data.decode('utf-8')}")
except socket.timeout:
print("サーバからの応答がありませんでした")
通信テスト
サーバ起動
% python3 udp_server.py
UDPサーバを起動しました: 0.0.0.0:12345
クライアントの起動
最初にサーバのIPアドレス入力を求められます。
% python3 udp_client.py
サーバのIPアドレスを入力してください:
- そのまま Enter を押すと 127.0.0.1(ローカル)に送信
- サーバと異なる機器からメッセージを送信する場合、サーバのローカルIPアドレスを指定します。
メッセージ送受信
続いて、サーバに送信するメッセージを指定するように求められます。入力すると、サーバからの応答が表示され、次のメッセージを入力するプロンプトに戻ります。
送信メッセージを入力してください(endで終了): Hello from client!
[サーバからの応答] メッセージを受信しました
送信メッセージを入力してください(endで終了):
再び、サーバ側のターミナルに戻ると、クライアントからのメッセージ受信を確認できます。
% python3 udp_server.py
UDPサーバを起動しました: 0.0.0.0:12345
[受信] ('127.0.0.1', 50434) から: Hello from client!
[送信] ('127.0.0.1', 50434) へメッセージを返却しました
クライアントからメッセージを再送信
ここで、別のターミナルを開いて、クライアントからメッセージを送信してみます。
サーバ側のターミナルを見ると追加のメッセージを受信したことを確認できますが、ポート番号が初回の通信と変わっていることが分かります。
% python3 udp_server.py
UDPサーバを起動しました: 0.0.0.0:12345
[受信] ('127.0.0.1', 50434) から: Hello from client!
[送信] ('127.0.0.1', 50434) へメッセージを返却しました
[受信] ('127.0.0.1', 61272) から: dfd
[送信] ('127.0.0.1', 61272) へメッセージを返却しました
これは、クライアント側でbind()
をしていないため、OSが毎回異なるエフェメラルポート(動的に割り当てられる短命なポート)を使っていることによります。もし、ポート番号を固定したい場合は、クライアント側でもbind()
する必要があります。
【補足】エフェメラルポート
通信時にクライアント側で自動的に割り当てられる一時的なポート番号のことを「エフェメラルポート」と呼びます。別記事にしましたので、よろしければご覧ください。
最後に
TCPやUDPは概念的には理解していたつもりでしたが、実際にソケットを使って通信の流れを体験してみることで、その特徴をより実感を持って理解できた気がします。
Discussion