TCP/IPとソケットとwockerの関係

※ 当サイトではアフィリエイト広告を利用しています。リンクは広告リンクも含みます。

この記事は約45分で読めます。

TCP/IPとHTTPはなんとなく分かるが
ソケットという言葉がよく出てくる。
TCP/IPとソケットとwockerの関係を実験と考察してみた。
Nginxを使うと、この辺の関係に対する疑問が湧いてくる。
多数のクライアントのリクエストとサーバーの中で処理結果が
どうやて結びついているか、
いつもモヤモヤしてます。
私が誤解して書いてあることもあると思うので、ご了承ください。
また今回のプログラムは終了処理がうまくいかず、プロセスが残り続ける可能性があります。
その他にも問題があるかもしれないのでコード実行は自己責任でお願いします。

(2025/9/19追記)前半は、ChatGptをはじめAIに聞きながら書き換えました。

広告
MINISFORUM日本公式ストア

TCP/IPとソケットの概略を勉強

TCP/IPについて、こちらがおすすめです。
パケットの中身を見てTCP/IPを絶対に理解したい (youtube.com)
ソケットについては、こちらがおすすめです。
【ITを勉強しよう!】第18回:ネットワーク ソケット – YouTube

ソケットとworker

サーバーの中でどのように処理されているかという視点の場合、TCP/IPは気にしなくてソケットについて気にすればいいと思う。そこで、workerとの関係についてまとめます。ここの内容は私が経験したことによる解釈の変化やAIを使って学習中の内容なので都度、更新します。

ソケット

個人的解釈
クライアントのIPとport番号が混線を避けるために重要な情報!
バッファ情報(送受信データの一時保存領域)
ソケットとは①の識別情報を持つ②送受信データの塊と解釈することにしました。

ソケットには、通信を成立させるために必要な接続情報状態管理のためのデータが含まれています。
TCP/IP通信におけるソケットの中身を分解すると、以下のような構成になります。


🧩 ソケットに含まれる主な情報

🔗 接続識別情報(4つの要素)

TCP通信では、以下の4つの情報で接続を一意に識別します:

  • 送信元IPアドレス
  • 送信元ポート番号
  • 宛先IPアドレス
  • 宛先ポート番号

📌 例:
クライアント → 192.168.1.10:54321ブラウザのタブが違うだけでport番号が変わるのでIPと合わせるとユニークな情報になる。
サーバー → 203.0.113.5:80(サーバー側は、1つのport番号で複数の接続を扱うのでユニークな情報ではなくなる。)
このペアが1つのソケット接続を構成します。概念的なソケットのユニークさ(識別子)はクライアント側のIPとport番号になります。実際の識別子はファイルディスクリプタというOSが管理する番号。


📦 通信状態に関する情報(TCPの場合)

TCPソケットは、信頼性の高い通信を行うために、以下のような状態情報を保持します:

  • 接続状態(LISTEN, SYN_SENT, ESTABLISHED, FIN_WAITなど)
  • シーケンス番号(送信データの順序管理)
  • ACK番号(受信確認応答)
  • バッファ情報(送受信データの一時保存領域)
  • 再送制御情報(未達データの再送管理)
  • フロー制御・輻輳制御のパラメータ

これらはTCPヘッダーに含まれる情報で、ソケットが内部的に保持・管理しています。

ソケットにおけるバッファ情報とは、基本的に「送受信されるデータを一時的に保管しておく領域」のことです。もう少し詳しく見てみましょう。


📦 バッファとは何か?

バッファは、データの一時保管場所です。ソケット通信では、以下の2種類があります:

バッファの種類役割
送信バッファアプリケーションが送るデータを一時的に保管。OSが順次ネットワークに送信。
受信バッファネットワークから届いたデータを一時的に保管。アプリケーションが読み取るまで保持。

つまり、送受信されるデータそのものがバッファに入っていると考えてOKです。


🧠 なぜバッファが必要なのか?
  • ネットワークは非同期で動くため、アプリケーションと通信のタイミングがズレることがあります。
  • バッファがあることで、アプリケーションがすぐにデータを送れなくても、OSが代わりに保持してくれます。
  • 逆に、受信側もアプリがすぐに読み取らなくても、OSが届いたデータをバッファに保持してくれます。

🧪 実際のコードで見る(Python)
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('example.com', 80))

# 送信バッファにデータを入れる
sock.send(b'GET / HTTP/1.1\r\nHost: example.com\r\n\r\n')

# 受信バッファからデータを読む
response = sock.recv(4096)
print(response.decode())

ここで send() は送信バッファに書き込み、recv() は受信バッファから読み取っています。


⚙️ バッファサイズの調整
  • OSやライブラリによってデフォルトサイズが決まっています(例:4096バイト)。
  • 必要に応じて setsockopt() などでサイズを変更できます。

🧭 まとめ
用語意味
バッファデータの一時保管領域
送信バッファアプリ→OS→ネットワーク
受信バッファネットワーク→OS→アプリ
データとの関係バッファには送受信されるデータそのものが入る

🧠 OSレベルの管理情報

OSがソケットを管理する際には、以下のような情報も含まれます:

  • ファイルディスクリプタ(プログラムから見たソケットの識別子)
  • プロセスIDやスレッドID(どのプロセスが使っているか)
  • タイムアウト設定
  • TLS/SSLの暗号化情報(HTTPSなどの場合)

