整理:【Linux网络】深入理解 TCP 协议(一):报头设计与可靠性基石(整理分享)

这两天一直在研究这个话题,踩了几个坑,把遇到的东西整理成文,供有需要的朋友参考。

🔥草莓熊Lotso:个人主页

❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》

✨生活是默默的坚持,毅力是永久的享受!


🎬 博主简介:

文章目录

二. TCP 协议格式深度解析 2.3 Linux 内核中 TCP 报头的实现2.4 TCP 与 UDP 报头的关键区别 三. TCP 可靠性核心:确认应答与超时重传 结尾:

前言:

在互联网世界中,TCP 协议无疑是最核心的基石之一 —— 我们每天用的 HTTP、HTTPS、SSH、FTP 等几乎所有可靠通信都建立在 TCP 之上。不少人知道 TCP 是 “可靠的传输控制协议”,但很少有人深入搞懂它的可靠性究竟是如何实现的,以及它的报头设计背后隐藏着怎样的精妙考量。这里将从最基础的 TCP 数据发送流程讲起,深度拆解 TCP 报头的每一个字段,结合 Linux 内核源码分析其底层实现,并详细讲解 TCP 可靠性的两大核心机制:确认应答与超时重传。所有内容均严格基于 TCP 协议规范和 Linux 内核实现,力求做到理论与实践相结合。

一. TCP 的简单回顾

1.1 TCP 在网络分层中的位置

我们先回顾一下经典的网络分层模型,明确 TCP 的定位:

OSI 参考模型TCP/IP 分层模型典型协议应用层应用层HTTP、HTTPS、SSH、FTP表示层--会话层--传输层传输层TCP、UDP、SCTP网络层互联网层IP、ICMP、ARP数据链路层网卡层以太网协议物理层(硬件)-

TCP 位于传输层,负责在两台主机的进程之间提供可靠的、面向连接的、字节流的通信服务。

1.2 TCP 数据发送的本质

不少初学者会误以为调用write/send函数就是直接把数据发送到网络上,这是一个常见的误区。实际上,我们向网络发送数据的本质是将数据拷贝到操作系统内核的 TCP 发送缓冲区中。

应用程序 write() → 拷贝数据到TCP发送缓冲区 → TCP协议栈控制发送时机、速率 → 网络

TCP 协议栈完全自主决定:

  • 发多少数据
  • 什么时候发
  • 以什么速率发
这就是 “传输控制协议” 名称的由来。同时,TCP 必须维护接收缓冲区,用于存放收到的数据,等待应用层读取。

正是因为有了发送和接收缓冲区,TCP 才可以实现面向字节流的特性 —— 数据被看作是一连串无结构的字节流,传输层不关心应用层的报文边界。


二. TCP 协议格式深度解析

TCP 报头是 TCP 协议的核心,所有的控制信息都包含在报头中。搞懂 TCP 报头是掌握 TCP 协议的基础。

2.1 TCP 报头整体结构

TCP 报头由固定 20 字节的标准部分最多 40 字节的可选部分组成,总长度范围为 20~60 字节。

字段长度字段名称核心作用16 位源端口号标识发送方进程16 位目的端口号标识接收方进程32 位序号本报文段第一个数据字节的编号32 位确认序号期望收到对方下一个字节的编号4 位首部长度以 4 字节为单位的 TCP 首部总长度6 位保留位预留为将来扩展,必须置 06 位标志位控制 TCP 连接状态和数据传输16 位窗口大小接收方的接收能力(流量控制)16 位检验和校验 TCP 首部和数据的完整性16 位紧急指针标识紧急数据的末尾位置0~40 字节选项扩展 TCP 功能(如 MSS、窗口扩大因子)可变长度数据应用层有效载荷

2.2 核心字段详解

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

这两个字段解决了有效载荷的分用麻烦—— 即数据理应交付给哪个进程。

端口号范围是 0~65535,其中:

  • 0~1023:知名端口,分配给标准服务(如 HTTP=80,HTTPS=443,SSH=22)
  • 1024~49151:注册端口,供用户进程用
  • 49152~65535:动态端口,由操作系统自动分配

2.2.2 32 位序号与确认序号

这两个字段是 TCP 可靠性的基础。TCP 将每个字节的数据都进行了编号,序号就是本报文段中第一个数据字节的编号。

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

举个例子: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 位标志位

这 6 个标志位用于控制 TCP 连接的状态和数据传输方法:

  • URG:紧急指针有效
  • ACK:确认序号有效(所有数据传输阶段的报文都必须置 1)
  • PSH:提示接收端立即将数据从 TCP 缓冲区推送给应用层
  • RST:强制重置连接
  • SYN:请求建立连接
  • FIN:请求关闭连接

2.3 Linux 内核中 TCP 报头的实现

我们来看一下 Linux 内核中tcphdr结构体的定义(位于include/linux/tcp.h),这是 TCP 报头在代码中的直接映射:

