type
status
date
slug
summary
tags
category
icon
password
在上一篇中,我们介绍了 TCP 报文头格式,以及两台主机之间是如何通过三次握手建立连接的。
在本篇中,我们将深入了解 TCP 数据通信 的过程,以及协议栈如何管理这些数据的发送与接收。
此外,我们还将实现一个供应用程序调用的 Socket 接口(Socket API),让上层程序可以通过这个接口进行网络通信。最后,我们将使用该接口开发一个简单的示例应用,向网站发送一个 HTTP 请求。
传输控制块(Transmission Control Block,简称 TCB)
在实现 TCP 数据传输之前,我们需要先定义一个记录连接状态的核心结构:
这就是每个 TCP 连接都会维护的 传输控制块(TCB)。
TCB(Transmission Control Block)用于跟踪数据流的发送与接收状态,是 TCP 协议中最重要的状态管理单元之一。
发送端状态变量(Send Sequence Variables)
变量 | 含义 |
SND.UNA | 未被确认的最小序列号(Send Unacknowledged)即:已发送但未收到 ACK 的第一个字节 |
SND.NXT | 下一个将要发送的序列号(Send Next) |
SND.WND | 发送窗口大小(Send Window)接收方通告的缓冲区可用空间 |
SND.UP | 发送方的紧急指针(Urgent Pointer) |
SND.WL1 | 用于更新窗口的上次 SEQ 值 |
SND.WL2 | 用于更新窗口的上次 ACK 值 |
ISS | 初始发送序列号(Initial Send Sequence Number) |
这些字段用于控制数据何时发送、是否需要重传、拥塞控制窗口的位置等。
接收端状态变量(Receive Sequence Variables)
变量 | 含义 |
RCV.NXT | 期望收到的下一个序列号(Receive Next) |
RCV.WND | 接收窗口大小(Receive Window)也就是我还能接收多少数据 |
RCV.UP | 接收方的紧急指针 |
IRS | 初始接收序列号(Initial Receive Sequence Number) |
这些字段决定了是否接收数据,ACK 应该发给谁,以及 是否是乱序/重复段。
当前报文段的辅助变量(Current Segment Variables)
变量 | 含义 |
SEG.SEQ | 当前报文段的 SEQ 号 |
SEG.ACK | 当前报文段的 ACK 号 |
SEG.LEN | 当前报文段负载的长度 |
SEG.WND | 报文段通告的窗口大小 |
SEG.UP | 报文段携带的紧急指针 |
SEG.PRC | 报文段优先级(仅在某些环境使用) |
这些变量不是全局状态,而是临时用于处理当前 TCP segment 的辅助变量。
示例:C 语言结构定义(简化版)
总结:为什么 TCB 很重要?
TCB 是每个 TCP 连接的“控制中枢”,它:
- 保存了发送和接收的完整上下文
- 驱动状态机中的所有转移和响应逻辑
- 为
socket
API 提供底层数据支持
- 是重传、滑动窗口、ACK 管理等所有机制的核心
TCP 数据通信(TCP Data Communication)
在 TCP 三次握手完成、连接建立后,TCP 就会进入数据传输阶段。此时,协议栈必须通过一套明确的机制来管理数据流的发送与确认。
关键状态变量(三个核心)
以下三个变量来自传输控制块(TCB),是管理 TCP 数据流的关键:
变量 | 含义 |
SND.NXT | 发送方即将使用的序列号(Send Next) |
RCV.NXT | 接收方期望接收的下一个序列号(Receive Next) |
SND.UNA | 发送方未被确认的最小序列号(Send Unacknowledged) |
在连接空闲(即无数据收发)一段时间后,这三个变量的值通常会趋于相等。
示例:一次数据段发送与确认过程
假设主机 A 向主机 B 发送一段数据,数据流程如下:
- A 发送数据段,并将
SND.NXT += 数据长度
- B 收到数据段,将
RCV.NXT += 数据长度
,同时发回 ACK
- A 收到 ACK,将
SND.UNA += 数据长度
这就是 TCP 协议对可靠数据传输的基本控制机制。
实战:使用 tcpdump 捕获连接和数据流程
连接建立(三次握手)
解释:
- 主机 A(10.0.0.4)从端口 12000 向主机 B(10.0.0.5)的端口 8000 建立连接
- 三次握手成功后,进入 ESTABLISHED 状态
- 起始序列号分别为 A=1525252,B=825056904
数据发送与 ACK 循环
- A 发送了 17 字节数据,
SND.NXT += 17
- B 收到后回复 ACK=18,表示期望的下一个字节是 seq 18,
RCV.NXT += 17
- A 收到 ACK 后将
SND.UNA = 18
后续数据往返(多段)
说明:
- 主机 B 发送了两段数据,分别为 138 字节 和 218 字节
- 主机 A 对每一段都按长度精确返回 ACK
连接关闭过程
- B 通过
FIN
告知自己数据已发完
- A 通过 ACK 表示确认(
ack = FIN 的 seq + 1
)
此时,A 仍需再发送自己的 FIN 才算完成连接的 四次挥手。
小结:TCP 数据传输的控制关键点
步骤 | 动作 | TCB 更新 |
发出数据段 | 增加 SND.NXT | 记录发送位置 |
收到数据段 | 更新 RCV.NXT | 并返回 ACK |
收到 ACK | 更新 SND.UNA | 表示已确认的数据量 |
收到 FIN | 更新状态为 CLOSE-WAIT | 回复 ACK |
这些变量推动整个 TCP 传输状态机的进行。
TCP 连接终止(TCP Connection Termination)
TCP 的连接关闭同样是一个有状态的过程,并不像 UDP 那样“发完就完”。
它可以通过 优雅关闭(FIN) 或 强制关闭(RST) 来完成,整个过程比连接建立还要稍复杂。
基本的优雅关闭过程(四次挥手)
对比三次握手,TCP 的连接关闭一般需要四个数据段(segments):
正常关闭流程(主动关闭者为 A,B 为被动):
TCP 四次挥手图(A 主动关闭,B 被动)
状态转换说明:
步骤 | 发起方 | 动作 | 接收方状态变更 |
① | A | 发送 FIN,进入 FIN_WAIT_1 | ㅤ |
② | B | 回复 ACK,进入 CLOSE_WAIT | A 进入 FIN_WAIT_2 |
③ | B | 自己也发 FIN,进入 LAST_ACK | ㅤ |
④ | A | 回复 ACK,进入 TIME_WAIT | B → CLOSED |
ㅤ | A | 等待 2MSL,最后 CLOSED | ㅤ |

