TCPとUDPのソケット通信について学んだことをまとめてみた

ネットワーク

PythonでTCPのソケット通信を学ぶと、サーバー側には「接続を待ち受けるソケット」と「接続した相手と通信するソケット」が登場します。

一方、UDPのサーバーでは、1つのソケットを使ってデータを直接送受信します。

さらに、TCPには「データを順番どおりに届ける」という特徴もあります。ここで、次のような疑問が浮かびました。

  • TCPサーバーには、なぜ2種類のソケットが必要なのか
  • クライアント側は、なぜ1つのソケットで通信できるのか
  • UDPでは、なぜ接続相手ごとのソケットを作らないのか
  • 接続相手ごとのソケットを作ることと、データの順番を保証することは関係しているのか

この記事では、Pythonのコードをもとに、これらを一つずつ整理します。

先に結論

最初に、重要なポイントをまとめます。

項目 役割
TCPサーバーの待ち受け用ソケット 新しいTCP接続を受け付ける待ち受け用ソケット
TCPサーバーの接続済みソケット 特定のクライアントと通信する接続済みソケット
TCPクライアントのソケット 特定のサーバーと接続・通信するソケット
TCPの順序保証 シーケンス番号などを使い、データを正しい順番に整える仕組み

TCPサーバーで使う2つのオブジェクトは、どちらもソケットです。ただし、役割が異なります。

また、接続相手ごとのソケットを作る目的と、データを並べ替える目的は別です。

接続済みソケットは「誰との接続なのか」を分けるもの。
順序保証は「その接続のデータをどの順番で渡すのか」を管理するもの。

ソケットとは

ソケットとは、アプリケーションがネットワークを通じてデータを送受信するための出入り口です。

Pythonでは、次のコードでソケットオブジェクトを作成します。

import socket

tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  • AF_INET:IPv4を使用する
  • SOCK_STREAM:TCPを使用する

この時点のtcp_socketは、TCPで通信するためのソケットです。その後にどのメソッドを使うかによって、サーバー側・クライアント側の役割が決まります。

教材やサンプルコードでは、ソケットをs、接続をconnと短く書くことがあります。しかし、どちらもPythonで決められた特別な名前ではなく、開発者が自由に付けた変数名です。

短い変数名 この記事で使う名前 意味
s(サーバー側) listening_socket 接続を待ち受けるソケット
s(クライアント側) client_socket サーバーと接続・通信するソケット
conn connection_socket 接続した相手と通信するソケット
addr client_address 接続相手のIPアドレスとポート番号

この記事では役割が伝わる変数名を使います。教材でsconnが出てきたら、上の表のように読み替えてください。

TCPサーバーの2つのソケット

次のコードは、TCPサーバーの簡単な例です。

import socket

listening_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listening_socket.bind(("127.0.0.1", 60001))
listening_socket.listen(1)

connection_socket, client_address = listening_socket.accept()
connection_socket.send(b"Hello World!")
connection_socket.close()

listening_socket.close()

待ち受け用ソケット

listening_socket.bind(("127.0.0.1", 60001))
listening_socket.listen(1)

bind()でIPアドレスとポート番号を紐づけ、listen()で接続待ちの状態にします。

ここでのlistening_socketは、新しいクライアントからの接続を受け付ける待ち受けソケット(リスニングソケット)です。

listen(1)1は、このプログラムが同時に処理できるリクエスト数ではなく、接続を待たせておくキューの大きさです。

接続済みソケット

connection_socket, client_address = listening_socket.accept()

クライアントが接続すると、accept()は次の2つを返します。

戻り値 内容
connection_socket 接続したクライアントと通信するための新しいソケット
client_address クライアントのIPアドレスとポート番号

変数が自分でソケットを作っているのではありません。accept()が返した接続済みソケットを、connection_socketという変数で受け取っています。

実際のデータ通信には、この接続済みソケットを使います。

connection_socket.send(b"Hello World!")

