banner
ShuWa

ShuWa

是进亦忧,退亦忧。然则何时而乐耶?
twitter

TCP

TCP 基本概念#

TCP ヘッダメッセージフォーマット#

image

  • シーケンス番号:接続を確立する際にコンピュータによって生成されるランダムな数値を初期値として、SYN パケットを受信側ホストに送信し、データを 1 回送信するごとに「データバイト数」の大きさを「累加」します。ネットワークパケットの順序の問題を解決するために使用されます。
  • 確認応答番号:次に「期待」されるデータのシーケンス番号を指し、送信側はこの確認応答を受け取った後、このシーケンス番号以前のデータが正常に受信されたと見なすことができます。パケットロスの問題を解決するために使用されます。
  • 制御ビット:
    ACK:このビットが 1 の場合、「確認応答」のフィールドが有効になり、TCP は接続を確立する際の最初の SYN パケットを除いて、このビットを 1 に設定する必要があります。
    RST:このビットが 1 の場合、TCP 接続に異常が発生し、強制的に接続を切断する必要があることを示します。つまり、Socket.close () 関数を呼び出し、4 回のハンドシェイクは必要ありません。
    SYN:このビットが 1 の場合、接続を確立したいことを示し、その「シーケンス番号」のフィールドでシーケンス番号の初期値を設定します。
    FIN:このビットが 1 の場合、今後データを送信しないことを示し、接続を切断したいことを示します。通信が終了し、接続を切断したい場合、通信の両方のホスト間で FIN ビットが 1 の TCP セグメントを相互に交換できます。

どのようにして TCP 接続を一意に特定するのか?#

TCP の 4 つのタプルは接続を一意に特定できます。4 つのタプルは以下を含みます:
ソースアドレス、ソースポート、宛先アドレス、宛先ポート

  • ソースアドレスと宛先アドレスのフィールド(32 ビット)は IP ヘッダーにあり、IP プロトコルを介してメッセージを相手のホストに送信する役割を果たします。
  • ソースポートと宛先ポートのフィールド(16 ビット)は TCP ヘッダーにあり、TCP プロトコルがメッセージをどのプロセスに送信すべきかを知らせる役割を果たします。

ある IP のサーバーがポートをリッスンしている場合、その TCP の最大接続数はどのくらいですか?

サーバーは通常、特定のローカルポートでリッスンし、クライアントの接続要求を待機します。

したがって、クライアントの IP とポートは可変であり、その理論値の計算式は以下の通りです:

TCP の最大接続数 = クライアント IP の数 * クライアントポートの数

IPv4 の場合、クライアントの IP 数は最大で 2 の 32 乗、クライアントのポート数は最大で 2 の 16 乗、つまりサーバー単体の最大 TCP 接続数は約 2 の 48 乗です。
もちろん、サーバーの最大同時 TCP 接続数は理論的上限には達せず、以下の要因に影響されます:

  1. ファイルディスクリプタの制限、各 TCP 接続は 1 つのファイルであり、ファイルディスクリプタが満杯になると、Too many open filesが発生します。Linux は開けるファイルディスクリプタの数に対して 3 つの側面で制限を設けています:

    • システムレベル:現在のシステムで開ける最大数は、cat /proc/sys/fs/file-max で確認できます;
    • ユーザーレベル:指定されたユーザーが開ける最大数は、cat /etc/security/limits.conf で確認できます;
    • プロセスレベル:単一プロセスが開ける最大数は、cat /proc/sys/fs/nr_open で確認できます;
  2. メモリ制限、各 TCP 接続は一定のメモリを占有し、オペレーティングシステムのメモリは有限であり、メモリリソースが満杯になると、OOMが発生します。

IP 層がフラグメンテーションを行うのに、なぜ TCP 層は MSS が必要なのか?#

image

  • MTU:ネットワークパケットの最大長であり、イーサネットでは一般的に 1500 バイトです;
  • MSS:IP および TCP ヘッダーを除いた場合、ネットワークパケットが収容できる TCP データの最大長;

IP 層に MTU サイズを超えるデータ(TCP ヘッダー + TCP データ)を送信する必要がある場合、IP 層はフラグメンテーションを行い、データをいくつかのフラグメントに分割し、各フラグメントが MTU より小さいことを保証します。IP データグラムをフラグメンテーションした後、ターゲットホストの IP 層が再組み立てを行い、次に上位の TCP トランスポート層に渡します。
これは一見整然としているように見えますが、潜在的なリスクがあります。もし 1 つの IP フラグメントが失われた場合、全ての IP メッセージのフラグメントが再送信される必要があります。

IP 層自体にはタイムアウト再送信メカニズムがなく、タイムアウトと再送信はトランスポート層の TCP が担当します。
特定の IP フラグメントが失われた場合、受信側の IP 層は完全な TCP メッセージ(ヘッダー + データ)を組み立てることができず、データグラムを TCP 層に送信できないため、受信側は送信側に ACK を応答しません。送信側は ACK 確認メッセージを受け取れないため、タイムアウト再送信がトリガーされ、「全ての TCP メッセージ(ヘッダー + データ)」が再送信されます。

したがって、最適な伝送効率を達成するために、TCP プロトコルは接続を確立する際に通常、双方の MSS 値を交渉します。TCP 層がデータが MSS を超えていることを発見すると、最初にフラグメンテーションを行います。もちろん、形成された IP パケットの長さも MTU を超えることはなく、自然に IP フラグメンテーションは不要になります。
TCP 層でフラグメンテーションを行った後、特定の TCP フラグメントが失われた場合、再送信も MSS 単位で行われ、全てのフラグメントを再送信する必要がなくなり、再送信の効率が大幅に向上します。

UDP と TCP の違いは何ですか?それぞれのアプリケーションシーンは?#

image