🧪 プロトコル依存の情報
  • TCPの場合:信頼性重視、順序保証、再送制御など
  • UDPの場合:軽量で高速、順序保証なし、状態管理なし

UDPソケットはTCPに比べて保持する情報が少なく、接続状態も持ちません。


🧭 まとめ表
項目内容例・役割
IPアドレス(送信元/宛先)通信相手の識別
ポート番号(送信元/宛先)アプリケーションの識別(ブラウザでタブが違うと番号も違う)
接続状態TCPの状態遷移(ESTABLISHEDなど)
シーケンス番号・ACK番号データの順序・確認応答
バッファ情報データの一時保存
ファイルディスクリプタOSがソケットを識別するための番号
暗号化情報TLS/SSLによるセキュア通信設定

このように、ソケットは単なる「通信の窓口」ではなく、通信の信頼性・順序・安全性を支える重要な情報の塊なんです。

Worker

ここでいうwockerはDjangoをgunicornで立ち上げているときのwockerについて、まず考えます。
個人的解釈
ソケットの情報を処理するDjangoアプリの実体。

以下はChatGptに聞いた回答だが、サーバーのポートとgunicornのポートが直接バインドされているタイプの例でまず語られる。Nginxが入ると、どうなるのかも聞いた。

プログラムの動作を理解するうえで「プロセス」と「スレッド」はとても重要な概念です。以下にわかりやすく整理してみました👇


🧠 プロセスとは?

  • 実行中のプログラムのこと。OS(オペレーティングシステム)によって管理される。
  • 各プロセスは独立したメモリ空間を持ち、他のプロセスとは基本的に干渉しない。
  • 例:WordやExcel、Chromeなど、それぞれが1つのプロセスとして動作。

特徴

  • メモリやリソースの管理単位。
  • 他のプロセスに影響を与えにくく、安全性が高い。
  • 起動や切り替えにオーバーヘッド(負荷)が大きい。

🧵 スレッドとは?

  • プロセス内の実行単位。1つのプロセスの中に複数のスレッドが存在できる。
  • スレッド同士はメモリ空間を共有しているため、通信が高速。
  • 例:Wordの「画面描画」と「キーボード入力」は別々のスレッドで処理されることがある。

特徴

  • 軽量で高速な処理が可能。
  • メモリ共有により並列処理がしやすい。
  • ただし、共有メモリの扱いを誤るとバグやクラッシュの原因になる(スレッドセーフ設計が重要)。

🔍 違いをまとめると…

項目プロセススレッド
実行単位プログラム全体プログラム内の処理単位
メモリ空間独立共有
安全性高い(他プロセスに影響しない)低い(共有メモリによる干渉あり)
起動・切り替え重い(オーバーヘッド大)軽い(高速切り替え可能)
並列処理の効率性低め(プロセス間通信が重い)高い(スレッド間通信が軽い)

🛠 使い分けのポイント

  • 安全性重視 → プロセス(例:バックエンド処理)
  • パフォーマンス重視 → スレッド(例:Webサーバーのリクエスト処理)

ソケット・スレッド・Gunicornのワーカー(worker) の関係を説明しますね。


通信の識別(ソケット)

  • TCP通信では、接続は次の4つの情報で一意に識別されます。 クライアントIP : クライアントPort番号 サーバーIP : サーバーPort番号
  • 例えばブラウザが同じWebサイトにタブを2つ開くと、クライアント側のポート番号が違うので別セッションとして区別できます。

つまり「サーバーは80/443を共有するが、裏側では接続ごとにソケットが作られて区別される」という仕組みです。


サーバーでの処理方法(スレッドやプロセス)

  • サーバーは ポート(80/443) を1つだけ開けて待ち受けます。
  • 新しい接続が来ると、OSは新しいソケットを割り当てます。
  • そのソケットをどう処理するかはサーバーソフト次第です。

一般的なやり方:

  • マルチスレッド方式: 接続ごとに新しいスレッドを作って処理。
  • マルチプロセス方式: 接続ごとにプロセス(またはプロセスプール)で処理。
  • イベント駆動方式: 1スレッドで複数ソケットを非同期で扱う。

Gunicornのワーカー(worker)

Gunicornは「PythonのWSGIサーバー」で、主に以下のモデルを使えます。

  • sync worker(デフォルト)
    • 1つのワーカーが「1リクエストを処理し終わるまで」次のリクエストを受けない。
    • だから同時に多くのクライアントをさばくために、ワーカー数を増やす必要がある。
    • つまり「プロセス数 = 同時処理できるリクエスト数」。
  • gthread worker
    • 各ワーカーが内部でスレッドを立てる。
    • 1ワーカーが複数のリクエストを同時に並列処理できる。
  • gevent / eventlet worker
    • 非同期I/Oを使って1ワーカーで大量の接続を効率的にさばく。
    • チャットアプリやストリーミング向き。

スレッドとワーカーの関係

  • ワーカー = 基本的には「プロセス」
    • 1ワーカーは独立したPythonプロセスで、他のワーカーとはメモリを共有しない。
  • スレッド = ワーカーの中で動く小さな処理単位
    • gthreadワーカーを選べば「1プロセス内で複数スレッド」が動きます。

つまり:

  • Gunicornは「何プロセス(ワーカー)で処理するか」設定できる
  • 必要なら「そのプロセス内で何スレッド動かすか」も設定できる

まとめイメージ

