ソケットの基本概念
ソケット(Socket)は、ネットワーク上でプロセス間通信を実現するためのインターフェースです。BSD UNIXを起源とし、異なるホスト上で動作するプログラム同士がデータをやり取りするための基盤を提供します。電話網に例えるなら、ソケットは電話の差込口に相当し、IPアドレスとポート番号の組み合わせが市外局番と電話番号の役割を果たします。
ソケットの種類
ソケットはアドレスファミリとソケットタイプによって分類されます。
- アドレスファミリ
AF_UNIX: 同一ホスト内のプロセス間通信用AF_INET: IPv4を用いたネットワーク通信用AF_INET6: IPv6を用いたネットワーク通信用- その他:
AF_NETLINKやAF_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サーバーの最も大きなアーキテクチャ上の違いです。