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 で暗号化し、受け取ったデータには上限とチェックを設けます。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