ソケットは、プログラムがネットワーク越しに相手とデータをやり取りするときの端点です。用語としては「通信の出入り口」と説明されますが、実際に手が動くようになるのはコードを書いて接続が張られる様子を見たときでしょう。この記事では、ソケットが何の組み合わせで決まるのかを整理したうえで、Pythonの標準ライブラリだけで動くTCPサーバーとクライアント、UDP、UNIXドメインソケットのコードを順に動かします。あわせて、ss コマンドでのソケット状態の観測、タイムアウト設計、生のソケットを自前で書くべきかどうかの判断までを、公式ドキュメントを出典として並べました。
まとめ:ソケットは通信の端点で、Pythonなら十数行で動かして確かめられる
ソケットは、アドレスファミリー(どの経路で通信するか)とソケットタイプ(データをどう区切って運ぶか)の組み合わせで作られる通信の端点です。IPネットワークであれば「IPアドレスとポート番号の組」がその端点を指します。POSIXの socket() 関数は、ドメイン・タイプ・プロトコルの3引数を受け取って記述子を返す関数として規定されており、この形はC言語でもPythonでもGoでも変わりません。
実装で最初に押さえる分岐は3つあります。1つ目はTCP(SOCK_STREAM)かUDP(SOCK_DGRAM)かで、データの境界が保たれるかを決めます。2つ目はブロッキングかノンブロッキングかで、応答が返らない相手にプログラムが固まるかどうかが変わる。3つ目は同一ホスト内で完結するかで、閉じるならUNIXドメインソケットの方が経路は短くなります。
そして稼働後に効いてくるのが、ソケットの状態を見る手段です。ss -tanp を叩けば、待ち受け中(LISTEN)・確立済み(ESTAB)・後片付け中(TIME-WAIT)が一覧で出る。再起動したサーバーが「アドレスが既に使われています」で立ち上がらない現象も、ここで確認できます。
ソケットの正体をアドレスファミリーとソケットタイプの組み合わせで確認する
「ソケットとは何か」を一言で言えば、OSが管理する通信の端点で、プログラムからはファイル記述子として見えるものです。実際に作るときは、必ず2つの軸を指定します。
IPアドレスとポート番号の組がソケットを一意に識別する仕組み
1つ目の軸がアドレスファミリーで、通信相手をどう名指しするかを決めます。Pythonのsocketモジュールのドキュメント(3.14.7時点)によれば、AF_INET はIPv4のアドレスファミリーでアドレスを (host, port) の2要素タプルで表し、AF_INET6 は (host, port, flowinfo, scope_id) の4要素タプル、AF_UNIX はファイルシステム上のノードに束縛されるソケットで文字列またはバイト列を取ります。
IPを使う場合、通信は「送信元IPアドレス・送信元ポート・宛先IPアドレス・宛先ポート」の4つ組で識別されます。だから1つのサーバーポートに対して何百ものクライアントが同時に繋がっても、送信元ポートが違うぶんだけ別の接続として区別できる。TCP/IPの階層構造そのものについてはTCP/IPとは?4階層モデルとOSI参照モデルの違いを実装目線で解説で整理しています。ソケットが階層のどこに位置し、TCPとUDPのAPI手順がどう違うかを概念から追いたい場合は、ソケット通信とは?TCP/UDPソケットの仕組み・API手順・実装の勘所を実装者向けに解説を先に読んでおくと、本記事のコードが何をしているか掴みやすくなります。
SOCK_STREAMとSOCK_DGRAMが決めるデータ境界の扱いを押さえる
2つ目の軸がソケットタイプです。SOCK_STREAM は接続を張ってからバイト列を流す方式で、TCPが対応します。送った順にすべて届く代わりに、データの切れ目は保たれません。100バイトを2回送っても、受け取る側では200バイトが一度に来ることもあれば、60バイトと140バイトに割れて来ることもある。
SOCK_DGRAM は接続を張らずに1個の塊(データグラム)を投げる方式で、UDPが対応します。こちらは境界が保たれ、送った1個が受け取る側でも1個として出てくる代わりに、届く保証も順序の保証もない。この違いが受信ループの形を決めます。
| 比較軸 | SOCK_STREAM(TCP) | SOCK_DGRAM(UDP) |
|---|---|---|
| 接続の確立 | 必要(3ウェイ) | 不要 |
| データの境界 | 保たれない | 1発ずつ保たれる |
| 到達と順序 | 再送で保証する | 保証しない |
| 主な用途 | API・DB接続・ファイル転送 | DNS・映像配信・計測値 |
Pythonの標準ライブラリだけでTCPのサーバーとクライアントを動かす
ここからは実際に動くコードを置く。追加のライブラリは要らず、標準の socket モジュールだけで完結します。手元にPythonが入っていれば、2つの端末を開いてそのまま試せます。
create_serverで待ち受け側を立ててacceptで接続を受け取る
サーバー側は、待ち受け用のソケットを作り、アドレスに紐づけ(bind)、接続待ちの列を用意し(listen)、来た接続を取り出す(accept)という順に進みます。Pythonにはこの3手をまとめた socket.create_server() があり、ドキュメントによればバージョン3.8で追加された関数です。引数は family・backlog・reuse_port・dualstack_ipv6 をキーワードで受け取ります。
import socket
# 待ち受け側:ポート50007で接続を待ち、受け取った文字列を大文字にして返す
with socket.create_server(("127.0.0.1", 50007), backlog=16) as server:
print("listening on", server.getsockname())
conn, addr = server.accept()
with conn:
print("connected from", addr)
while True:
data = conn.recv(4096)
if not data:
break
conn.sendall(data.upper())
accept() が返す conn は、待ち受け用とは別の、この接続専用のソケットです。待ち受け用は接続を受け付ける役目だけを持ち、読み書きは conn に対して行う。1つのポートで複数のクライアントを同時に捌けるのは、接続ごとにこの conn が作られるからです。
create_connectionで接続しsendallとrecvでデータを送り返す
クライアント側は、socket.create_connection() にアドレスとタイムアウトを渡します。この関数はホスト名がIPv4とIPv6の両方に解決される場合、成功するまで順に試す。引数の source_address はバージョン3.2で、失敗を ExceptionGroup でまとめて送出する all_errors はバージョン3.11で追加されました。
import socket
with socket.create_connection(("127.0.0.1", 50007), timeout=5.0) as sock:
sock.sendall(b"hello socket")
sock.shutdown(socket.SHUT_WR) # 送信側だけ閉じて「送り終えた」と伝える
chunks = []
while True:
chunk = sock.recv(4096)
if not chunk:
break
chunks.append(chunk)
print(b"".join(chunks).decode())
shutdown(SHUT_WR) は送信方向だけを閉じる呼び出しです。Socket Programming HOWTOでは、クライアントが要求を出してすぐ shutdown(1) することでサーバーに「送信は完了したがまだ受信はできる」と伝えられる、という使い方が示されています。同ドキュメントは、終わったら必ず close を呼ぶようにも促している。閉じ忘れた相手側は、こちらの応答が遅れているだけだと解釈してハングし得るためです。
recvが要求どおりの長さを返さない前提で受信ループを組み立てる
初学者がつまずくのはここです。HOWTOは send と recv がネットワークバッファに対して働くもので、手渡した分や要求した分をすべて処理してくれるとは限らないと明記しています。さらにソケットには伝送終了(EOT)が無いため、メッセージの終わりを判定するには、固定長にする・区切り文字を使う・長さ標識を付ける・接続を閉じて終わらせる、のいずれかが要ると書かれています。
実務で使いやすいのは長さ標識です。先頭に4バイトの長さを付け、その分だけ確実に読み切る補助関数を用意します。
import struct
def recv_exactly(sock, size):
"""指定バイト数を読み切るまでrecvを繰り返す"""
buf = bytearray()
while len(buf) != size:
chunk = sock.recv(size - len(buf))
if not chunk:
raise ConnectionError("peer closed before all bytes arrived")
buf.extend(chunk)
return bytes(buf)
def send_message(sock, payload):
sock.sendall(struct.pack("!I", len(payload)) + payload)
def recv_message(sock):
header = recv_exactly(sock, 4)
length = struct.unpack("!I", header)[0]
return recv_exactly(sock, length)
なお recv() が0バイトを返したときの意味は特別です。HOWTOはこれを「向こう側が接続を閉じてしまった、または閉じようとしている途中」であり、その接続でこれ以上データを受け取ることはないと説明しています。上のコードで if not chunk を切断として扱っているのはこのためです。
UDPソケットをsendtoとrecvfromで動かしてTCPとの差を確かめる
UDPは接続を張りません。accept() も connect() も無く、宛先を毎回指定して投げるだけです。そのぶんコードは短くなりますが、届かなかったときの面倒はアプリケーション側が見ることになります。
接続を張らないUDPのサーバーとクライアントを最小構成で動かす
サーバー側は bind() して recvfrom() で待ち、返信先のアドレスは受信時に一緒に受け取ります。
import socket
# 受信側:1発受け取って、そのまま送り主へ返す
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as server:
server.bind(("127.0.0.1", 50008))
data, addr = server.recvfrom(65535)
print("received", len(data), "bytes from", addr)
server.sendto(data.upper(), addr)
# 送信側:2秒待っても返らなければTimeoutErrorで抜ける
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client:
client.settimeout(2.0)
client.sendto(b"ping", ("127.0.0.1", 50008))
print(client.recvfrom(65535))
TCP版と比べると、待ち受けの列(listen)も接続専用ソケット(conn)も出てきません。recvfrom() が返すのは1個のデータグラム全体で、TCPのように途中で割れることはない。
到達保証も順序保証もないUDPが実装の側にどう跳ね返るかを見る
UDPを選ぶと、再送・順序整列・重複排除がアプリケーションの仕事になります。計測値を毎秒送るような用途では、1件落ちても次の値が来るので割り切れる。順番に意味がある処理をUDPで組むと、TCPが内部でやることを自分で書き直す羽目になります。
判断の目安は「落ちたデータを捨ててよいか」です。捨ててよいなら遅延の小さいUDP、捨てられないならTCP、という切り分けで大半は片が付きます。トランスポート層の役割分担をOSIの階層で確認したい場合は、OSI参照モデルとは?7階層の役割・各層プロトコル・TCP/IPとの違いを解説を参照してください。
UNIXドメインソケットを作って同一ホスト内の通信経路を切り替える
通信相手が同じホスト内にいるなら、IPを経由しない選択肢があります。UNIXドメインソケット(AF_UNIX)で、アドレスにはIPとポートではなくファイルパスを使う。アプリケーションサーバーとリバースプロキシの間や、データベースへの接続で見かける形式です。
パス名にバインドするUNIXドメインソケットを作って通信する
Linuxのunix(7)マニュアルによれば、UNIXドメインソケットは SOCK_STREAM(ストリーム指向)、SOCK_DGRAM(メッセージ境界を保つデータグラム)、SOCK_SEQPACKET(接続指向で境界を保ち順序も保証。Linux 2.6.4以降)の3タイプに対応します。コードの形はTCPとほとんど同じで、アドレスの与え方だけが変わります。
import os
import socket
path = "/tmp/echo.sock"
if os.path.exists(path):
os.unlink(path) # 前回のソケットファイルが残っているとbindが失敗する
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as server:
server.bind(path)
server.listen(16)
conn, _ = server.accept()
with conn:
conn.sendall(conn.recv(4096))
接続する側は sock.connect(path) と書くだけです。パス名に紐づくため、そのファイルへのアクセス権限がそのまま接続の可否になる。ファイアウォールやポート番号の割り当てを考えずに、ファイルのパーミッションで制御できます。
抽象名前空間とsun_pathの長さ制限が運用に効いてくる場面
マニュアルはアドレスの形式を3つに区別しています。ファイルシステム上のパス名に紐づく pathname、バインドされていない unnamed、ファイルシステムと無関係な abstract です。抽象名前空間は sun_path の先頭がヌルバイトであることで識別され、名前はファイルシステムのパス名と何の関係も持ちません。Pythonでは sock.bind("\0echo") のように先頭にヌル文字を置いて指定します。
もう1つ踏みやすいのが長さ制限です。マニュアルには「Linuxでは sun_path は108バイト」と明記され、移植性のためには92バイト程度を想定すべきとも書かれている。コンテナ内の深い階層にソケットファイルを置くと、この上限に当たって bind が失敗します。置き場は短いパスに固定しておくのが無難です。
ssコマンドでソケットの状態を観測してTIME-WAITの滞留を見る
コードが動いたら、次はOS側からソケットがどう見えているかを確認します。ここを見られるかどうかで、障害時の切り分け速度が変わります。
ss -tanpでリッスン中と確立済みのソケットを一覧で確かめる
Linuxでは ss コマンドが標準です。-t でTCP、-a で全状態、-n で名前解決を止め、-p でプロセスを表示します。
$ ss -tanp
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 4096 127.0.0.1:50007 0.0.0.0:*
ESTAB 0 0 127.0.0.1:50007 127.0.0.1:41902
TIME-WAIT 0 0 127.0.0.1:41900 127.0.0.1:50007
読み方は単純で、LISTENが待ち受け中、ESTABが確立して通信中、TIME-WAITが閉じた後の後片付け中のソケットです。Recv-Qに数字が溜まっていればアプリケーションが読み出せていない、Send-Qに溜まっていれば相手が受け取れていない、という当たりが付きます。送信側のキューが詰まる背景にはネットワーク側の混雑もあるため、そちらの見立ては輻輳とは?ネットワーク混雑の仕組み・原因・TCP輻輳制御から対策の判断まで実装者向けに解説にまとめました。
TIME-WAITが2MSLで消えるまでにポートが埋まる条件を押さえる
TCPの仕様であるRFC 9293(2022年8月発行、RFC 793ほかを廃止)では、接続を先に閉じた側がTIME-WAIT状態に入り、2×MSL(Maximum Segment Lifetime)のあいだ待つと規定されています。同文書はMSLの値を2分としており、規定どおりなら1接続が4分間ポートを掴んだままになる計算だ。実際のLinuxでの待ち時間は実装依存で短く設定されていますが、短時間に大量の接続を張って閉じる処理を回すと、送信元ポートが尽きて新規接続が張れなくなります。
サーバーを再起動した直後に「アドレスが既に使われています」で立ち上がらない現象も、同じTIME-WAITが原因です。対処は SO_REUSEADDR の設定で、socket(7)マニュアルはこのオプションについて、アクティブなリスニングソケットがそのアドレスにバインドされていない限りバインドを許す、と説明しています。
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("0.0.0.0", 50007))
server.listen(16)
クライアント側で送信元ポートが枯渇する場合は、オプションでは解決しません。接続を使い回す(コネクションプール)か、接続を閉じる側をサーバーに寄せるか、という設計の話になります。
タイムアウトとノンブロッキングを設定して固まる通信を切り離す
既定のソケットはブロッキングです。相手が応答しなければ、その行でプログラムが止まり続けます。HOWTOは、相手が close せずにダウンした場合について「自分側のソケットは高確率でハングする」「TCPは信頼性の高いプロトコルなので、ずっと待ち続けてなかなか見捨てない」と書いています。
settimeoutの三つのモードを使い分けて無限待ちを止める
Pythonのソケットには3つのモードがあります。ドキュメントによれば、settimeout(None) がブロッキングモードで操作は完了かシステムエラーまでブロックし、settimeout(0.0) がノンブロッキングモードで完了できない場合はすぐ例外を送出し、正の数値を渡すとタイムアウトモードになって指定時間内に完了しなければ例外を送出します。setblocking(True) は settimeout(None) と、setblocking(False) は settimeout(0.0) と等価だとも明記されている。
例外の名前にも変遷があります。socket.timeout はバージョン3.10で TimeoutError の非推奨エイリアスになりました。新しく書くコードでは TimeoutError を捕まえる形にしておきます。
import socket
try:
with socket.create_connection(("127.0.0.1", 50007), timeout=3.0) as sock:
sock.settimeout(3.0) # 接続後の読み書きにも個別に効かせる
sock.sendall(b"ping")
print(sock.recv(4096))
except TimeoutError:
print("3秒以内に応答が返らなかったため打ち切った")
except ConnectionRefusedError:
print("相手が待ち受けていない")
create_connection() の timeout は接続の確立に効きますが、確立後の recv にも効かせたいなら明示的に settimeout() を呼ぶ。OS側にも SO_RCVTIMEO と SO_SNDTIMEO があり、socket(7)によれば期限までにデータが転送されなかった場合は -1 が返って errno に EAGAIN または EWOULDBLOCK が設定され、既定値のゼロではタイムアウトしません。
selectorsで複数ソケットを一つのループにまとめて捌く方法
接続が増えると、1接続1スレッドの構成は割に合わなくなります。HOWTOも select を使う方法を示しており、readableに入ったソケットは recv が何かを返す確実性が高く、サーバーソケットがreadableなら accept はほぼ確実に成功する、と説明しています。Pythonには、より扱いやすい selectors モジュールが標準で入っている。
import selectors
import socket
sel = selectors.DefaultSelector()
listener = socket.create_server(("127.0.0.1", 50007))
listener.setblocking(False)
sel.register(listener, selectors.EVENT_READ, data=None)
while True:
for key, _ in sel.select(timeout=1.0):
if key.data is None: # 待ち受けソケットが読める=新規接続
conn, addr = key.fileobj.accept()
conn.setblocking(False)
sel.register(conn, selectors.EVENT_READ, data=addr)
else: # 既存接続にデータが届いた
chunk = key.fileobj.recv(4096)
if chunk:
key.fileobj.sendall(chunk)
else:
sel.unregister(key.fileobj)
key.fileobj.close()
OS側の対応もマニュアルに整理されていて、socket(7)は fcntl() で O_NONBLOCK を立てるとブロックする操作が EAGAIN を返し、connect() は EINPROGRESS を返すと説明しています。その後 poll() または select() でイベントを待つ流れで、Pythonの setblocking(False) と selectors はこの仕組みに乗った薄い包みだ。
生のソケットを自分で書く場面と上位プロトコルへ寄せる場面の線引き
ここまでのコードは動きますが、業務のシステムで生のソケットをどこまで自前で書くべきかは別の判断です。結論から言えば、書くべき場面は限られます。
生のソケットを自前で書き切ってよい条件と、見送るべき場面の判断
自前で書き切ってよいのは、次の3条件がすべて揃うときです。1つ目は、通信相手のプロトコルが既製のライブラリで賄えないこと。産業機器やレガシー機器が独自の電文形式を持ち、TCPの上に自社仕様のフレームを載せる場合が典型だ。2つ目は、電文の設計を自分たちで決められること。長さ標識やタイムアウトの規約を相手と合意できないなら、実装は当て推量になります。3つ目は、切断・再接続・重複受信の扱いを運用として決めてあること。
逆に、ブラウザやモバイルアプリと繋ぐ、複数の言語からアクセスする、社外に公開する、という条件が1つでも入るなら見送ります。認証・TLS・再接続・プロキシ通過を自前のプロトコルで作り直すことになり、後から入る担当者が仕様を読み解けません。既存の業務システムと外部サービスをどの方式で繋ぐかを決める段は、API開発・システム連携で扱う領域と重なります。要件から通信方式を決める段階でご相談ください。
WebSocketやgRPCへ寄せた方が運用が軽くなる場面の見極め
双方向でリアルタイムに送り合いたい、けれどブラウザから繋ぐ、という要件なら生のソケットは選べません。ブラウザにTCPソケットを直接開く手段が無いためで、その代わりがWebSocketです。HTTPのハンドシェイクから始まるためプロキシやファイアウォールを通りやすく、実装も各言語に揃っています。詳しくはWebSocketとは?仕組み・ハンドシェイクからNode.js実装・運用の判断まで実装者向けに解説にまとめました。
サーバー間で型の決まった通信を大量に流すならgRPCが候補になります。スキーマから両側のコードが生成され、ストリーミングも仕様として用意されている。既製品で足りるなら既製品を使い、足りない部分だけをソケットで書く、という順序で考えると判断を誤りにくくなります。
よくある質問
ソケットとポートは何が違いますか?
ポートは番号そのもので、ソケットは通信の端点というオブジェクトです。1つのポート番号に対して、待ち受け用のソケットが1つと、接続ごとの通信用ソケットが複数、同時に存在し得ます。ss -tanp でLISTEN行が1行なのにESTAB行が何行も出るのは、この関係を表しています。
ソケット通信はどの言語で書いても同じですか?
呼び出しの順序はほぼ同じです。POSIXの socket() 関数が土台にあり、bind・listen・accept・connect・send・recv・close という流れはC言語でもPythonでもGoでも共通する。違いは包みの厚さで、Pythonの create_server() のように複数の手順をまとめた関数があるか、例外で返るか戻り値で返るか、といった部分です。
ポート番号はどの範囲を使えばよいですか?
0から1023はシステム側で予約された範囲なので、自前のアプリケーションでは避けます。1024以上を使い、OSが送信元ポートとして自動で割り当てる範囲(Linuxでは net.ipv4.ip_local_port_range で確認できます)とも重ならない番号を選ぶと、起動時の衝突を減らせます。
UNIXドメインソケットとTCPのローカル接続はどちらが速いですか?
同一ホスト内であればUNIXドメインソケットの方が経路は短くなります。IPヘッダの処理やチェックサムの計算を通らないためです。ただし差が出るのは通信量が多い場合で、接続先をホスト外へ動かす可能性があるかどうかで選ぶ方が現実に合う。
ソケットが勝手に切れてしまうのはなぜですか?
間に入る機器が無通信のコネクションを落としているケースが大半です。NAT機器やロードバランサはアイドル時間の上限を持っており、一定時間データが流れないと接続情報を破棄する。TCPキープアライブを有効にするか、アプリケーション側で定期的に短い電文を流すと維持できます。上限の値は機器の設定次第なので、何分で切れるかを測ってから間隔を決めてください。
関連記事
- ソケット通信とは?TCP/UDPソケットの仕組み・API手順・実装の勘所を実装者向けに解説:TCPとUDPの仕組みとAPI手順を概念から追う
- TCP/IPとは?4階層モデルとOSI参照モデルの違いを実装目線で解説:ソケットが位置する階層構造の全体像
- OSI参照モデルとは?7階層の役割・各層プロトコル・TCP/IPとの違いを解説:トランスポート層の役割分担を7階層で確認する
- 輻輳とは?ネットワーク混雑の仕組み・原因・TCP輻輳制御から対策の判断まで実装者向けに解説:送信キューが詰まる背景の見立て
- WebSocketとは?仕組み・ハンドシェイクからNode.js実装・運用の判断まで実装者向けに解説:ブラウザから双方向通信を張る場合の選択肢