TCP と UDP の違い:#

  1. 接続
    TCP は接続指向のトランスポート層プロトコルであり、データを送信する前に接続を確立する必要があります。
    UDP は接続を必要とせず、即座にデータを送信します。
  2. サービス対象
    TCP は一対一の 2 点サービスであり、1 つの接続には 2 つのエンドポイントしかありません。
    UDP は一対一、一対多、多対多のインタラクティブ通信をサポートします。
  3. 信頼性
    TCP はデータを信頼性高く配信し、データはエラーなく、失われず、重複せず、順序通りに到達します。
    UDP は最大限の努力で配信し、信頼性のあるデータ配信を保証しません。しかし、UDP トランスポートプロトコルに基づいて信頼性のあるトランスポートプロトコルを実装することができます。たとえば、QUIC プロトコルについては、この記事を参照してください:UDP プロトコルに基づいて信頼性のある伝送を実現する方法?(opens new window)
  4. 輻輳制御、フロー制御
    TCP には輻輳制御とフロー制御メカニズムがあり、データ伝送の安全性を保証します。
    UDP にはこれがなく、ネットワークが非常に混雑していても、UDP の送信速度には影響しません。
  5. ヘッダーオーバーヘッド
    TCP ヘッダーの長さは比較的長く、一定のオーバーヘッドがあります。オプションフィールドを使用しない場合、ヘッダーは 20 バイトであり、オプションフィールドを使用すると長くなります。
    UDP ヘッダーはわずか 8 バイトであり、固定されており、オーバーヘッドは小さいです。
  6. 伝送方式
    TCP はストリーム伝送であり、境界がなく、順序と信頼性を保証します。
    UDP はパケット単位で送信され、境界がありますが、パケットが失われたり、順序が乱れたりする可能性があります。
  7. フラグメンテーションの違い
    TCP のデータサイズが MSS サイズを超える場合、トランスポート層でフラグメンテーションが行われ、ターゲットホストはトランスポート層で TCP データパケットを組み立てます。途中でフラグメントが 1 つ失われた場合、失われたフラグメントのみを再送信する必要があります。
    UDP のデータサイズが MTU サイズを超える場合、IP 層でフラグメンテーションが行われ、ターゲットホストは IP 層でデータを組み立てた後、トランスポート層に渡します。
TCP と UDP のアプリケーションシーン:#

TCP は接続指向で、データの信頼性のある配信を保証できるため、以下のような用途でよく使用されます:

  • FTP ファイル転送;
  • HTTP / HTTPS;

UDP は接続を必要とせず、いつでもデータを送信でき、UDP 自体の処理がシンプルで効率的であるため、以下のような用途でよく使用されます:

  • パケット数が少ない通信、例えば DNS、SNMP など;
  • ビデオ、音声などのマルチメディア通信;
  • ブロードキャスト通信;

TCP 接続の確立#

TCP の三回のハンドシェイクプロセスはどのようなものですか?#

image

  • 最初に、クライアントとサーバーは CLOSE 状態にあります。最初にサーバーが特定のポートをアクティブにリッスンし、LISTEN 状態になります。
  • クライアントはランダムにシーケンス番号(client_isn)を初期化し、このシーケンス番号を TCP ヘッダーの「シーケンス番号」フィールドに置き、SYN フラグを 1 に設定して SYN パケットをサーバーに送信します。これはサーバーに接続を開始することを示し、このパケットにはアプリケーション層のデータは含まれていません。その後、クライアントは SYN-SENT 状態になります。
  • サーバーはクライアントの SYN パケットを受信した後、まず自分のシーケンス番号(server_isn)をランダムに初期化し、このシーケンス番号を TCP ヘッダーの「シーケンス番号」フィールドに入力します。次に、TCP ヘッダーの「確認応答番号」フィールドに client_isn + 1 を入力し、SYN と ACK フラグを 1 に設定します。最後に、このパケットをクライアントに送信し、このパケットにもアプリケーション層のデータは含まれていません。その後、サーバーは SYN-RCVD 状態になります。
  • クライアントはサーバーからのパケットを受信した後、最後の応答パケットをサーバーに返す必要があります。最初に、この応答パケットの TCP ヘッダーの ACK フラグを 1 に設定し、「確認応答番号」フィールドに server_isn + 1 を入力し、最後にこのパケットをサーバーに送信します。このパケットにはクライアントからサーバーへのデータを含めることができ、その後、クライアントは ESTABLISHED 状態になります。
  • サーバーはクライアントの応答パケットを受信した後、ESTABLISHED 状態になります。

上記のプロセスから、3 回目のハンドシェイクはデータを含むことができ、最初の 2 回のハンドシェイクはデータを含むことができないことがわかります。これは面接でよく尋ねられる質問です。
三回のハンドシェイクが完了すると、双方は ESTABLISHED 状態になり、この時点で接続が確立され、クライアントとサーバーは相互にデータを送信できます。

Linux システムで TCP 状態を確認するには?

Linux では、netstat -napt コマンドを使用して確認できます。

最初のハンドシェイクが失われた場合、何が起こりますか?

クライアントがサーバーの SYN-ACK パケット(2 回目のハンドシェイク)を受信できない場合、「タイムアウト再送信」メカニズムがトリガーされ、SYN パケットが再送信されます。再送信される SYN パケットのシーケンス番号はすべて同じです。
Linux では、クライアントの SYN パケットの最大再送信回数はtcp_syn_retriesカーネルパラメータによって制御され、このパラメータはカスタマイズ可能で、デフォルト値は通常 5 です。各タイムアウトの時間は前回の 2 倍です。

2 回目のハンドシェイクが失われた場合、何が起こりますか?

  • クライアントは SYN パケットを再送信します。これは最初のハンドシェイクであり、最大再送信回数は tcp_syn_retries カーネルパラメータによって決まります;
  • サーバーは SYN-ACK パケットを再送信します。これは 2 回目のハンドシェイクであり、最大再送信回数は tcp_synack_retries カーネルパラメータによって決まります。
    Linux では、SYN-ACK パケットの最大再送信回数はtcp_synack_retriesカーネルパラメータによって決まります。デフォルト値は 5 です。

3 回目のハンドシェイクが失われた場合、何が起こりますか?

3 回目のハンドシェイクが失われた場合、サーバー側がこの確認パケットを受信できないと、タイムアウト再送信メカニズムがトリガーされ、SYN-ACK パケットが再送信されます。3 回目のハンドシェイクを受信するか、最大再送信回数に達するまで再送信されます。
注意、ACK パケットは再送信されません。ACK が失われた場合、相手が対応するパケットを再送信します。

