HOWTO · Python

Python でのソケットプログラミング:初心者ガイド

この記事では、Python でのソケットプログラミングについて説明します。Python でのソケットプログラミングを使用した TCP プロトコルと UDP プロトコルを使用した通信について説明しました。

このページの内容

Python のソケットプログラミングでは、ソケットと呼ばれる通信端点を介して、2 つのプロセス間でデータをやり取りできます。このガイドでは、標準ライブラリの socket モジュールを使ってローカル環境で TCP と UDP のクライアント・サーバー間通信を構築し、それぞれのプロトコルがどのような場合に役立つかを説明します。パッケージのインストールは不要です。

Python のソケットプログラミング:概要と準備

サーバーはクライアントからの接続を待ち、クライアントはサーバーとの通信を開始します。ここでの例では、どちらも同じコンピューター上で実行し、127.0.0.1 と同じポートを使用します。socket.socket() コンストラクターでソケットを作成します。TCP には socket.SOCK_STREAM、UDP には socket.SOCK_DGRAM を選びます。テキストを送信する前にバイト列へエンコードし、受信後にデコードする必要があります。

以下のコードブロックは、それぞれ全体を 1 つのターミナルで一度実行してください。各スクリプトはバックグラウンドスレッドでサーバーを起動し、その後、同じスクリプトがクライアントとして動作します。ループバックインターフェイスを介してメッセージを 1 つ交換し、ソケットを閉じます。

TCP サーバーとクライアントを作成する

TCP は、接続を確立して通信する、信頼性のある順序付きのバイトストリームを使用します。サーバーはアドレスとポートにバインドし、listen() を呼び出して、accept() で接続を待ちます。クライアントは同じアドレスとポートに接続します。接続後、双方がバイト列を送受信できます。sendall() は指定されたバッファー内のすべてのバイトを送信し、recv(1024) は最大 1,024 バイトを読み取ります。TCP の読み取りではアプリケーションのメッセージ境界は示されません。そのため、より大きなプロトコルを実装する場合は、長さのプレフィックスや区切り文字など、独自のフレーミングが必要です。

次の例を実行します。

このセクションのコードと表示されるラベルや出力は英語のままです。

import socket
import threading

HOST = "127.0.0.1"
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
    probe.bind((HOST, 0))
    PORT = probe.getsockname()[1]
ready = threading.Event()


def serve():
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
        server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        server.bind((HOST, PORT))
        server.listen(1)
        ready.set()
        connection, _ = server.accept()
        with connection:
            message = connection.recv(1024).decode("utf-8")
            connection.sendall(("Received: " + message).encode("utf-8"))


thread = threading.Thread(target=serve)
thread.start()
ready.wait()
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client:
    client.connect((HOST, PORT))
    client.sendall(b"Hello over TCP")
    reply = client.recv(1024).decode("utf-8")
thread.join()
print(reply)

オペレーティングシステムが使用可能なローカルポートを割り当てるため、固定ポートの競合を避けて例を実行できます。スレッドを使うことで、クライアントが接続する間にサーバーが接続を受け入れられます。通信後、それぞれのソケットはコンテキストマネージャーによって閉じられます。

UDP サーバーとクライアントを作成する

UDP は、TCP のような接続を確立せず、独立したデータグラムを送信します。サーバーはアドレスにバインドし、recvfrom() で受信を待ちます。この関数は、受信したバイト列と送信元アドレスの両方を返します。クライアントはサーバーのアドレスを指定して sendto() を呼び、その後 recvfrom() で返信と送信元アドレスを受け取ります。次のローカル環境の例では、同じループバックアドレスを使い、サーバーのポートはオペレーティングシステムに選択させます。

次の例を実行します。

このセクションのコードと表示されるラベルや出力は英語のままです。

import socket
import threading

HOST = "127.0.0.1"
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as probe:
    probe.bind((HOST, 0))
    PORT = probe.getsockname()[1]
ready = threading.Event()


def serve():
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as server:
        server.bind((HOST, PORT))
        ready.set()
        message, client_address = server.recvfrom(1024)
        reply = ("Received: " + message.decode("utf-8")).encode("utf-8")
        server.sendto(reply, client_address)


thread = threading.Thread(target=serve)
thread.start()
ready.wait()
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client:
    client.sendto(b"Hello over UDP", (HOST, PORT))
    reply, server_address = client.recvfrom(1024)
thread.join()
print(reply.decode("utf-8"))

データグラムは宛先のアドレスとポートに送られますが、UDP は到着も、複数のデータグラムが順番どおりに到着することも保証しません。この例では、返信を組み立てる前にメッセージのバイト列をデコードします。そのため、返信には Python のバイト列の表現ではなく、読み取り可能なテキストが含まれます。

TCP と UDP の選び方

アプリケーションで順序付きのバイトストリームと、トランスポート層による配信や再送の動作が必要な場合は TCP を選びます。それでも、アプリケーション側で接続の失敗に対処し、メッセージのフレーミングを定義する必要があります。また、TCP は相手側のアプリケーションがメッセージを処理することまでは保証しません。独立したデータグラムが用途に合い、アプリケーションがデータの損失や順序の入れ替わりを許容できる場合、またはそれらに自ら対処できる場合は UDP を選びます。UDP が本質的に高速というわけではありません。性能はネットワーク、処理内容、アプリケーションが必要とする信頼性機能によって異なります。ソケットとネットワークの設定が許せば UDP でブロードキャストできますが、信頼性のあるクライアント・サーバー間通信の一般的な代替手段ではありません。

まとめ

socket モジュールは、どちらのプロトコルでも通信を実装するための基本機能を提供します。TCP の例では、接続を介したデータ交換に SOCK_STREAM、accept()、sendall()、recv() を使います。UDP の例では、宛先を指定したデータグラムの交換に SOCK_DGRAM、sendto()、recvfrom() を使います。まずローカル環境でクライアントとサーバーを動かし、アプリケーションに必要な配信動作に応じて TCP と UDP を選びましょう。