コメントを受けて一部修正しました。
他、データの加算が間違っていたので修正。
フロー制御
HTTP2にはストリームで受信できるデータ量をコントロールするフロー制御という仕組みがあります。
フロー制御では二つのことを決めています。
⚫️コネクション全体で受信できるデータ量 (INITIAL_WINDOW_SIZE)
(デフォルト65535)
⚫️一つのストリームで受信できるデータ量
(デフォルト65535)
これは最初に交換するSETTINGフレームでクライアントとサーバーが取り決めます。その後に一つのストリームでデータを受信するたびに減っていきます。
0になったらデータを受信することはできないので、WINDOW_UPDATEフレームで任意のバイト数を加算させます。
例
▪️step0.
初期値
INITIAL_WINDOW_SIZE 100
コネクション全体 100
▪️step1.
ストリームID1で50K受信
ウィンドウサイズ
ストリームID1 50K
コネクション全体 50K
▪️step2.
ストリームID1で50K受信
ウィンドウサイズ
ストリームID1 0K
コネクション全体 0K
▪️step3.
WINDOW_UPDATEフレームでストリームID1に200K指定
ウィンドウサイズ
ストリームID1 200K
コネクション全体 0K
このままではコネクション全体が0なのでストリームID1に余裕があっても受信できない。
▪️step4.
WINDOW_UPDATEフレームでストリームID0(コネクション全体)に200K指定
ウィンドウサイズ
ストリームID1 200K
コネクション全体 200K
これで再び受信可能になる。
▪️step5.
WINDOW_UPDATEフレームでストリームID0(コネクション全体)を70000K指定
ウィンドウサイズ
ストリームID1 200K
コネクション全体 70200K
コネクション全体を初期値より増やすことが可能
つまり、受信時は両方減り、加算時は指定したものだけ加算します。
youtubeでテスト
youtbeでテストしてみたところ、データ受信量が多くて良いサンプルが採れました。
nghttp2を使用してフロー制御の簡単な例を見てみました。
vagrant@nghttp2:~$ nghttp -nv https://www.youtube.com/
[ 5.016] Connected
[ 5.022][NPN] server offers:
* h2-15
* h2-14
* spdy/3.1
* spdy/3
* http/1.1
The negotiated protocol: h2-14
NPNによりHTTP2ドラフト14が採用されました。
続いてSETTINGフレームの交換が行われます。
[ 5.030] recv SETTINGS frame <length=18, flags=0x00, stream_id=0>
(niv=3)
[SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
[SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
[SETTINGS_MAX_FRAME_SIZE(0x05):16384]
[ 5.032] recv WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
(window_size_increment=983041)
[ 5.033] send SETTINGS frame <length=12, flags=0x00, stream_id=0>
(niv=2)
[SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
[SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
最初のやりとりの概要
youtubeサーバーは自身が受け取るストリームの初期ウィンドウサイズが65535。
自身が受け取る1回のフレームの最大サイズ(SETTINGS_MAX_FRAME_SIZE)が16384。
そのすぐ後でWINDOW_UPDATEでコネクション全体のウィンドウサイズに983041を加算してアップデートしている。
クライアントはストリームの初期ウィンドウサイズを65535に設定。
クライアント
初期ストリームウィンドウサイズ 65535
初期コネクションウィンドウサイズ 65535
[ 5.034] send SETTINGS frame <length=0, flags=0x01, stream_id=0>
; ACK
(niv=0)
[ 5.034] send HEADERS frame <length=39, flags=0x05, stream_id=1>
; END_STREAM | END_HEADERS
(padlen=0)
; Open new stream
:method: GET
:path: /
:scheme: https
:authority: www.youtube.com
accept: */*
accept-encoding: gzip, deflate
user-agent: nghttp2/0.7.10-DEV
[ 5.039] recv SETTINGS frame <length=0, flags=0x01, stream_id=0>
; ACK
(niv=0)
[ 5.089] recv (stream_id=1) :status: 200
[ 5.089] recv (stream_id=1) accept-ranges: none
[ 5.090] recv (stream_id=1) alternate-protocol: 443:quic,p=0.5
[ 5.090] recv (stream_id=1) cache-control: no-cache
[ 5.090] recv (stream_id=1) content-type: text/html; charset=utf-8
[ 5.090] recv (stream_id=1) date: Thu, 02 Apr 2015 12:00:48 GMT
[ 5.090] recv (stream_id=1) expires: Tue, 27 Apr 1971 19:44:06 EST
[ 5.090] recv (stream_id=1) p3p: CP="This is not a P3P policy! See http://support.google.com/accounts/answer/151657?hl=ja for more info."
[ 5.090] recv (stream_id=1) server: gwiseguy/2.0
[ 5.090] recv (stream_id=1) set-cookie: YSC=BR-dzjKtehY; path=/; domain=.youtube.com; HttpOnly
[ 5.090] recv (stream_id=1) set-cookie: VISITOR_INFO1_LIVE=6ZUYN3dlY40; expires=Tue, 01-Dec-2015 23:53:48 GMT; path=/; domain=.youtube.com; HttpOnly
[ 5.090] recv (stream_id=1) set-cookie: PREF=f1=50000000; expires=Tue, 01-Dec-2015 23:53:48 GMT; path=/; domain=.youtube.com
[ 5.090] recv (stream_id=1) vary: Accept-Encoding
[ 5.090] recv (stream_id=1) x-content-type-options: nosniff
[ 5.090] recv (stream_id=1) x-frame-options: SAMEORIGIN
[ 5.090] recv (stream_id=1) x-xss-protection: 1; mode=block; report=https://www.google.com/appserve/security-bugs/log/youtube
[ 5.090] recv HEADERS frame <length=528, flags=0x04, stream_id=1>
; END_HEADERS
(padlen=0)
; First response header
ここまでがACKの送受信、リクエストヘッダの送信とレスポンスヘッダの受信です。
[ 5.091] recv DATA frame <length=5057, flags=0x00, stream_id=1>
[ 5.496] recv DATA frame <length=16384, flags=0x00, stream_id=1>
[ 5.504] recv DATA frame <length=16384, flags=0x00, stream_id=1>
[ 5.505] recv DATA frame <length=16384, flags=0x00, stream_id=1>
[ 5.505] recv DATA frame <length=11326, flags=0x00, stream_id=1>
「GET /」に対するbodyデータを受信しています。
5つのDATAフレームの受信で
5057 + 16384 + 16384 + 16384 + 11326 = 65535
65535消費したので、クライアントのコネクション全体とストリームID1の初期ストリームウィンドウサイズが0になりました。
[ 5.505] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
(window_size_increment=32951)
[ 5.506] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=1>
(window_size_increment=32951)
[ 5.512] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
(window_size_increment=33738)
[ 5.513] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=1>
(window_size_increment=33738)
クライアント側からWINDOW_UPDATEフレームを送信して、ストリームID1とストリームID0(コネクション全体)のウィンドウサイズを加算させています。
それぞれ
コネクション全体とストリームID1に対して「33738 + 32951 = 66689」なので、今のウィンドウサイズは66689になります。
この時に送信できるサイズは「2^31」(2147483648)なので、nghttp2がなぜ2回に小分けにして送信しているのかは今のところ謎です。
この時の状態
コネクション全体 66689
ストリームID1 66689
[ 5.514] recv DATA frame <length=16384, flags=0x00, stream_id=1>
[ 5.525] recv DATA frame <length=15298, flags=0x00, stream_id=1>
[ 5.525] recv DATA frame <length=1269, flags=0x00, stream_id=1>
[ 5.526] recv DATA frame <length=16384, flags=0x00, stream_id=1>
今回受信したデータ量
16384 + 15298 + 1269 + 16384 = 49335
コネクション全体 66689 - 49335 = 17354
ストリームID1 66689 - 49335 = 17354
[ 5.526] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
(window_size_increment=32951)
[ 5.527] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=1>
(window_size_increment=32951)
今回、加算したデータ量
コネクション全体 32951
ストリームID1 32951
コネクション全体 17354 + 32951 = 50305
ストリームID1 17354 + 32951 = 50305
[ 5.529] recv DATA frame <length=15115, flags=0x00, stream_id=1>
[ 5.530] recv DATA frame <length=2239, flags=0x00, stream_id=1>
今回受信したデータ量
15115 + 2239 = 17354
コネクション全体 50305 - 17354 = 32951
ストリームID1 50305 - 17354 = 32951
以下同じ流れ。
[ 5.532] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
(window_size_increment=33738)
[ 5.533] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=1>
(window_size_increment=33738)
[ 5.534] recv DATA frame <length=16384, flags=0x00, stream_id=1>
[ 5.536] recv DATA frame <length=16193, flags=0x00, stream_id=1>
[ 5.537] recv DATA frame <length=374, flags=0x00, stream_id=1>
[ 5.537] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
(window_size_increment=32951)
[ 5.538] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=1>
(window_size_increment=32951)
[ 5.539] recv DATA frame <length=16384, flags=0x00, stream_id=1>
[ 5.541] recv DATA frame <length=16010, flags=0x00, stream_id=1>
[ 5.542] recv DATA frame <length=1344, flags=0x00, stream_id=1>
[ 5.542] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
(window_size_increment=33738)
[ 5.542] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=1>
(window_size_increment=33738)
[ 5.544] recv DATA frame <length=16384, flags=0x00, stream_id=1>
[ 5.547] recv DATA frame <length=15040, flags=0x00, stream_id=1>
[ 5.548] recv DATA frame <length=1527, flags=0x00, stream_id=1>
[ 5.549] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
(window_size_increment=32951)
[ 5.550] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=1>
(window_size_increment=32951)
[ 5.551] recv DATA frame <length=16384, flags=0x00, stream_id=1>
[ 5.552] recv DATA frame <length=14857, flags=0x00, stream_id=1>
[ 5.553] recv DATA frame <length=2497, flags=0x00, stream_id=1>
[ 5.556] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
(window_size_increment=33738)
[ 5.557] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=1>
(window_size_increment=33738)
[ 5.557] recv DATA frame <length=16384, flags=0x00, stream_id=1>
[ 5.560] recv DATA frame <length=15935, flags=0x00, stream_id=1>
[ 5.560] recv DATA frame <length=632, flags=0x00, stream_id=1>
[ 5.563] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
(window_size_increment=32951)
[ 5.564] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=1>
(window_size_increment=32951)
[ 5.565] recv DATA frame <length=16384, flags=0x00, stream_id=1>
[ 5.569] recv DATA frame <length=15752, flags=0x00, stream_id=1>
[ 5.570] recv DATA frame <length=1602, flags=0x00, stream_id=1>
[ 5.570] recv DATA frame <length=965, flags=0x01, stream_id=1>
; END_STREAM
[ 5.570] recv PING frame <length=8, flags=0x00, stream_id=0>
(opaque_data=0000000000000000)
[ 5.570] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
(window_size_increment=33549)
[ 5.570] send GOAWAY frame <length=8, flags=0x00, stream_id=0>
(last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[])
最終的にウィンドウサイズは65000程度を保つようになっているようです。
受信ウィンドウの最大サイズは「2^31」(2147483648)で、WINDOW_UPDATEで加算させることができる最大サイズも「2^31」(2147483648)です。
しかしこのような巨大な数字は今のところは見たことがないです。
⚫️Twitter.comの場合
[ 5.423] recv SETTINGS frame <length=6, flags=0x00, stream_id=0>
(niv=1)
[SETTINGS_INITIAL_WINDOW_SIZE(0x04):65536]
⚫️nghttp2.orgの場合
[ 5.135] recv SETTINGS frame <length=12, flags=0x00, stream_id=0>
(niv=2)
[SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
[SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
⚫️http2.akamai.comの場合
[ 5.040] recv SETTINGS frame <length=18, flags=0x00, stream_id=0>
(niv=3)
[SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
[SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
[SETTINGS_MAX_HEADER_LIST_SIZE(0x06):16384]
[ 5.041] recv WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
(window_size_increment=1)
ウィンドウサイズを減らされた場合
負の値を保持して待機する必要がある。
6.9.2. 初期フロー制御ウインドウサイズ
例えば、クライアントがコネクション確立と同時に60KBを送信し、サーバーが初期ウインドウサイズを16KBに設定した場合、クライアントは SETTINGS フレームの受信時に、使用可能なフロー制御ウインドウを-44KBに再計算します。クライアントは、WINDOW_UPDATE フレームが>ウインドウを正の値に戻すまで、負の値のフロー制御ウインドウを保持します。ウインドウが正の値に戻った後に、クライアントは送信を再開します。