Pythonによるソケット通信の基礎と実装

ソケットの基本概念

ソケット(Socket)は、ネットワーク上でプロセス間通信を実現するためのインターフェースです。BSD UNIXを起源とし、異なるホスト上で動作するプログラム同士がデータをやり取りするための基盤を提供します。電話網に例えるなら、ソケットは電話の差込口に相当し、IPアドレスとポート番号の組み合わせが市外局番と電話番号の役割を果たします。

ソケットの種類

ソケットはアドレスファミリとソケットタイプによって分類されます。

  • アドレスファミリ
    • AF_UNIX: 同一ホスト内のプロセス間通信用
    • AF_INET: IPv4を用いたネットワーク通信用
    • AF_INET6: IPv6を用いたネットワーク通信用
    • その他: AF_NETLINKAF_TIPCなどのLinux特有のファミリ

Pythonはこれらのアドレスファミリをサポートしていますが、一般的なネットワークプログラミングではAF_INETが最も頻繁に使用されます。AF_INETはさらに以下の2つのソケットタイプに分かれます。

  • コネクション型ソケット (SOCK_STREAM): TCPプロトコルを実装し、信頼性の高い双方向通信を提供します。
  • コネクションレス型ソケット (SOCK_DGRAM): UDPプロトコルを実装し、データグラムと呼ばれる単位で通信を行います。

TCP通信の実装

Pythonでは標準ライブラリのsocketモジュールを使用してソケットプログラミングを行います。ソケットの生成はsocket.socket(family, type)で行います。

まず、TCPを用いたエコーサーバーとクライアントの実装例を見てみましょう。サーバーは複数のクライアントを同時に処理できるようにスレッドを使用します。

TCPエコーサーバー

import socket
import threading

MAX_BUFFER = 4096

class TcpEchoServer:
    def __init__(self, host, port):
        self.server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.bind_addr = (host, port)
        self.is_running = True

    def serve(self):
        self.server_sock.bind(self.bind_addr)
        self.server_sock.listen(10)
        print("TCP Server started...")
        try:
            while self.is_running:
                client_conn, client_addr = self.server_sock.accept()
                thread = threading.Thread(target=self._handle_client, args=(client_conn, client_addr))
                thread.daemon = True
                thread.start()
        except KeyboardInterrupt:
            pass
        finally:
            self.server_sock.close()

    def _handle_client(self, conn, addr):
        print(f"Connection established from {addr}")
        with conn:
            while True:
                received_data = conn.recv(MAX_BUFFER)
                if not received_data:
                    break
                response = f"Echo: {received_data.decode()}".encode()
                conn.sendall(response)

if __name__ == '__main__':
    TcpEchoServer('127.0.0.1', 9000).serve()

TCPエコークライアント

import socket

TARGET_HOST = '127.0.0.1'
TARGET_PORT = 9000
BUFFER_SIZE = 4096

def run_tcp_client():
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.connect((TARGET_HOST, TARGET_PORT))
        while True:
            try:
                user_input = input("Message> ")
                if not user_input:
                    continue
                sock.sendall(user_input.encode())
                response = sock.recv(BUFFER_SIZE)
                print(f"Server replied: {response.decode()}")
            except (EOFError, KeyboardInterrupt):
                break

if __name__ == '__main__':
    run_tcp_client()

複数のクライアントが接続する際、TCPサーバーはどのように動作するのでしょうか。accept()メソッドが呼ばれると、元のサーバーソケット(リスニングソケット)は新しい接続要求を待ち続けます。一方で、確立された接続ごとに新しいクライアント通信用ソケットが生成され、スレッドに渡されて並行処理が行われます。

UDP通信の実装

次に、UDPを用いた実装例です。UDPはコネクションを確立しないため、TCPのようにlisten()accept()を必要としません。データの送受信にはsendto()recvfrom()を使用します。

UDPエコーサーバー

import socket

MAX_BUFFER = 4096

class UdpEchoServer:
    def __init__(self, host, port):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.bind_addr = (host, port)

    def serve(self):
        self.sock.bind(self.bind_addr)
        print("UDP Server started...")
        try:
            while True:
                data, sender_addr = self.sock.recvfrom(MAX_BUFFER)
                print(f"Received from {sender_addr}: {data.decode()}")
                reply = f"Ack: {data.decode()}".encode()
                self.sock.sendto(reply, sender_addr)
        except KeyboardInterrupt:
            pass
        finally:
            self.sock.close()

if __name__ == '__main__':
    UdpEchoServer('127.0.0.1', 9001).serve()

UDPエコークライアント

import socket

TARGET_HOST = '127.0.0.1'
TARGET_PORT = 9001
BUFFER_SIZE = 4096

def run_udp_client():
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
        while True:
            try:
                user_input = input("Message> ")
                if not user_input:
                    continue
                sock.sendto(user_input.encode(), (TARGET_HOST, TARGET_PORT))
                response, _ = sock.recvfrom(BUFFER_SIZE)
                print(f"Server replied: {response.decode()}")
            except (EOFError, KeyboardInterrupt):
                break

if __name__ == '__main__':
    run_udp_client()

UDPサーバーはコネクションレスであるため、クライアントからのデータグラムを受信した時点で、その送信元アドレスに対して直接応答を返します。これがTCPサーバーとUDPサーバーの最も大きなアーキテクチャ上の違いです。

タグ: Python socket TCP udp ネットワークプログラミング

6月29日 16:00 投稿