クライアントA(IP, Port) ──┐
クライアントB(IP, Port) ──┤ TCP接続 → OSがソケットで区別
クライアントC(IP, Port) ──┘

Gunicorn
  ├─ Worker 1 (プロセス)
  │    ├─ Thread 1
  │    └─ Thread 2
  ├─ Worker 2 (プロセス)
  └─ Worker 3 (プロセス)

以下、簡単なコードで実験できますが、実験しなくても結果だけで、上記の解説の内容がイメージできます。

「最小限の接続で Gunicorn がどう処理するか」を、できるだけシンプルな実験でイメージできるように説明します。


実験用アプリ(hello.py)

とても単純な WSGI アプリを書きます。

# hello.py
import time

def app(environ, start_response):
    # 処理が分かりやすいように 5 秒待つ
    time.sleep(5)
    data = b"Hello World\n"
    start_response("200 OK", [("Content-Type", "text/plain")])
    return [data]

1. sync ワーカー(デフォルト)

起動:

gunicorn -w 1 hello:app

実験:

  • ターミナル1: curl http://127.0.0.1:8000 → 5秒待って「Hello World」
  • ターミナル2(同時に実行): curl http://127.0.0.1:8000 → 先に実行したリクエストが終わるまで待たされる(計10秒かかる)。

👉 理由: sync worker は「1プロセス・1リクエスト同時処理」だから。


2. ワーカーを2つに増やす

起動:

gunicorn -w 2 hello:app

実験:

  • ターミナル1で curl 実行 → 5秒待つ
  • すぐにターミナル2で curl 実行 → 同時並行で5秒後に両方返る

👉 理由: プロセスが2つあるので、別々のプロセスが並列で処理する。


3. gthread ワーカー(スレッド並列)

起動:

gunicorn -w 1 --threads 2 hello:app

実験:

  • ターミナル1で curl 実行
  • すぐにターミナル2で curl 実行
  • 同時並行で5秒後に両方返る

👉 理由: 1つのプロセス内でスレッドを2つ動かして、並行に処理できる。


4. 結論(最小構成での違い)

  • -w 1 (sync, デフォルト) → 同時に1リクエストしか処理できない
  • -w 22リクエスト同時処理できる(プロセス並列)
  • -w 1 --threads 22リクエスト同時処理できる(スレッド並列)

💡 つまり「最小限の接続実験」をすると、Gunicornのworker数とthread数がサーバーの同時処理能力をどう変えるかが体感できます。

「ソケットとワーカーがごっちゃにならないか?」

整理すると以下のようになります。

ソケットは接続の単位

  • ソケットは OS が管理する「通信のパイプ」みたいなもの。
  • クライアントごとに 別々のソケット が作られるので、混線することはないです。

例:

ClientA(1.2.3.4:50123) ⇔ Server(10.0.0.1:8000)  → ソケット①
ClientB(5.6.7.8:60123) ⇔ Server(10.0.0.1:8000)  → ソケット②

どちらもサーバーは同じポート8000で受けているけど、
OS が (クライアントIP, クライアントPort, サーバーIP, サーバーPort) の組み合わせで区別してくれる。

👉 なので、混線は「物理的に起こらない仕組み」になっています。


sync worker(-w 1)の場合

  • ワーカー(プロセス)は1つ。
  • OSは複数のソケットを作っても、Gunicornのワーカーは 1つのリクエスト処理が終わるまで次を処理しない
  • だから「順番待ち」が発生する。

👉 混線はしないけど詰まる


workerを複数にした場合(-w 2)

  • ワーカーは2プロセス。
  • OS が新しい接続(ソケット)を受けると、Gunicorn がそれを空いているワーカーに渡す。
  • 各ワーカーは自分の担当ソケットだけを処理する。

👉 ワーカーとソケットは1対多の関係ではなく、処理中は1対1で結びつく


gthread worker(スレッド並列)の場合

  • ワーカーは1プロセス、スレッドは複数。
  • それぞれのスレッドが独立してソケットを担当できる。
  • スレッド間でソケットを取り違えることはなく、OSとライブラリが「このソケットはこのスレッド」という対応を管理している。

👉 スレッドがソケットを間違えることはない


まとめイメージ

OS (ソケット管理)
   ├─ ソケット① (ClientAとの接続)
   └─ ソケット② (ClientBとの接続)

Gunicorn
   ├─ Worker1 (ソケット①を処理中)
   └─ Worker2 (ソケット②を処理中)
  • ソケット = 接続の窓口(OSが管理、混線しない)
  • ワーカー = ソケットを処理する担当者
  • syncは「担当者が1人で順番待ち」、
    マルチワーカーやスレッドは「担当者が複数いて並列処理できる」という違いだけ。

👉 なのでご心配の「ソケットとワーカーがごっちゃになる」ことはありません。
OSがきちんと「ソケットの区別」を保証してくれるので安全です。

Nginxもwockerですか?


結論から言うと、Nginx は「ワーカー(worker)」という仕組みを持っているけど、Gunicorn のワーカーとは意味が少し違う んです。


「masterプロセス」と「workerプロセス」は、特にWebサーバーや並列処理を行うアプリケーションでよく登場する概念です。以下にその違いをわかりやすくまとめました👇


🧭 Masterプロセスとは?

  • 全体の管理者のような存在。
  • サーバーの起動、設定の読み込み、ポートのバインドなどを担当。
  • workerプロセスを生成・監視する役割を持つ。
  • 自身はリクエスト処理を行わず、裏方に徹する。

