どんなスキルも意図的な練習が必要であり、さらに 10 年のように一貫した努力が求められます。
最近、TCP に関連する知識を整理し、パケットキャプチャを通じて検証し、TCP 接続の確立から終了までのプロセスを分析しました。以前、私と同じように授業で理解できなかった方々も、この文章を読めばほぼ理解できると思います。
TCP は接続指向で信頼性のあるバイトストリームサービスを提供します。つまり、2 つの TCP アプリケーションはデータを交換する前に TCP 接続を確立する必要があり、1 つの TCP 接続では 2 者のみが通信します。TCP と UDP は同じネットワーク層を使用します。
TCP を使用してデータを送信する際、データは TCP が送信に最も適していると考えるデータブロックに分割されます。これは UDP とは異なり、UDP が生成するデータグラムの長さは一定です。このデータブロックはセグメント(segment)と呼ばれ、各セグメントの初期シーケンス番号 ISN(Initial Sequence Number)は特定のアルゴリズムに基づいてランダムに生成されます。もちろん、このシーケンス番号はそのセグメントの最初のデータバイトのデータ番号でもあります。この記事では、以下のいくつかの側面から TCP プロトコルを紹介します:
- TCP プロトコルのデータフォーマット
- TCP の接続の確立方法
- TCP の接続の切断方法
- TCP 状態遷移図
- Wireshark による分析と検証
- なぜ SYN と FIN が 1 つのシーケンス番号を占めるのか
TCP のデータフォーマット#
TCP データは IP データグラムにカプセル化されます。以下の図を参照してください:
- ソースポート(Source Port):データ送信者のポート;
- デスティネーションポート(Destination Port):データ受信者のポート;
- シーケンス番号(Sequence number):16 ビットで 4 バイトを占め、TCP 送信側から TCP 受信側に送信されるデータバイトストリームを識別するために使用され、その値はそのセグメントの最初のデータバイトのデータ番号です。このシーケンス番号は 32 ビットの符号なし数で、シーケンス番号が 2^32 - 1 に達すると 0 から再スタートします;
- 確認番号(Acknowledgment number):16 ビットで 4 バイトを占め、期待される受信データバイトのデータ番号を指し、つまり前回のセグメントの最後のデータバイト番号に 1 を加えた値です;
- SYN:フラグビット、TCP 接続を開始するための同期番号で、
SYN = 1
に設定; - ACK:フラグビット、確認番号が有効であることを示し、
ACK = 1
に設定; - RST:フラグビット、接続を再構築するために、
RST = 1
に設定; - FIN:フラグビット、送信者が送信タスクを完了し、接続を切断したいことを示し、
FIN = 1
に設定; - URG:フラグビット、緊急ポインタが有効であることを示し、
URG = 1
に設定; - PSH:フラグビット、受信者がこのデータをできるだけ早くアプリケーション層に渡すべきであることを示し、
PSH = 1
に設定;
TCP の接続の確立方法#
TCP セグメントにはソースポート番号とデスティネーションポート番号が含まれており、送信側と受信側のアプリケーションプロセスを特定するために使用されます。さらに、IP ヘッダーのソース IP アドレスとターゲット IP アドレスが TCP 接続を一意に特定します。これにより、クライアントとサーバー間の通信が可能になり、TCP 接続の基礎が築かれます。
また、各セグメントの初期シーケンス番号 ISN(Initial Sequence Number)は特定のアルゴリズムに基づいてランダムに生成され、異なります。さらに、接続を確立するためのフラグビット SYN が 1 つのシーケンス番号を占めることを保証するために、後でさらに分析します。
TCP は 3 回のハンドシェイクを通じて接続を確立します。プロセスは以下の通りです:
- クライアントが接続を要求する際に、
seq = x
のセグメントを送信し、フラグビットSYN = 1
を設定してサーバーに TCP 接続を開始します。サーバーはSYN = 1
を見て、クライアントが接続を確立しようとしていることを理解します; - サーバーは受信後、クライアントに確認を要求し、その後
seq = y
のセグメントを送信し、フラグビットACK = 1
、SYN = 1
を設定し、確認番号 ack をクライアントのシーケンス番号に 1 を加えた値、つまりack = x + 1
に設定します; - クライアントはサーバーから受信後、ack がクライアントが前回送信したセグメントのシーケンス番号に 1 を加えたものであるかを確認します。つまり、
ack = x + 1
が満たされる場合、正しければサーバーにseq = x + 1
のセグメントを送信し、フラグビットack = y + 1
を設定します。サーバーが受信後、クライアントとサーバーの TCP 接続が確立され、相互に通信できるようになります。
TCP による 3 回のハンドシェイクの接続確立の図は以下の通りです:
皆さんは TCP が 3 回のハンドシェイクを通じて接続を確立することを知っていると思いますが、この接続確立のプロセスをより良く理解するにはどうすればよいでしょうか?
実際、TCP 接続の確立は 2 つのホストが「互いに呼びかけ」て通信を行うプロセスです。クライアントとサーバーのどちらのプロセスも同じで、[SYN]
パケットを送信して接続を確立するリクエストを行い、その後、対応するホストからACK
応答を待ちます。全体のプロセスは 2 回の接続リクエストと 2 回の応答で構成され、どちらかが正しく応答すれば接続が成功します。2 回目のハンドシェイクの際には、2 つのプロセスに分けることができます:
- サーバーがクライアントのリクエストに応答するために
ACK
セグメントを送信; - サーバーがクライアントに接続を確立するための
SYN
セグメントを送信。
明らかに、これら 2 つのプロセスの目標はクライアントであるため、1 つにまとめられました。このようにして、2 つのホストが「互いに呼びかけ」て TCP 接続を確立しました。2 つのホストがそれぞれ応答する際の確認番号は、対応するホストが以前に送信したセグメントのシーケンス番号に 1 を加えたもので、つまりack = seq + 1
です。
TCP の接続の切断方法#
接続の切断方法を紹介する前に、まず TCP の半閉状態を理解する必要があります。
TCP は送信を終了した後でも、もう一方からの受信を続ける能力を提供します。これが TCP の半閉状態です。例えば、クライアントがデータ送信タスクを完了し、フラグビットFIN = 1
のセグメントをサーバーに送信します。この時、クライアントはデータを送信する能力を失いますが、サーバーからのデータを受信する能力は残っています。サーバーがクライアントにフラグビットFIN = 1
のセグメントで応答するまで、TCP 接続は切断されません。また、接続を切断するためのフラグビット FIN が 1 つのシーケンス番号を占めることを保証するために、後でさらに分析します。
TCP の半閉状態があるため、TCP 接続の切断には 4 回のハンドシェイクが必要です。実際、TCP 接続の切断プロセスも 2 つのホストが「互いに呼びかけ」て終了するプロセスです。両方のホストが相手の接続切断リクエストに応答しなければ、TCP 接続は完全に切断されません。
前述の通り、TCP 接続の 2 回目のハンドシェイクプロセスは 2 つの段階に分けることができ、最終的に 1 つにまとめられました。同様に、TCP 接続の切断プロセスでは、このプロセスを分けて送信する理由は TCP の半閉状態にあります。この半閉状態には応用の可能性があるため、TCP 接続の切断プロセスでは 4 回のハンドシェイクを完了する必要があります。
TCP は 4 回のハンドシェイクを通じて接続を切断します。プロセスは以下の通りです:
- クライアントが送信タスクを完了した後、サーバーに
seq = m
のセグメントを送信し、フラグビットFIN = 1
を設定し、確認シーケンス番号 ack をサーバーが送信した前のセグメントのシーケンス番号に 1 を加えた値に設定し、サーバーに接続を切断することを伝えます; - サーバーはクライアントが接続を切断するためのセグメントを受信した後、クライアントに
seq = n
のセグメントで応答し、フラグビットACK = 1
を設定し、確認番号 ack をクライアントが送信した前のセグメントのシーケンス番号に 1 を加えた値、つまりack = m + 1
に設定します。クライアントがこのセグメントを正しく受信すると、サーバーとの接続が一方向に切断され、半閉状態に入ります。つまり、サーバーからのデータを受信することはできますが、サーバーにデータを送信することはできません; - サーバーが送信タスクを完了した後、クライアントに
seq = n + 1
のセグメントを送信し、フラグビットFIN = 1
を設定し、確認番号 ack をクライアントが送信した前のセグメントのシーケンス番号に 1 を加えた値、つまりack = m + 1
に設定し、クライアントに接続を切断することを伝えます; - クライアントがサーバーが接続を切断するためのセグメントを受信した後、サーバーに
seq = m + 1
のセグメントで応答し、フラグビットACK = 1
を設定し、確認番号 ack をサーバーが送信した前のセグメントのシーケンス番号に 1 を加えた値、つまりack = n + 1
に設定します。サーバーがこのセグメントを正しく受信すると、クライアントとの接続が切断され、クライアントとサーバーの接続が完全に切断されます。
以下は TCP 接続の切断の図です:
Wireshark による分析と検証#
Wireshark を開いてパケットキャプチャを行うことで、上記の内容を検証できます。TCP 接続と切断のプロセスを検証するだけであれば、対応するネットワークカードを選択し、パケットキャプチャを開始するだけです。その後、ブラウザを開いていくつかのページにアクセスすると、通常は対応するネットワークパケットをキャプチャできます。その後、表示フィルターに tcp を入力して TCP プロトコルをフィルタリングし、任意のパケットを選択し、右クリックして「ストリームを追跡」、「TCP ストリーム」を選択して、その TCP 接続に関する情報を確認します。以下のようになります:
具体的な分析は行いませんが、この TCP 接続はデータを送信したことがなく、TCP 接続の確立と切断のプロセスを分析するのにちょうど良いです。
TCP 状態遷移図#
TCP は接続から切断までのプロセスで合計 11 種類の状態があります。以下に TCP の状態遷移図を添付します:
なぜ SYN と FIN が 1 つのシーケンス番号を占めるのか#
前述の分析で、SYN と FIN はそれぞれ 1 つのシーケンス番号を占めます。シーケンス番号の定義に基づくと、これらのセグメントは 1 バイトのデータを持っています。次のセグメントのシーケンス番号は、前のセグメントのシーケンス番号に 1 を加えたものです。
TCP が 3 回のハンドシェイクを通じて接続を確立する例を考えてみましょう。通常、クライアントがSYN = 1,seq = x
のセグメントを送信して接続を確立するリクエストを行い、サーバーがクライアントから送信されたセグメントを受信した後、クライアントのリクエストに応答する必要があります。つまり、サーバーはACK = 1,ack = x + 1
のセグメントを送信します。この時、ACK = 1
はクライアントの接続リクエストを受け取ったことを示し、確認番号ack = x + 1
はシーケンス番号 x のセグメントを受け取ったことを示します。期待される次のセグメントのシーケンス番号はx + 1
です。明らかに、サーバーがクライアントの接続リクエストに応答するシーケンス番号は x のセグメントです。
もし SYN が 1 つのシーケンス番号を占めなければ、サーバーがクライアントの接続確立リクエストのセグメントを受信した際、サーバーが応答するセグメントはACK = 1,ack = x
となります。この場合、確認シーケンス番号 ack の定義によりack = x
は、シーケンス番号がx - 1
のセグメントを受け取ったことを示します。これではクライアントの接続リクエストのセグメントを確認できず、TCP は正常に 3 回のハンドシェイクを完了できず、TCP 接続を確立できなくなります。
もちろん、FIN も同様の理由です。したがって、TCP プロトコルでは SYN と FIN はそれぞれ 1 つのシーケンス番号を占めることになります。誤りがあればご指摘ください。