2026年10月9日
2026年10月9日
Pythonのソケットプログラミング入門|TCPサーバーとクライアント・メッセージの区切り・並行処理
Python のソケットプログラミングは、標準ライブラリの socket モジュールで、サーバー側は bind・listen・accept、クライアント側は connect を呼び、sendall() と recv() でバイト列をやり取りするのが基本です。この記事では、最小の TCP エコーサーバーから始めて、初心者がつまずきやすい「TCP にはメッセージの区切りがない」問題の解き方、複数のクライアントを同時に扱う方法、UDP との違い、タイムアウトとエラー処理、TLS での暗号化までを、動くコードで説明します。
ソケットとは何か
ソケットは、プログラムがネットワーク越しにデータを送受信するための窓口です。Web ブラウザも WordPress が動くサーバーも、内部ではソケットを使って通信しています。普段は HTTP ライブラリやフレームワークがこの部分を隠してくれていますが、独自のプロトコルで通信したい、監視用の小さなサーバーを作りたい、通信の不具合を調べたいといった場面では、ソケットを直接扱う知識が役に立ちます。
主に使うのは次の2種類です。
- TCP(SOCK_STREAM):接続してから通信し、データが順番どおり欠けずに届くことを TCP が保証します。多くのアプリケーションはこちらを使います。
- UDP(SOCK_DGRAM):接続せずに1つずつパケットを送ります。軽い代わりに、届かない・順番が入れ替わることがあります。
この記事のコードは Python 3.8 以降で動く書き方にしています(2026年10月現在、サポート中の Python であれば問題なく動きます)。追加のインストールは不要です。
最小の TCP エコーサーバーとクライアント
受け取ったデータをそのまま送り返す「エコーサーバー」で、通信の流れを確認します。
# echo_server.py
import socket
HOST = "127.0.0.1" # 自分のPCからの接続だけを受け付ける
PORT = 50007
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((HOST, PORT))
s.listen()
print(f"listening on {HOST}:{PORT}")
conn, addr = s.accept()
with conn:
print("connected by", addr)
while True:
data = conn.recv(4096)
if not data: # 空のバイト列は「相手が接続を閉じた」合図
break
conn.sendall(data)
# echo_client.py
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(5)
s.connect(("127.0.0.1", 50007))
s.sendall("こんにちは".encode("utf-8"))
print(s.recv(4096).decode("utf-8"))
ターミナルを2つ開き、先にサーバー、次にクライアントを実行すると「こんにちは」が返ってきます。
それぞれの処理の意味は次のとおりです。
- bind:待ち受けるアドレスとポートを決めます。
127.0.0.1は自分の PC からの接続だけ、0.0.0.0はすべてのネットワークからの接続を受け付けます。試すときは 127.0.0.1 にしておくと、意図せず外部に公開することを防げます。 - listen と accept:接続を待ち、接続が来ると、そのクライアント専用の新しいソケット(
conn)が返ります。以降のやり取りはconnで行います。 - SO_REUSEADDR:サーバーを止めてすぐ起動し直すと「Address already in use」で失敗することがあります。この設定はそれを避けるためのものです。
- bytes と str:ソケットで送れるのはバイト列だけです。文字列は
encode("utf-8")で変換して送り、受け取った側でdecode("utf-8")します。
recv() と send() の違いにも注意してください。send() は渡したデータの一部しか送らないことがあり、戻り値で送れたバイト数を返します。全部を送り切るまで繰り返してくれる sendall() を使うのが安全です。
TCP にはメッセージの区切りがない
上のコードは短い文字列なら動きますが、そのまま実用にすると問題が起きます。TCP は「バイトの流れ」を届ける仕組みで、送った単位のまま届くとは限らないためです。
- 10,000 バイト送っても、
recv(4096)が 1 回で全部を返すとは限らない - 短いメッセージを続けて 2 回送ると、受信側で 1 回の
recv()にまとめて届くことがある
手元の PC 内で試すと偶然うまく動くことが多く、ネットワーク越しになってから不具合が出るのが典型的なつまずき方です。
解決するには、メッセージの区切りを自分で決めます。よく使うのは「先頭に長さを付ける」方法と「改行などの区切り文字で区切る」方法です。ここでは前者を struct で実装します。
# framing.py
import struct
HEADER = struct.Struct("!I") # ネットワークバイトオーダーの 4 バイト符号なし整数
MAX_MESSAGE = 1024 * 1024 # 1 メッセージの上限(1MB)
def recv_exact(sock, n):
"""n バイトそろうまで受信を繰り返す"""
buf = bytearray()
while len(buf) < n:
chunk = sock.recv(n - len(buf))
if not chunk:
raise ConnectionError("接続が途中で閉じられました")
buf.extend(chunk)
return bytes(buf)
def send_msg(sock, data: bytes):
sock.sendall(HEADER.pack(len(data)) + data)
def recv_msg(sock) -> bytes:
(length,) = HEADER.unpack(recv_exact(sock, HEADER.size))
if length > MAX_MESSAGE:
raise ValueError("メッセージが大きすぎます")
return recv_exact(sock, length)
送信側は「長さ(4 バイト)+本体」を送り、受信側はまず 4 バイトを読んで長さを知り、その長さぶんを読み切ります。MAX_MESSAGE の上限チェックは省かないでください。悪意のある相手が巨大な長さを送ってくると、そのぶんのメモリを確保しようとしてサーバーが止まる原因になります。
複数のクライアントを同時に扱う
最初のエコーサーバーは 1 つのクライアントしか相手にできません。accept() のたびにスレッドを作ると、複数の接続を同時に処理できます。
# threaded_server.py
import socket
import threading
from framing import recv_msg, send_msg
def handle(conn, addr):
with conn:
conn.settimeout(30) # 30 秒何も来なければ切断
try:
while True:
data = recv_msg(conn)
send_msg(conn, data.upper())
except (ConnectionError, socket.timeout, ValueError) as e:
print(addr, "切断:", e)
def serve(host="127.0.0.1", port=50008):
with socket.create_server((host, port)) as server:
print(f"listening on {host}:{port}")
while True:
conn, addr = server.accept()
threading.Thread(target=handle, args=(conn, addr), daemon=True).start()
if __name__ == "__main__":
serve()
socket.create_server() は、ソケットの作成・bind・listen をまとめて行う関数です(Python 3.8 で追加)。クライアント側も socket.create_connection(("127.0.0.1", 50008), timeout=5) で接続できます。
スレッド方式は分かりやすい反面、接続数が数百、数千と増えるとスレッドの数も増えて重くなります。多数の接続を扱う場合は、標準ライブラリの asyncio.start_server() を使ったイベントループ方式が向いています。asyncio のストリームは改行区切りの readline() や、指定バイト数を読み切る readexactly() を持っているので、上の recv_exact() に相当する処理を自分で書かずに済みます。
UDP の基本
UDP は接続を作らず、sendto() と recvfrom() で 1 つずつデータを送受信します。
# udp_server.py
import socket
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.bind(("127.0.0.1", 50009))
while True:
data, addr = s.recvfrom(2048)
s.sendto(b"pong:" + data, addr)
# udp_client.py
import socket
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.settimeout(3)
s.sendto(b"ping", ("127.0.0.1", 50009))
print(s.recvfrom(2048))
UDP は 1 回の sendto() が 1 つのデータとして届くため、TCP のような区切りの問題はありません。その代わり、届かなかったときの再送や順番の並べ替えはアプリ側の責任です。クライアント側に settimeout() を付けているのは、応答が失われたときに待ち続けないためです。
タイムアウトとエラーの読み方
ソケットでよく出るエラーと、確認する場所をまとめます。
- ConnectionRefusedError:接続先のポートで誰も待ち受けていません。サーバーが起動しているか、ポート番号とアドレスが合っているかを確認します。Linux なら
ss -tlnpで待ち受け中のポートを一覧できます。 - OSError: Address already in use:同じポートを別のプロセスが使っているか、直前に止めたサーバーの接続が残っています。
ss -tlnpで使っているプロセスを探すか、SO_REUSEADDR を設定します。 - socket.timeout(TimeoutError):
settimeout()の時間内に応答がありません。ファイアウォールで通信が止められている場合も、拒否ではなくタイムアウトになることがあります。 - ConnectionResetError / BrokenPipeError:相手が接続を突然切りました。サーバー側では 1 つの接続のエラーでプログラム全体が止まらないよう、接続ごとに例外を捕まえます。
タイムアウトを付けないソケットは、相手が応答しないと永久に待ち続けます。クライアントもサーバーの各接続も、settimeout() を付けておくのが基本です。
自作のサーバーの動作確認には、nc 127.0.0.1 50007(netcat)のような汎用のツールで接続してみると、クライアント側の問題かサーバー側の問題かを切り分けやすくなります。
外部と通信するときの安全対策
ソケットで作ったサーバーをインターネットに公開する場合は、次の点を確認してください。
- 暗号化する:ソケットの通信は暗号化されていません。外部と通信するなら標準ライブラリの
sslを使い、クライアント側はssl.create_default_context()で作ったコンテキストのwrap_socket(sock, server_hostname="example.com")で証明書を検証して接続します。証明書の検証を無効にする設定は使わないでください。 - 受け取ったデータを信用しない:長さの上限、形式のチェックを行い、受け取った内容をそのままコマンドやファイルパスに使いません。
- ポートを必要な範囲だけ開ける:試験中は 127.0.0.1 で待ち受け、公開するときはファイアウォールで接続元を絞ります。
- 既存の仕組みを検討する:HTTP で済む用途なら、ソケットから作るより Web フレームワークや既存のプロトコルを使うほうが、認証や暗号化の実装漏れを減らせます。
まとめ
- TCP サーバーは bind・listen・accept、クライアントは connect で接続し、
sendall()とrecv()でバイト列を送受信します。 - TCP にはメッセージの区切りがないため、長さを先頭に付けるなどの方法で区切りを決め、指定バイト数を読み切る処理を書きます。
- 複数のクライアントはスレッドで扱えますが、接続数が多いなら asyncio が向いています。
- UDP は軽い代わりに、再送や順番の管理がアプリ側の責任です。
- ソケットにはタイムアウトを付け、外部と通信するなら ssl で暗号化し、受け取ったデータには上限とチェックを設けます。