主な役割

  • workerの起動・停止・再起動
  • 設定ファイルの読み込み
  • シグナル(再起動・終了など)の受け取りと伝達

🛠 Workerプロセスとは?

  • 実際の作業を担当するプロセス
  • クライアントからのリクエストを受け取り、処理してレスポンスを返す。
  • 通常は複数起動され、並列処理を実現する。

主な役割

  • HTTPリクエストの処理
  • 静的ファイルの配信
  • バックエンドとの通信(API、DBなど)

🔍 違いを比較すると…

項目MasterプロセスWorkerプロセス
役割管理・監視実際のリクエスト処理
リクエスト処理行わない行う
通常1つ複数(CPUコア数などに応じて)
特権レベル高い(ポートバインドなど)低い(セキュリティ上の配慮)
再起動の制御自身でworkerを再起動masterに制御される

🧪 実例:UnicornやNginxの場合

  • Unicorn(Rubyのアプリケーションサーバー)では、masterがworkerをforkして並列処理を実現。workerは1リクエストずつ処理するシングルスレッド設計。
  • Nginxでは、masterが設定を読み込み、複数のworkerを起動。workerはイベント駆動型で非同期にリクエストを処理。

Gunicorn の worker

  • Gunicorn のワーカーは Python アプリを処理するプロセス
  • リクエストを受け取って Python コードを実行し、レスポンスを返す役割。
  • -w で増やすと「同時に実行できる Python プロセス」が増える。

Nginx の worker

  • Nginx も「master プロセス」と「worker プロセス」で動いています。
  • Nginx の worker は クライアントとの接続・転送・静的ファイル配信などを処理するプロセス
  • CPUコア数に応じて worker を増やすのが一般的(worker_processes auto;)。
Nginx
  ├─ Master process (設定を読み込むだけ)
  └─ Worker process 1 (接続を処理)
  └─ Worker process 2 (接続を処理)
  ...

👉 Gunicorn の worker と同じ「worker」という単語を使っていますが、

  • Nginx worker → ネットワーク接続・リバースプロキシ・静的ファイル担当
  • Gunicorn worker → Pythonアプリ担当

と役割が違います。


全体の構成イメージ

Client ──▶ [ Nginx worker ] ──▶ [ Gunicorn worker ] ──▶ Pythonアプリ
  • Nginx worker: リクエストを受け取り、静的ファイルならその場で返す。動的なら Gunicorn に渡す。
  • Gunicorn worker: Pythonアプリを実行して応答を返す。

まとめ

  • Nginx にも worker がある(接続処理用のプロセス)
  • Gunicorn の worker とは役割が違う
  • 本番環境では、
    • Nginx worker = フロントエンド(玄関)
    • Gunicorn worker = Pythonアプリの実行者(裏方)

私が勝手に解釈したポイントは
ソケットはTCP/IPネットワークを利用するときの出入り口
OSIとの関係https://youtu.be/xbAFhCxgEQI?si=q4KMP8-gBflAL_Fv&t=617
TCPの上で動いているということを頭に置きつつTCPと切り離して考えたほうがいい。
TCPのパケット群を仮想回線という新たなやり取りとして
とらえなおしているということだと思います。
TCPとソケット通信という別の回線みたいなのが存在するわけではないということです。
TCP通信がちゃんと動いている中でソケット通信は出てくる話だと思います。
TCP/IPの参考動画ではhttps://youtu.be/fmN3xrqEz_0?si=R6oaArBb6fWOoCd6&t=319
httpのデータ通信のところで
そのデータを再構築してsocket通信として使われていると思われます。後で実験します。(後述)

ブラウザは別々のタブで送信するときport番号を変えます。またサーバーは1つのポートで通信をさばいています。なので各仮想回線に対してサーバーのスレッドが作られることで個別に処理をするのだと思われます。
スレッドはクライアントのipとport番号を記録しておくことで仮想回線の相手を間違えないのだと思います。
スレッドは1個ずつ独立しているので、1個のスレッドが遮断するまで通信内容は混線しない。

こちらも参考にさせてもらいました。https://qiita.com/t_katsumura/items/a83431671a41d9b6358f
https://www.youtube.com/watch?v=xKMtiIAH9Us


目次へ


ここから下は2024.04.10に書いた記事がベースです。上は2025.09.19は、ほとんどがAIに聞いて更新したものです。なので、ここから下は、2024.04.10当時の個人的解釈で書いてます。2025.09.19の時点の知識で明らかな間違いは修正しました。

実際にPythonでソケット通信してみる

こちら↓のコードを利用させてもらいました。
https://qiita.com/kotai2003/items/53ffaf05d2ca084830bb
クライアントはWindows11。私の場合Pythonが実行できるのは
AnacondaPoweshellなのでこれを使う。
サーバーはお名前.comVPS。
VPS OS Ubuntu 20.04.3 LTS。

DockerでNginx(Modesecurity付き)、WordPress、Djangoを動かしています。
詳細はこちらを参照してください。

お名前ドットコムVPSへ移行への道

2024/4お名前.comのレンタルサーバーからVPSへ移行したときの記録になります。

Pythonが動くか確認