なぜ三回のハンドシェイクが必要なのか?#

  1. 古い接続を避ける
    三回のハンドシェイクの主な理由は、古い重複接続の初期化による混乱を防ぐためです。
    あるシナリオを考えてみましょう。クライアントが最初に SYN(seq = 90)パケットを送信し、その後クライアントがダウンし、この SYN パケットがネットワークでブロックされ、サーバーは受信しませんでした。その後、クライアントが再起動し、再度サーバーに接続を確立し、SYN(seq = 100)パケットを送信しました(注意!これは SYN の再送信ではなく、再送信された SYN のシーケンス番号は同じです)。
    三回のハンドシェイクが古い接続をどのように防ぐかを見てみましょう:
    クライアントが同じ 4 つのタプルで接続を確立するために複数回 SYN を連続して送信した場合、ネットワークが混雑している状況で:
    • ある「古い SYN パケット」が「最新の SYN」パケットよりも早くサーバーに到達した場合、サーバーはこの時点でクライアントに SYN + ACK パケットを返します。このパケットの確認番号は 91(90 + 1)です。
    • クライアントは受信後、自分が期待する確認番号は 100 + 1 であり、90 + 1 ではないことに気づき、RST パケットを返します。
    • サーバーは RST パケットを受信後、接続を解放します。
    • 後続の最新の SYN がサーバーに到達した後、クライアントとサーバーは正常に三回のハンドシェイクを完了できます。

上記の「古い SYN パケット」は歴史的接続と呼ばれ、TCP が三回のハンドシェイクを使用して接続を確立する主な理由は「歴史的接続」の初期化を防ぐことです。
二回のハンドシェイクの場合、サーバーはクライアントに古い接続を防ぐための中間状態を持たず、サーバーが古い接続を確立する可能性があり、リソースの無駄遣いを引き起こします。
2. 双方の初期シーケンス番号を同期する
シーケンス番号は信頼性のある伝送の重要な要素であり、その役割は:
- 受信側は重複データを除去できます;
- 受信側はデータパケットのシーケンス番号に基づいて順序通りに受信できます;
- 送信されたデータパケットの中で、どれが相手に受信されたかを識別できます(ACK パケットのシーケンス番号を通じて知ることができます);

したがって、クライアントが「初期シーケンス番号」を持つ SYN パケットを送信する際、サーバーは ACK 応答パケットを返す必要があります。これはクライアントの SYN パケットがサーバーによって正常に受信されたことを示します。また、サーバーがクライアントに「初期シーケンス番号」を送信する際も、クライアントの応答を受け取る必要があります。このように、相互にやり取りすることで、双方の初期シーケンス番号が信頼性を持って同期されることが確保されます。
3. リソースの無駄遣いを避ける
「二回のハンドシェイク」のみの場合、クライアントが送信した SYN パケットがネットワークでブロックされ、クライアントが ACK パケットを受信できない場合、再度 SYN を送信します。三回目のハンドシェイクがないため、サーバーはクライアントが自分の ACK パケットを受信したかどうかを知ることができず、サーバーは受信するたびに SYN を受け取ると、まず接続を確立します。
クライアントが送信した SYN パケットがネットワークでブロックされた場合、複数回 SYN パケットを再送信すると、サーバーは要求を受信した後、複数の冗長な無効な接続を確立し、不要なリソースの無駄遣いを引き起こします。

まとめ:「二回のハンドシェイク」と「四回のハンドシェイク」を使用しない理由:
「二回のハンドシェイク」:古い接続の確立を防ぐことができず、双方のリソースの無駄遣いを引き起こし、双方のシーケンス番号を信頼性を持って同期することができません;
「四回のハンドシェイク」:三回のハンドシェイクは理論的に最小の信頼性のある接続確立であるため、より多くの通信回数を使用する必要はありません。

なぜ TCP 接続を確立する際、初期シーケンス番号は異なる必要があるのか?#

歴史的なパケットが次の同じ 4 つのタプルの接続で受信されるのを防ぐため(主な理由);
セキュリティのため、ハッカーが偽造した同じシーケンス番号の TCP パケットが相手に受信されるのを防ぐため;

image
プロセスは以下の通りです:

  • クライアントとサーバーが TCP 接続を確立し、クライアントがデータパケットを送信する際にネットワークでブロックされ、そのデータパケットがタイムアウト再送信されます。この時、サーバーのデバイスが電源を切って再起動し、以前にクライアントと確立した接続が消失します。そのため、クライアントのデータパケットを受信した際、RST パケットを送信します。
  • 次に、クライアントはサーバーと前の接続と同じ 4 つのタプルの接続を確立します;
  • 新しい接続が確立された後、前の接続でネットワークでブロックされたデータパケットがちょうどサーバーに到達し、そのデータパケットのシーケンス番号がサーバーの受信ウィンドウ内にあるため、そのデータパケットはサーバーによって正常に受信され、データの混乱が発生します。

接続を確立する際、クライアントとサーバーの初期シーケンス番号が同じである場合、歴史的なパケットが次の同じ 4 つのタプルの接続で受信される問題が発生しやすいことがわかります。

初期シーケンス番号 ISN はどのようにランダムに生成されるのか?

初期 ISN は時計に基づいており、4 マイクロ秒ごとに + 1 され、1 周するのに 4.55 時間かかります。
RFC793 では、初期シーケンス番号 ISN のランダム生成アルゴリズムが言及されています:ISN = M + F (localhost, localport, remotehost, remoteport)。

  • M はタイマーで、このタイマーは 4 マイクロ秒ごとに + 1 されます。
  • F はハッシュアルゴリズムで、ソース IP、宛先 IP、ソースポート、宛先ポートに基づいてランダムな値を生成します。ハッシュアルゴリズムは外部から簡単に推測できないようにする必要があり、MD5 アルゴリズムは比較的良い選択です。

ランダム数は時計タイマーに基づいて増加するため、基本的に同じ初期シーケンス番号がランダムに生成されることはありません。

SYN 攻撃とは何か?SYN 攻撃を回避する方法は?#

私たちは TCP 接続の確立には三回のハンドシェイクが必要であることを知っています。仮に攻撃者が短時間で異なる IP アドレスの SYN パケットを偽造した場合、サーバーは受信するたびに SYN_RCVD 状態に入り、サーバーが送信した ACK + SYN パケットは未知の IP ホストの ACK 応答を受け取ることができず、次第にサーバーの半接続キューが満杯になり、サーバーは正常なユーザーにサービスを提供できなくなります。