なぜTCPサーバーは接続済みソケットを作るのか

サーバーは、複数のクライアントから接続される可能性があります。

そのため、次の2つの仕事を分ける必要があります。

  1. 新しい接続を受け付ける
  2. 接続済みのクライアントと通信する

待ち受け用ソケットを受付窓口として残し、接続したクライアントごとに接続済みソケットを用意します。

待ち受け用ソケット
 ├─ 接続済みソケット1 ⇄ クライアント1
 ├─ 接続済みソケット2 ⇄ クライアント2
 └─ 接続済みソケット3 ⇄ クライアント3

それぞれの役割は次のとおりです。

  • 待ち受け用ソケット:新しい接続を受け付ける
  • 接続済みソケット1:クライアント1と通信する
  • 接続済みソケット2:クライアント2と通信する
  • 接続済みソケット3:クライアント3と通信する

connection_socket.close()を実行しても、終了するのはそのクライアントとの接続だけです。待ち受け用ソケットを閉じなければ、新しい接続を引き続き受け付けられます。

受付に例えるなら、待ち受け用ソケットは総合受付、接続済みソケットは受付後に割り当てられる個別の担当窓口です。

クライアント側はなぜ1つのソケットでよいのか

クライアント側のコードを見てみます。

import socket

client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect(("127.0.0.1", 60001))

data = client_socket.recv(4096)
print(data)

client_socket.close()

クライアント側では、同じclient_socketを使って次の処理を行います。

  1. サーバーに接続する
  2. データを送受信する
  3. 接続を終了する
client_socket.connect(...)   # 接続
client_socket.recv(4096)     # 受信
client_socket.close()        # 終了

クライアント側のソケットは、最初から特定のサーバーとの1つの接続を担当します。そのため、サーバーのように「受付用」と「個別通信用」を分ける必要がありません。

実際に通信する組み合わせは、次のようになります。

サーバー側の接続済みソケット ⇄ クライアント側のソケット

サーバー側の待ち受け用ソケットと接続済みソケットが、お互いにデータを送受信するわけではありません。

UDPではなぜ接続済みソケットを作らないのか

UDPサーバーは、次のように記述できます。

import socket

udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_socket.bind(("127.0.0.1", 60002))

data, client_address = udp_socket.recvfrom(4096)
print(data, client_address)

udp_socket.close()

UDPでは、TCPのような次の処理がありません。

listening_socket.listen(...)
listening_socket.accept()

その代わり、1つのudp_socketで直接データを受信します。

data, client_address = udp_socket.recvfrom(4096)

UDPはコネクションレスの通信です。通信前にクライアントと継続的な接続を確立しないため、クライアント専用の接続済みソケットも作られません。

どのクライアントから届いたデータなのかは、recvfrom()が返すclient_addressで確認します。

クライアント1 ──データ──→ UDPサーバーのソケット
クライアント2 ──データ──→ UDPサーバーのソケット
クライアント3 ──データ──→ UDPサーバーのソケット

サーバーから返信する場合も、client_addressを宛先に指定します。

data, client_address = udp_socket.recvfrom(4096)
udp_socket.sendto(b"Hello Client!", client_address)

TCPが相手ごとに専用の電話回線をつなぐ通信だとすれば、UDPは宛先を書いた手紙をその都度送る通信に近いイメージです。

TCPはなぜ順番どおりに届くのか

ネットワーク上では、分割されたデータが送信順とは異なる順番で届くことがあります。

TCPでは、送るデータのバイト位置をシーケンス番号で管理します。

シーケンス番号 データ
1001 Hello(5バイト)
1006 半角スペース(1バイト)
1007 World(5バイト)

データが異なる順番で到着しても、受信側のTCP処理がシーケンス番号を確認して並べ直します。

到着した順番:3 → 1 → 2
並べ直した後:1 → 2 → 3

TCPでは、主に次のような仕組みが働いています。