サーバー側でpythonが実行できるか確認
SSH接続したVsコードの場合
表示→ターミナルで
mkdir pythontest
ファイル→フォルダを開くで今作ったフォルダを開きます。
試しに新規ファイルでexample.py作成。中身は
print("Hello, world!")
ターミナルで
python3 example.py
または
python example.py
でHello, world!と表示されればpythonは実行できます。

クライアント側でpythonが実行できるか確認
AnacondaPoweshellを立ち上げ
mkdir client_test
cd client_test
code .
でVsコードが立ち上がる
後は上と同じ。
ちなみにpythonのversion確認はpython --version
目次へ

サーバーの5000番ポートを開放

VPSの管理画面での設定とubuntuのufwの2か所の設定が必要です。
お名前.comVPSのサーバーのコントロールパネルで
インバウンドルール(ポート)一覧→設定追加→port番号5000、ルール名sockettest
セキュリティグループ一覧→設定を追加→グループ名sockettest、インバウンドルール
(ポート)作ったsockettestを選択→+→追加する
サーバー一覧→詳細→IP セキュリティ→変更→追加したい項目でsockettestを選択→+→変更する

サーバーでrootユーザーで
ufw allow from 自分のIPアドレス(パソコンで接続しているところ) to any port SSHのポート番号
参考サイトのコードで変更するのはhost名。
それから機能を追加します。
サーバーからクライアントへ"Hello, Client!"
クライアントからサーバーへ"hello Server!"と文字を送ってみます。
他は、そのまま使用します。
目次へ

コード

クライアント側

import socket

HOST = 'ホストのIPアドレス'  # Replace with the server's IP address
PORT = 5000
BUFSIZE = 4096

# 01. Preparing Socket: socket()
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# socket.AF_INET: IPv4
# socket.SOCK_STREAM: TCP

# 02. Configuring Socket and Connect to the Server: connect()
client.connect((HOST, PORT))

# 03. Send Data: sendall()
message = "hello Server!"
client.sendall(message.encode('UTF-8'))  # Encode message to bytes

# 04. Receive Data: recv()
data = client.recv(BUFSIZE)
print("Received from server:", data.decode('UTF-8'))

# 05. Closing the connection: close()
client.close()

サーバー側

import socket

# ポート番号
PORT = 5000

# メイン処理
# 1. ソケット作成
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2. アドレスとポート番号の割り当て
server.bind(("", PORT))

# 3. 接続待ち
server.listen()

# 4. 接続の取得
client, addr = server.accept()

# 5. データ受信
while True:
    data = client.recv(1024)
    if data:
        print(data.decode())
        break

# 6. クライアントにレスポンスを返す
response = "Hello, Client!"
client.sendall(response.encode())

# 7. 接続の終了
client.close()
server.close()
実行はサーバーのターミナルで
python3 server_single_connection.py
パソコンのターミナルで
python client_socket_IP4.py
うまく動作しました。
直後にもう1回実行すると
python3 server_single_connection.py
以下のエラーになる。ポートがすでに使われているということだと思います。
Traceback (most recent call last):
  File "server_single_connection.py", line 11, in <module>
    server.bind(("", PORT))
OSError: [Errno 98] Address already in use

TCP接続のポートの状況を見てみる
ss -natp
TIME-WAIT   0        0          ホストのIP:5000        パソコンのIP:48741   

TIME-WAIT状態は、通常、TCP接続が正常に終了した後に残る状態です。具体的には、TCP接続が通信を完了し、片方または両方の側からFIN(接続終了)パケットが送信され、受信された後に発生します。TIME-WAIT状態は、TCPプロトコルの仕様に基づいて、一定期間保持されます。

TIME-WAIT状態のTCP接続が発生する理由としては、以下のようなものがあります:

  1. TCP通信の完了: 通信が完了した後、両端のホストが接続を終了する際にTIME-WAIT状態が発生します。これにより、通信が完了してから一定期間の間、再送信されたパケットが意図せずに新しい接続と混同されることを防ぎます。
  2. パケットの最終到着待ち: TIME-WAIT状態は、最後のACKパケットが受信されてから、そのパケットが確実に相手に到達したことを確認するための待ち時間です。これにより、ネットワーク上でのパケットの混乱や重複が防止されます。

TIME-WAIT状態のTCP接続は、通常、数分から数時間の間保持されます。この期間は、一般的に2倍の最大セグメントライフタイム(Maximum Segment Lifetime、MSL)と呼ばれる値に等しいです。MSLは通常、2分から4分間です。

TIME-WAIT状態のTCP接続は、通常、ネットワークのパフォーマンスに直接的な影響を与えることはありませんが、大量のTIME-WAIT状態の接続が残っている場合は、ネットワークリソースの消費や接続数の制限などの問題が発生する可能性があります。

数分、待ってからss -natpで調べると
TIME-WAIT   0        0          ホストのIP:5000        パソコンのIP:48741   
は消えていてプログラムも実行できます。

プログラムを修正していろいろ実験していて
上記のようなエラーがでて、どうしよもなくなったら
ss -natpで調べて5000番のポートの項をコピーしてChatGptなどに
止めたいの後ろに貼り付けて聞けば教えてくれます。
killやkill -9やssを使った方法がでてきますが、慎重に実行した方がいいかもしれません。
目次へ

ブラウザでアクセスしてみる

サーバーのコードを修正してブラウザでアクセスしてみます。
ブラウザには以下のindex.htmlを作りPOSTでサーバーへ入力したデータを送ります。
日本語は対応してません。(%エンコードされる)
サーバーは、受信したデータを表示し、受信データをそのまま返します。