半接続キューと全接続キュー?
別名 SYN キューと accept キュー;
image
正常なプロセス:

  • サーバーがクライアントの SYN パケットを受信すると、半接続オブジェクトを作成し、カーネルの「SYN キュー」に追加します;
  • 次に、クライアントに SYN + ACK を送信し、クライアントの ACK パケットの応答を待ちます;
    サーバーが ACK パケットを受信すると、「SYN キュー」から半接続オブジェクトを取り出し、新しい接続オブジェクトを「Accept キュー」に追加します;
  • アプリケーションは accept () ソケットインターフェースを呼び出して、「Accept キュー」から接続オブジェクトを取り出します。

半接続キューと全接続キューの両方には最大長の制限があり、制限を超えると、デフォルトではパケットが破棄されます。

SYN 攻撃の最も直接的な影響は、TCP 半接続キューを満杯にすることです。このように、TCP 半接続キューが満杯になると、以降に受信した SYN パケットは破棄され、クライアントはサーバーと接続を確立できなくなります。
SYN 攻撃を回避する方法は以下の 4 つです:
1. netdev_max_backlog を増やす
ネットワークカードがデータパケットを受信する速度がカーネルの処理速度を上回ると、これらのデータパケットを保存するキューが作成されます。このキューの最大値を制御するパラメータは、デフォルト値は 1000 であり、このパラメータの値を適切に増やす必要があります。たとえば、10000 に設定します:
2. TCP 半接続キューを増やす
TCP 半接続キューを増やすには、以下の 3 つのパラメータを同時に増やす必要があります:

  1. net.ipv4.tcp_max_syn_backlog を増やす
  2. listen () 関数内の backlog を増やす
  3. net.core.somaxconn を増やす

3. net.ipv4.tcp_syncookies を有効にする
syncookies 機能を有効にすると、SYN 半接続キューを使用せずに接続を確立でき、SYN 半接続を回避して接続を確立することができます。
image
tcp_syncookies が有効になっている場合、SYN 攻撃を受けて SYN キューが満杯になっても、正常な接続が確立されることが保証されます。
net.ipv4.tcp_syncookies パラメータには、以下の 3 つの値があります:
0:この機能を無効にする;
1:SYN 半接続キューが満杯のときのみ有効にする;
2:無条件に機能を有効にする;
したがって、SYN 攻撃に対処する際には、1 に設定するだけで済みます。
4. SYN + ACK の再送信回数を減らす
サーバーが SYN 攻撃を受けると、多くの TCP 接続が SYN_REVC 状態になります。この状態の TCP は SYN + ACK を再送信します。再送信回数が上限に達すると、接続が切断されます。
したがって、SYN 攻撃のシナリオに対処するために、SYN-ACK の再送信回数を減らして、SYN_REVC 状態の TCP 接続を迅速に切断することができます。
SYN-ACK パケットの最大再送信回数は tcp_synack_retries カーネルパラメータによって決まります(デフォルト値は 5 回です)。たとえば、tcp_synack_retries を 2 回に減らすことができます。

TCP 接続の切断#

TCP の四回のハンドシェイクプロセス#

image

  • クライアントは接続を閉じることを決定し、この時点で TCP ヘッダーの FIN フラグを 1 に設定したパケット、すなわち FIN パケットを送信し、その後クライアントは FIN_WAIT_1 状態になります。
  • サーバーはこのパケットを受信した後、クライアントに ACK 応答パケットを送信し、その後サーバーは CLOSE_WAIT 状態になります。
  • クライアントはサーバーの ACK 応答パケットを受信した後、FIN_WAIT_2 状態に移行します。
  • サーバーがデータの処理を完了した後、クライアントに FIN パケットを送信し、その後サーバーは LAST_ACK 状態になります。
  • クライアントはサーバーの FIN パケットを受信した後、ACK 応答パケットを返し、その後 TIME_WAIT 状態に移行します。
  • サーバーは ACK 応答パケットを受信した後、CLOSE 状態に移行し、これによりサーバーは接続の切断を完了します。
  • クライアントは 2MSL の時間が経過した後、自動的に CLOSE 状態に移行し、これによりクライアントも接続の切断を完了します。

各方向には FIN と ACK が必要であるため、通常は四回のハンドシェイクと呼ばれます。
ここで注意すべき点は、接続を積極的に閉じる側だけが TIME_WAIT 状態になります。

なぜハンドシェイクに四回が必要なのか?

サーバーは通常、データの送信と処理が完了するのを待つ必要があるため、サーバーの ACK と FIN は通常分けて送信されるため、四回のハンドシェイクが必要です。

最初のハンドシェイクが失われた場合、何が起こりますか?

最初のハンドシェイクが失われた場合、クライアントは受動側の ACK を受信できないため、タイムアウト再送信メカニズムがトリガーされ、FIN パケットが再送信されます。再送信回数は tcp_orphan_retries パラメータによって制御されます。
クライアントが FIN パケットの再送信回数が tcp_orphan_retries を超えると、FIN パケットの送信を停止し、一定の時間(前回のタイムアウト時間の 2 倍)待機します。それでも 2 回目のハンドシェイクを受信できない場合、直接 CLOSE 状態に移行します。

2 回目のハンドシェイクが失われた場合、何が起こりますか?

ACK パケットは再送信されないため、サーバーの 2 回目のハンドシェイクが失われた場合、クライアントはタイムアウト再送信メカニズムがトリガーされ、FIN パケットを再送信します。サーバーの 2 回目のハンドシェイクを受信するか、最大再送信回数に達するまで再送信されます。

3 回目のハンドシェイクが失われた場合、何が起こりますか?

クライアントはサーバーの ACK パケットを受信した後、FIN_WAIT2 状態になり、この状態ではサーバーの 3 回目のハンドシェイク、すなわちサーバーの FIN パケットを待つ必要があります。
close 関数で閉じた接続の場合、データの送受信ができなくなるため、FIN_WAIT2 状態は長く続けることができず、tcp_fin_timeout がこの状態での接続の持続時間を制御します。デフォルト値は 60 秒です。
これは、close を呼び出して閉じた接続の場合、60 秒後に FIN パケットを受信できなければ、クライアント(積極的に閉じた側)の接続が直接閉じられることを意味します。
ただし、積極的に閉じた側が shutdown 関数を使用して接続を閉じ、送信方向のみを閉じ、受信方向は閉じていない場合、積極的に閉じた側はデータを受信できることを意味します。
この場合、積極的に閉じた側が 3 回目のハンドシェイクを受信できない限り、積極的に閉じた側の接続は FIN_WAIT2 状態のままになります。
サーバー(受動的に閉じた側)がクライアント(積極的に閉じた側)の FIN パケットを受信すると、カーネルは自動的に ACK を返し、接続は CLOSE_WAIT 状態になります。
サーバーが CLOSE_WAIT 状態のときに close 関数を呼び出すと、カーネルは FIN パケットを送信し、接続は LAST_ACK 状態になり、クライアントからの ACK を待って接続を閉じることを確認します。
この ACK を長時間受信できない場合、サーバーは FIN パケットを再送信し、再送信回数は tcp_orphan_retries パラメータによって制御されます。これは、クライアントが FIN パケットを再送信する回数の制御方法と同じです。