仕組み 役割
シーケンス番号 データのバイト位置を管理する
ACK(確認応答) どこまで受信できたかを送信側へ伝える
再送制御 届かなかったデータを再送する
受信バッファ データがそろうまで一時的に保管する

これらはPythonのコードで自分が直接並べ替えているのではなく、主にOS内部のTCP機能が処理しています。

connection_socket.recv()は、TCPが順番を整えたデータをアプリケーションから読み取るための窓口です。

UDPにも番号を付ければよいのでは?

UDPで送るデータに、自分で順番を表す番号を付けることはできます。

udp_socket.sendto(b"1:Hello", server_address)
udp_socket.sendto(b"2: ", server_address)
udp_socket.sendto(b"3:World", server_address)

受信側で番号を確認すれば、3 → 1 → 2の順に届いたデータを、1 → 2 → 3に並べ直せます。

ただし、UDP自身は並べ替えも再送もしてくれません。アプリケーション側で、次の処理を実装する必要があります。

  • 番号を付ける
  • 順番が違うデータを一時保存する
  • 欠けたデータを判定する
  • 再送を要求する
  • 送信済みデータを保存しておく
  • 重複したデータを除外する
  • タイムアウトを管理する

ここまで実装すると、TCPが持つ機能を一部自作している状態になります。

一方で、UDP上に独自の制御を作る意味もあります。オンラインゲームなら、チャットは再送する一方、古くなったキャラクターの位置情報は再送しない、といった調整ができます。

接続済みソケットと順序保証は別の問題

ここが今回の最も重要なポイントです。

仕組み 解決する問題
接続済みソケット どのクライアントとのTCP接続なのかを区別する
シーケンス番号・ACK・再送制御 データを正しい順番で確実に渡す

接続済みソケットがデータを並べ替えているわけではありません。

より正確には、OS内部のTCP機能が接続ごとに順番を管理し、整えたデータをrecv()から受け取れるようにしています。

つまり、両方ともTCPに関係する仕組みですが、目的は別です。

誰との接続なのかを分ける
        ↓
  接続済みソケット

その接続のデータの順番を整える
        ↓
シーケンス番号・ACK・再送制御

TCPとUDPの使い分け

比較 TCP UDP
通信方式 コネクション指向 コネクションレス
接続処理 あり なし
到達保証 あり なし
順序保証 あり なし
再送制御 あり なし
向いている通信 正確さ・確実性が重要 速さ・リアルタイム性が重要

TCPは、データが欠けたり順番が変わったりすると困る通信に向いています。

  • WebサイトやWeb API
  • メール
  • ファイル転送
  • SSH
  • データベース通信
  • 注文や決済

UDPは、多少データが欠けても、古いデータを待つより最新データを早く届けたい通信に向いています。

  • 音声通話
  • ライブ配信
  • オンラインゲームの位置情報
  • DNS
  • 時刻同期

判断の目安は次のとおりです。

1つも欠けず、順番どおりに届けたい → TCP
少し欠けても、今すぐ届けることを優先したい → UDP

なお、HTTP/3で利用されるQUICはUDPを土台にしていますが、単純なUDP通信ではありません。UDP上に再送制御や暗号化などの仕組みを実装しています。

まとめ

  • TCPサーバーでは、待ち受け用と接続後の通信用という2種類のソケットを使う
  • 待ち受け用ソケットは、新しい接続を受け付ける
  • 接続済みソケットは、特定のクライアントと通信する
  • クライアント側のソケットは、特定のサーバーとの接続と通信の両方を担当する
  • UDPは接続を確立しないため、接続相手ごとのソケットを作らない
  • TCPの順序保証は、シーケンス番号・ACK・再送制御などによって実現される
  • 接続済みソケットを作る目的と、データの順番を保証する目的は別である

最後に、一文で整理します。

接続済みソケットは「誰と通信しているのか」を表し、TCPの順序制御は「その相手から届くデータをどの順番で渡すのか」を管理する。

コメント

タイトルとURLをコピーしました