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 网络之上,构建出可靠、有序的数据通道。
如何实现可靠传输?
在真实网络中会出现以下问题:
- 等待多久才能确认对方已收到?
- 如果接收端处理不过来怎么办?
- 如果中间网络设备(如路由器)拥塞怎么办?
- 如果 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 | 双方确认建立连接,可传数据 |
三次握手示意图

三次握手字段说明
报文 | 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/tcp
是 open
- 实际上,我们没有运行任何真正的应用服务在该端口!
- 这是因为我们的协议栈 在收到 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 检测通过 | ✅ |