はじめに
この記事では簡易的にプロトコルの説明も行いますが,目的としてはデータの送信単位およびデータ終端の表現方法に焦点をあてて解説を行います.またTCPやHTTP/2については深く説明しません.
用語の定義
クライアント | 通信要求を行う側 |
---|---|
サーバ | 通信応答を行う側 |
プロトコル | クライアントとサーバが通信を行うための取り決め |
リクエストヘッダ | これからクライアントが送信するデータに対する説明 |
---|---|
リクエストボディ | クライアントが送信するデータ |
レスポンスヘッダ | これからサーバが送信するデータに対する説明 |
レスポンスボディ | サーバが送信するデータ |
TCP | バイト列を双方向に伝送し合うためのプロトコル |
---|---|
HTTP | バイト列にファイル単位での意味を与えるための TCPを利用したプロトコル |
WebSocket | バイト列にイベント単位での意味を与えるための TCPを利用したプロトコル |
「コネクション」や「セッション」に関しては定義が曖昧ですが,この記事では以下のようにします.
- 通信路と呼べるものは「コネクション」とします.
- 単なる二者間のやりとりは「セッション」とします.
TCPコネクション | TCPの通信路 |
---|---|
HTTPコネクション | TCPコネクションの上に成り立つ HTTP/2の通信路 |
WebSocketコネクション | TCPコネクションの上に成り立つ WebSocketの通信路 |
HTTPセッション |
HTTP/1.xのやりとり |
セグメント | TCPの最小データ単位 |
チャンク | HTTPセッションのうちレスポンスボディが分割された単位 (HTTP/1.1専用) |
ストリーム | HTTPコネクションの上に成り立つ HTTP1.xのセッションに相当するもの (HTTP/2専用) |
メッセージ | WebSocketコネクションの上に成り立つ HTTP1.xのセッションに相当するもの |
フレーム | HTTP/2またはWebSocketの最小データ単位 |
1つのTCPコネクションの上に連なるプロトコル群を図に示すとこんな感じです. (SG = Segment
)
HTTP/1.x: | HTTP Session |
|--------------|
TCP: | SG | SG | SG |
| HTTP Session |
HTTP/1.1: | Chunk | Chunk|
|--------------|
TCP: | SG | SG | SG |
| Stream | Stream |
HTTP/2: | Frame | Frame | Frame | Frame |
|-------------------------------|
TCP: | SG | SG | SG | SG | SG | SG |
| Message |
WebSocket:| Frame | Frame |
|---------------|
TCP: | SG | SG | SG |
昔からある方式 (~2010)
HTTP/1.0
HTTP/1.0: | HTTP Session |
|--------------|
TCP: | SG | SG | SG |
最もシンプルな方式です.
ヘッダフォーマット | テキスト |
---|---|
1つのTCPコネクションに 何個のHTTPセッションが載るか? |
1つ |
サーバ側から能動的にデータを送信できるか? | できない |
レスポンスボディ終端の表現方法 | TCPセグメントのFINオプション |
- HTTP/1.1で
Connection: close
としてヘッダで明示した場合もこちらの動作になります. - 詳細は割愛しますが,TCPコネクション切断時にはFIN(Finish)オプションを持ったTCPセグメントが相互に送られることになっています.
例
TCPコネクションを生成
HTTPセッションを開始
GET /main.html HTTP/1.0
HTTP/1.0 200 OK
Content-Type: text/html
<iframe src="hello.html"></iframe>
HTTPセッションを終了
TCPコネクションを切断
TCPコネクションを生成
HTTPセッションを開始
GET /hello.html HTTP/1.0
HTTP/1.0 200 OK
Content-Type: text/html
<h1>Hello</h1>
HTTPセッションを終了
TCPコネクションを切断
HTTP/1.1
HTTP/1.1: | HTTP Session |
|--------------|
TCP: | SG | SG | SG |
| HTTP Session |
HTTP/1.1: | Chunk | Chunk|
|--------------|
TCP: | SG | SG | SG |
HTTP/1.0では毎回TCPコネクションを生成しては切断を繰り返していましたが,効率が悪いため,TCPコネクションの再利用を行うようにしたプロトコルです.その代わり送信されるデータの長さを明確に表現する必要があります.またHost: xxx
というリクエストヘッダが必須になっています.
ヘッダフォーマット | テキスト |
---|---|
1つのTCPコネクションに 何個のHTTPセッションが載るか? |
多数 (並列送信はできなくはないが困難) |
サーバ側から能動的にデータを送信できるか? | できない |
レスポンスボディ終端の表現方法 |
|
- HTTP/1.0で
Connection: keep-alive
としてヘッダで明示した場合もこちらの動作になります. - 複数のHTTPセッションの並列処理を可能にする 「HTTP/1.1 Pipelining」という規格もありますが,Webブラウザ向けには問題点が多いためあまり利用されていません.このニーズはHTTP/2で満たすほうが主流です.
Transfer-Encoding: chunked について
- 任意のチャンクにデータを分割でき,それぞれのサイズは16進数で表現されます.
- 最後は0バイトのチャンクを送信してデータの終わりを通知します.
- これは送信時点でデータサイズが不明であり,かつTCPコネクションを切断せずに再利用したいという要件を満たすために利用されます.分割される単位に意味があるわけではありません.
4
[4バイトのデータ]
f
[15バイトのデータ]
0
例: Content-Length: xxx
を利用する場合
TCPコネクションを生成
HTTPセッションを開始
GET /main.html HTTP/1.1
Host: example.com
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 34
<iframe src="hello.html"></iframe>
HTTPセッションを終了
HTTPセッションを開始
GET /hello.html HTTP/1.1
Host: example.com
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 14
<h1>Hello</h1>
HTTPセッションを終了
例: Transfer-Encoding: chunked
を利用する場合
TCPコネクションを生成
HTTPセッションを開始
GET /main.html HTTP/1.1
Host: example.com
HTTP/1.1 200 OK
Content-Type: text/html
Transfer-Encoding: chunked
19
<iframe src="hello.html">
11
</iframe>
0
HTTPセッションを終了
HTTPセッションを開始
GET /hello.html HTTP/1.1
Host: example.com
HTTP/1.1 200 OK
Content-Type: text/html
Transfer-Encoding: chunked
e
<h1>Hello</h1>
0
HTTPセッションを終了
比較的新しい方式 (2010~)
HTTP/2
| Stream | Stream |
HTTP/2: | Frame | Frame | Frame | Frame |
|-------------------------------|
TCP: | SG | SG | SG | SG | SG | SG |
HTTP/1.xであった問題点を,HTTP/2は以下のように解決しています.
- 並列に通信を行うためには,TCPコネクションを多数使用する必要がある
→ HTTP自身がコネクションとなり,HTTPセッションをその中にストリームとして確保する - 毎回似たようなヘッダを送信しなければならない
→ 最初に送信したヘッダは変更がない限り省略 - テキスト形式であるためヘッダサイズが増えやすく,解釈も曖昧になる
→ 厳格なバイナリ形式を定義
見た目は大きく変化しているものの,意味的にはHTTP/1.xと互換性があり,相互に変換出来るようになっています.
ヘッダフォーマット | バイナリ |
---|---|
1つのTCPコネクションに 何個のHTTPコネクションが載るか? |
1つ |
1つのHTTPコネクションに 何個のストリームが載るか? |
多数 (並列送信可能) |
サーバ側から能動的にデータを送信できるか? | できる (但し用途が高速化のみに限られる) |
フレーム終端の表現方法 | フレームのLengthフィールド |
ストリーム終端の表現方法 | フレームのEND_STREAMオプション |
- TCPコネクションの切断にはTCPセグメントのFINオプションが用いられると述べましたが,ストリームの終了にはHTTP/2フレームの
END_STREAM
というオプションが用いられます. - HTTP/2ではヘッダ終端を示す
END_HEADERS
というオプションも用いられます.
HTTP/2について完全に説明するとそれだけで記述が膨大になるので,他のスライドや記事をリンクしておきます.
例: main.htmlとhoge.htmlへの同時リクエストを行う場合
実際にはHTTPコネクションを生成するための手順が別にありますが,ここでは省略します.また,HTTP/2のバイナリヘッダに関してはHTTP/1.x風に書くことにします.
TCPコネクションを生成
HTTPコネクションを生成
ストリーム#01を開始
method: GET
path: /main.html
scheme: https
authority: example.com
ストリーム#02を開始
path: /hoge.html
status: 200
content-type: text/html
<iframe src="hello.html"></iframe>
ストリーム#01を終了
status: 404
<h1>Not Found</h1>
ストリーム#02を終了
ストリーム#03を開始
path: /hello.html
status: 200
<h1>Hello</h1>
ストリーム#03を終了
Server-Sent Events
HTTP/1.xでは「クライアントからリクエストがあったらサーバが応答する」という動作方式に則っており,「サーバからクライアントに対して主体的にデータを送信する」ということが出来ませんでした.
(HTTP/2ではできるようになってはいますが用途が限定的です)
このままではチャットなどのリアルタイムなアプリケーションに対応するのが難しいです.そこで,HTTPを使用して実現される「Server-Sent Events」という方式が作られました.
- HTTP/1.xにおいては,1つのHTTPセッションにおけるレスポンスボディを継続的に送信し続けます.
- HTTP/2においては,1つのストリームを通じてデータフレームを継続的に送信し続けます.
一応これもプロトコルといえますが,TCPではなくHTTPの上に載っているので,ただのHTTP通信であるとも言えます.なお,この通信を行うための専用のオブジェクトがJavaScriptには用意されており,Webブラウザ側の実装はこれを使って簡単に行うことが出来ます.
以下にTwitterを模したストリーミングAPIの例を示します.
例: HTTP/1.0から利用する場合
TCPコネクションを生成
HTTPセッションを開始
GET /streaming HTTP/1.0
HTTP/1.0 200 OK
Content-Type: text/event-stream
event: tweet
data: {"id":123,"text":"あいうえお","user":{"id":893,"screen_name":"mpyw"}}
event: tweet
data: {"id":456,"text":"今からこれふぁぼります","user":{"id":893,"screen_name":"mpyw"}}
event: favorite
data: {"id":456,"user_id":893}
HTTPセッションを終了
TCPコネクションを切断
例: HTTP/1.1から利用する場合
長さがあらかじめ分かっているわけではないので,Transfer-Encoding: chunked
を使わなければなりません.但し先ほども書いたように,分割される単位に意味があるわけではないことに注意してください.どこで分割されるかわかりません.
TCPコネクションを生成
HTTPセッションを開始
GET /streaming HTTP/1.1
Host: example.com
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Content-Type: text/event-stream
5f
event: tweet
data: {"id":123,"text":"あいうえお","user":{"id":893,"screen_name":"mpyw"}}
28
event: tweet
data: {"id":456,"text":"今
48
からこれふぁぼります","user":{"id":893,"screen_name":"mpyw"}}
25
event: favorite
data: {"id":456,"user_id":893}
0
HTTPセッションを終了
例: HTTP/2から利用する場合
TCPコネクションを生成
HTTPコネクションを生成
ストリーム#01を開始
method: GET
path: /streaming
scheme: https
authority: example.com
status: 200
content-type: text/event-stream
event: tweet
data: {"id":123,"text":"あいうえお","user":{"id":893,"screen_name":"mpyw"}}
event: tweet
data: {"id":456,"text":"今
からこれふぁぼります","user":{"id":893,"screen_name":"mpyw"}}
event: favorite
data: {"id":456,"user_id":893}
ストリーム#01を終了
備考: 実際のTwitterはどうなっているか?
TwitterのストリーミングAPIは, Server-Sent Events の規格ができる前から存在するものなので,独自のフォーマットに基づいています.イベントタイプをデータに内包する形式です.但し,ツイートの場合は外を包むものが無く,あまり一貫性のない実装となっています.
また,Connection: close
なのにTransfer-Encoding: chunked
を使うという無駄なことをやっているようにも思えますが,TCPレイヤーでいきなり切断を行うよりも,上位のHTTPレイヤーから切断を行ったほうが緩やかでお行儀が良いと考えられるのかもしれません.
TCPコネクションを生成
HTTPセッションを開始
GET /1.1/statuses/sample.json HTTP/1.1
Host: stream.twitter.com
Authorization: OAuth ...
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Connection: close
Content-Type: application/json
4b
{"id":123,"text":"あいうえお","user":{"id":893,"screen_name":"mpyw"}}
15
{"id":456,"text":"今
48
からこれふぁぼります","user":{"id":893,"screen_name":"mpyw"}}
2c
{"event":"favorite","target_id":456,"user_id":893}
1c
{"disconnect":{"code":810}}
0
HTTPセッションを終了
TCPコネクションを切断
WebSocket
| Message |
WebSocket:| Frame | Frame |
|---------------|
TCP: | SG | SG | SG |
Server-Sent Events の際はHTTPを利用していましたが,こちらはTCPを直接利用するプロトコルとなっています.但し,HTTPとの互換性を考慮するため,まずHTTPで接続し,101 Switching Protocols
というHTTPステータスコードを用いて後からWebSocketに切り替える方式を採用しています.
ヘッダフォーマット | バイナリ |
---|---|
1つのTCPコネクションに 何個のWebSocketコネクションが載るか? |
1つ |
1つのWebSocketコネクションに 何個のメッセージが載るか? |
多数 (並列送信可能) |
サーバ側から能動的にデータを送信できるか? | できる |
フレーム終端の表現方法 | フレームのPayload-Lengthフィールド |
メッセージ終端の表現方法 | フレームのFINオプション |
WebSocketのヘッダフォーマットはバイナリですが,それでもHTTPに比べて極めてシンプルです.
例
もしTwitterがWebSocketでのストリーミングAPIに対応したとすればどうなるかのイメージを示してみます.
TCPコネクションを生成
HTTPセッションを開始
GET /1.1/statuses/sample.json HTTP/1.1
Host: stream.twitter.com
Authorization: OAuth ...
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: ...
Sec-WebSocket-Version: 13
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: ...
HTTPセッションを終了
WebSocketコネクションを生成
{"id":123,"text":"あいうえお","user":{"id":893,"screen_name":"mpyw"}}
{"id":456,"text":"今
からこれふぁぼります","user":{"id":893,"screen_name":"mpyw"}}
{"event":"favorite","target_id":456,"user_id":893}
WebSocketコネクションを切断
TCPコネクションを切断
この実装ではクライアントはただサーバからのメッセージを読み取っているだけですが,この間にクライアントからサーバに別のメッセージを送ることもできるのがWebSocketの最大の長所です.TCPをほぼそのままの形でWeb用途に特化させた感じです.TwitterがSiteStreamsとして一部ユーザに限定的に提供していた,動的にカスタマイズ可能なストリーミングAPIは,WebSocketを使えばもっとシンプルに実現できることでしょう.