4 回目のハンドシェイクが失われた場合、何が起こりますか?

クライアントはサーバーの 3 回目のハンドシェイクである FIN パケットを受信した後、ACK パケットを返します。これは 4 回目のハンドシェイクであり、この時点でクライアントの接続は TIME_WAIT 状態になります。
Linux システムでは、TIME_WAIT 状態は 2MSL の後に閉じる状態に移行します。
その後、サーバー(受動的に閉じた側)は ACK パケットを受信するまで LAST_ACK 状態に留まります。
4 回目のハンドシェイクの ACK パケットがサーバーに到達しない場合、サーバーは FIN パケットを再送信し、再送信回数は前述の tcp_orphan_retries パラメータによって制御されます。

TIME_WAIT 状態に関連する#

なぜ TIME_WAIT の待機時間は 2MSL なのか?#

MSL はMaximum Segment Lifetime、パケットの最大生存時間であり、ネットワーク上に存在するパケットの最長時間であり、この時間を超えるとパケットは破棄されます。TCP パケットは IP プロトコルに基づいているため、IP ヘッダーには TTL フィールドがあり、IP データグラムが通過できる最大ルータ数を示します。各ルータを通過するたびにこの値は 1 減少し、この値が 0 になるとデータグラムは破棄され、送信元ホストにICMPパケットが通知されます。
MSL と TTL の違い:MSL の単位は時間であり、TTL は通過したルータの数です。したがって、MSL は TTL が 0 になるまでの時間以上である必要があり、パケットが自然に消失することを保証します。
TTL の値は一般的に 64 であり、Linux は MSL を 30 秒に設定しています。これは、Linux がデータパケットが 64 のルータを通過するのにかかる時間が 30 秒を超えないと考えていることを意味し、超えた場合はパケットがネットワーク中で消失したと見なされます。
TIME_WAIT は MSL の 2 倍の時間を待機することが比較的合理的な説明です。ネットワークには送信元からのデータパケットが存在する可能性があり、これらのデータパケットが受信側で処理された後、相手に応答を送信するため、往復で 2 倍の時間を待つ必要があります。

なぜ TIME_WAIT 状態が必要なのか?#

主に 2 つの理由があります:

  1. 古い接続のデータが後続の同じ 4 つのタプルの接続で誤って受信されるのを防ぐ;
  2. 「受動的に接続を閉じる」側が正しく閉じられることを保証する;

TIME_WAIT が多すぎるとどのような危害があるのか?#

サーバーはシステムリソースを占有します。たとえば、ファイルディスクリプタ、メモリリソース、CPU リソース、スレッドリソースなど;
クライアントはポートリソースを占有します。ポートリソースも有限であり、一般的に開けるポートは 32768~61000 であり、net.ipv4.ip_local_port_range パラメータを使用して範囲を指定できます。

TIME_WAIT を最適化する方法は?#

  • net.ipv4.tcp_tw_reuse と net.ipv4.tcp_timestamps オプションを有効にする;
    TIME_WAIT 状態のソケットを新しい接続に再利用できます。ただし、tcp_tw_reuse 機能はクライアント(接続を開始する側)のみで使用できることに注意が必要です。この機能を有効にすると、connect () 関数を呼び出す際に、カーネルは TIME_WAIT 状態が 1 秒を超えた接続をランダムに選択して新しい接続に再利用します。
  • net.ipv4.tcp_max_tw_buckets
    この値はデフォルトで 18000 であり、システム内の TIME_WAIT 接続がこの値を超えると、システムは後続の TIME_WAIT 接続状態をリセットします。この方法は比較的強引です。
  • プログラム内で SO_LINGER を使用し、RST で強制的に閉じる。
    l_onoff が 0 以外で、l_linger 値が 0 の場合、close を呼び出すと RST フラグが相手に送信され、この TCP 接続は四回のハンドシェイクをスキップし、TIME_WAIT 状態をスキップして直接閉じます。

サーバーに大量の TIME_WAIT 状態が発生する原因は?#

TIME_WAIT 状態は積極的に接続を閉じる側が発生する状態であるため、サーバーに大量の TIME_WAIT 状態の TCP 接続が発生する場合、サーバーが多くの TCP 接続を積極的に切断したことを示しています。
サーバーが接続を積極的に切断するシナリオは何ですか?

  • 最初のシナリオ:HTTP が長接続を使用していない
    いずれかの側の HTTP ヘッダーに Connection情報が含まれている場合、HTTP 長接続メカニズムを使用できません。このため、1 回の HTTP リクエスト / 処理が完了した後、接続が閉じられます。
    ほとんどの Web サービスの実装では、どちらの側が HTTP Keep-Alive を無効にしても、サーバーが接続を積極的に閉じます。
  • 2 番目のシナリオ:HTTP 長接続のタイムアウト
    HTTP 長接続のタイムアウト時間が 60 秒に設定されていると仮定すると、nginx は「タイマー」を起動します。クライアントが最後の HTTP リクエストの後、60 秒以内に新しいリクエストを発起しない場合、タイマーの時間が到達すると、nginx はコールバック関数をトリガーして接続を閉じます。この時、サーバーに TIME_WAIT 状態の接続が発生します。
  • 3 番目のシナリオ:HTTP 長接続のリクエスト数が上限に達する
    nginx の keepalive_requests パラメータは、HTTP 長接続が確立された後、nginx がこの接続にカウンターを設定し、クライアントリクエストの数を記録します。このパラメータで設定された最大値に達すると、nginx はこの長接続を積極的に閉じます。この時、サーバーに TIME_WAIT 状態の接続が発生します。