index.html
メモ帳で作る場合、保存するときは、ファイル→名前を付けて保存→ファイルの種類を「すべてのファイル」にしてファイル名はindex.htmlで保存。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>送信テスト</title>
</head>
<body>
    <form action="http://ホストのIP:5000" method="post">
        <input type="text" name="message">
        <input type="submit" value="送信">
    </form>
</body>
</html>

サーバー側

import socket

# ポート番号
PORT = 5000

# メイン処理
# 1. ソケット作成
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2. アドレスとポート番号の割り当て
server.bind(("", PORT))
print(f"Server listening on port {PORT}")
# 3. 接続待ち
server.listen()

# 4. 接続の取得
client, addr = server.accept()
print(f"Accepted connection from {addr[0]}:{addr[1]}")
# 5. データ受信
while True:
    data = client.recv(1024)
    if data:
        print(data.decode())
        break

response = """\
HTTP/1.1 200 OK

Hello from server!
Request received: {}
""".format(data)
client.sendall(response.encode('utf-8'))

# 7. 接続の終了
client.close()
server.close()

responseはただ文字を送っただけではブラウザでエラーになります。
上記のようにHTTP/1.1 200 OKを先頭に書いた文字列でないと表示しません。
index.htmlをダブルクリックしてブラウザ立ち上げて
以下は777と入力して送信ボタンを押した結果、ブラウザで表示したものです。

Hello from server!
Request received: b'POST / HTTP/1.1\r\nHost: ホストのIP:5000\r\nConnection: keep-alive\r\nContent-Length: 11\r\nCache-Control: max-age=0\r\nUpgrade-Insecure-Requests: 1\r\nOrigin: null\r\nContent-Type: application/x-www-form-urlencoded\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7\r\nAccept-Encoding: gzip, deflate\r\nAccept-Language: ja,en;q=0.9,en-GB;q=0.8,en-US;q=0.7\r\n\r\nmessage=777'

これはHTTPプロトコルに基づくリクエストメッセージです。
また、このときブラウザの開発者ツールのネットワークタブで見ても
普通にサーバーにPOSTで送受信した記録しかなく
特別なソケット通信の痕跡はない。
つまりソケットはクライアントの送受信データをためておくバッファのようなものではないかと思います。
それはサーバーのsocket関数で領域が確保され。
acceptでipとportが、その領域に結びつき。
そこからの受信データだけが、その領域に記録されrecvによって取り出すことができる。
ここで後続の他の処理に、
その受信したデータを引き渡し(多分Unix Domain Socketで)処理したあと
返答したいデーターをsendallで、その領域に書くことで、
特定のipとportへTCP/IPで送信しているのではないかと推察します。

つまりサーバー側は、このソケットのrecvでHTTPリクエストを取り出し
apatchやNginxさらにWordPressやDjango(私の環境の場合)などの処理後に
sendallでクライアントに送っていると思われます。
またサーバー側のポートは1個固定ですが、クライアント側のポートは都度変わっているようです。
今回ターミナルにAccepted connection fromのところにクライアントのportが表示されます。
ブラウザのタブを変えて送信やF5でも変わります。
今までHTTPは、パソコンも80番に固定だろうと思っていましたが違いました。
この送信側のportが都度変わることによって、
パソコンごとのソケットではなくportごとのソケットになり、
ブラウザの処理でサーバーと複数同時通信していても区別が可能なんだと思います。
このソケットはportだけでなくクライアントのIPでも区別されているので
サーバーはソケットを使うことで複数のクライアントの処理ができるのだと思います。

実行時のプロセスとportの状況を確認してみます。
末尾に&を付けて実行するとバックグラウンドで実行され別のコマンドが入力できます。
python3 server_single_connection.py &
[1] 2914900
 今実行しいてるTCPソケットの情報を見てみます。
ss -natp
State    Recv-Q   Send-Q        Local Address:Port         Peer Address:Port    Process                                          
LISTEN   0        128                 0.0.0.0:5000              0.0.0.0:*        users:(("python3",pid=2961936,fd=3)) 
このpid=2961936が表すものはソケットではなく、
server_single_connection.pyである。
参考:https://kotaroito.hatenablog.com/entry/20120123/1327331386
fdが何かを調べる
ls -la /proc/2961936/fd/
total 0
dr-x------ 2 user user  0 Apr  9 17:44 .
dr-xr-xr-x 9 user user  0 Apr  9 17:43 ..
lrwx------ 1 user user 64 Apr  9 17:46 0 -> /dev/pts/0
lrwx------ 1 user user 64 Apr  9 17:46 1 -> /dev/pts/0
l-wx------ 1 user user 64 Apr  9 17:46 19 -> /home/user/.vscode-server/data/logs/20240409T035134/ptyhost.log
lrwx------ 1 user user 64 Apr  9 17:45 2 -> /dev/pts/0
l-wx------ 1 user user 64 Apr  9 17:46 20 -> /home/user/.vscode-server/data/logs/20240409T035134/remoteagent.log
l-wx------ 1 user user 64 Apr  9 17:46 26 -> /home/user/.vscode-server/data/logs/20240409T035134/network.log
lrwx------ 1 user user 64 Apr  9 17:46 3 -> 'socket:[9597310]'
fdの実体はシンボリックリンクみたいです。
またsocket:[9597310]が何か生成AIに聞いた。
「3というシンボリックリンクが、socket:[9597310]という内容を持つことを示しています。このリンクが示すのは、通常、ソケット通信を表します。9597310は、ソケットファイルの一意の識別子です。」(Gemini談)
「'socket:[9597310]'は実際のファイルではなく、カーネル内のデータ構造を指しています。
そのため、通常のファイルとは異なり、ファイルシステム上に存在するわけではありません。」(ChatGpt談)
ちなみに/dev/pts/0はデバイスファイル名
ttyコマンドで現在の環境における標準入出力のデバイスファイル名を調べられます。

