Let's code a TCP/IP stack, 3: TCP Basics & Handshake
2025-3-30
| 2025-3-30
Words 4340Read Time 11 min
type
status
date
slug
summary
tags
category
icon
password

传输控制协议(Transmission Control Protocol)

现在我们已经完成了用户态 TCP/IP 协议栈中的 以太网(Ethernet)IPv4 的最小实现,是时候面对洪荒之力般的传输控制协议 TCP 了。

TCP 简介:传输层的支柱

TCP(Transmission Control Protocol) 工作于 OSI 第四层 —— 传输层(Transport Layer)。它的职责是:
  • 修复错误连接
  • 处理丢包、乱序、重复、流控、拥塞
  • 确保可靠通信
在现实世界中,从网页浏览到文件下载,从数据库同步到实时通信,几乎所有关键网络通信都运行在 TCP 之上。

一点历史

  • TCP 并不新鲜,首次规范发布于 1974 年(RFC 675)。
  • 在几十年里,它经历了大量的补丁、扩展与增强(如 SACK、Window Scaling、MSS、Fast Retransmit)。
  • 即使今天,它依然是互联网的传输“主力军”,虽然面临 QUIC 等新兴协议的挑战。

TCP 的设计动机

TCP 被设计为一个 “可靠的、有序的、面向连接的传输协议”,解决了 IP 层的诸多缺陷:
IP 层问题
TCP 的应对策略
包可能丢失
➜ 重传机制
包可能乱序
➜ 序列号与重组
包可能重复
➜ ACK 与去重
不知道对方是否收到
➜ 三次握手 + 超时重试
无法控制发送速率
➜ 拥塞控制与窗口

TCP 的可靠性机制(Reliability Mechanisms)

数据报(Datagram)式网络中,实现可靠传输远比表面上看起来要复杂。这正是 TCP 的价值所在:在不可靠的 IP 网络之上,构建出可靠、有序的数据通道

如何实现可靠传输?

在真实网络中会出现以下问题:
  1. 等待多久才能确认对方已收到?
  1. 如果接收端处理不过来怎么办?
  1. 如果中间网络设备(如路由器)拥塞怎么办?
  1. 如果 ACK 报文被丢弃或损坏怎么办?

方案一:滑动窗口(Sliding Window)

TCP 通过滑动窗口协议解决了大部分数据可靠性与流量控制问题。

滑动窗口示意图:

滑动窗口的核心机制:

  • 每一方都维护一个“窗口”区域,表示允许传输且未确认的字节范围
  • 窗口随着 ACK 移动,意味着接收方已经处理了这些数据
  • 当接收方窗口较小时,会限制发送方的发送速度,这就是流量控制

方案二:流量控制(Flow Control)

接收方处理能力跟不上发送方的速度时,TCP 利用窗口大小进行反馈:
  • TCP 报文中有一个 window 字段(接收窗口)
  • 接收方将它设置为当前缓冲区的可用空间
  • 发送方会根据这个值决定最多还能发多少数据

方案三:拥塞控制(Congestion Control)

即便发送方和接收方速度匹配,中间网络(如路由器)仍可能成为瓶颈。
TCP 提供了拥塞控制算法来避免 网络过载(Congestion)

两种方式:

方法
描述
显式(Explicit)
协议中使用字段告知网络拥塞情况(如 ECN)
隐式(Implicit)
发送方猜测网络是否拥塞(如丢包、RTT 增长)

代表性算法(TCP 实现中可选):

  • Slow Start(慢启动)
  • Congestion Avoidance(拥塞避免)
  • Fast Retransmit / Fast Recovery(快速重传)
  • CUBIC / Reno / BBR 等现代算法

总结