サーバーに大量の CLOSE_WAIT 状態が発生する原因は?
サーバーに大量の CLOSE_WAIT 状態の接続が発生する場合、サーバーのプログラムが close 関数を呼び出して接続を閉じていないことを示しており、通常はコードの問題です。

ソケットプログラミング#

TCP に対してソケットプログラミングをどのように行うべきか?#

image

  • サーバーとクライアントがソケットを初期化し、ファイルディスクリプタを取得します;
  • サーバーは bind を呼び出して、ソケットを指定された IP アドレスとポートにバインドします;
  • サーバーは listen を呼び出してリッスンを行います;
  • サーバーは accept を呼び出してクライアントの接続を待ちます;
  • クライアントは connect を呼び出して、サーバーのアドレスとポートに接続要求を送信します;
  • サーバーの accept は、データ転送用のソケットのファイルディスクリプタを返します;
  • クライアントは write を呼び出してデータを書き込み、サーバーは read を呼び出してデータを読み取ります;
  • クライアントが接続を切断する際、close を呼び出すと、サーバーは read で EOF を読み取り、データ処理が完了した後、サーバーは close を呼び出して接続を閉じます。

ここで注意すべき点は、サーバーが accept を呼び出すと、接続が成功した場合、完了した接続のソケットが返され、以降はデータ転送に使用されます。
したがって、リッスンソケットと実際にデータを送信するためのソケットは「2 つ」のソケットであり、1 つはリッスンソケット、もう 1 つは完了した接続ソケットです。
接続が確立された後、双方は read と write 関数を使用してデータを読み書きします。これはファイルストリームにデータを書き込むのと同じです。

listen 時のパラメータ backlog の意味は?#

Linux カーネルは 2 つのキューを管理します:

  • 半接続キュー(SYN キュー):SYN 接続要求を受信し、SYN_RCVD 状態にある;
  • 全接続キュー(Accept キュー):TCP 三回のハンドシェイクプロセスが完了し、ESTABLISHED 状態にある;

初期の Linux カーネルでは backlog は SYN キューのサイズ、つまり未完了のキューのサイズでした。
Linux カーネル 2.2 以降、backlog は accept キュー、つまり接続が確立された後のキューの長さに変更されたため、現在では backlog は accept キューと見なされることが一般的です。
ただし、上限値はカーネルパラメータ somaxconn のサイズであり、つまり accept キューの長さは min (backlog, somaxconn) です。

TCP 半接続キューと全接続キューが満杯になると何が起こるのか?
TCP 全接続キューが溢れた場合
TCP の最大全接続キューを超えると、サーバーは以降の TCP 接続を破棄します。
TCP 半接続キューが溢れた場合
半接続キューが満杯で、tcp_syncookies が有効でない場合、破棄されます;
全接続キューが満杯で、SYN + ACK パケットの接続要求が 1 つ以上再送信されない場合、破棄されます;
tcp_syncookies が無効で、max_syn_backlog から現在の半接続キューの長さを引いた値が (max_syn_backlog>> 2) 未満の場合、破棄されます;

accept は三回のハンドシェイクのどのステップで発生するのか?#

image
クライアントの connect 成功は 2 回目のハンドシェイクであり、サーバーの accept 成功は三回目のハンドシェイクが成功した後に発生します。

クライアントが close を呼び出した場合、接続切断のプロセスはどのようになりますか?#

image

  • クライアントが close を呼び出すと、クライアントは送信するデータがないことを示し、この時点でサーバーに FIN パケットを送信し、FIN_WAIT_1 状態になります;
  • サーバーは FIN パケットを受信すると、TCP プロトコルスタックは FIN パケットに EOF ファイル終了符号を受信バッファに挿入します。アプリケーションはこの FIN パケットを感知するために read を呼び出すことができます。この EOF は、他の受信されたデータの待機キューの後に配置されます。これは、サーバーがこの異常状態を処理する必要があることを意味します。EOF は、この接続上で追加のデータが到着しないことを示します。この時、サーバーは CLOSE_WAIT 状態になります;
  • 次に、データを処理した後、自然に EOF を読み取ることになり、サーバーも close を呼び出してソケットを閉じ、FIN パケットを送信し、その後 LAST_ACK 状態になります;
  • クライアントはサーバーの FIN パケットを受信し、ACK 確認パケットをサーバーに送信します。この時点でクライアントは TIME_WAIT 状態になります;
  • サーバーは ACK 確認パケットを受信した後、最終的に CLOSE 状態になります;
  • クライアントは 2MSL の時間が経過した後、CLOSE 状態に移行します;

accept なしで TCP 接続を確立できますか?#

できます。
accept システムコールは TCP 三回のハンドシェイクプロセスに参加せず、単に TCP 全接続キューから確立された接続のソケットを取り出す役割を果たします。ユーザーレベルは accept システムコールを呼び出して、確立された接続のソケットを取得し、そのソケットに対して読み書き操作を行うことができます。

listen なしで TCP 接続を確立できますか?#

できます。
クライアントは自分自身に接続を確立することができ(TCP 自己接続)、または 2 つのクライアントが同時に相手に接続要求を発信することもできます(TCP 同時オープン)。これらの 2 つのシナリオには共通点があり、サーバーが関与せず、つまり listen がない状態で TCP 接続を確立できます。
サーバーが関与する場合、サーバーが listen 関数を呼び出さないと、リッスンしているポートのソケットを見つけることができず、RST でこの接続を中止します。

TCP の信頼性メカニズム#

再送信メカニズム#

タイムアウト再送信#

再送信メカニズムの 1 つの方法は、データを送信する際にタイマーを設定し、指定された時間を超えて相手の ACK 確認応答パケットを受信できない場合、そのデータを再送信することです。これが一般的に言われるタイムアウト再送信です。
TCP は以下の 2 つの状況でタイムアウト再送信を行います:

  • データパケットが失われた
  • 確認応答が失われた

再送信されたデータが再度タイムアウトした場合、再送信の間隔は倍増します。
つまり、タイムアウト再送信が発生するたびに、次回のタイムアウト時間を前回の 2 倍に設定します。2 回のタイムアウトは、ネットワーク環境が悪化していることを示し、頻繁に再送信することは適切ではありません。
タイムアウトによる再送信には問題があり、タイムアウト周期が比較的長い可能性があります。

高速再送信#

