ソケット通信とは、IPアドレスとポート番号で決まる「通信の端点」をOSに作らせ、その端点どうしでデータをやり取りする仕組みです。Webサーバーもデータベース接続もSSHも、下をたどるとソケットの上で動いています。この記事では、TCPソケットとUDPソケットの違い、socket・bind・listen・accept・connectという呼び出しの順番を押さえたうえで、LinuxとmacOSで実際に動かして分かった挙動の差(TIME_WAITの長さ、「Address already in use」が消える条件、送信元ポートの範囲)までを扱います。実測は2026年10月17日に、Ubuntu 24.04(カーネル6.8・Python 3.12.15)のコンテナとmacOS 26.6.2(Python 3.9.6)で行いました。
まとめ:ソケット通信の仕組みと実装前に押さえる要点
- ソケットは「IPアドレス+ポート番号+プロトコル」で決まる通信の端点で、LinuxやmacOSではファイルディスクリプタで扱い、WindowsのWinsockではSOCKETハンドルで扱います。
- TCPソケットは境界の無いバイト列です。3回に分けて送ったデータが、受信側では1回の
recvでまとめて返りました。区切りはアプリケーションが決めます。UDPは1回の送信が1データグラムのまま届きます。 - サーバーは socket→bind→listen→accept、クライアントは socket→connect の順に呼びます。
acceptは待ち受けとは別の、接続ごとのソケットを返します。 - UDPでも
connectを呼ぶと宛先が固定され、相手のポートが閉じているときにECONNREFUSEDを受け取れるようになります。 - TIME_WAITの残り時間はLinuxで約60秒、macOSで約30秒でした。再起動直後の「Address already in use」は、Linuxでは旧・新の両方のソケットにSO_REUSEADDRが要り、macOSでは新しい側だけで消えます。
- 長時間無通信の接続は、AWSのNAT Gateway(350秒)やALB(既定60秒)で切られます。NAT GatewayにはLinuxのキープアライブ既定(7200秒)では間に合わず、ALBはデータの送受信が無いと切れます。
以下、仕組み→APIの流れ→OSごとの違い→運用の順に説明します。
ソケット通信の仕組み:IPアドレスとポート番号で決まる通信の端点
「ソケット」という語は二つの意味で使われます。一つは通信の端点そのもの、もう一つはその端点を操作するためにOSが用意した関数群(ソケットAPI)です。TCP/IPの階層でいえば、ソケットはアプリケーションとトランスポート層(TCP・UDP)の境目にある窓口で、パケットの分割や再送、チェックサムの計算はOSが受け持ちます。プログラムは「どこに接続するか」と「何バイト送るか」だけを指示します。階層の全体像はTCP/IPの4階層モデルで確認できます。
ソケットはOSから渡されるファイルディスクリプタ
Linuxや macOSでは、socket()を呼ぶとOSが端点を作り、整数の番号(ファイルディスクリプタ)を返します。ファイルを開いたときと同じ番号の体系なので、read・write・closeがそのまま使えます。後で示す実測では、待ち受け用のソケットが3番、acceptが返した通信用のソケットが4番でした。プロセスのファイルディスクリプタ上限(ulimit -n)は、同時接続数を制約します。ただし標準入出力、ログファイル、待ち受けソケットなども枠を使うため、接続数の上限と同じ値にはなりません。
5つの値の組で接続を見分ける仕組み
端点の住所はIPアドレスとポート番号の組です。IPアドレスがホストを、ポート番号がそのホスト上のどのプログラムかを指します。TCPの接続は「送信元IP・送信元ポート・宛先IP・宛先ポート・プロトコル」の5つの値で区別されます。サーバーが443番だけで待ち受けていても、クライアントごとに送信元のIPとポートが違うので、何千本の接続でも混ざりません。クライアント側の送信元ポートはOSが自動で払い出します(実測ではLinuxで43698・43704・43718のように割り当てられました)。
TCPソケットとUDPソケットの違いと使い分け
ソケットを作るときに、ストリーム型(SOCK_STREAM=TCP)かデータグラム型(SOCK_DGRAM=UDP)かを指定します。この選択で、届き方の保証とデータの区切り方が決まります。
| 観点 | TCPソケット | UDPソケット |
|---|---|---|
| 接続 | 三ウェイハンドシェイクで確立 | 張らない |
| 到達・順序 | 再送と順序番号で保証 | 保証しない |
| データの区切り | 無い(バイト列) | 1送信=1データグラム |
| 主な用途 | HTTP・DB接続・SSH | DNS・音声/映像・ゲームの状態同期 |
TCPソケットの接続確立とバイト列の境界
TCPは通信の前にSYN、SYN+ACK、ACKの3つのやり取りで接続を確立します(RFC 9293の「three-way handshake」)。TCPは再送と順序制御によって信頼性のあるバイト列を提供しますが、通信障害による接続失敗は起こり得ます。また、送信の単位は保たれません。クライアントからsendallで hello・world・!! の3回に分けて送り、サーバーが0.3秒待ってからrecv(1024)を呼ぶと、LinuxでもmacOSでも1回のrecvで b'helloworld!!' がまとめて返りました。逆に、大きなデータが複数回のrecvに分かれて届くこともあります。
つまり、TCPでメッセージをやり取りするなら、区切りをアプリケーションが決める必要があります。よく使うのは「先頭4バイトに本文の長さを入れる」方式と「改行で区切る」方式です。HTTP/1.1ではヘッダー行をCRLFで区切り、本文の境界はContent-Lengthやチャンク転送などで示します。recvが要求した長さより短く返る前提で受信ループを組む具体的なコードは、Pythonで作って動かすソケットの実装入門で扱っています。
UDPソケットのデータグラム境界と配送の制約
同じ3回の送信をUDPで行うと、受信側のrecvfromは b'hello'、b'world'、b'!!' の3回に分かれて返りました。UDPはデータグラムの境界を保持するため、メッセージの区切りを別途設ける必要はありません。ただし受信バッファが小さいと、データグラムは切り詰められます。また、届かない、順番が入れ替わる、重複するといったことは起こり得ます。DNSのように1往復で済む問い合わせや、古いデータを再送しても意味がない音声・映像に向きます。データを取りこぼせない用途でUDPを選び、再送と順序制御を自作するのは、TCPを作り直すことになるので避けてください。
ソケットAPIの流れ:サーバーはsocket→bind→listen→accept、クライアントはsocket→connect
ここではTCPの呼び出し順を示します。C言語でもPythonでも基本的な流れは同じですが、UDPではlistenやacceptを使いません。サーバーとクライアントで次のように対になります。
| 順番 | サーバー側 | クライアント側 |
|---|---|---|
| 1 | socket:端点を作る | socket:端点を作る |
| 2 | bind:待ち受けIPとポートを結びつける | (bindは省略。ポートはOSが払い出す) |
| 3 | listen:接続要求の受付を始める | connect:宛先へ接続する |
| 4 | accept:接続を1本取り出す | (ハンドシェイク完了で戻る) |
| 5 | recv・send:データをやり取りする | send・recv:データをやり取りする |
| 6 | close:閉じる | close:閉じる |
PythonのTCP通信:最小サーバーとクライアントの実行例
上の表の順番をそのまま書いたのが次のコードです。Python標準のsocketモジュールだけで動きます。まずサーバーを起動し、別の端末でクライアントを実行します。
# echo_server.py
import socket
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 1. socket
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", 50050)) # 2. bind
srv.listen() # 3. listen
print("待ち受け中", srv.getsockname())
conn, addr = srv.accept() # 4. accept
print("接続:", addr, "待受fd", srv.fileno(), "通信fd", conn.fileno())
while True:
data = conn.recv(1024) # 5. recv
if not data: # 空=相手がclose
break
conn.sendall(data) # 5. send
conn.close() # 6. close
srv.close()
# echo_client.py
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 1. socket
sock.connect(("127.0.0.1", 50050)) # 3. connect
print("自分の端点:", sock.getsockname(), "相手:", sock.getpeername())
sock.sendall(b"ping")
reply = bytearray()
while len(reply) < 4:
chunk = sock.recv(4 - len(reply))
if not chunk:
raise ConnectionError("応答の途中で接続が終了しました")
reply.extend(chunk)
print("応答:", bytes(reply))
sock.close()
macOSで実行した出力は次のとおりです。クライアントの送信元ポート(63766)はOSが払い出したもので、bindしていません。
待ち受け中 ('127.0.0.1', 50050)
接続: ('127.0.0.1', 63766) 待受fd 3 通信fd 4
自分の端点: ('127.0.0.1', 63766) 相手: ('127.0.0.1', 50050)
応答: b'ping'
recvが空のバイト列を返したら、相手が接続を閉じた合図です。sendは渡したデータの一部しか送らないことがあり、戻り値で送れたバイト数を返します。全部を送り切るまで繰り返すsendallを使うのが安全です(Pythonのsocketモジュール公式ドキュメント)。
acceptが返すのは待ち受けとは別のソケット
acceptは、待ち受け用のソケットとは別の、その相手専用のソケットを新しく作って返します。待ち受け用は受付窓口として残り、データのやり取りは接続ごとのソケットで行います。Linuxでss -tanを実行すると、待ち受け中は1行、接続後は3行に増えるのが分かります。
$ ss -tan # 接続前
LISTEN 0 128 127.0.0.1:50007 0.0.0.0:*
$ ss -tan # 接続後
LISTEN 0 128 127.0.0.1:50007 0.0.0.0:*
ESTAB 0 0 127.0.0.1:50007 127.0.0.1:55508
ESTAB 0 0 127.0.0.1:55508 127.0.0.1:50007
ESTABの2行は、サーバー側の通信用ソケットとクライアント側のソケットです。macOSにはssが無いので、netstat -an -p tcpで同じ状態(LISTEN・ESTABLISHED)を確認できます。
listenのbacklogの既定値と上限
listenの引数backlogは、ハンドシェイクを終えてacceptを待っている接続を何本まで溜めるかの指定です(listen(2))。上のssの出力でLISTEN行の3列目にある128がこの値で、Pythonは引数を省くと「SOMAXCONNと128の小さい方」を使います(CPythonのsocketmodule.cのPy_MIN(SOMAXCONN, 128))。
OS側の上限はLinuxがnet.core.somaxconnで、カーネル5.4から既定が4096(それ以前は128)です。実測でも4096でした。macOSのkern.ipc.somaxconnは128です。上限を超える値を渡してもエラーにはならず、黙って上限に切り詰められます。接続が急に集中するサーバーでbacklogを大きくするなら、OS側の上限も合わせて上げないと効きません。
ブロッキングとノンブロッキングの選び方
既定のソケットはブロッキングで、acceptやrecvはデータが来るまでスレッドを止めます。少数の接続では1接続1スレッドが選択肢になります。接続数の増加に伴ってスレッドのメモリ消費や切り替えが負荷になる場合は、ソケットをノンブロッキングにして、準備のできたソケットだけをOSに教えてもらう方式に切り替えます。仕組みはOSごとに違い、Linuxはepoll、macOSやFreeBSDはkqueue、どこでも使える古い方式がselectです。PythonのselectorsモジュールやNode.js、Goのランタイムはこの違いを吸収しているので、自分でepollを直接呼ぶ場面は多くありません。受信側の処理が追いつかないときの流量制御はバックプレッシャーの仕組みも参考になります。
UDPソケットでconnect()を呼ぶ意味:宛先の固定とICMPエラーの受け取り
UDPは接続を張らないのに、connectを呼べます。Linuxのconnect(2)によると、UDPソケットにconnectすると、指定したアドレスが既定の送信先になり、受信もそのアドレスからのものだけに絞られます。パケットは1つも送られません。送信元ポートはこの時点で決まり、getsockname()で確認できます。
実用上の違いは、相手のポートが閉じているときに表れます。閉じたポート(127.0.0.1の50009番)にUDPで1バイト送り、1秒のタイムアウトで応答を待った結果が次の表です。
| 送り方 | Linux(Python 3.12) | macOS(Python 3.9) |
|---|---|---|
| connectせずsendto | タイムアウト | タイムアウト |
| connectしてsend | ECONNREFUSED(111) | ECONNREFUSED(61) |
相手のホストは「ポート到達不能」のICMPを返しています。Linuxのカーネル(net/ipv4/udp.cの__udp4_lib_err)は、IP_RECVERRを有効にしていない限り、このエラーを接続済みのUDPソケットにだけ渡します。udp(7)には「未接続のソケットでもネットワークからの非同期エラーを返す」という記述がありますが、既定の設定で実測した結果はタイムアウトでした。1つの相手とだけやり取りするUDPクライアント(DNSの問い合わせや社内の計測エージェントなど)では、connectしておくと、相手が落ちていることをタイムアウトを待たずに知れます。不特定の相手から受けるUDPサーバーではconnectしません。
なお「connect-udp」という語は、HTTPのプロキシ経由でUDPを通すための別の仕組み(RFC 9298「Proxying UDP in HTTP」)で使うアップグレードトークンの名前で、ここで扱うソケットのconnectとは関係ありません。
LinuxとmacOSで挙動が違うソケットの落とし穴(実測)
ソケットAPIは共通でも、待ち時間や再利用の条件はOSの実装で違います。開発機がMac、本番がLinuxの場合は、TIME_WAITやアドレス再利用条件の違いを確認します。
TIME_WAITの残り時間:Linuxは約60秒、macOSは約30秒
TCP接続を先に閉じた側のソケットは、TIME_WAITという状態でしばらく残ります。遅れて届いた古いパケットが、同じアドレスとポートの次の接続に紛れ込まないための待ち時間です。RFC 9293はこの時間を2MSL(MSLは2分とされ、2MSLで4分)としていますが、実装はこれより短くしています。接続を閉じてからTIME_WAITの行が消えるまでを計ったところ、Linuxは約61.0秒、macOSは約30.6秒でした。
| 項目 | Linux(カーネル6.8) | macOS 26.6.2 |
|---|---|---|
| TIME_WAITの実測 | 約61.0秒 | 約30.6秒 |
| 決めている値 | TCP_TIMEWAIT_LEN(60秒・固定) | net.inet.tcp.msl=15000ms(×2) |
| sysctlの設定項目 | 無い(tcp_fin_timeoutは別物) | net.inet.tcp.msl |
Linuxの60秒はカーネルのソース(include/net/tcp.hのTCP_TIMEWAIT_LEN (60*HZ))に定数で書かれています。よく「tcp_fin_timeoutを下げればTIME_WAITが短くなる」と書かれますが、カーネルのip-sysctl文書のとおり、この値はFIN_WAIT_2状態の保持時間で、TIME_WAITの長さは変わりません。TIME_WAITの再利用を許すtcp_tw_reuseは、既定値が2(ループバック通信だけで有効)でした。
TIME_WAITが溜まって困るのは、短い接続を高い頻度で張り直すクライアント側です。対策の第一は接続を使い回すこと(HTTPのキープアライブ、DBのコネクションプール)で、カーネルの設定を変えるのはその後です。
SO_REUSEADDRによるアドレス再利用:Linuxの旧・新ソケットへの設定条件
サーバーを止めてすぐ起動し直すと、bindが「Address already in use」(EADDRINUSE)で失敗することがあります。原因の一つは、サーバー側から先に接続を閉じ、待ち受けポートを使うTIME_WAITのソケットが残ることです。別プロセスが同じアドレスで待ち受けていないかも確認します。対策としてSO_REUSEADDRを付けますが、どのソケットに付ければよいかがOSで違いました。古いサーバーのソケット(旧)と、起動し直したソケット(新)のそれぞれで付ける・付けないを変えて、bindの結果を見た表です。
| 旧ソケット | 新ソケット | Linux | macOS |
|---|---|---|---|
| 付けない | 付けない | EADDRINUSE | EADDRINUSE |
| 付けない | 付ける | EADDRINUSE | 成功 |
| 付ける | 付けない | EADDRINUSE | EADDRINUSE |
| 付ける | 付ける | 成功 | 成功 |
macOSでは起動し直す側だけに付ければ通りますが、Linuxでは前回のプロセスも付けていないと通りません。この差はLinuxのsocket(7)にも、前のプログラムと再利用するプログラムの両方で設定が要り、FreeBSDなどは後のプログラムだけでよい、と書かれています。Macで「新しい側に付けたら直った」と確認しても、本番のLinuxでは最初の再起動で失敗します。待ち受けるソケットには最初から必ず付けておくのが確実です。Pythonのsocket.create_server()はPOSIX環境でSO_REUSEADDRを自動で付けます(Linuxの実測でgetsockoptが1を返しました)。
送信元ポートの払い出し範囲:Linuxは32768〜60999
クライアントの送信元ポートは、OSが決まった範囲から払い出します。RFC 6335は0〜1023をSystem Ports(ウェルノウンポート)、1024〜49151をUser Ports(登録済みポート)、49152〜65535をDynamic Ports(エフェメラルポート)としていて、macOSはこの範囲(net.inet.ip.portrangeが49152〜65535)でした。一方Linuxの既定はip_local_port_rangeが32768〜60999で、IANAの区分とは一致しません。
この範囲の広さとTIME_WAITの長さで、同じ宛先(IPアドレスとポートの組)へ張り直せる接続の頻度に目安が出ます。Linuxは28,232個のポートを60秒間使えないので、TIME_WAITを再利用しない場合、同じ宛先へ毎秒約470本を超えて新しい接続を張り続けると、送信元ポートが尽きてconnectが失敗し始めます。負荷試験で急にエラーが出るときは、ここを疑ってください。Linuxで自作サービスの待ち受けポートを32768〜60999の中に置くと、送信元ポートの払い出しとぶつかることがある点にも注意が要ります。
NAT・ロードバランサーで無通信の接続が切られる問題とキープアライブ
NATやロードバランサーは、通過中の接続を対応表で覚えています。無通信が続くと対応表から消され、両端のプログラムが生きていても次の送受信で失敗します。AWSでは、NAT Gatewayを通る接続は350秒以上アイドルだとタイムアウトし、ALBのアイドルタイムアウトは既定60秒です。
NAT Gatewayについては、AWSの文書が「接続に通信を流すか、350秒未満の間隔でTCPキープアライブを有効にする」ことを対策に挙げており、タイムアウト後に送ろうとするとNAT GatewayがRSTを返します。TCPキープアライブは、OSが無通信の接続に小さな確認パケットを送る仕組みです。ただしLinuxの既定(tcp(7))は、最初の確認を送るまで7200秒、間隔75秒、9回応答が無ければ切断で、350秒には間に合いません。長時間つなぎっぱなしにするクライアントでは、ソケットごとにTCP_KEEPIDLEを300秒未満などに下げるか、アプリケーション側で定期的にpingを送ります。データベースのドライバーやHTTPクライアントの多くは、この間隔を設定項目として持っています。
ALBはこれと条件が違い、アイドルタイムアウトの前に少なくとも1バイトのデータを送受信する必要があります。HTTP/2のPINGフレームでもALBのアイドルタイムアウトはリセットされないと、ALBの属性の文書に書かれています。ALBの先で長く接続を保つなら、アプリケーションのデータとしてpingを流すか、アイドルタイムアウトの設定値を延ばします。AWS上でNAT Gatewayやロードバランサーを含めたネットワーク構成を見直すときは、AWS・Google Cloud・Azureのインフラ構築支援でも相談を受けています。
ソケット通信とHTTP通信・WebSocketの違い
HTTPとソケットの関係:HTTP/1.1・HTTP/2とHTTP/3の違い
ソケット通信とHTTP通信は、並べて比べる対象ではなく上下の関係です。HTTP/1.1やHTTP/2は、TCPソケットの上に「メソッド・パス・ヘッダー・本文」という書式を決めたプロトコルです(HTTP/3はUDPの上のQUICを使います)。HTTP/1.1では、ブラウザやHTTPクライアントが内部でソケットを開き、必要に応じてTLSを重ねてGET / HTTP/1.1のような文字列を書き込んでいます。「ソケット通信」と言うときは、こうした既存の書式に乗らず、自分でデータの区切りや手順を決めて通信する場面を指すことが多く、その分、前の章で見た区切りの設計や再接続を自分で作ります。
HTTP/1.1でのWebSocket接続:Upgradeによる切り替え
WebSocketは、名前に「ソケット」と入っていますが、生のTCPソケットではありません。最初にHTTPでUpgrade: websocketを付けた要求を送り、サーバーが101 Switching Protocolsを返すと、同じTCP接続の上でWebSocketの形式のフレームをやり取りする方式に切り替わります(RFC 6455)。このHTTP/1.1方式はTCP上で動きます。ただしRFC 9220には、UDP上のQUICを使うHTTP/3でWebSocketを運ぶ方式も定義されています。メッセージの区切りはWebSocketのフレームが受け持つので、TCPのように自分で長さを付ける必要はありません。通常のWebページのJavaScriptには生のTCP・UDPソケットAPIが公開されていないため、サーバーとの双方向通信にはWebSocketが選択肢になります。用途によってはWebRTCのデータチャネルも利用できます。ハンドシェイクと実装はWebSocketの仕組みとNode.js実装、SSEとの使い分けはSSEとWebSocketの違いと使い分けで解説しています。
言語ごとのソケットAPIの呼び名
呼び出しの順番はどの言語でも同じで、名前とまとめ方が違うだけです。C言語のAPIがBSD UNIXで生まれた元の形で、ほかの言語はそれを包んでいます。
| 言語 | 待ち受け | 接続を受ける | 接続する |
|---|---|---|---|
| C(sys/socket.h) | socket・bind・listen | accept | socket・connect |
| Python(socket) | socket.create_server | accept | socket.create_connection |
| Java(java.net) | new ServerSocket(port) | accept | new Socket(host, port) |
| Go(net) | net.Listen | Listener.Accept | net.Dial |
| Node.js(net) | net.createServer・listen | connectionイベント | net.createConnection |
JavaやGo、Node.jsは、bindとlistenを1回の呼び出しにまとめています。C言語で書くときは、sockaddr_in構造体にアドレスとポートを詰め、ポート番号をhtonsでネットワークバイトオーダーに変換してからbind・connectに渡します。Pythonのsocket.socket()はC言語の関数とほぼ1対1に対応しているので、C言語の解説を読むときの対照にも使えます。
よくある質問
ソケット通信とTCP通信は何が違いますか?
TCPは「確実に順番どおり届ける」ための通信規約(プロトコル)で、ソケットはそのTCPやUDPをプログラムから使うための窓口です。TCP通信をするプログラムは、ソケットを通じてTCPを使っています。ソケット通信と言った場合は、TCPだけでなくUDPやUNIXドメインソケットを使う通信も含みます。
ポート番号はどの範囲を使えばよいですか?
自作サービスの待ち受けには、RFC 6335の登録済みポート(1024〜49151)のうち、既存のサービスと重ならない番号を選びます。0〜1023はウェルノウンポートで、Linuxでは既定で管理者権限が無いとbindできません。Linuxでは送信元ポートの払い出しに32768〜60999が使われるので、この範囲を待ち受けに使うのも避けた方が安全です。
UNIXドメインソケットとは何ですか?
同じホストの中のプロセス同士で通信するためのソケットで、IPアドレスとポートの代わりにファイルのパス(例:/var/run/docker.sock)を住所にします。TCP/IPの処理を通らないので、同じマシン上のWebサーバーとアプリケーションサーバーやデータベースをつなぐときに使われます。作り方と注意点はソケットの実装入門の章で扱っています。
ソケット通信は暗号化されていますか?
されていません。TCPソケットで送ったバイト列は、そのまま経路を流れます。インターネットを通すなら、ソケットの上にTLSを重ねます。PythonならsslモジュールのSSLContext.wrap_socketで既存のソケットを包めます。TLSの仕組みと版の選び方はTLSとは?SSLとの違い・仕組みとバージョン選定を参照してください。
Windowsでソケット通信を書くときの違いは何ですか?
WindowsのソケットAPIはWinsock(Windows Sockets 2)と呼ばれ、BSD UNIXのソケットの方式を元にしているので、socket・bind・listen・accept・connectの流れは同じです。違いは、どのWinsock関数よりも先にWSAStartupを呼ぶ必要があることと、閉じるのがclosesocketであることです。PythonやJavaから使う場合は、ランタイムがこの違いを吸収します。