支持半关闭(Half-Close)
由于 TCP 是 全双工协议,连接可以做到:
- 一方通过
FIN
表示:“我不发数据了”
- 但仍然保持连接继续 接收数据
这叫做 TCP 半关闭(half-close),比如:
连接关闭中的非理想情况
因为 TCP 基于不可靠的 IP 网络,关闭阶段可能会遇到问题:
FIN 丢失或延迟
- 如果 FIN 丢失,连接将一直保持在
FIN-WAIT
、CLOSE-WAIT
、TIME-WAIT
状态
- Linux 中
tcp_fin_timeout
参数用于设置 最多等待 FIN 多久(默认 60 秒)
虽然这 违反了 TCP 规范(RFC 没限制超时),但操作系统这样做是为了防止 DoS 攻击和资源泄漏
强制中断连接:TCP RST
如果通信的一方 无法继续或出错,可以使用 TCP RST(重置) 来立刻终止连接:
常见 RST 触发场景:
- 请求连接的端口不存在
- 对方 TCP 崩溃、状态丢失或重启
- 主动攻击或破坏连接(例如:端口扫描器、DoS 工具)
正常的数据传输过程中不应出现 RST
示例:tcpdump 抓取连接关闭过程
说明:
- A 发起关闭(FIN)
- B 确认(ACK),然后自己也发 FIN
- A 最后 ACK 确认,连接关闭
小结
模式 | 特点 |
四次挥手 | 双方优雅关闭,需要 4 个段 |
半关闭 | 一方发送 FIN 后仍保持接收通道打开 |
超时关闭 | 系统在等待对方 FIN 过久后强制关闭 |
RST 强制中断 | 非正常关闭,立即释放资源 |
Socket API(套接字接口)
为了让用户应用能够使用我们实现的 TCP/IP 协议栈,必须提供一套标准接口供调用。
最广泛使用的就是 BSD Socket API,最早出现在 1983 年的 4.2BSD UNIX,并成为几乎所有现代操作系统(包括 Linux)网络编程的基础。
什么是 Socket?
Socket 是一个 抽象的网络通信端点,允许应用程序通过它发送或接收数据。
本质上,socket 是对底层协议栈的封装与桥梁,隐藏了复杂的 TCP/IP 操作细节。
常见的 Socket 调用流程(以 TCP 为例)
函数 | 作用说明 |
socket() | 创建一个 socket,指定协议族与类型(如 TCP) |
connect() | 主动发起连接(会触发 TCP 三次握手) |
send() / write() | 发送数据 |
recv() / read() | 接收数据 |
close() | 关闭连接(触发四次挥手) |
实际系统调用观察:用 curl
请求网页
使用
strace
追踪实际调用流程:输出示例(重点片段):
流程说明:
- socket():分配一个 TCP socket(fd=3)
- connect():发起连接(即三次握手)
- sendto():发送 HTTP 请求数据
- recvfrom():接收服务器返回的数据
- close():关闭连接
小细节:Socket 其实是“文件描述符”!
Socket 在 Linux 中就是一种特殊的文件描述符,因此也可以使用
read()
/ write()
/ sendfile()
等通用 I/O 接口进行数据操作。参考
man socket(7)
:Standard I/O operations like write(2), read(2), writev(2) and readv(2) can be used on a socket descriptor.
总结:Socket API 的价值
特性 | 说明 |
屏蔽协议栈复杂性 | 应用程序无需理解 TCP 状态机 |
文件描述符兼容性 | 可用标准 I/O 操作操作 socket |
跨平台统一编程接口 | BSD Socket API 几乎通用 |
支持多种协议族(如 IPv6) | 可切换为 AF_INET6 |
测试我们自定义的 Socket API(Testing Our Socket API)
现在我们已经在用户态协议栈中实现了自己的 Socket 接口,接下来我们可以编写用户程序来使用它进行网络通信!
模拟 curl 请求:发送 HTTP GET 请求
我们可以用自己的 API 实现类似
curl
的行为,向某个主机发送一个简单的 HTTP GET 请求,并打印响应内容:示例命令:
输出结果(示例):
实现核心步骤
我们可以使用自定义的 socket 接口函数(如
my_socket()
/ my_connect()
/ my_send()
/ my_recv()
)实现这一过程:伪代码框架
这样测试的意义
虽然发送一个 HTTP GET 是很简单的操作,但它:
✅ 检验了 socket 接口是否正常工作
✅ 测试了三次握手是否成功建立
✅ 测试了数据收发、ACK、窗口更新等逻辑
✅ 验证了从上到下所有协议栈层次的集成完整性
✅ 模拟了真实应用对 socket API 的依赖方式
总结(Conclusion)
至此,我们已经基本完成了一个 简化版的 TCP 实现,包括:
- ✅ 三次握手建立连接
- ✅ 简单的数据发送与接收逻辑
- ✅ 自定义的 Socket 接口供应用程序调用
这些功能让我们的用户态协议栈具备了最小可用性的 TCP 支持,足以承载像
curl
这样发送 HTTP 请求的场景。但现实中的 TCP 远比这复杂得多:
- 数据包可能会丢失、损坏或乱序到达
- 网络中的路由器或链路可能会 发生拥塞
- 对端可能来不及处理收到的数据
- 恶意行为(如 RST 攻击)可能会影响连接稳定性
为了实现“真正健壮的 TCP”,我们还需要:
功能模块 | 说明 |
📦 滑动窗口管理(Window Management) | 控制发送/接收速度,防止溢出 |
🔁 重传机制(Retransmission) | 数据未确认时超时重发 |
⏱ RTO(Retransmission Timeout)计算 | 根据 RTT 动态估算超时重传时间 |
📉 拥塞控制(Congestion Control) | 如 Slow Start、AIMD、CUBIC 等算法 |
🔍 乱序包处理、SACK 支持 | 提高在高丢包场景下的吞吐率 |
下一篇预告
💡 Let’s Code a TCP/IP Stack, Part 5: TCP Window Management & Retransmission Timeout
我们将探索:
- 如何管理
SND.WND
/RCV.WND
窗口变量
- 如何实现重传计时器 + RTT 动态估算
- 如何处理 ACK 丢失、延迟确认等异常情况