TCP にはもう 1 つの高速再送信(Fast Retransmit)メカニズムがあります。これは時間ではなくデータに基づいて再送信を行います。
image
上の図では、送信側が 1、2、3、4、5 のデータを送信しました:

  • 最初の Seq1 が先に到着したため、Ack は 2 に戻ります;
  • Seq2 は何らかの理由で受信されず、Seq3 が到着したため、Ack は依然として 2 に戻ります;
  • 後続の Seq4 と Seq5 も到着しましたが、Ack は依然として 2 に戻ります。Seq2 はまだ受信されていないためです;
  • 送信側は 3 つの Ack = 2 の確認を受け取り、Seq2 がまだ受信されていないことを知り、失われた Seq2 をタイマーが期限切れになる前に再送信します。
  • 最後に、Seq2 を受信しました。この時点で Seq3、Seq4、Seq5 も受信されているため、Ack は 6 に戻ります。

したがって、高速再送信の動作方式は、3 つの同じ ACK パケットを受信したときに、タイマーが期限切れになる前に失われたパケットを再送信することです。
高速再送信メカニズムは、タイムアウト時間の問題を解決しますが、再送信時に 1 つを再送信するのか、すべてを再送信するのかという別の問題に直面します。

SACK メソッド#

SACK(Selective Acknowledgment)、選択的確認。この方法では、TCP ヘッダーの「オプション」フィールドに SACK を追加する必要があります。これにより、受信したデータの情報を「送信側」に送信できるため、送信側はどのデータが受信され、どのデータが受信されていないかを知ることができます。この情報を知ることで、失われたデータのみを再送信できます。
送信側は 3 回の同じ ACK 確認パケットを受信すると、高速再送信メカニズムがトリガーされ、SACK 情報を通じて 200〜299 のデータが失われたことを発見した場合、再送信時にはこの TCP セグメントのみを選択して再送信します。
image

重複 SACK#

重複 SACK は D-SACK とも呼ばれ、主に SACK を使用して「送信側」にどのデータが重複して受信されたかを知らせるために使用されます。
ACK が失われた場合:
image

  • 「受信側」が「送信側」に送信した 2 つの ACK 確認応答が失われたため、送信側はタイムアウト後に最初のデータパケット(3000〜3499)を再送信します。
  • その後、「受信側」はデータが重複して受信されたことを発見し、SACK = 3000〜3500 を返します。「送信側」に 3000〜3500 のデータがすでに受信されたことを知らせます。ACK はすでに 4000 に到達しており、4000 以前のすべてのデータが受信されたことを示しています。したがって、この SACK は D-SACK を表します。
  • これにより、「送信側」はデータが失われていないことを知り、受信側の ACK 確認パケットが失われたことがわかります。

ネットワーク遅延の場合:
image

  • データパケット(1000〜1499)がネットワークで遅延し、「送信側」は ACK 1500 の確認パケットを受信できません。
  • その後、後続の 3 つの同じ ACK 確認パケットが到達し、高速再送信メカニズムがトリガーされますが、再送信後に遅延されたデータパケット(1000〜1499)が「受信側」に到達します;
  • したがって、「受信側」は SACK = 1000〜1500 を返します。ACK はすでに 3000 に到達しているため、この SACK は D-SACK であり、重複したパケットが受信されたことを示しています。
  • これにより、送信側は高速再送信がトリガーされた理由が、送信されたパケットが失われたのか、受信側の ACK パケットが失われたのか、ネットワークが遅延したのかを知ることができます。

D-SACK には以下のような利点があります:

  • 「送信側」に対して、送信されたパケットが失われたのか、受信側の ACK パケットが失われたのかを知らせることができます;
  • 送信側のデータパケットがネットワークで遅延したかどうかを知ることができます;
  • ネットワーク内で送信側のデータパケットが複製されたかどうかを知ることができます;

スライディングウィンドウ#

TCP はウィンドウという概念を導入し、パケットの往復時間が長くなるほど通信効率が低下する問題を解決します。ウィンドウサイズは、確認応答を待たずに続けてデータを送信できる最大値を示します。
ウィンドウの実装は、オペレーティングシステムが開放したキャッシュスペースを操作することです。送信側ホストは確認応答が返るまで、送信したデータをバッファ内に保持する必要があります。確認応答を期日通りに受け取った場合、この時点でデータはキャッシュから削除できます。

image
図中の ACK 600 確認応答パケットが失われても問題ありません。次の確認応答によって確認できます。送信側が ACK 700 確認応答を受信した場合、700 以前のすべてのデータが「受信側」に受信されたことを示します。このパターンは累積確認または累積応答と呼ばれます。

ウィンドウサイズはどちらの側が決定するのか?

TCP ヘッダーには Window というフィールドがあり、ウィンドウサイズが含まれています。
このフィールドは受信側が送信側に対して、どれだけのバッファを受信できるかを知らせます。したがって、送信側は受信側の処理能力に基づいてデータを送信でき、受信側が処理できなくなることはありません。
通常、ウィンドウサイズは受信側のウィンドウサイズによって決定されます。
送信側が送信するデータのサイズは、受信側のウィンドウサイズを超えてはならず、そうでないと受信側はデータを正常に受信できません。

送信ウィンドウ
送信されたデータストリームは以下の 4 つの部分に分けられます:送信済みかつ確認済みの部分 | 送信済みだが未確認の部分 | 未送信だが送信可能な部分 | 送信不可能な部分。送信ウィンドウ = 送信済みだが未確認の部分 + 未送信だが送信可能な部分。

受信ウィンドウ
受信されたデータストリームは以下のように分けられます:受信済み | 未受信だが受信準備ができている | 未受信で受信準備ができていない。受信ウィンドウ = 未受信だが受信準備ができている部分。

受信ウィンドウと送信ウィンドウのサイズは等しいのか?

完全に等しいわけではなく、受信ウィンドウのサイズは送信ウィンドウのサイズに近いです。
スライディングウィンドウは一成不変ではありません。たとえば、受信側のアプリケーションプロセスがデータを読み取る速度が非常に速い場合、受信ウィンドウはすぐに空きが出る可能性があります。この場合、新しい受信ウィンドウサイズは TCP パケット内の Windows フィールドを通じて送信側に通知されます。この伝送プロセスには遅延が存在するため、受信ウィンドウと送信ウィンドウは近似的な関係にあります。

フロー制御#