// linux kernel include/linux/tcp.h
struct tcphdr {
    __be16 source;    // 16位源端口号,网络字节序(大端)
    __be16 dest;      // 16位目的端口号,网络字节序
    __be32 seq;       // 32位序号
    __be32 ack_seq;   // 32位确认序号
#if defined(__LITTLE_ENDIAN_BITFIELD)
    __u16 res1:4,     // 保留位4位
          doff:4,     // 4位首部长度(数据偏移)
          fin:1,      // FIN标志:关闭连接
          syn:1,      // SYN标志:建立连接
          rst:1,      // RST标志:重置连接
          psh:1,      // PSH标志:推送数据
          ack:1,      // ACK标志:确认号有效
          urg:1,      // URG标志:紧急指针有效
          ece:1,      // ECE标志:显式拥塞通知回显
          cwr:1;      // CWR标志:拥塞窗口减小
#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  defines"
#endif
    __be16 window;    // 16位窗口大小
    __sum16 check;    // 16位检验和
    __be16 urg_ptr;   // 16位紧急指针
};

源码解读

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

2.4 TCP 与 UDP 报头的关键区别

很多人会问:为什么 UDP 报头有 16 位长度字段,而 TCP 没有?

答案在于两者的服务模型不同:

  • UDP 是面向数据报的:每个 UDP 报文都是独立的、有边界的。长度字段告诉接收方这个 UDP 报文的总长度,从而可以准确分离报头和有效载荷。
  • TCP 是面向字节流的:TCP 将数据看作是连续的字节流,传输层不关心应用层的报文边界。TCP 只需要保证字节流可靠到达,报文的边界由应用层自己处理。

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

TCP 最核心的特性就是可靠性。那么,TCP 是如何在不可靠的网络层之上实现可靠传输的呢?

3.1 可靠性的本质

首先我们要明确一个重要结论:网络世界中没有 100% 可靠的协议

这是一个经典的 “蓝军红军麻烦”:永远有最新的消息还没有得到应答,无法确认对方是否收到。但 TCP 通过确认应答机制,保证了被应答的历史数据 100% 可靠

3.2 确认应答(ACK)机制

TCP 可靠性的基础就是确认应答机制:A 主机发送给 B 主机的每一个数据段,B 主机都必须给 A 主机回复一个 ACK 确认报文

开发流程

  • A 发送数据段(序号 1~1000)给 B
  • B 收到数据后,回复 ACK 报文(确认序号 1001)
  • A 收到 ACK 后,就知道 1~1000 字节已经被 B 可靠接收
  • A 继续发送下一个数据段(序号 1001~2000)
两个关键细节
  • ACK 是由对方操作系统的 TCP 层自动回复的,不需要应用层参与。这保证了即使应用层很忙,也不会影响 TCP 的可靠性。
  • ACK 本身不需要被应答,否则会陷入无限循环。TCP 通过超时重传机制来处理 ACK 丢失的情况。

3.3 超时重传机制

如果 A 在一定时间内没有收到 B 的 ACK,会发生什么?

对于 A 来说,无法区分是以下哪种情况:

  • 数据本身在传输过程中丢失了,B 根本没有收到
  • B 收到了数据并回复了 ACK,但 ACK 在传输过程中丢失了
因此,TCP 统一按照 “数据丢失” 处理:只要在超时时间内没有收到 ACK,就重传对应的数据段

TCP 的去重机制:由于可能会重传数据,接收方可能会收到重复的报文段。TCP 通过序号来识别重复的报文段,并将重复的丢弃,保证应用层只会收到一次数据。

动态超时时间计算:TCP 的超时时间不是固定的,而是根据网络状况动态计算的:

  • Linux 系统以 500ms 为基本单位
  • 第一次超时等待 500ms
  • 如果重传后仍然没有收到 ACK,等待时间加倍(1000ms)
  • 以此类推,以指数形式递增
  • 累计重传一定次数后,TCP 觉得网络或对端异常,强制关闭连接

结尾:

🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!

** 结语:这里我们从 TCP 的基本开发流程讲起,深度解析了 TCP 报头的每一个字段,特别是 4 位首部长度的设计精髓,并结合 Linux 内核源码分析了其底层实现。同时,我们详细讲解了 TCP 可靠性的两大核心机制:确认应答和超时重传,搞懂了 TCP 如何在不可靠的网络之上实现可靠的数据传输。当然,TCP 的麻烦之处远不止于此。为了提高传输性能,TCP 还引入了滑动窗口、快速重传、流量控制、拥塞控制等机制;为了管理连接,TCP 设计了三次握手和四次挥手的流程。这些内容我们将在后续的文章中逐一拆解。

✨把这些内容吃透超牛的!放松下吧✨
>
ʕ˘ᴥ˘ʔ
>
づきらど

本次分享就到这里。技术这东西越研究越有意思,后续有新的收获我也会继续更新。

评论 (0)

暂无评论