TCPのフロー制御
受信窓の変化から、送信を止める条件と再開の範囲を確認します。
説明
TCPの受信側は、受け取れるデータ量をwindowで知らせます。アプリケーションが受信データを読み出さずバッファが埋まると、送信側は通常の新規データ送信を止める必要があります。
容量3000バイトの受信バッファと通信記録を照合し、ACK・window・送信済み番号から追加送信できる範囲を計算します。空きの回復とゼロウィンドウプローブ、別接続のWindow Scaleも確認します。
1windowが閉じて再び開く通信
window.pcapは接続途中の記録です。この接続ではWindow Scaleを使用しておらず、表示されたwindow値をバイト数として扱います。
tcpdump -n -tt -SACKとwindowの推移 -r window.pcap受信制限の記録受信側のwindowは2000→1000→0となり、後に3000へ戻ります。0の時点でACKは4001で、3000へ開き直した時点でもACKは4001です。新しいデータを受け取らなくても、空きの回復だけを通知できます。
2受信バッファとアプリケーションの読出し
buffer.csvは容量3000バイトの受信バッファを整理した記録です。
cat buffer.csv使用量・空き・次の受信番号データが届くと使用量が増え、アプリケーションが読まない間に空きが0になります。アプリケーションが3000バイト読み出すと空きが戻ります。受信側の処理が遅い場合の流量制御であり、必ずしもネットワークの混雑を意味しません。
3許される番号範囲の右端
受信側のACKにwindowを加え、送信できる範囲の排他的な右端を求めます。
python3 -c広告された受信範囲 'import json; rows=json.load(open("window.json")); print([(r["ack"],r["window"],r["ack"]+r["window"]) for r in rows if r["direction"]=="S>C"])'ACKとwindowの和最初の三行は右端4001で一定です。データを受け取るとACKが進み、その分windowが減ります。空きを回復した通知では右端7001まで広がり、再びデータを送れるようになります。
4未確認の送信済みデータを差し引く
ACK2001・window2000のとき、送信側が既に3001まで送っている場合を計算します。
python3 -c既送信分を引いた余裕 'ack=2001; window=2000; snd_nxt=3001; print(ack+window-snd_nxt); print(list(range(snd_nxt,ack+window))[:3],ack+window-1)'追加できる範囲新たに送れるのは1000バイトで、番号3001から4000です。window2000をそのまま追加送信量と読むと、既に飛行中の1000バイトを二重に数えてしまいます。実際には輻輳ウィンドウによる制限も同時に受けます。
5windowが0のままに見える場合
空きを通知したACKだけが失われると、送信側は受信側の回復を知りません。ゼロウィンドウと回復後を同じ番号で比較します。
python3 -cゼロと回復後の送信余裕 'ack=4001; snd_nxt=4001; print([(w,max(0,ack+w-snd_nxt)) for w in (0,3000)])'同じACKでwindowだけ変更0の間は通常の新規データを進められず、回復後は3000バイト分の余裕があります。送信側はゼロウィンドウプローブで現在の受信窓を確認する仕組みを持ちます。プローブは通常のデータを大量送信してよいという意味ではありません。
6Window Scaleを使う別の接続
別の接続で、受信側がSYN時にshift count7を広告したとします。確立後のwindow欄500を実際のバイト数へ変換します。
python3 -c欄の値をスケールする 'field=500; shift=7; print(field*(2**shift)); print(65535*(2**shift))'shift count7の倍率500は64000バイト、欄の最大65535は8388480バイトを表せます。倍率は方向ごとにSYNで交渉し、SYNそのもののwindow欄には適用しません。途中の記録だけでは倍率が分からない場合があります。
まとめ
ACKとwindowで受信範囲を求め、送信済みのデータを差し引きます。
受信側の空きによる制限と、ネットワークの混雑による制限は別です。
操作履歴と解説を振り返れます。
シェル / PCAPと通信の記録
tcpdump -n -tt -S -r window.pcapreading from file window.pcap, link-type EN10MB (Ethernet) 0.000000 IP 198.51.100.20.8080 > 192.0.2.10.49152: Flags [.], ack 2001, win 2000, length 0 0.010000 IP 192.0.2.10.49152 > 198.51.100.20.8080: Flags [P.], seq 2001:3001, ack 9001, win 8192, length 1000: HTTP 0.020000 IP 198.51.100.20.8080 > 192.0.2.10.49152: Flags [.], ack 3001, win 1000, length 0 0.030000 IP 192.0.2.10.49152 > 198.51.100.20.8080: Flags [P.], seq 3001:4001, ack 9001, win 8192, length 1000: HTTP 0.040000 IP 198.51.100.20.8080 > 192.0.2.10.49152: Flags [.], ack 4001, win 0, length 0 0.050000 IP 198.51.100.20.8080 > 192.0.2.10.49152: Flags [.], ack 4001, win 3000, length 0 0.060000 IP 192.0.2.10.49152 > 198.51.100.20.8080: Flags [P.], seq 4001:5001, ack 9001, win 8192, length 1000: HTTP 0.070000 IP 198.51.100.20.8080 > 192.0.2.10.49152: Flags [.], ack 5001, win 2000, length 0
cat buffer.csvEVENT,RCV_NXT,BUFFER_USED,BUFFER_FREE initial,2001,1000,2000 receive_1000,3001,2000,1000 receive_1000,4001,3000,0 application_reads_3000,4001,0,3000 receive_1000,5001,1000,2000
python3 -c 'import json; rows=json.load(open("window.json")); print([(r["ack"],r["window"],r["ack"]+r["window"]) for r in rows if r["direction"]=="S>C"])'[(2001, 2000, 4001), (3001, 1000, 4001), (4001, 0, 4001), (4001, 3000, 7001), (5001, 2000, 7001)]
python3 -c 'ack=2001; window=2000; snd_nxt=3001; print(ack+window-snd_nxt); print(list(range(snd_nxt,ack+window))[:3],ack+window-1)'1000 [3001, 3002, 3003] 4000
python3 -c 'ack=4001; snd_nxt=4001; print([(w,max(0,ack+w-snd_nxt)) for w in (0,3000)])'[(0, 0), (3000, 3000)]
python3 -c 'field=500; shift=7; print(field*(2**shift)); print(65535*(2**shift))'64000 8388480
演習完了