TCP/IPとHTTPはなんとなく分かるが
ソケットという言葉がよく出てくる。
TCP/IPとHTTPとソケットの関係を実験と考察してみた。
Nginxを使うと、この辺の関係に対する疑問が湧いてくる。
多数のクライアントのリクエストとサーバーの中で処理結果が
どうやて結びついているか、いつもモヤモヤしてます。
私が誤解して書いてあることもあると思うので、ご了承ください。
また今回のプログラムは終了処理がうまくいかず、プロセスが残り続ける可能性があります。
その他にも問題があるかもしれないのでコード実行は自己責任でお願いします。
TCP/IPとソケットの概略を勉強
TCP/IPについて、こちらがおすすめです。
パケットの中身を見てTCP/IPを絶対に理解したい (youtube.com)
ソケットについては、こちらがおすすめです。
【ITを勉強しよう!】第18回:ネットワーク ソケット – YouTube
私が勝手に解釈したポイントは
ソケットは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通信が使われていると思われます。後で実験します。(後述)
listenしているソケットにクライアントのipとport番号が記録される。
ソケットは1個ずつ独立しているので、1個のソケットが遮断するまで通信内容は混線しない。
ブラウザは他タブで送信するときport番号を変える。
こちらも参考にさせてもらいました。https://qiita.com/t_katsumura/items/a83431671a41d9b6358f
https://www.youtube.com/watch?v=xKMtiIAH9Us
目次へ
実際にPythonでソケット通信してみる
こちら↓のコードを利用させてもらいました。
https://qiita.com/kotai2003/items/53ffaf05d2ca084830bb
クライアントはWindows11。私の場合Pythonが実行できるのは
AnacondaPoweshellなのでこれを使う。
サーバーはお名前.comVPS。
VPS OS Ubuntu 20.04.3 LTS。
調べるとPython3が入っていた。どこかで自分で入れて忘れてるかも。
DockerでNginx(Modesecurity付き)、WordPress、Djangoを動かしています。
詳細はこちらを参照してください。
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接続が発生する理由としては、以下のようなものがあります:
- TCP通信の完了: 通信が完了した後、両端のホストが接続を終了する際にTIME-WAIT状態が発生します。これにより、通信が完了してから一定期間の間、再送信されたパケットが意図せずに新しい接続と混同されることを防ぎます。
- パケットの最終到着待ち: 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(ソケット番号)。
所感
全然勘違いしているかもしれないが
ソケットの実験をすることで、前に進んだ気がします。
今回、全く頭にはありませんでしたがDjangoでよく目にする
WSGIやgunicornも前よりは理解できそうな気がしてきました。
runserverを使っているけどgunicornとの違いが、どうもよく分からない。
イチゲをOFUSEで応援する(御質問でもOKです)Vプリカでのお支払いがおすすめです。
MENTAやってます(ichige)
目次へ
コメント