【Linux网络】深入TCP 协议(一):从报头结构到可靠性机制

🔥个人主页:Cx330🌸

❄️个人专栏:《C语言》《LeetCode刷题集》《数据结构-初阶》《C++知识分享》

《优选算法指南-必刷经典100题》《Linux操作系统》:从入门到入魔

《Git深度解析》:版本管理实战全解 《Qt 极境架构》MySQL 核心技术与实战

🌟心向往之行必能


🎥Cx330🌸的简介:


目录

前言:

[一. TCP 的简单回顾](#一. TCP 的简单回顾)

[1.1 TCP 在网络分层中的位置](#1.1 TCP 在网络分层中的位置)

[1.2 TCP 数据发送的本质](#1.2 TCP 数据发送的本质)

[二. TCP 协议格式深度解析](#二. TCP 协议格式深度解析)

[2.1 TCP 报头整体结构](#2.1 TCP 报头整体结构)

[2.2 核心字段详解](#2.2 核心字段详解)

[2.2.1 源 / 目的端口号(16 位)](#2.2.1 源 / 目的端口号(16 位))

[2.2.2 32 位序号与确认序号](#2.2.2 32 位序号与确认序号)

[2.2.3 4 位首部长度(重点)](#2.2.3 4 位首部长度(重点))

[2.2.4 6 位标志位(Control Flags)](#2.2.4 6 位标志位(Control Flags))

[2.3 Linux 内核中 TCP 报头的实现](#2.3 Linux 内核中 TCP 报头的实现)

[2.4 TCP 与 UDP 报头的关键区别](#2.4 TCP 与 UDP 报头的关键区别)

[三. TCP 可靠性核心:确认应答与超时重传](#三. TCP 可靠性核心:确认应答与超时重传)

[3.1 可靠性的本质](#3.1 可靠性的本质)

[3.2 确认应答(ACK)机制](#3.2 确认应答(ACK)机制)

示例工作流程:

乱序与重排:

[3.3 超时重传机制](#3.3 超时重传机制)

[1. 情况一:发送数据包丢失](#1. 情况一:发送数据包丢失)

[2. 情况二:ACK 应答包丢失](#2. 情况二:ACK 应答包丢失)

[3. 重传超时时间(RTO)的动态计算](#3. 重传超时时间(RTO)的动态计算)

结尾:


前言:

在现代计算机网络体系中,传输控制协议(Transmission Control Protocol,简称 TCP)是构建互联网服务(如 Web 服务、数据库交互、远程连接等)的最核心基石之一。与 UDP 协议的"发后即忘"不同,TCP 被设计为一个面向连接的、可靠的、基于字节流的传输层协议。

本文将依据 TCP 协议的核心要点,逐层深入剖析:从 TCP 在 Linux 体系中的定位、报头的结构设计与 Linux 内核源码实现,再到 TCP 实现"绝对可靠性"的核心保障机制。


一. TCP 的简单回顾

1.1 TCP 在网络分层中的位置

在标准的 TCP/IP 四层(或 OSI 七层)模型中,TCP 位于传输层(Transport Layer)

  • 应用层(Application Layer):HTTP、SSH、DNS 等协议,负责具体的业务逻辑。

  • 传输层(Transport Layer):TCP、UDP,负责端到端(Process-to-Process)的数据传输服务。

  • 网络层(Network Layer):IP 协议,负责主机到主机(Host-to-Host)的路由与寻址。

  • 链路层(Link Layer):以太网、WiFi 等,负责局域网内物理帧的传输。

从操作系统的角度来看: 应用层代码通常运行在用户态(User Space) ,而 TCP/IP 协议栈完整实现在 Linux 内核态(Kernel Space) 。应用层与内核 TCP 栈之间的交互接口,正是操作系统提供的 Socket API(如 socket(), bind(), connect(), write(), read() 等)。

1.2 TCP 数据发送的本质

许多初学者容易产生一个误区:认为调用 write()send() 函数就是把数据直接发到了物理网线上。这种理解是不准确的。

TCP 数据发送的本质是:数据拷贝(Buffer Copy)实际上,我们向网络发送数据的本质是将数据拷贝到操作系统内核的 TCP 发送缓冲区中。

复制代码
应用程序 write() → 拷贝数据到TCP发送缓冲区 → TCP协议栈控制发送时机、速率 → 网络
  1. 发送端 :应用程序调用 write() 时,操作系统只是将数据从用户态缓冲区(User Buffer)拷贝到 Linux 内核为该 Socket 分配的发送缓冲区(Send Buffer)中。

  2. 内核调度:内核 TCP 协议栈会根据当前的拥塞状况、滑动窗口大小以及 MTU/MSS 限制,自主决定何时、将多少数据打包成 TCP 报文段(Segment)递交给 IP 层。

  3. 接收端 :网卡收到数据帧后触发中断,内核将 TCP 报文解析并存入 Socket 的接收缓冲区(Receive Buffer) 。应用程序调用 read() 时,再从内核接收缓冲区拷贝数据到用户态。

因此,TCP 的"面向字节流"特性,本质上就是内核通过发送与接收缓冲区对数据流进行的解耦与动态调度。


二. TCP 协议格式深度解析

2.1 TCP 报头整体结构

为了实现复杂的控制逻辑(如序号管理、流量控制、状态切换等),TCP 报头设计了丰富的字段。标准 TCP 报头不包含选项时长度为 20 字节 ,加上可选项后最大可达 60 字节

其标准格式示意图如下:

复制代码
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          源端口号 (16 bits)     |        目的端口号 (16 bits)   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         序号 (Sequence Number, 32 bits)       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      确认序号 (Acknowledgment Number, 32 bits)|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 首部长度 | 保留 |U|A|P|R|S|F|                    |
| (4 bits) |(6bit)|R|C|S|S|Y|I|       窗口大小 (Window, 16 bits)  |
|          |      |G|K|H|T|N|N|                                 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          校验和 (Checksum, 16 bits) |   紧急指针 (Urgent Pointer, 16 bits)|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    选项 (Options, 0 - 40 bytes)               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                             数据部分                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

2.2 核心字段详解

2.2.1 源 / 目的端口号(16 位)
  • 作用:建立进程到进程的映射。

  • 大小:16 bits(对应端口范围 0 - 65535)。

  • 逻辑:IP 地址标识互联网上唯一的主机,而端口号则标识该主机上唯一的网络应用进程。两者结合构成套接字(Socket Pair)。

2.2.2 32 位序号与确认序号

TCP 将发送的字节流中的每一个字节都编上了一个序号(Sequence Number)。

  • 32 位序号(Seq) :表示本报文段所发送的数据的第一个字节的序号

  • 32 位确认序号(Ack) :接收端发回的确认,表示接收端期望下一次收到的字节序号。这意味着:序号为 Ack - 1 及之前的所有字节数据都已经成功且连续地接收到了(累积确认)。

确认序号则表示:我已经收到了确认序号之前的所有字节,下一次请从确认序号开始发送。

例如:A 发送了序号为 1~1000 的字节数据,B 收到后会回复确认序号为 1001 的 ACK 报文。

2.2.3 4 位首部长度(重点)

这是 TCP 报头设计中最精妙的字段之一,也是很多面试的高频考点。

  • 为什么需要这个字段?

    • TCP 报头包含可变长度的选项部分,因此接收方无法预先知道报头的总长度。4 位首部长度字段就是用来告诉接收方,TCP 报头到底有多长。
  • 字段规则

    • 4 位二进制范围:0000~1111(十进制 0~15)
    • 基本单位:4 字节
    • 实际首部长度 = 字段值 × 4 字节
  • 取值范围

    • 最小值:5(5×4=20 字节),表示没有选项的标准报头
    • 最大值:15(15×4=60 字节),表示选项部分占满 40 字节

设计精髓:4 字节对齐

  • 为什么要以 4 字节为单位?因为 4 位最多只能表示 15 个值,直接表示字节长度的话最多只能到 15 字节,远远不够。通过规定基本单位为 4 字节,将表达范围从 0~15 字节扩展到了 0~60 字节。
  • 更深层次的原因是:TCP 报头长度必须是 4 字节的整数倍,因此报头长度的二进制表示的低两位永远是 0。我们不需要存储这两位,只需要存储高 4 位即可,读取时再左移两位(×4)补回低两位的 0。

计算示例

  • 标准报头 20 字节:20 ÷ 4 = 5 → 字段值为 5(二进制 0101)
  • 带 12 字节选项的报头:20+12=32 字节 → 32 ÷ 4 = 8 → 字段值为 8(二进制 1000)
2.2.4 6 位标志位(Control Flags)

标志位用于控制 TCP 的状态变迁与报文属性,每一位占用 1 bit:

  1. URG (Urgent):紧急指针有效标记。表明本报文中包含高优先级紧急数据(与 16 位紧急指针结合使用)。

  2. ACK (Acknowledgment):确认号有效标记。TCP 规定连接建立后,发出的所有报文段都必须将 ACK 置 1。

  3. PSH (Push):提示接收方应尽快将接收缓冲区的数据交付给应用层,而不是等待缓冲区填满。

  4. RST (Reset):复位标记。表示连接出现严重异常(如对端断电、非法连接),要求强制重置或关闭连接。

  5. SYN (Synchronize):同步序号标记。在建立连接(三次握手)时用于同步初始序号(ISN)。

  6. FIN (Finish):结束标记。表示发送方已无数据发送,请求释放连接(四次挥手)。

2.3 Linux 内核中 TCP 报头的实现

在 Linux 内核源码(如 <netinet/tcp.h><linux/tcp.h>)中,TCP 报头是通过结构体 struct tcphdr 定义的。

为了应对大端序(Big-Endian,即网络字节序)与小端序(Little-Endian,即主机字节序)的差异,内核使用条件编译对位域进行了处理:

复制代码
struct tcphdr {
    __be16 source;    /* 源端口号 (16位,网络字节序) */
    __be16 dest;      /* 目的端口号 (16位,网络字节序) */
    __be32 seq;       /* 32位序号 */
    __be32 ack_seq;   /* 32位确认序号 */
#if defined(__LITTLE_ENDIAN_BITFIELD)
    __u16   res1:4,   /* 保留位 */
            doff:4,   /* 首部长度 (Data Offset, 4 bits) */
            fin:1,    /* FIN 标志 */
            syn:1,    /* SYN 标志 */
            rst:1,    /* RST 标志 */
            psh:1,    /* PSH 标志 */
            ack:1,    /* ACK 标志 */
            urg:1,    /* URG 标志 */
            ece:1,    /* ECN Echo */
            cwr:1;    /* Congestion Window Reduced */
#elif defined(__BIG_ENDIAN_BITFIELD)
    __u16   doff:4,   /* 首部长度 */
            res1:4,   /* 保留位 */
            cwr:1,
            ece:1,
            urg:1,
            ack:1,
            psh:1,
            rst:1,
            syn:1,
            fin:1;
#else
#error "Adjust your <asm/byteorder.h> defines"
#endif
    __be16 window;    /* 16位接收窗口大小 */
    __sum16 check;     /* 16位校验和 */
    __be16 urg_ptr;   /* 16位紧急指针 */
};

源码解读

  • 位段的使用:TCP 报头中有很多单个位的标志,使用 C 语言的位段特性可以节省内存,同时方便按位操作。
  • 大小端适配:由于不同 CPU 架构的字节序不同,内核通过条件编译来适配小端和大端模式下的位段顺序。
  • 网络字节序 :所有多字节字段(如sourcedestseq等)都使用__be16__be32类型,表示网络字节序(大端)。

2.4 TCP 与 UDP 报头的关键区别

比较维度 TCP 报头 UDP 报头
报头长度 可变长度(20 ~ 60 字节) 固定长度(8 字节)
复杂程度 结构复杂,包含各种控制状态与序号字段 结构极简,仅含端口、长度和校验和
连接与状态 面向连接,报头包含状态同步位(SYN/FIN/RST) 无连接,无状态标记
可靠性保证 包含 Seq、Ack 字段,原生支持重传与乱序重排 无 Seq/Ack,不保障可靠性与顺序
流量与拥塞控制 包含 16 位滑动窗口字段,支持动态调节 无流量控制信息

三. TCP 可靠性核心:确认应答与超时重传

TCP 能够在不可靠的 IP 网络层之上提供 100% 可靠的数据传输服务,其核心底座正是 确认应答(ACK)超时重传(Timeout Retransmission) 机制。

3.1 可靠性的本质

在物理世界与网络传输中,不存在 100% 不丢包、不损坏的物理介质。因此,可靠性的本质不是"保证数据永远不丢",而是"能够准确感知数据是否丢失,并在丢失时进行补救,确保最终交付的数据正确且完整"。

TCP 实现这一点的金科玉律是:发送方发出的每一个数据包,都需要收到接收方的明确应答;若未收到应答,发送方必须有能力重新发送。

复制代码
发送方                          接收方
  |                               |
  | -------- 数据 (Seq=1) -------> |
  |                               | (成功收到)
  | <------- 应答 (Ack=2) -------- |
  |                               |

3.2 确认应答(ACK)机制

TCP 使用连续字节序与累积确认(Cumulative Acknowledgment)来保证可靠交付。

示例工作流程:
  1. 发送方欲发送 1000 字节的数据,拆分为两段:

    • Segment 1: Seq = 1, 数据长度 Len = 500 字节(包含字节 1 - 500)。

    • Segment 2: Seq = 501, 数据长度 Len = 500 字节(包含字节 501 - 1000)。

  2. 接收方收到 Segment 1 后,校验数据无误,将 ACK 标志位置 1,返回 Ack = 501。

    • 含义:"我已成功收到 500 及以前的所有字节,下一次请从第 501 字节开始发送。"
  3. 接收方收到 Segment 2 后,返回 Ack = 1001。

    发送方 接收方
    | |
    | -- Seg1: Seq=1, Len=500 -------> | (收到 1~500)
    | <-- ACK: Ack=501 ---------------- |
    | |
    | -- Seg2: Seq=501, Len=500 -----> | (收到 501~1000)
    | <-- ACK: Ack=1001 --------------- |
    v v

两个关键细节

  • ACK 是由对方操作系统的 TCP 层自动回复的,不需要应用层参与。这保证了即使应用层很忙,也不会影响 TCP 的可靠性。
  • ACK 本身不需要被应答,否则会陷入无限循环。TCP 通过超时重传机制来处理 ACK 丢失的情况。
乱序与重排:

由于网络路由的动态性,数据包可能出现乱序到达 。接收端依靠 Seq 字段将乱序的报文段在接收缓冲区中重新排队;若发现中间缺失了某一段(例如收到了 1 - 500 和 1001 - 1500,缺失了 501 - 1000),接收端返回的 Ack 将保持为 501,直到缺失的数据段补齐。

3.3 超时重传机制

如果在传输过程中发生了数据包丢失ACK 应答包丢失 ,确认应答机制就会被中断。此时就需要依靠超时重传进行修复。

1. 情况一:发送数据包丢失
  • 发送方发出了 Seq = 1 的数据包,但在路由器节点被丢弃。

  • 接收方未收到数据,因此不会发送 Ack。

  • 发送方在等待一段时间后(超时),触发重传。

2. 情况二:ACK 应答包丢失
  • 发送方发出了 Seq = 1,接收方成功接收并回复 Ack = 501。

  • 然而 Ack = 501 报文在网络中丢失。

  • 发送方依然会在超时后重发 Seq = 1 数据包。

  • 接收方的去重机制 :接收方发现收到的 Seq = 1 字节已经在接收缓冲区中存在,会直接丢弃重发的重复数据,同时再次补发 Ack = 501。

    [数据丢失场景]
    发送方 接收方
    | -- Seg1: Seq=1 (丢失) ----x |
    | |
    | (定时器超时 RTO) |
    | -- Seg1: Seq=1 (重传) ----------> |
    | <-- ACK: Ack=501 ---------------- |

    [ACK 丢失场景]
    发送方 接收方
    | -- Seg1: Seq=1 -----------------> |
    | x--- ACK: Ack=501 (丢失) ---|
    | (定时器超时 RTO) |
    | -- Seg1: Seq=1 (重传) ----------> | (去重,重新发送 ACK)
    | <-- ACK: Ack=501 ---------------- |

3. 重传超时时间(RTO)的动态计算

超时重传的时间间隔(RTO, Retransmission Time-Out)绝不能设为静态固定值:

  • 若 RTO 设置过短:会导致网络稍微拥堵时触发大量不必要的重传,加剧拥塞。

  • 若 RTO 设置过长:数据丢失后需要等待很久才重传,严重降低传输效率。

Linux 内核采用动态测量网络往返时间(RTT, Round Trip Time)的算法,结合平滑加权平均(SRTT)与 RTT 波动方差(RTTVAR)动态计算 RTO:

RTO = SRTT + 4 * RTTVAR

此外,当连续发生超时重传时,Linux 内核会启用指数退避(Exponential Backoff)策略,即每次重传的等待时间翻倍(如 1s, 2s, 4s, 8s...),以防止在网络严重拥塞时雪上加霜。


结尾:

TCP 协议作为计算机网络史上最成功的工程设计之一,其精髓在于通过极为精密的报头格式设计与内核逻辑,在完全不可靠的网络环境中搭建出了一条可靠的通信管道

通过本文的梳理,我们清楚地了解了:

  1. TCP 的数据发送本质上是内核缓冲区之间的内存拷贝与调度。

  2. TCP 报头通过 32 位序号/确认序号、4 位首部长度(4 字节单位)以及标志位,提供了强大的状态指示能力,在 Linux 内核中以 struct tcphdr 结构体形式精确映射。

  3. 确认应答(ACK)与超时重传(RTO 动态计算)构成了 TCP 可靠性的双翼,解决了丢包、乱序、重复数据等所有常见的网络异常。

理解 TCP 的报头结构与可靠性基石,也是后续深入掌握 TCP 三次握手/四次挥手、滑动窗口、流量控制与拥塞控制等高级特性的必经之路。

相关推荐
长江&后浪1 小时前
华为wlan实验-三层旁挂
运维·服务器·网络·华为
梓沂1 小时前
CentOS 7.9 静默安装 Oracle 11gR2 企业版全记录
linux·oracle·centos
蜀道山老天师1 小时前
Zabbix监控Docker容器应用实践完整指南
linux·运维·docker·容器·zabbix
hweiyu001 小时前
Linux命令:info
linux·运维
兔兔兔兔11 小时前
记录C++ 3
开发语言·c++
xiaoxiangsiyan1 小时前
机房交换机交换技术与服务器链路聚合深度指南
运维·服务器·网络·学习·自动化·bond
中议视控1 小时前
展厅可视化网络中央控制系统:支持双向反馈,可视化播控,回显,状态显示,反向控制电脑,声光电网络视讯设备统一管理
网络·分布式·信息可视化
星空2 小时前
WordPress在docker中部署
运维·docker·容器
代码地平线2 小时前
C++类与对象(中):默认成员函数与日期类实战
c++·笔记·算法
hansang_IR2 小时前
【题解】可持久化区间仿射区间和(Persistent Range Affine Range Sum)
c++·算法·线段树