Help us understand the problem. What is going on with this article?

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を使えばもっとシンプルに実現できることでしょう.

mpyw
PHP(Laravel) / JavaScript(React/Redux/ReactNative/Vue) / MySQL あたりが得意分野なWeb系エンジニア。最近マンネリ化がひどいので Go / Kotlin / Rust / Swift あたりから何か掘り下げたいと思っている。Go は 2.x 出てから書きます。古い記事はそのまま参考にしないようにご注意ください
http://gravatar.com/mpyw
synapse
Synapseは、オンラインサロンサービスにおけるパイオニアとして、かつて存在していたスタートアップです。
https://synapseam.github.io/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした