it

TCPのシーケンス番号とは?役割と仕組みも!(順序制御:確認応答:初期値:ISN:重複検出:データの順序保証など)

当サイトでは記事内に広告を含みます

TCPのシーケンス番号とは?役割と仕組みも!(順序制御:確認応答:初期値:ISN:重複検出:データの順序保証など)

インターネット通信において、データが正確に、そして順序通りに届くことは非常に重要です。しかし、ネットワークの世界ではパケットが異なる経路を通ったり、遅延が発生したりすることが珍しくありません。そこで活躍するのが、TCPのシーケンス番号という仕組みです。

シーケンス番号は、送受信されるデータに「番号札」を付けることで、順序の乱れや重複、欠落を検出し、確実な通信を実現する役割を担っています。本記事では、シーケンス番号の基本的な概念から、初期値(ISN)の決まり方、確認応答との連携、重複検出のメカニズム、さらにはPythonコードを用いた具体的な実装例まで、丁寧に解説していきます。

TCPの内部動作に興味がある方や、ネットワークの基礎をしっかり固めたい方にとって、きっと役立つ内容となっているでしょう。ぜひ最後までお読みください。

TCPシーケンス番号の結論:データの順序と信頼性を守る番号札

それではまず、TCPのシーケンス番号とは何か、その本質から解説していきます。

TCPのシーケンス番号とは、送信するデータの各バイトに割り振られる32ビットの識別番号です。TCPはデータをセグメントという単位に分割して送信しますが、ネットワーク上ではこれらのセグメントが必ずしも送信した順番で届くとは限りません。シーケンス番号があることで、受信側は届いたセグメントを正しい順序に並べ直すことができます。

シーケンス番号の本質は「データの何バイト目から始まるか」を示す番号です。たとえばシーケンス番号が1000であれば、そのセグメントは通信全体の1000バイト目から始まるデータを含んでいることを意味します。

TCPが「信頼性のある通信プロトコル」と呼ばれる理由の一つが、このシーケンス番号の存在にあります。UDPと異なり、TCPはデータが正しく届いたかどうかを確認しながら通信を進めていきます。

シーケンス番号が果たす3つの役割

シーケンス番号には主に3つの役割があります。

役割 内容
順序制御 バラバラに届いたセグメントを正しい順序に並べ直す
重複検出 同じシーケンス番号のセグメントが複数届いた場合に重複と判断する
データの順序保証 アプリケーション層に対して、送信した順序のままデータを渡す

これら3つの機能が組み合わさることで、TCPはアプリケーションに対して「送った通りのデータが届く」という保証を提供できます。

TCPヘッダー内でのシーケンス番号の位置

シーケンス番号はTCPヘッダーの中に格納されます。TCPヘッダーの構造を確認しておきましょう。

フィールド ビット数 概要
送信元ポート番号 16ビット データを送信したポート
宛先ポート番号 16ビット データを受信するポート
シーケンス番号 32ビット 送信データの先頭バイト位置
確認応答番号 32ビット 次に受信を期待するバイト位置
ウィンドウサイズ 16ビット 受信バッファの空き容量

32ビットで表現できる最大値は約43億(2の32乗)です。この番号が最大値に達すると、0に戻って再利用されます。これを「ラップアラウンド」と呼びます。

シーケンス番号とUDPとの根本的な違い

UDPにはシーケンス番号の概念がありません。UDPはデータを送りっぱなしにするため、高速ですが順序保証や再送制御は行わない仕組みです。

一方TCPは、シーケンス番号を用いることで信頼性と順序保証を両立しています。動画ストリーミングなどリアルタイム性が重要な場面ではUDP、ファイル転送やWebページの読み込みなど正確性が求められる場面ではTCPが使われる理由はここにあります。

初期シーケンス番号(ISN)の決まり方と3ウェイハンドシェイク

続いては、TCPの接続確立時に登場する初期シーケンス番号(ISN)について確認していきます。

TCP通信を始める前に、送受信双方はまず「コネクションの確立」を行います。このとき最初に決めるのが、初期シーケンス番号(ISN:Initial Sequence Number)です。

ISNがランダムである理由

ISNは毎回固定値ではなく、ランダムな値が使用されます。その理由はセキュリティにあります。

ISNが予測可能な値であれば、悪意のある第三者が偽のTCPパケットを注入し、通信を乗っ取る「セッションハイジャック」攻撃が成立しやすくなってしまいます。ランダムなISNを使うことで、このような攻撃を困難にしているのです。

RFC 6528では、ISNの生成に暗号学的なハッシュ関数を用いることが推奨されています。IPアドレス、ポート番号、シークレットキー、時刻などを組み合わせることで、予測困難な値を生成します。

3ウェイハンドシェイクの流れ

TCPのコネクション確立は「3ウェイハンドシェイク」と呼ばれる3ステップで行われます。

ステップ 送信方向 フラグ シーケンス番号の動き
1. SYN クライアント → サーバー SYN クライアントのISN(例:seq=1000)を送信
2. SYN-ACK サーバー → クライアント SYN + ACK サーバーのISN(例:seq=5000)とack=1001を送信
3. ACK クライアント → サーバー ACK ack=5001を送信してコネクション確立

SYNパケット自体は1バイトのデータとして扱われるため、次に期待するシーケンス番号は「ISN + 1」になる点がポイントです。

PythonでISN生成のイメージを掴む

実際にISNがどのように生成されるかを、Pythonで簡単なシミュレーションとして確認してみましょう。


import hashlib
import time
import random
def generate_isn(src_ip, dst_ip, src_port, dst_port, secret_key="dragon_fruit_secret"):
# ハッシュ関数を使ってISNを生成するシミュレーション
timestamp = int(time.time())
base_str = f"{src_ip}{dst_ip}{src_port}{dst_port}{secret_key}{timestamp}"
# SHA-256でハッシュ化
hash_value = hashlib.sha256(base_str.encode()).hexdigest()

# 32ビットの範囲(0〜4294967295)に収める
isn = int(hash_value, 16) % (2**32)
return isn
サーモンサーバーとドラゴンフルーツクライアントの例
client_isn = generate_isn("192.168.1.10", "203.0.113.5", 54321, 80)
server_isn = generate_isn("203.0.113.5", "192.168.1.10", 80, 54321)
print(f"クライアント(ドラゴンフルーツ)のISN: {client_isn}")
print(f"サーバー(サーモン)のISN: {server_isn}")
print(f"クライアントの次の期待番号(SYN後): {client_isn + 1}")
出力結果:
クライアント(ドラゴンフルーツ)のISN: 2847361920
サーバー(サーモン)のISN: 1093847562
クライアントの次の期待番号(SYN後): 2847361921

このように、IPアドレスやポート番号、シークレットキー、タイムスタンプを組み合わせてハッシュ化することで、予測困難なISNが生成できます。実際のOSカーネルではさらに複雑なアルゴリズムが採用されています。

確認応答(ACK)とシーケンス番号の連携による信頼性の確保

続いては、シーケンス番号と確認応答(ACK)がどのように連携して信頼性を確保するかを確認していきます。

TCPの信頼性を支えるもう一つの柱が確認応答(Acknowledgment:ACK)です。シーケンス番号と確認応答番号が組み合わさることで、「どのデータが届いたか」「次は何番のデータが必要か」を送受信双方が把握できます。

確認応答番号の意味と計算方法

確認応答番号(ACK番号)は、「次に受信したいバイトの番号」を示しています。たとえばシーケンス番号1000から始まる500バイトのデータを受け取った場合、受信側はACK番号として1500(1000 + 500)を返します。

ACK番号 = 受信したシーケンス番号 + 受信したデータのバイト数
この計算式が、TCPの確認応答の基本です。

この仕組みにより、送信側は「ACK番号 − 1まではデータが届いた」と判断できます。非常にシンプルかつ強力な設計といえるでしょう。

再送制御とタイムアウトの仕組み