'socket:[9597310]'は識別子とされていて
実際にデータが入っているところがどうなっているのかは分からないですが。

ソケットの仕組みをまとめると
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
で3 -> 'socket:[9597310]'ができて
data = client.recv(1024)
でシステムコールのreadでfd3からデータを読み込み
client.sendall(response.encode('utf-8'))
でシステムコールのwriteでfd3に書き込みしてるのではないか。

ソケットのプログラムでデータの受信と送信を行う場合、ファイルディスクリプタとシステムコールの関係は次のようになります。
ファイルディスクリプタ:
ソケット通信においても、通信のチャネルを表すファイルディスクリプタが使用されます。これは、通常、整数値で表され、プログラムがソケットを操作する際に使用されます。
システムコール:

  • システムコールは、ユーザー空間からカーネル空間に制御を移すためのインターフェースです。ソケット通信のプログラムでは、受信や送信などの操作を行うために、特定のシステムコールが使用されます。
  • 例えば、データの受信にはrecv()システムコールが使われ、データの送信にはsendall()システムコールが使われます。これらのシステムコールは、ファイルディスクリプタを引数として
  • 受け取り、指定されたファイルディスクリプタに対して操作を行います。

具体的に、client.recv(1024)client.sendall(response.encode('utf-8'))の場合、ファイルディスクリプタとシステムコールの関係は次のようになります。

  • client.recv(1024): このメソッドは、クライアントソケットに対してデータを受信するための操作を行います。この操作は、カーネルに対してrecv()システムコールを呼び出し、受信したデータを指定されたバッファーに格納します。clientはソケットオブジェクトであり、そのソケットに対応するファイルディスクリプタが使用されます。
  • client.sendall(response.encode('utf-8')): このメソッドは、クライアントソケットを通じてデータを送信するための操作を行います。この操作は、カーネルに対してsendall()システムコールを呼び出し、指定されたデータをクライアントの接続先に送信します。ここでも、clientはソケットオブジェクトであり、そのソケットに対応するファイルディスクリプタが使用されます。

つまり、これらの操作は、ファイルディスクリプタを介してカーネルに指示され、ソケット通信が行われます。

目次へ

マルチスレッド化してみる

マルチスレッド化とは実行した結果から言うと。
親と言っていいか分からないが1個プロセスが常にlisten状態で
接続が行われるたびに新しいもの(多分これがスレッド)ができpid番号は、
すべて最初の親と同じ。fdはすべて違う。
ブラウザ(Edge)を使うと、送信ボタンを押したとき以外に、
タブをかえたときなど複数回接続してくる。
そのたびにスレッドができる。

今回作ったコードの停止方法
新しくindex.htmlをダブルクリックしstopと入力し「送信」でプロセスを終了するようにしてあります。(不安定、場合によってはプロセスが残ってます。)
目次へ

サーバーのコード

server_multi_conweb_stop.py

import socket
import threading

# ポート番号
PORT = 5000

# メイン処理
# 1. ソケット作成
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2. アドレスとポート番号の割り当て
server.bind(("", PORT))
print(f"Server listening on port {PORT}")
# 3. 接続待ち
server.listen()
# 停止要求フラグ
stop_requested = False
def handle_client(client, addr):
    """
    クライアントとの接続処理
    """
    print(f"Accepted connection from {addr[0]}:{addr[1]}")
    # 5. データ受信
    while True:
        data = client.recv(1024)
        if data:
            print(data.decode())
            # 受信データに "message=stop" がある場合は処理を終了
            if "message=stop" in data.decode():
                print("Received stop message. Exiting...")
                global stop_requested  # グローバルフラグにアクセス
                stop_requested = True
                client.close()
                # server.close()
                return
            break

    response = """\
HTTP/1.1 200 OK

Hello from server!
Request received: {}
""".format(data)
    client.sendall(response.encode('utf-8'))
    print(response.encode('utf-8'))
    # 7. 接続の終了
    client.close()

# 4. 接続取得とスレッド起動
# while True:
# 停止要求フラグを確認しながらループ
while not stop_requested:
    client, addr = server.accept()
    # スレッドを起動して接続処理を行う
    threading.Thread(target=handle_client, args=(client, addr)).start()
# すべての接続が終了したらサーバーソケットを閉じる
server.close()

結果

バックグラウンド実行
python3 server_multi_conweb_stop.py &

index.htmlをダブルクリック→777送信
index.htmlをダブルクリック→555送信

○○○○$は出てなくてもコマンドは受け付ける。
ctrl+Cで○○○○$を表示してもコマンド受け付け、プロセスは動き続けている。

