ユーザーがデータを送信し、サーバーがレスポンスを返す、というクライアントとサーバーの機能を、私はたいてい理解できます。
しかし、それだけでは、あまりに浅いレベルです。そこで、そのあたりの機能が、どのように対話するかということに焦点を合わせて、より深く学んでみることに決めました。
ありがたいことに、そういった対話を表示できる Wireshark
や tcpdump
といったツールもあります。
私は主にWebテクノロジーを扱っているので、Webに関連するTCPクライアントを分析したいと思いました。
以下の実験は、すべて Ubuntu20.04 で行いました。
基本的な HTTPクライアントと 理論を少し
とても基本的な例から始めましょう。python の requests モジュールを使用して htmlページを取得します。
>>> import requests
>>> r = requests.get('http://example.com/')
ダイアログを確認すると、次のようになっています。
requests
がConnection: keep-alive
ヘッダーをデフォルトで追加します。- gzip 圧縮されたレスポンスを、受け取りました。
基本的なHTTPクライアントは keepalive を使用しません。簡単にするために、gzipをオフにして続行します。
>>> r = requests.get('http://example.com/', headers={'Accept-Encoding': 'identity', 'Connection': 'Close'})
次に出力を見てみましょう。
または、概略ダイアログは次のようになります。
まず、すべてのTCP要求または応答には、制御ビットと呼ばれる SYN、ACKなどのフラグがあり、輻輳通知に使用されていることがわかります。この種のフラグは6つあります。
- URG:緊急ポインタフィールド 重要
- ACK:確認フィールド 重要
- PSH:プッシュ機能
- RST:接続をリセット
- SYN:シーケンス番号を同期
- FIN:送信者からのデータは以上
4つのフラグは有名で、次のことを示しています。
SYN - 接続を開始します
ACK - 受信データを確認します
FIN - 接続を閉じます
RST - エラーに応答して接続を中止します
その他2つのフラグ PSH(push)とURG (urgent)は、あまり知られていません。ここで詳細を参照することができます。
上記のクライアント(C)とサーバー(S)の間のダイアログを見ると、次のように読むことができます。
- CはSとの接続を確立したい(SYNを送信)
- SはACKで応答します-接続を許可します
- Cは再度ACKで応答します-接続が確立されます。
- CはGEThtmlページ(PSH、ACK)のリクエストを送信します
- SはACKで応答します*そしてリクエストされたページを送信します。
- Cはデータを受信したと応答し、ACKを送信します
- CはFIN、ACKを送信します-クライアントにはSに送信するデータがありません
- SもFIN、ACKで応答します
- 両側に交換するデータがないため、接続を閉じることができます。そして、そうしたわけです。CはACKを送信します
- Sは接続を閉じます。ACKを送信します。
上記のコードを再度実行すると (requests.get('http://example.com/', headers={'Accept-Encoding': 'identity', 'Connection': 'Close'})
)クライアントは同じダイアログで再び接続を確立します。
keepalive を備えた HTTP クライアント
Connection
ヘッダー値を keepalive
に変更したときの動作を確認しましょう。
この場合、クライアントがサーバーから最後のACKを取得しなかったことを除いて、ダイアログは前の例と同じであることがわかります。そのため、サーバーは接続を閉じません。keepalive の利点を確認するには、クライアントからサーバーへ、複数のリクエストを実行しなくてはなりません。セッションを使用するようにコードを変更し、複数のリクエストを行うことができます。しかし、私は、TCPクライアント自体であるブラウザーを使用することにしました。カンタンなウェブ検索で、supervisord が見つかりました。 ブラウザで開くと、 /
ページが読み込まれ、次にjs、cssファイル、画像などが読み込まれます。その場合のTCPダイアログの結果は次のとおりです。
クライアントは接続を1回だけ開き、サーバーからデータ (httpページ、静的ファイル、メディアファイル) を取得します。そして、タイムアウトに達し、サーバーは接続を閉じます(タイムアウトの値は、keepalive によって制御される場合があります:timeout=X、max=Y ヘッダー
)。 もちろん、このようなアプローチはCPU使用率を最小限に抑え、Webサイトのパフォーマンス向上に役立ちます。 HTTPでのKeep-aliveの詳細については、
1)https://www.imperva.com/learn/performance/http-keep-alive/
2)https://www.oreilly.com/library/view/http-the-definitive/1565925092/ch04s05.html
を参照してください。
Websocket
WebSocketがhttpリクエストとどのように異なるかを確認しましょう。simple-websocket-server にて、単純なWSサーバーを使用することにしました 。 まず、 echo WS server を開始しました。
from SimpleWebSocketServer import SimpleWebSocketServer, WebSocket
class SimpleEcho(WebSocket):
def handleMessage(self):
# メッセージをクライアントに echo で返す
self.sendMessage('resp: %s' % self.data)
def handleConnected(self):
print(self.address, 'connected')
def handleClose(self):
print(self.address, 'closed')
server = SimpleWebSocketServer('127.0.0.1', 8000, SimpleEcho)
server.serveforever()
そのリポジトリから websocket.html
ファイルを開いて、ブラウザーを使用してWSをテストします。
ここで、クライアントとサーバーが接続を確立します。その後、両側ともが、接続を閉じないことがわかります。CはFINフラグセット付きのデータを送信し、SもFINで応答し、CはACKを送信します。Connection:keep-alive
を使用したHTTPと比較すると、WSはタイムアウト後にも接続を閉じていません。WebSocketの動作に興味がある場合は、この記事を読むのも良いでしょう。
HTTP2
HTTP2プロトコルの理解に興味をお持ちの場合、こちらの記事 に基づいて次の設定をお勧めします。
1. 私は、セキュリティで保護されていない接続を許可するDockerコンテナを選択しました。
> docker run -p "9090:8080" -d lkwg82/h2o-http2-server
- また、http2サポートで curl を使用しました。
> curl --version
curl 7.68.0 ...
Release-Date: 2020-01-08
Protocols: ...
Features: AsynchDNS brotli GSS-API HTTP2 HTTPS-proxy ...
# リクエストする
> curl --head --http2 -H 'Accept-Encoding: identity' -v http://localhost:9090
# 応答として404 Not Foundとなりますが、問題ありません
そしてこちらがダイアログフローです。
Connection:Close
ヘッダーを 追加すると、プロトコルがHTTP2に切り替わらないの、面白いところです。上記の例では、 Accept- Encoding: identity
ヘッダーを使用しましたが、プロトコルはデータ圧縮を使用し、プレーンテキストの代わりにバイトを送信するため、http2の場合に使用しても意味がないのです。
まとめ
お気づきかもしれませんが、安全でないhttpとwsのリクエストを行い、httpsとwssは行いませんでした。安全なリクエストを行うと、暗号化された対話を見ることとなりますが、これは現在の私にとっては意味がありません。HTTPおよびWSリクエストに加えて、他にも興味深いものもあります。AJAXリクエスト、MySQL / PostgreSQLデータベースへの接続、ファイルのアップロードまたはダウンロードです。