HTTP
Web
websocket
Protocol
Server-Sent-Events

HTTP/1.0, HTTP/1.1, HTTP/2, Server-Sent Events, WebSocket におけるデータの送信単位

More than 3 years have passed since last update.


はじめに

この記事では簡易的にプロトコルの説明も行いますが,目的としてはデータの送信単位およびデータ終端の表現方法に焦点をあてて解説を行います.また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セッションが載るか?
多数
(並列送信はできなくはないが困難)

サーバ側から能動的にデータを送信できるか?
できない

レスポンスボディ終端の表現方法



  • Content-Length: xxx


  • Transfer-Encoding: chunked


  • 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を開始


ヘッダフレーム (Stream ID: 01; END_STREAM | END_HEADERS)→

method: GET

path: /main.html
scheme: https
authority: example.com

ストリーム#02を開始


ヘッダフレーム (Stream ID: 02; END_STREAM | END_HEADERS)→

path: /hoge.html



←ヘッダフレーム (Stream ID: 01; END_HEADERS)

status: 200

content-type: text/html


←データフレーム (Stream ID: 01; END_STREAM)

<iframe src="hello.html"></iframe>


ストリーム#01を終了


←ヘッダフレーム (Stream ID: 02; END_HEADERS)

status: 404



←データフレーム (Stream ID: 02; END_STREAM)

<h1>Not Found</h1>


ストリーム#02を終了

ストリーム#03を開始


ヘッダフレーム (Stream ID: 03; END_STREAM | END_HEADERS)→

path: /hello.html



←ヘッダフレーム (Stream ID: 03; END_HEADERS)

status: 200



←データフレーム (Stream ID: 03; END_STREAM)

<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を開始


ヘッダフレーム (Stream ID: 01; END_STREAM | END_HEADERS)→

method: GET

path: /streaming
scheme: https
authority: example.com


←ヘッダフレーム (Stream ID: 01; END_HEADERS)

status: 200

content-type: text/event-stream


←データフレーム (Stream ID: 01)

event: tweet

data: {"id":123,"text":"あいうえお","user":{"id":893,"screen_name":"mpyw"}}



←データフレーム (Stream ID: 01)

event: tweet

data: {"id":456,"text":"今


←データフレーム (Stream ID: 01)

からこれふぁぼります","user":{"id":893,"screen_name":"mpyw"}}



←データフレーム (Stream ID: 01; END_STREAM)

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コネクションを生成


←テキストフレーム (FIN: 1)

{"id":123,"text":"あいうえお","user":{"id":893,"screen_name":"mpyw"}}



←テキストフレーム (FIN: 0)

{"id":456,"text":"今



←テキストフレーム (FIN: 1)

からこれふぁぼります","user":{"id":893,"screen_name":"mpyw"}}



←テキストフレーム (FIN: 1)

{"event":"favorite","target_id":456,"user_id":893}



←コネクション切断通知フレーム (FIN: 1)


WebSocketコネクションを切断

TCPコネクションを切断

この実装ではクライアントはただサーバからのメッセージを読み取っているだけですが,この間にクライアントからサーバに別のメッセージを送ることもできるのがWebSocketの最大の長所です.TCPをほぼそのままの形でWeb用途に特化させた感じです.TwitterがSiteStreamsとして一部ユーザに限定的に提供していた,動的にカスタマイズ可能なストリーミングAPIは,WebSocketを使えばもっとシンプルに実現できることでしょう.