问题
机制
是否已在我们协议栈中实现?
数据可靠送达
ACK + Retransmit
❌(之后实现)
有序传输
Sequence Numbers
⏳(已解析字段)
接收方处理能力不足
Flow Control
❌(通过窗口限制)
网络中拥塞
Congestion Control
❌(高级功能

TCP 基础(TCP Basics)

与 IP 和 UDP 相比,TCP 的内部机制要复杂得多,但也正是这些机制使得它成为了可靠、有序、全双工的通信主力协议。本节我们深入理解 TCP 的核心特性与动机。

1. TCP 是面向连接的协议(Connection-Oriented)

TCP 在通信开始前,必须建立一条“连接”通道,这与 UDP 的“即发即扔”模式完全不同。
  • 通信双方通过 三次握手 明确彼此身份、初始序列号等信息。
  • 一旦连接建立,双方都需要维护这个连接状态,包括当前的发送窗口、接收窗口、序列号等。
  • 连接状态的生命周期包含多个阶段:LISTEN → SYN_RECEIVED → ESTABLISHED → FIN_WAIT 等。

2. TCP 是流协议(Streaming Protocol)

和 UDP 不同,TCP 不是基于“消息(message)”传递的协议,而是面向“字节流”的协议。
  • 应用层交给 TCP 的数据可能是碎片化的
  • TCP 底层会将字节流打包为 多个大小不等的数据包(segment)
  • TCP 收到乱序或损坏的包后,会 缓存在接收缓冲区中,等待重传或排序
  • 只有当 TCP 确认数据完整无误时,才会交付给上层的 socket
结论:应用程序看到的是一个可靠的“字节流”,而不是一个个独立的数据包。

3. 分段与序列号(Packetization & Sequencing)

TCP 的 packetization 会为每一个段(segment)赋予一个 序列号(Sequence Number)
  • 序列号 = 字节流中数据的“偏移量”
  • 发送方可以拆分流为任意大小的数据块(受窗口限制),并打包成 TCP segment
  • 接收方用序列号来 重组正确顺序的字节流
这正是 TCP 能够处理乱序、重发、丢包等问题的核心机制。

4. 完整性校验(Checksum with Pseudo-header)

TCP 使用与 IP 类似的 16-bit 校验和算法(Internet checksum),但在计算时增加了额外信息:
  • 校验范围包括:TCP header + TCP payload
  • 还包括一个 伪首部(pseudo-header),由以下字段组成:
    • 源 IP 地址
    • 目标 IP 地址
    • 协议类型(TCP = 6)
    • TCP 长度
伪首部不参与传输,仅用于计算校验和,以增强端对端验证的准确性。

5. 超时与重传(Retransmission via Timeout)

如果接收端收到一个损坏的 TCP 报文段,它不会主动告知发送端,而是默默丢弃
  • 发送端维护一个 超时定时器(RTO)
  • 如果某个数据段在超时时间内未被确认(ACK),则会 重传该数据段
  • 更高级的 TCP 实现还支持 快速重传(Fast Retransmit)重传时间估计(RTT 估算)

6. 全双工通信(Full-Duplex)

TCP 是 双向的 —— 在一次连接中,双方都可以同时发送和接收数据
  • 每一方都有独立的发送和接收缓冲区
  • 每个 TCP 报文都可携带:
    • 自己要发送的数据
    • 对对方数据的 ACK(确认)
  • 因此,TCP 报文常常同时包含数据和确认

核心思想:序列号驱动的数据同步

“维护数据流的序列同步,是 TCP 的本质。”
但这并不简单:
  • 需要追踪本地与远端的序列号(Send/Recv)
  • 需要处理乱序、重复、丢包、网络抖动等异常
  • 还要与窗口机制配合,实现流量控制与拥塞避免

总结

特性
TCP 的设计体现
可靠传输
ACK + 超时重传
有序传输
序列号 + 缓冲区
流量控制
滑动窗口 + 接收窗口
拥塞控制
拥塞窗口 + RTT 控制
全双工通信
双向序列号维护
数据完整性校验
checksum + pseudo-header

TCP 报文头格式(TCP Header Format)

TCP 报文头虽然只有 20 个字节(不含 options),但它承载的信息量非常丰富 —— 它记录了连接的状态、流控制信息、可靠性保障机制、以及数据流的控制信号。

TCP Header 结构图(20 字节基础格式)

字段说明(逐项解析)

字段
位数
描述
Source Port
16 位
源端口号(标识哪个应用/服务发出的)
Destination Port
16 位
目标端口号(用于区分多个服务)
Sequence Number
32 位
数据流中的偏移位置。用于可靠数据有序传输,握手时携带 ISN(初始序列号)
Acknowledgment Number
32 位
下一个希望收到的字节的序号。表示已成功收到前面所有字节。握手完成后该字段始终有效
Header Length (HL)
4 位
TCP 报文头长度,以 32-bit(4 字节)为单位。最小值为 5(表示 20 字节)
rsvd(保留位)
4 位
保留未使用
Flags (9 bits)
9 位
TCP 控制位(详见下方)
Window Size
16 位
接收端可接受的剩余窗口大小(流量控制)
Checksum
16 位
校验和,包含 TCP 头 + 数据 + 伪首部
Urgent Pointer
16 位
如果 U-flag 设置,该字段表示“紧急数据”偏移

TCP Flags(控制位)

Flag 位
名称
描述
C
CWR(Congestion Window Reduced)
拥塞控制反馈
E
ECN-Echo
表示收到拥塞通知(Explicit Congestion Notification)
U
URG
表示有紧急数据,Urgent Pointer 有效
A
ACK
表示 Ack 字段有效;握手完成后始终为 1
P
PSH
建议接收方立即将数据推送给应用层
R
RST
重置连接(一般用于错误或拒绝连接)
S
SYN
用于初始化连接,携带 ISN
F
FIN
通知对方,已发送完数据,准备关闭连接

Checksum:完整性验证

TCP 的校验和计算包括:
  • TCP 报文头 + 数据
  • 一个 伪首部(Pseudo Header),由 IP 层构建(不在 TCP 报文中)
伪首部包括:

可选字段(Options)

在 HL > 5 时,TCP header 后可以附带 Options 字段,常见有:
  • MSS(Maximum Segment Size)
  • Window Scale(窗口缩放)
  • SACK(选择性确认)
选项部分是 32 位对齐的。

典型用法举例

场景
使用字段/标志
建立连接
SYN(第一次握手)SYN+ACK(第二次)ACK(第三次)
数据传输
带 ACK、SEQ,带数据
正常断开
FIN(发送完毕)+ ACK
异常断开
RST
拥塞通知反馈
CWR / ECN
推送给应用层
PSH
通知紧急数据
URG + Urgent Pointer

总结:TCP 报文头设计之美

  • 结构紧凑,仅 20 字节基础字段
  • 同时承载连接控制、流控、可靠性、完整性等多重责任
  • 可扩展设计(通过 HL 和 Options)
  • 双向数据独立管理(全双工)
  • 实现“有状态字节流”的基础保障

TCP 三次握手详解(TCP Handshake)

在 TCP 中,建立连接是正式通信的第一步,通常通过**三次握手(Three-Way Handshake)**完成。虽然流程看似简单,但背后设计精妙且细节丰富。

连接建立的阶段划分

阶段
描述
CLOSED
套接字未打开
LISTEN
服务器处于监听状态
SYN-SENT
客户端已发送 SYN 请求
SYN-RECEIVED
服务器收到 SYN 并回复 SYN+ACK
ESTABLISHED
双方确认建立连接,可传数据

三次握手示意图

notion image

三次握手字段说明

报文
SEQ
ACK
控制位
说明
第一次握手
100
SYN
客户端发起连接请求
第二次握手
300
101
SYN + ACK
服务端确认并回应
第三次握手
101
301
ACK
客户端确认建立连接

常见问题解析

Q1: 初始序列号(ISN)是怎么选的?

  • TCP 要求每个连接的序列号尽量唯一且不可预测
  • 原始 RFC 建议每 4 微秒增加一次计数器
  • 现代系统(如 Linux)采用更加复杂的方法(如 hash + 时间 + 随机扰动)

Q2: 两端同时发起连接会怎样?

  • 称为 Simultaneous Open(同时打开)
  • 双方都发送 SYN,同时进入 SYN-RECEIVED 状态
  • 然后彼此发送 ACK → 都进入 ESTABLISHED
  • 虽少见,但 RFC 允许此过程发生

Q3: 如果连接迟迟未建立?

  • TCP 设置 连接超时定时器(Connection Timeout)
  • 通常会 重试多次,且每次延迟递增(指数回退 Exponential Backoff)
  • 超过尝试次数或时间上限后,认为连接失败

在我们的实现中如何处理?

为了构建三次握手,我们需要:

客户端收到 SYN 时:

  • 保存远端信息(端口/IP/序列号)
  • 生成本地 ISN
  • 回复 SYN+ACK
  • 进入 SYN-RECEIVED 状态

收到 ACK 确认时:

  • 检查 ACK 是否为我们期待的序列号 + 1
  • 如果确认无误 → 进入 ESTABLISHED 状态
  • 连接建立完成,可以发送/接收数据

状态机片段(简化 C 样例)

如何测试?

可以用命令测试握手是否成功:
如果你的实现正确,服务器应当进入 ESTABLISHED 状态。

总结

特性
描述
三次握手
建立连接,协商 ISN、确认双向能力
状态同步
TCP 各方跟踪当前连接状态
ISN 安全性
使用复杂算法避免预测攻击
同时打开(SimOpen)
双方都发 SYN,并交换 ACK
超时与重试
指数回退,最大尝试次数限制

TCP 选项(TCP Options)

在 TCP 报文头之后(当 Header Length > 5 时),可以附加多个 TCP Options。虽然这些字段在原始规范中只定义了三个,但随着时间推移,它们变得丰富且重要。
选项字段可选但非常有用,尤其在现代网络中处理大流量、高延迟、丢包等场景。

TCP 常见选项一览

1️⃣ MSS(Maximum Segment Size)

  • 作用:声明本端希望接收的最大 TCP 段大小(不含 TCP 头)
  • 常见值:IPv4 中典型为 1460 字节(1500 MTU - 20 IP - 20 TCP)
  • 使用场景:连接建立时(SYN 段中协商)
  • 格式

    2️⃣ SACK(Selective Acknowledgement)

    • 作用:在丢包严重时,接收方告知发送方 “哪些包收到了,哪些缺失”
    • 默认 TCP ACK 是累计式,只能告诉“我收到了 X 之前的所有包”,但 SACK 可以指出“我收到了 X-Y 和 Z-W 中间没收到”
    • 提高丢包环境下的吞吐率
    • 协商方式
      • 在 SYN 中声明支持 SACK(Kind = 4, Length = 2)
      • 在后续报文中发送具体的 SACK block(Kind = 5)

    3️⃣ Window Scale(窗口扩大因子)

    • 作用:突破 16-bit 窗口大小(最大 65,535 字节)限制
    • 适用于大带宽-高延迟(BDP)环境,如数据中心、大文件传输
    • 协商方式
      • 双方都在 SYN 中发送此选项,表示支持
      • 报文中窗口大小将乘以 2^scale 才是真实窗口
    • 格式

      4️⃣ Timestamps

      • 作用:发送方为每个 segment 加上时间戳,接收方回传时间戳
      • 用途
        • 精准测量 RTT(往返时间)
        • 估算重传超时(RTO)
        • PAWS(Protect Against Wrapped Sequence Numbers)
      • 格式

        TCP 选项的结构规范

        所有选项都以 TLV 格式(Type-Length-Value)表示:
        Byte
        含义
        1
        Kind
        2
        Length
        3~N
        Value(s)
        有两种例外的 1-byte 选项:
        • Kind = 0:End of Option List(结束)
        • Kind = 1:No-Operation(NOP,用于对齐)

        示例:MSS + WS + SACK + Timestamp 的选项段

        小结

        Option 名称
        功能描述
        用途
        MSS
        协商每个 TCP 段最大大小
        减少 IP 分片
        SACK
        指定哪些包没收到
        提高丢包网络下的吞吐率
        Window Scale
        扩大窗口大小
        支持大带宽传输
        Timestamps
        用于 RTT 测量与 RTO 估算
        提高重传精度,支持 PAWS

        测试 TCP 三次握手实现(Testing the TCP Handshake)

        现在我们已经实现了用户态协议栈中的 TCP 三次握手部分,并且让它监听所有端口(或特定端口)。我们可以用实际工具来验证它的行为是否符合 TCP 协议的预期。

        使用 nmap 进行 SYN 扫描(SYN Scan)

        输出结果:

        你看到了什么?

        • Nmap 报告端口 1337/tcpopen
        • 实际上,我们没有运行任何真正的应用服务在该端口!
        • 这是因为我们的协议栈 在收到 SYN 后正确返回了 SYN+ACK骗过了 nmap

        为什么会被“欺骗”?

        nmap 默认使用的扫描方式是 SYN 扫描
        步骤
        动作
        扫描器 → 目标
        发送 SYN
        目标 ← 扫描器
        如果收到 SYN+ACK → 认为端口 “open”
        扫描器 → 目标
        丢弃连接,不发送 ACK(即未完成三次握手)
        所以,只要你的协议栈正确返回了 SYN+ACK,即便没有应用层监听,也能让 nmap 判断“端口是开启的”。

        说明你的协议栈:

        • 已正确解析 TCP 报文中的 SYN 标志
        • 正确生成并返回 SYN+ACK,带有:
          • 合理的 ISN
          • ACK = 对方序列号 + 1
          • TCP 头校验和正确
        • 有基础的 TCP 状态管理(LISTEN → SYN_RECEIVED

        小技巧:这种测试可以批量验证 TCP 是否响应 SYN

        你会看到所有被你“假装”监听的端口都被识别为 open

        小结

        检查点
        是否成功
        TCP SYN 是否被接收
        TCP 是否返回 SYN+ACK
        协议栈是否维护连接状态
        ✅(初步)
        能否被 nmap 识别为 open 端口

        总结(Conclusion)

        我们已经成功实现了一个最小可用的 TCP 三次握手机制!它虽然简单,但验证了协议栈对 TCP 报文的接收、处理和响应的能力:
        • ✅ 接收并识别 SYN 报文
        • ✅ 正确构造并发送 SYN+ACK
        • ✅ 使用了有效的初始序列号(ISN)
        • ✅ 正确计算了 TCP 校验和
        • ✅ 能够被 nmap 等工具识别为“端口开放”
        虽然这个过程看起来轻松,但实际上我们已经完成了 TCP 的核心特性之一 —— 有状态连接的建立与协商。

        接下来的挑战

        可靠数据传输(Reliable Data Transfer)

        建立连接只是开始,真正让 TCP 成为互联网基石的,是它对数据传输的保障机制,包括:
        • 滑动窗口管理(流控、拥塞控制基础)
        • 重传机制(基于超时或快速重传)
        • 乱序/重复数据处理
        • 数据流按序交付给应用层

        模拟 Socket API(Berkeley Sockets)

        为了让真正的应用程序可以使用我们实现的 TCP 协议栈,下一步将探索:
        • 🛠 如何设计一个 模拟 socket API(如 bind(), listen(), accept(), recv()
        • 🧱 如何将连接、发送、接收等操作映射到协议栈内部状态
        • 🎯 目标是:让用户空间应用可以透明地使用我们自定义的 TCP 实现
         

        你现在已经实现:

        功能模块
        状态
        以太网帧解析
        ARP 地址解析
        IPv4 报文处理
        ICMP Echo 回复
        TCP 报文头解析
        三次握手逻辑
        nmap 检测通过
         
      • tcp/ip
      • 网络
      • Let's code a TCP/IP stack, 4: TCP Data Flow & Socket APILet's code a TCP/IP stack, 2: IPv4 & ICMPv4
        Loading...
        Catalog
        0%