送信側は無制限にデータを受信側に送信できず、受信側の処理能力を考慮する必要があります。
もし無制限にデータを送信し続けると、受信側が処理できなくなり、再送信メカニズムがトリガーされ、ネットワークトラフィックの無駄な浪費を引き起こします。
このような現象を防ぐために、TCP は「送信側」が「受信側」の実際の受信能力に基づいて送信するデータ量を制御できるメカニズムを提供します。これがいわゆるフロー制御です。
オペレーティングシステムのバッファとスライディングウィンドウの関係
送信ウィンドウと受信ウィンドウに格納されるバイト数は、すべてオペレーティングシステムのメモリバッファに格納されており、オペレーティングシステムによって調整されます。
アプリケーションプロセスがバッファの内容をタイムリーに読み取れない場合、バッファにも影響を与えます。
もしバッファが先に減少し、ウィンドウが縮小されると、パケットロスが発生する可能性があります。
このような状況を防ぐために、TCP は同時にバッファを減少させたりウィンドウを縮小したりすることを許可せず、まずウィンドウを縮小し、しばらくしてからバッファを減少させることで、パケットロスを回避します。
ウィンドウの閉鎖
ウィンドウサイズが 0 の場合、送信側が受信側にデータを送信することを阻止し、ウィンドウが非 0 になるまで待機します。これがウィンドウの閉鎖です。
受信側が送信側にウィンドウサイズを通知する際は、ACK パケットを通じて通知します。
ウィンドウが閉鎖された場合、受信側がデータを処理した後、送信側にウィンドウが非 0 であることを通知する ACK パケットを送信します。この通知 ACK パケットがネットワークで失われた場合、送信側は受信側の非 0 ウィンドウ通知を待ち続け、受信側も送信側のデータを待ち続けます。この相互待機プロセスはデッドロックを引き起こす可能性があります。
この問題を解決するために、TCP は各接続に持続タイマーを設定します。TCP 接続の一方が相手のゼロウィンドウ通知を受信すると、持続タイマーが起動します。
持続タイマーがタイムアウトすると、ウィンドウプローブ(Window probe)パケットが送信され、相手はこのプローブパケットを確認する際に現在の受信ウィンドウサイズを示します。
ウィンドウプローブの回数は通常 3 回であり、各回は約 30〜60 秒(異なる実装によって異なる場合があります)です。3 回を超えて受信ウィンドウが依然として 0 の場合、一部の TCP 実装では RST パケットを送信して接続を中断します。
混乱ウィンドウ症候群
受信側が非常に忙しく、受信ウィンドウ内のデータを取り出す余裕がない場合、送信側の送信ウィンドウがどんどん小さくなります。
最終的に、受信側が数バイトの空きができ、送信側に現在のバイト数のウィンドウを通知すると、送信側はその数バイトを送信します。これは混乱ウィンドウ症候群です。
TCP + IP ヘッダーには 40 バイトがあり、数バイトのデータを送信するためにこれほど大きなオーバーヘッドがかかるのは非常に非経済的です。
混乱ウィンドウ症候群を解決するためには、2 つの問題を同時に解決する必要があります:

  1. 受信側が送信側に小さなウィンドウを通知しないようにする
    受信側の一般的な戦略は以下の通りです:
    「ウィンドウサイズ」が min(MSS、バッファサイズ / 2)未満、つまり MSS とバッファサイズの 1/2 の最小値よりも小さい場合、受信側は送信側にウィンドウを 0 として通知し、送信側がデータを送信するのを防ぎます。
    受信側がいくつかのデータを処理した後、ウィンドウサイズが MSS 以上、または受信側のバッファスペースが半分使用可能になった場合、ウィンドウを開いて送信側にデータを送信させることができます。
  2. 送信側が小さなデータを送信しないようにする
    送信側の一般的な戦略は以下の通りです:
    Nagle アルゴリズムを使用します。このアルゴリズムの考え方は遅延処理であり、以下の 2 つの条件のいずれかを満たす場合にのみデータを送信します:
    条件 1:ウィンドウサイズ >= MSS かつデータサイズ >= MSS;
    条件 2:以前に送信したデータの ACK 応答パケットを受信した;
    上記の 2 つの条件のいずれも満たされない場合、送信側はデータを蓄積し続け、上記の送信条件が満たされるまで待機します。

輻輳制御#

ネットワークが混雑している場合、大量のデータパケットを送信し続けると、データパケットの遅延や損失が発生する可能性があります。この場合、TCP はデータを再送信しますが、再送信することでネットワークの負担がさらに増し、より大きな遅延やさらなるパケット損失が発生します。この状況は悪循環に陥り、次第に拡大します....
したがって、輻輳制御が必要であり、その目的は「送信側」のデータがネットワーク全体を埋め尽くすのを避けることです。
「送信側」が送信するデータ量を調整するために、「輻輳ウィンドウ」という概念が定義されており、ネットワークの輻輳程度に応じて動的に変化します。
輻輳制御アルゴリズム:
1. スロースタート
送信側が ACK を受信するたびに、輻輳ウィンドウ cwnd のサイズが 1 増加します。スロースタートがスロースタート閾値 ssthresh(slow start threshold)に達するまで。
- cwnd < ssthresh の場合、スロースタートアルゴリズムを使用します。
- cwnd >= ssthresh の場合、「輻輳回避アルゴリズム」を使用します。
2. 輻輳回避
ACK を受信するたびに、cwnd が 1/cwnd 増加します。
3. 輻輳が発生した場合
ネットワークが混雑している場合、データパケットの再送信が発生します。再送信メカニズムによって異なるため、輻輳が発生した場合のアルゴリズムも異なります。

タイムアウト再送信が発生した場合の輻輳が発生するアルゴリズム

  • ssthresh を cwnd/2 に設定します。
  • cwnd を 1 にリセットします(cwnd の初期値に戻します。ここでは cwnd の初期値を 1 と仮定します)。

image

高速再送信が発生した場合の輻輳が発生するアルゴリズム

  • cwnd = cwnd/2、つまり元の半分に設定します;
  • ssthresh = cwnd;
  • 高速回復アルゴリズムに入ります。

4. 高速回復

  • 輻輳ウィンドウ cwnd = ssthresh + 3
  • 失われたデータパケットを再送信します;
  • さらに重複 ACK を受信した場合、cwnd が 1 増加します;
  • 新しいデータの ACK を受信した場合、cwnd を最初のステップでの ssthresh の値に設定します。
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。