送信側は一定時間内にACKが返ってこない場合、データが失われたと判断して再送を行います。この待機時間をRTO(Retransmission TimeOut)と呼びます。

RTOはネットワークの往復遅延時間(RTT:Round Trip Time)をもとに動的に計算されます。RTTが大きいネットワークではRTOも大きくなり、頻繁な再送によるネットワーク過負荷を防ぐ設計になっています。

用語 意味
RTO 再送タイムアウト時間。この時間内にACKが来なければ再送する
RTT パケットが往復するのにかかる時間
SRTT 平滑化RTT。RTTの変動を平均化した値
重複ACK 同じACK番号が複数届くこと。パケットロスのサインとなる

Pythonでシーケンス番号とACKの流れを再現する

シーケンス番号とACKのやり取りをPythonでシミュレートすると、流れが一層理解しやすくなります。


# シーケンス番号とACKの基本的なやり取りをシミュレーション
class TCPSender:
def init(self, isn):
self.seq = isn
self.unacked_segments = {}
def send_segment(self, data):
    seg = {"seq": self.seq, "data": data, "length": len(data)}
    self.unacked_segments[self.seq] = seg
    print(f"送信: seq={self.seq}, データ='{data}', バイト数={len(data)}")
    self.seq += len(data)
    return seg

def receive_ack(self, ack_num):
    # ACK番号より小さいシーケンス番号のセグメントを確認済みにする
    confirmed = [k for k in self.unacked_segments if k < ack_num]
    for k in confirmed:
        print(f"確認済み: seq={k}(データ: '{self.unacked_segments[k]['data']}')")
        del self.unacked_segments[k]
class TCPReceiver:
def init(self, expected_seq):
self.expected_seq = expected_seq
self.buffer = {}
def receive_segment(self, seg):
    if seg["seq"] == self.expected_seq:
        print(f"受信OK: seq={seg['seq']}, データ='{seg['data']}'")
        self.expected_seq += seg["length"]
        ack = self.expected_seq
    else:
        print(f"順序外受信: seq={seg['seq']}(期待値: {self.expected_seq})→ バッファへ保存")
        self.buffer[seg["seq"]] = seg
        ack = self.expected_seq
    print(f"ACK送信: ack={ack}")
    return ack
ボルトとネジの通信シミュレーション
isn_client = 3000
sender = TCPSender(isn_client)
receiver = TCPReceiver(isn_client)
seg1 = sender.send_segment("ボルト")
seg2 = sender.send_segment("ネジ")
seg3 = sender.send_segment("キーボード")
ack1 = receiver.receive_segment(seg1)
sender.receive_ack(ack1)
ack2 = receiver.receive_segment(seg3)  # 順序外(ネジをスキップ)
sender.receive_ack(ack2)
ack3 = receiver.receive_segment(seg2)  # 遅れて届いたネジ
sender.receive_ack(ack3)
出力結果:
送信: seq=3000, データ='ボルト', バイト数=9
送信: seq=3009, データ='ネジ', バイト数=6
送信: seq=3015, データ='キーボード', バイト数=15
受信OK: seq=3000, データ='ボルト'
ACK送信: ack=3009
確認済み: seq=3000(データ: 'ボルト')
順序外受信: seq=3015(期待値: 3009)→ バッファへ保存
ACK送信: ack=3009
受信OK: seq=3009, データ='ネジ'
ACK送信: ack=3015
確認済み: seq=3009(データ: 'ネジ')

このシミュレーションでは、順序が乱れたセグメントが届いた場合にバッファへ一時保存し、正しい順序が揃ったタイミングでACKが進む様子を確認できます。実際のTCP実装もこの原理をベースにしています。

重複検出とデータの順序保証の仕組み

続いては、シーケンス番号が実現する重複検出とデータの順序保証の具体的な仕組みを確認していきます。

ネットワーク上では同じパケットが複数届く「重複」が発生することがあります。たとえば、ACKが遅延したために送信側が再送を行い、その後に元のパケットも届く、というケースです。このような状況でも、シーケンス番号によって重複を検出し、不正なデータが処理されることを防ぎます。

