Let's code a TCP/IP stack, 4: TCP Data Flow & Socket API
2025-3-30
| 2025-3-30
Words 3078Read Time 8 min
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 发送一段数据,数据流程如下:
  1. A 发送数据段,并将 SND.NXT += 数据长度
  1. B 收到数据段,将 RCV.NXT += 数据长度,同时发回 ACK
  1. 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
notion image

支持半关闭(Half-Close)

由于 TCP 是 全双工协议,连接可以做到:
  • 一方通过 FIN 表示:“我不发数据了”
  • 但仍然保持连接继续 接收数据
这叫做 TCP 半关闭(half-close),比如:

连接关闭中的非理想情况

因为 TCP 基于不可靠的 IP 网络,关闭阶段可能会遇到问题:

FIN 丢失或延迟

  • 如果 FIN 丢失,连接将一直保持在 FIN-WAITCLOSE-WAITTIME-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 追踪实际调用流程:

输出示例(重点片段):

流程说明:

  1. socket():分配一个 TCP socket(fd=3)
  1. connect():发起连接(即三次握手)
  1. sendto():发送 HTTP 请求数据
  1. recvfrom():接收服务器返回的数据
  1. 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 丢失、延迟确认等异常情况
  • tcp/ip
  • 网络
  • Let's code a TCP/IP stack, 5: TCP RetransmissionLet's code a TCP/IP stack, 3: TCP Basics & Handshake
    Loading...
    Catalog
    0%