5000番の状態とpid、fdを確認
ss -natp | grep ':5000'
LISTEN       0        128                0.0.0.0:5000            0.0.0.0:*       users:(("python3",pid=3124583,fd=3))                                           
CLOSE-WAIT   0        0          ホストのIP:5000      自分のパソコンのIP:4911    users:(("python3",pid=3124583,fd=5))                                           
ESTAB        0        0          ホストのIP:5000      自分のパソコンのIP:4918    users:(("python3",pid=3124583,fd=8))                                           
CLOSE-WAIT   0        0          ホストのIP:5000      自分のパソコンのIP:4912    users:(("python3",pid=3124583,fd=4))                                           
CLOSE-WAIT   0        0          ホストのIP:5000      自分のパソコンのIP:4904    users:(("python3",pid=3124583,fd=6))                                           
ESTAB        0        0          ホストのIP:5000      自分のパソコンのIP:4917    users:(("python3",pid=3124583,fd=7))  

1番上は常にLISTENしています。
この中のどれか2つが送信したときの接続です。
ブラウザ(Edge)の場合、何もしなくても接続してくるので
接続が増えていきます。そのたびにfdが増えていきます。
また接続してくるport番号が変わることもポイントだと思います。
これによりクライアントのソケットがユニークになり
クライアント側ではport番号によって
どのソケットへデータを送ればいいか判別できるのだと思います。

pid番号はLISTENしているものと同じ。
ブラウザのタブを2つとも閉じると接続はしてこなくなりますが
CLOSE-WAITで残ったままになります。待っていれば消えるかもしれませんが
新しくindex.htmlをダブルクリック→stop送信
サーバーはReceived stop message. Exiting...
ブラウザのほうはエラーが出るので閉じます。
ss -natp | grep ':5000'
CLOSE-WAIT  0        0          ホストのIP:5000       自分のパソコンのIP:4911    users:(("python3",pid=3124583,fd=5)) 
略

LISTENしていたプロセスは消えています。
しかしCLOSE-WAITがいっぱい残って消えないので調べる。
ps -p 3124583
    PID TTY          TIME CMD
3124583 pts/0    00:37:57 python3
残っているのでkill
kill 3124583
ss -natp | grep ':5000'
LAST-ACKになっていますが、しばらくするとなくなります。

プログラムがおかしくて、うまく終了で来てなったようですが
スレッドの動作は分かりました。
目次へ
import socket
import threading
import os
# ポート番号
PORT = 5000

# メイン処理
# 1. ソケット作成
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2. アドレスとポート番号の割り当て
server.bind(("", PORT))
print(f"Server listening on port {PORT}")
# 3. 接続待ち
server.listen()
# 停止要求フラグ
stop_requested = False

def handle_client(client, addr):
    """
    クライアントとの接続処理
    """
    print(f"Accepted connection from {addr[0]}:{addr[1]}")
    # 5. データ受信
    while True:
        data = client.recv(1024)
        if data:
            print(data.decode())
            # 受信データに "message=stop" がある場合は処理を終了
            if "message=stop" in data.decode():
                print("Received stop message. Exiting...")
                global stop_requested  # グローバルフラグにアクセス
                stop_requested = True
                client.close()
                # server.close()
                return
            break
    # Split the data by lines
    lines = data.decode().splitlines()
    resmes='-'
    # Find the line containing "message="
    for line in lines:
        if line.startswith("message="):
            # Extract the message by splitting and taking the second element
            resmes = line.split("=")[1]
            print(resmes)
            break
    # HTML形式のresponse
    response = """\
HTTP/1.1 200 OK

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>送信テスト</title>
</head>
<body>
    <h1>送信テスト</h1>
    <form action="http://ホストのIP:5000" method="post">
        <input type="text" name="message">
        <input type="submit" value="送信">
    </form>
    <p>メッセージを受け取りました!</p>
    <p>内容:{message}</p>
</body>
</html>
""".format(message=resmes)

    response = response.replace("\n", os.linesep)  # 改行コードをos.linesepに変換

    client.sendall(response.encode('utf-8'))

    print(response.encode('utf-8'))

    # 7. 接続の終了
    client.close()

# 停止要求フラグを確認しながらループ
while not stop_requested:
    client, addr = server.accept()
    # スレッドを起動して接続処理を行う
    threading.Thread(target=handle_client, args=(client, addr)).start()
# すべての接続が終了したらサーバーソケットを閉じる
server.close()

疑問と実験から得た答え

以前持っていた疑問 と答え
プロセスを識別する番号pidとソケットは1対1なのだろうか。
それならソケットをpid番号で区別して利用できるが、
他にソケットを識別するものはあるのだろうか。
→スレッドの実験をした結果、pidとソケットは1対1ではなくfdとソケットが1対1

実際に見たNginxのwokerプロセス(Nginxの実体)は2個だけど
worker_connections 1024;各ワーカプロセスが処理できる同時接続の最大数
この場合、wockerは接続ごとにソケットのプロセスを生成してpid番号で管理しているのだろうか。
→ソケットのプロセスは生成してそうだが、pid番号は同じだった。違うのはfd(ソケット番号)。
(2025/9/19追記:プロセスではなくスレッドだと思われます。)

所感

全然勘違いしているかもしれないが
ソケットの実験をすることで、前に進んだ気がします。
今回、全く頭にはありませんでしたがDjangoでよく目にする
WSGIやgunicornも前よりは理解できそうな気がしてきました。


イチゲをOFUSEで応援する(御質問でもOKです)Vプリカでのお支払いがおすすめです。
MENTAやってます(ichige)
目次へ

コメント

タイトルとURLをコピーしました