重複パケットの検出メカニズム

受信側は「すでに受信済みのシーケンス番号の範囲」を管理しています。新たに届いたセグメントのシーケンス番号がその範囲内に含まれている場合、重複パケットと判断してデータを破棄します。

ただし、ACKは送信します。これにより送信側は「相手にはちゃんと届いている」と認識でき、不要な再送を防ぐことができます。


# 重複パケット検出のシミュレーション
class DuplicateDetector:
def init(self, initial_seq):
self.received_ranges = set()
self.next_expected = initial_seq
def receive(self, seq, data, length):
    if seq < self.next_expected:
        # すでに受信済みの番号 → 重複と判断
        print(f"重複検出: seq={seq}('{data}')→ 破棄。ACKは返す: ack={self.next_expected}")
        return self.next_expected

    if seq == self.next_expected:
        print(f"正常受信: seq={seq}, データ='{data}'")
        self.next_expected += length
        return self.next_expected

    # 順序外(ギャップあり)
    print(f"順序外受信: seq={seq}(期待値: {self.next_expected})→ バッファへ")
    return self.next_expected
アボカドとゴリラのデータ通信
detector = DuplicateDetector(initial_seq=2000)
print("--- 正常な受信 ---")
detector.receive(2000, "アボカド", 12)
print("--- 順序外の受信 ---")
detector.receive(2024, "ゴリラ", 9)
print("--- 重複パケットの受信(アボカドが再送されてきた)---")
detector.receive(2000, "アボカド", 12)
print("--- 欠落していたパケットが届いた ---")
detector.receive(2012, "ロバ", 6)
出力結果:
--- 正常な受信 ---
正常受信: seq=2000, データ='アボカド'
--- 順序外の受信 ---
順序外受信: seq=2024(期待値: 2012)→ バッファへ
--- 重複パケットの受信(アボカドが再送されてきた)---
重複検出: seq=2000('アボカド')→ 破棄。ACKは返す: ack=2012
--- 欠落していたパケットが届いた ---
正常受信: seq=2012, データ='ロバ'

ラップアラウンドと番号の再利用問題

シーケンス番号は32ビットのため、最大値(約43億)を超えると0に戻ります。高速ネットワーク(10Gbps以上)ではこのラップアラウンドが数十秒以内に発生する可能性があり、「古いパケット」と「新しいパケット」のシーケンス番号が同じになる問題が生じます。

この問題に対処するために設計されたのが、TCP PAWS(Protection Against Wrapped Sequences)というオプション機能です。タイムスタンプオプションを組み合わせることで、同じシーケンス番号を持つ古いパケットを排除できます。

データの順序保証とアプリケーション層への受け渡し

TCPの受信バッファは、届いたセグメントをシーケンス番号順に並べ替え、アプリケーション層へは常に連続したデータとして渡します。アプリケーション側は「ネットワーク上でパケットが入れ替わった」という事実を意識しなくてよい設計になっています。

これがTCPの「ストリーム型通信」の本質です。送ったバイトの順序がそのまま受信側に再現される、という点でUDPとは根本的に異なるアプローチといえるでしょう。

まとめ

本記事では、TCPのシーケンス番号とは何か、その役割と仕組みについて詳しく解説してきました。

シーケンス番号は、単なる「番号」ではなく、TCPの信頼性通信を支える根幹の仕組みです。順序制御・確認応答・重複検出・データの順序保証といった機能がすべてシーケンス番号を軸に連携することで、インターネット上での安定したデータ転送が実現されています。

初期シーケンス番号(ISN)がランダムに決定される理由、3ウェイハンドシェイクでのシーケンス番号の動き、ACKとの連携による再送制御、そして重複検出のメカニズムまで、一連の流れとして理解できたでしょうか。

Pythonのサンプルコードを通じて、これらの概念を手を動かしながら確認することもできます。TCPの内部動作への理解が深まると、ネットワークのトラブルシューティングやアプリケーション設計においても、より的確な判断ができるようになるはずです。ぜひ今回の内容を出発点として、さらに深く学んでみてください。