send の境界はそのまま recv の境界にはならず、ACK が相手側によるビジネスロジックのコミットを意味するわけではありません。TCP は順序付きで信頼性の高いバイトストリームを提供しますが、Linux の実装を理解する際には、まずシーケンス番号、ウィンドウ、回復、およびクローズ方向 を分けて考える必要があります。
まだヘッダー、オプション、プロトコルレベルでのウィンドウ計算に慣れていない場合は、ネットワークシリーズの TCP を先に読んでください。本稿では Linux の接続キュー、送受信パス、およびソケットの動作に沿って展開します。2つの記事で共通するシーケンス番号とウィンドウのモデルが、プロトコルと実装の橋渡しとなります。
参照目標 読書位置
ルーティングとハンドシェイクキュー 第1~2節
バイト列、BDP、およびパケット損失 第3~5節
制御アルゴリズムとファストパス 第6~7節
ハーフクローズ実験 第8節
1. IP ルーティングはローカル配信か転送かを決定する
IPv4 では、基本ヘッダーの受信検証後、PREROUTING などの処理を経てルーティング判断が行われます。ローカル宛てのパケットは LOCAL_IN/INPUT を経由して TCP、UDP などに渡され、転送パスは FORWARD と出口処理を経ます。ローカルからの送信には初期ルーティング情報が必要であり、OUTPUT でのアドレス/マークの変更は再ルーティングをトリガーする可能性があり、その後 POSTROUTING に入ります。
IPv6 には独自の入力およびルーティング実装があります。基本ヘッダーには IPv4 のヘッダーチェックサムがなく、拡張ヘッダーの処理や断片化ルールも異なります。IPv6 ルーターは途中のノードでソースエンドポイントに代わって断片化を行わないため、Path MTU Discovery などのメカニズムは正しく処理されなければならず、IPv4 の関数チェーンをそのまま汎用プロトコルスタックとして適用することはできません。
2. 3ウェイハンドシェイクで確立されるのは接続状態
LISTEN 状態のソケットは LISTEN を維持し、ハンドシェイクで接続オブジェクトが確立される
クライアント
サーバーの LISTEN および接続状態
SYN: seq = x
SYN + ACK: seq = y, ack = x + 1
ACK: ack = y + 1
accept は確立済み接続キューから接続を取得します。クライアントの SYN をトリガーする役割はありません。
通常のアクティブオープンでは SYN_SENT を経て、正当な応答によって ESTABLISHED に進みます。サーバー側の LISTEN ソケットは引き続き LISTEN を維持し、半接続(half-open connections)は通常、より軽量な request socket で追跡され、ハンドシェイク完了後にのみ accept 可能な接続キューに入ります。SYN cookie や TCP Fast Open などは、具体的なリソースとタイミングを変更します。
SYN キューと accept キューは異なる段階のものであり、listen(backlog) はすべての半接続に対して単一の総数を直接設定するものではありません。accept の戻りが遅れると、ハンドシェイクが完了したキューが詰まることがあり、SYN 関連のパラメータを拡大するだけでは解決できません。
ハンドシェイクでは初期シーケンス番号とオプションが交換され、SYN 自体が1つのシーケンス番号を消費します。TCP の確立が成功したことは、トランスポート層の接続が利用可能であることを示すだけで、アプリケーションが認証済みであることや、ビジネスサービスが準備完了であることを意味するわけではありません。プロトコルの仕様については RFC 9293 を参照してください。
3. シーケンス番号、ACK、および受信キュー
受信範囲 受信状態 累積 ACK
[1000,2000) 順序通り受信済み 次のバイトは 2000 であるべき 2000
[3000,4000) が先に到着 中間にギャップがある 依然として 2000
ギャップが後に埋められる 連続した受信位置を進めることができる 連続範囲に応じて進む
TCP のシーケンス番号はバイト単位でカウントされます。SACK を使用すると、離散化した受信済み範囲を別途報告でき、送信者が再送をより正確に選択できます。累積 ACK は、埋められていないギャップをスキップしません。
相手側がバイトを確認して受信プロトコル状態に進んだとしても、それが相手側アプリケーションによって既に読み込まれたことを意味するわけではありません。さらに、データベースへの書き込みがコミットされたことを意味するわけでもありません。send の戻り値も、ACK の受信を意味しません。アプリケーションがメッセージの境界や完了確認を必要とする場合は、長さ、区切り文字、リクエスト ID、およびレスポンスプロトコルを定義する必要があります。
Linux は順序データを受信キューに配置し、順序不同のデータは対応する構造体に保存します。アプリケーションが読み込む際に、意味に従って順序付きのバイトストリームを配信します。PSH は信頼性の高いメッセージ境界を構成するものではなく、recv(4096) は 4096 バイト未満を返す場合もあれば、複数の send のデータを一度にマージする場合もあります。
任意のバイトセグメントをクリックして順序不同および重複到達をシミュレートしてください。累積 ACK は連続したプレフィックス沿ってのみ進行します。アプリケーションの読み取りは別途記録され、受信側の確認をアプリケーションが既に消費したものと見なしてはいけません。
図を準備しています 再読み込み
データが後から届いたのに、ACK はなぜギャップで止まっているのか? 累積 ACK は次に連続して受信されていないバイトを指します。SACK は後方の離散範囲を記述します。プロトコルの確認はアプリケーションの消費を意味せず、既知の範囲が重複して受信されても ACK は再度進みません。
{"id":"tcp-cumulative-ack","title":"データが後から届いたのに、ACK はなぜギャップで止まっているのか?","summary":"累積 ACK は次に連続して受信されていないバイトを指します。SACK は後方の離散範囲を記述します。プロトコルの確認はアプリケーションの消費を意味せず、既知の範囲が重複して受信されても ACK は再度進みません。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><p class=\"caption\" data-i18n=\"arrive\"></p><div class=\"choices\"><button data-segment=\"0\">1<small>[1000,2000)</small></button><button data-segment=\"1\">2<small>[2000,3000)</small></button><button data-segment=\"2\">3<small>[3000,4000)</small></button><button data-segment=\"3\">4<small>[4000,5000)</small></button><button data-segment=\"4\">5<small>[5000,6000)</small></button><button data-segment=\"5\">6<small>[6000,7000)</small></button><button data-segment=\"6\">7<small>[7000,8000)</small></button><button data-segment=\"7\">8<small>[8000,9000)</small></button></div><div class=\"track-row\"><span data-i18n=\"stream\"></span><div id=\"stream\" class=\"track\"></div></div><div class=\"metrics\"><div><span data-i18n=\"ack\"></span><strong id=\"ack\"></strong></div><div><span data-i18n=\"consumed\"></span><strong id=\"consumed\"></strong></div></div><div class=\"formula\" id=\"sack\"></div><p class=\"caption\" data-i18n=\"last\"></p><div id=\"last\" class=\"readout\"></div><div class=\"actions\"><button id=\"read\" data-i18n=\"read\"></button></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.choices button{min-height:65px}.choices small{display:block;font:10px var(--viz-font-ui);overflow-wrap:anywhere;margin-top:5px}.choices button.received{background:var(--accent-soft)}#sack{min-height:6.5em;white-space:pre-line}.readout{min-height:3.2em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let received=Array(8).fill(false),consumed=0,last='—';function contiguous(){const i=received.indexOf(false);return i<0?8:i;}function draw(){const n=contiguous(),sack=[];for(let i=n;i<8;){if(!received[i]){i++;continue;}let j=i+1;while(j<8&&received[j])j++;sack.push('['+(1000+i*1000)+','+(1000+j*1000)+')');i=j;}cells('stream',received.map((v,i)=>({text:String(i+1),cls:i<consumed?'gold':v?'on':'empty'})));document.querySelectorAll('[data-segment]').forEach(e=>e.classList.toggle('received',received[+e.dataset.segment]));set('ack',1000+n*1000);set('consumed',consumed*1000);set('sack','SACK\\n'+(sack.join(' ')||'—'));set('last',last);$('#read').disabled=consumed>=n;set('explanation',t('note'));}document.querySelectorAll('[data-segment]').forEach(e=>e.onclick=()=>{const i=+e.dataset.segment;last='['+(1000+i*1000)+','+(2000+i*1000)+')'+(received[i]?' ×2':'');received[i]=true;draw();});$('#read').onclick=()=>{if(consumed<contiguous())consumed++;draw();};$('#reset').onclick=()=>{received=Array(8).fill(false);consumed=0;last='—';draw();};draw();","audio":false,"strings":{"ack":"累積 ACK / バイトシーケンス番号","arrive":"このデータセグメントを受信させるにはクリック","consumed":"アプリケーションが読み取り済み / バイト","heading":"データが後から届いたのに、ACK はなぜギャップで止まっているのか?","intro":"番号をクリックして対応する 1000 バイトの範囲を受信させます。クリックを繰り返して重複データをシミュレートできます。ギャップを任意の順序で埋め、その後アプリケーションが読み取ります。","last":"最近受信した範囲","note":"まず 3、4 をクリックし、次に 1 をクリック。第 2 セグメントが到着していない間、ACK は 2000 で止まったままになる。2 を埋めると、保存済みの後続範囲を一度に跨ぐ。","read":"アプリケーションが次のセグメントを読み取る","reset":"リセット","scope":"8セグメント、各セグメント 1000 バイト。初期 RCV.NXT=1000。SYN/FIN、シーケンス番号のラップ、受信ウィンドウの制限、ACK パケット送信の遅延なし。SACK 行は論理区間を表示し、有限の TCP オプション容量や通知順序はシミュレートしない。アプリケーションは一度に連続したデータセグメントを消費する。","scopeLabel":"モデルの範囲と規約","stream":"バイトストリーム位置:青は受信済み、金色はアプリケーションが読み取り済み"}}
4. フロー制御と輻輳制御は別の尺度
受信ウィンドウ rwnd は、受信側がさらにどの程度のデータを受信できるかを示します。輻輳ウィンドウ cwnd は、送信側がネットワーク上で許容できる在途データ量の制限を示します。両者の単位は実装や表示において異なる場合があります。例えば、cwnd は通常 MSS セグメント数でカウントされるため、比較する前に換算が必要です。
大まかに言えば、送信可能な新しいデータ量は min(cwnd_bytes, rwnd_bytes) - bytes_in_flight によって制限され、さらにパシング、アプリケーションにデータがあるかどうか、再送、および実装の詳細も考慮する必要があります。ウィンドウは送信速度の直接単位ではありません。
ウィンドウは新規送信の制約であり、既に在途のデータを消去するものではありません。以下では、cwnd、rwnd、および在途量を同じバイト尺度で比較し、いずれかの上限を変更してから送信を試みます。
new_data ≤ max ( 0 , min ( cwnd , rwnd ) − flight )
3つの量はすべてバイト単位で計測されます。ここでは現在の送信枠を比較しており、輻輳アルゴリズム、SACK パイプライン推定、または実際のウィンドウ更新プロトコルはシミュレートしていません。
図を準備しています 再読み込み
2つのウィンドウの尺度が、同じ在途量を減らす 新しいデータの枠は max(0, min(cwnd, rwnd) − flight) で計算されます。ウィンドウが縮小すると、flight が新しい上限を超え、新規送信が停止し、確認応答による枠の解放を待ちます。
{"id":"tcp-flight-window","title":"2つのウィンドウの尺度が、同じ在途量を減らす","summary":"新しいデータの枠は max(0, min(cwnd, rwnd) − flight) で計算されます。ウィンドウが縮小すると、flight が新しい上限を超え、新規送信が停止し、確認応答による枠の解放を待ちます。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"cwnd\" data-i18n=\"cwnd\"></label><output for=\"cwnd\" id=\"cwnd-value\"></output><input id=\"cwnd\" type=\"range\" min=\"0\" max=\"16\" value=\"8\" step=\"1\"></div><div class=\"control\"><label for=\"rwnd\" data-i18n=\"rwnd\"></label><output for=\"rwnd\" id=\"rwnd-value\"></output><input id=\"rwnd\" type=\"range\" min=\"0\" max=\"16\" value=\"12\" step=\"1\"></div><p class=\"caption\" data-i18n=\"c\"></p><div class=\"band\"><i id=\"bar-c\"></i></div><p class=\"caption\" data-i18n=\"r\"></p><div class=\"band\"><i id=\"bar-r\"></i></div><p class=\"caption\" data-i18n=\"f\"></p><div class=\"band\"><i id=\"bar-f\" class=\"second\"></i></div><div class=\"axis\"><span>0</span><span>8000</span><span>16000 B</span></div><div class=\"actions\"><button id=\"send\" data-i18n=\"send\"></button><button id=\"ack\" data-i18n=\"ack\"></button></div><div class=\"metrics\"><div><span data-i18n=\"flight\"></span><strong id=\"flight\"></strong></div><div><span data-i18n=\"allowance\"></span><strong id=\"allowance\"></strong></div></div><div class=\"formula\" id=\"arithmetic\"></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n#arithmetic{min-height:6em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let flight=0;function draw(){const c=+$('#cwnd').value*1000,r=+$('#rwnd').value*1000,a=Math.max(0,Math.min(c,r)-flight);set('cwnd-value',c+' B');set('rwnd-value',r+' B');for(const[k,v]of [['c',c],['r',r],['f',flight]])$('#bar-'+k).style.width=v/160+'%';set('flight',flight);set('allowance',a);set('arithmetic','max(0, min('+c+', '+r+') − '+flight+') = '+a+' B');$('#send').disabled=a<1000;$('#ack').disabled=flight===0;set('explanation',t('note'));}$('#cwnd').oninput=$('#rwnd').oninput=draw;$('#send').onclick=()=>{if(Math.min(+$('#cwnd').value,+$('#rwnd').value)*1000-flight>=1000)flight+=1000;draw();};$('#ack').onclick=()=>{flight=Math.max(0,flight-1000);draw();};$('#reset').onclick=()=>{flight=0;draw();};draw();","audio":false,"strings":{"ack":"1000 B 確認","allowance":"新規送信可能 / B","c":"輻輳ウィンドウ","cwnd":"cwnd / 1000 B","f":"送信済み未確認","flight":"在途量 / B","heading":"2つのウィンドウの尺度が、同じ在途量を減らす","intro":"cwnd と rwnd を調整し、1000 バイトを送信または確認します。まず複数のセグメントを送信してからウィンドウを縮小し、既に在途のデータが空中に消えないことを確認します。","note":"青は現在の制限、金色は在途量です。2つの尺度は同じ接続を制約しており、cwnd を先に使い果たしてから rwnd を使用するわけではありません。","r":"受信ウィンドウ","reset":"リセット","rwnd":"rwnd / 1000 B","scope":"すべての量はバイト単位で統一され、図の1マスは 1000 B、最大 16000 B です。TCP 輻輳アルゴリズム、パシング、ウィンドウスケーリング、再送、ゼロウィンドウ探査はシミュレートしません。rwnd スライダーは制限のスナップショットを比較するために使用され、実際の受信側が通告済みの右端を任意に縮小することは推奨されません。","scopeLabel":"モデルの範囲と規約","send":"1000 B 送信"}}
在途量の計算:帯域幅が十分でも、ウィンドウがスループットを制限する可能性がある
条件または計算 結果
リンク 1 Gbit/s、RTT 20 ms BDP は約 2.5 MB(2.38 MiB)
有効な在途量が 64 KiB のみ このパスの BDP よりもはるかに小さい
64 KiB / 0.02 s 約 3.28 MB/s の簡易スループット上限
ここでは他の制限は無視しています。この計算は、ウィンドウが不足するとスループットが制限されることを示しており、バッファを拡大すればすべてのパフォーマンス問題が解決されるわけではないことを意味しています。
バッファを拡大することで特定の制限を解消できますが、パケット損失、誤った MTU、輻輳、または低速なアプリケーションは修復できません。受信側の自動調整にも上限、メモリ圧力、およびユーザー設定などの制約があります。SO_RCVBUF などのパラメータを設定すると、自動調整の動作にも影響を与えるため、「高 BDP では常にバッファをチェックする必要がない」とは限りません。TCP sysctl を参照してください。
5. パケット損失の検出にはタイムアウトだけではない
重複 ACK、SACK、および時間ベースの RACK などの情報は、RTO の期限前に損失を検出できます。RTO は依然として回復の重要なフォールバックメカニズムであり、平滑化された RTT と変動の推定に基づいて計算され、タイムアウト後にバックオフします。
RFC 6298 は RTO の計算フレームワークを提供しています。具体的な Linux の最小値、パス、および設定可能な項目は対象バージョンに照らして確認する必要があり、RFC の推奨値をすべての Linux タイマーの固定値として見なしてはいけません。再送後の RTT サンプルには曖昧さがあり、推定では任意の ACK を直近の送信に直接割り当てることはできません。
タイマーは目的別に分類される:ウィンドウ、アイドル、および損失
ゼロウィンドウ探査は受信側ウィンドウの閉塞に対処し、keepalive は長期アイドル接続の到達可能性をチェックします。これらは通常の損失再送タイマーの別名ではありません。TCP の再試行タイムアウトでは、相手側アプリケーションが以前のデータを既に処理していないことを保証できません。ビジネスロジックでの再試行には独自の重複排除セマンティクスが必要です。
6. CUBIC と BBR:制御目標を比較し、宣伝文句に惑わされない
CUBIC のウィンドウ増加は、最後の輻輳前のウィンドウ値を中心に設計されており、立方関数は旧ピーク値に近づくと増加率が変化し、遠ざかると増加率が異なる挙動を示します。TCP フレンドリーな動作などの詳細も含まれており、1つの立方関数でアルゴリズム全体を代替することはできません。RFC 9438 は CUBIC の仕様であり、BBR の仕様ではありません。
BBR は配信レートと伝搬 RTT などの推定に基づいてパスモデルを構築し、パシングと在途制限によって送信を制御します。異なる BBR バージョンでは、パケット損失、ECN、公平性、および探査動作が異なります。「完全にパケット損失を考慮しない」や「あらゆる輻輳リンクで CUBIC より優れている」と一概に言うことはできません。実際、カーネルモジュール名が自動的に特定の論文/パッチバージョンの採用を証明するわけではありません。
cat /proc/sys/net/ipv4/tcp_congestion_control
cat /proc/sys/net/ipv4/tcp_available_congestion_control
ss - tin
システムのデフォルトアルゴリズムは新規接続に影響を与えます。既存のソケットには独自の設定があることもあります。比較する際は、パス、RTT、ストリーム数、受信ウィンドウ、およびボトルネックキューを固定し、スループット、レイテンシ、および共存ストリームの動作を観察します。
7. ファストパスはプロトコルの正当性を迂回するものではない
tcp_rcv_established() には、一般的に確立済み接続のパケットに対する最適化処理が含まれています。ヘッダー予測などの条件が満たされると、一般的な状態処理のオーバーヘッドを削減できます。順序不同、異常なフラグ、ウィンドウ変更、オプションなどは、他の分岐へ処理を遷移させます。
関数名から「90% のパケットが固定で数十命令しか実行されない」と推論することはできません。チェックサムオフロード、GRO、ロック競合、ソケットバックログ、メモリ割り当て、およびウェイクアップはコストに影響を与えます。ソケットがユーザーコンテキストによって占有されている場合、パケットはバックログにキューに入れられ、適切なパスによって後処理されます。
MSG_DONTWAIT は、今回の読み書きで待機するかどうかを制御するだけであり、カーネルがパケットを受信して直接ユーザーバッファに書き込む条件として見なしてはいけません。ソースコードは net/ipv4/tcp_input.c、tcp.c、および tcp_output.c と合わせて読むべきです。
8. 片方向をクローズしても、接続全体がクローズされるわけではない
状況 典型的な状態/動作 説明
自発的に FIN を送信 FIN_WAIT1、FIN が確認されると FIN_WAIT2 自端の送信方向がクローズされ、受信は継続可能
相手から FIN を受信 CLOSE_WAIT 相手はデータを送信しないが、ローカルアプリケーションは送信可能
受動的側がその後クローズ LAST_ACK 自端の FIN が確認されるのを待機
能動側が双方のクローズを完了 TIME_WAIT 遅延パケットの処理や最終 ACK の再送など、プロトコル要件のため
双方が交差して FIN を送信 CLOSING を経る場合あり 毎回厳密に直列化されるわけではなく、同じパスを辿るとは限らない
TIME_WAIT と FIN_WAIT2 は異なる状態であり、tcp_fin_timeout が汎用の TIME_WAIT 期間スイッチであると見なしてはいけません。CLOSE_WAIT が長期にわたって蓄積する場合、アプリケーションのクローズ動作を確認する必要があります。大量の TIME_WAIT が発生した場合、その数だけでリークを判断することはできません。
実行可能なハーフクローズの例
以下では、ローカルループバックと一時的なポートのみを使用します。クライアントは書き込み後に送信方向をクローズし、サーバーは EOF を検知した後でも、受信したバイト数を返すことができます。
import socket
import threading
errors = [ ]
with socket . socket ( ) as listener :
listener . bind ( ( ' 127.0.0.1' , 0 ) )
listener . listen ( 1 )
listener . settimeout ( 3 )
def serve ( ) :
try :
conn , _ = listener . accept ( )
with conn :
conn . settimeout ( 3 )
size = 0
while data : = conn . recv ( 1024 ) :
size += len ( data )
conn . sendall ( str ( size ) . encode ( ' ascii' ) )
except Exception as exc :
errors . append ( exc )
worker = threading . Thread ( target = serve )
worker . start ( )
try :
with socket . create_connection ( listener . getsockname ( ) , timeout = 3 ) as client :
client . sendall ( b ' hello' )
client . shutdown ( socket . SHUT_WR )
parts = [ ]
while data : = client . recv ( 1024 ) :
parts . append ( data )
assert b ' ' . join ( parts ) == b ' 5'
finally :
worker . join ( timeout = 4 )
assert not worker . is_alive ( )
if errors :
raise errors [ 0 ]
print ( ' half-close: reply received after SHUT_WR' )
このプロトコルは、リクエストの境界として EOF を意図的に使用しています。長期間の接続再利用では、通常、長さまたはフレーム形式を別途設定する必要があります。これは TCP のハーフクローズセマンティクスを示すものであり、ネットワーク断絶、再送、またはアプリケーションの冪等性はシミュレートしていません。