欢迎光临
我们一直在努力

深度解读TCP原理

一、 宏观视角:生产者-消费者模型

核心架构:内核缓冲区的双向队列

在生产者-消费者模型中,核心组件是一个阻塞队列,在TCP中,这个队列被拆分成两个部分,分别位于发送端和接收端的OS内核中

  • 生产者:发送端的应用程序

    • 当调用write或者send时,并不是把数据发送到了网络上,而是将数据总用户空间拷贝到了内核中的发送缓冲区
    • 一旦拷贝完成,write系统调用就会返回,生产者任务暂时结束,至于数据什么时候发、发多少,完全由TCP协议栈决定
  • 消费者:接收端的应用程序

    • 数据从网卡驱动到达内核后,拷贝到接收缓冲区
    • 接收端的read或recv调用,本质是从内核的接收缓冲区里拷贝数据到用户空间
  • 全双工通道

    TCP是全双工的,意味着每一段既是生产者,也是消费者。每个TCP连接在内核中都有两个缓冲区,一个发送,一个接收

具体过程如下图所示:

image-20260212154642101

面向字节流的解耦特性

生产者-消费者模型的巨大优势是解耦:生产者的生产速率和消费者的消费速率不需要完全一致:

  • 生产和消费的不对等:

    • 生产者:可以一次性write100字节的数据
    • 消费者:可以分100次,每次read1个字节,也可以一次性read100个字节
  • 底层原理:

    因为由内核缓冲区的存在,消费者不需要知道生产者是怎么发的,只管从缓冲区里拿数据就好,只要缓冲区里有数据,就能拿,这种流式处理就消除了数据包边界的限制

同步机制:流量控制和阻塞

生产者-消费者模型必须解决两个问题:

  • 队列满了怎么办?(生产太快,消费太慢)
  • 队列空了怎么办?(生产太慢,消费太快)

下文详细讲解

传送带角色

在这个模型中,连接两个内核缓冲区的,是不可靠的网络层,就像一条不稳定的传送带:

  • 可能会丢包
  • 可能会乱序到达
  • 可能会网络拥塞

那么TCP就需要一套机制来保证数据不丢失(可靠性)、不乱序(有序性)且不爆仓(流量控制)

二、 微观视角:解剖TCP报头结构

image-20260302165627582

报文的身份证

在应用层来看,数据是一段完整的消息(如"hello"),在传输层,这段数据会被打上一个TCP报头,封装成一个TCP报文段

  • 标准尺寸:TCP报头有一个固定长度:20字节
  • 扩展能力:TCP可以通过选项字段扩展,报头最长可以达到60字节
  • 结构逻辑:按照32位对齐,像砌砖一样一层一层

核心字段的详细作用

  • 第一层:身份识别(寻址)
    • 16位源端口号&16位目的端口号
      • 作用:解决分用问题
      • 解析:IP地址解决了给哪台机器的问题,端口号解决发给这个机器上的哪个进程的问题
  • 第二层:可靠性的基石(顺序与确认)
    • 32位序列号
      • 作用:解决乱序和去重问题
      • 解析:TCP给发送的每一个字节都编了号,对方收到一堆乱序的包,可以根据序列号排队,还原出正确的数据
    • 32位确认应答号
      • 作用:解决丢包问题
      • 解析:表达含义是我收到了该序号之前的所有数据,下次请从这里开始发,比如给发送端发送1001,表示1000及以前的数据都收到了,请从1001开始发
  • 第三层:连接控制和管理
    • 4位首部长度
      • 为什么存在?-> 因为有选项字段存在,报头长度不固定
      • 计算方法:这个字段是4字节
        • 如果该值为5,表示报头长度为5×4=205 \\times 4 = 205×4=20字节(标准长度)
        • 最大值为15,表示报头最长为15×4=6015 \\times 4 = 6015×4=60字节
      • 所以:报头长度=4位首都长度×4+额外选项报头长度 = 4位首都长度 \\times 4 + 额外选项报头长度=4位首都长度×4+额外选项 -> [20,60]
    • 6位标志位
      • 这是TCP的控制面板,决定了当前报文的性质,服务器收到不同类型的报文,做出不同的动作
        • SYN:建立连接的请求
        • FIN:断开连接的请求
        • ACK:表明自己是一个确认报文,告诉对端“你要关心确认序号!”因为有捎带应答,所以大部分TCP报文的ACK都是1
        • RST:连接异常,强制断开连接
        • PSH:催促接收端尽快把数据读走,别在缓冲区墨迹
        • URG:说明包里有紧急数据,需要插队
    • 16位窗口大小
      • 作用:实现流量控制注意:流量控制!=减少发送
      • 解析:标识对方接收缓冲区的剩余空间大小,接收方通过这个字段告诉发送方:我还能吃得下多少,你悠着点来,别把我撑着或者饿着了
  • 第四层:数据完整性
    • 16位校验和
      • 作用:保证数据没坏,发送端填充,接收端校验,如果校验不通过,直接丢弃,当作没收到
    • 16位紧急指针
      • 作用:标识哪部分数据是紧急数据

内核代码视角:

struct tcphdr {
__be16 source; // 16位源端口
__be16 dest; // 16位目的端口
__be32 seq; // 32位序列号
__be32 ack_seq; // 32位确认序号
// … (标志位和首部长度,因大小端序不同而略有差异)
__be16 window; // 16位窗口大小
__sum16 check; // 16位校验和
__be16 urg_ptr; // 16位紧急指针
};

三、 可靠性的保证

铺垫思考

先理解一下确认应答:

  • 主机A给主机B发送“你好”,报文在网络上跑,需要花时间
  • 主机B要给主机A发的消息做出应答
  • 主机A要给主机B的应答继续应答!
  • 这样不就死循环下去了?

如下图:

image-20260302170957911

所以:长距离通信,是没有100%可靠性的,总会有最新的一条消息没有得到应答,老消息有应答就是100%可靠的

可靠性不是对最新消息的应答

我们站在不同角度理解可靠性:

  • 我是主机A:我要保证我发给主机B的消息可靠性!
  • 我是主机B:我要保证我发给主机A的消息可靠性!

而TCP是全双工的,所以,TCP是可靠的!如下图,保证两个方向的可靠性,就保证了整体的可靠性:

image-20260302171225049

上面的图演示的都是发一个报文,等一个应答,串行处理,效率难免太低了!

所以,我就可以发一批报文,同时应答,同时给报文带上编号,来完成对报文的确认!

image-20260302172012441

现在详细展开讲

序列号:解决乱序和去重问题

如果网络卡顿,对方重发了数据,结果之前没收到的数据包又到了,接收方岂不是收到了两份同样的数据包?

  • 痛点:网络层会导致数据包乱序到达,或者因为重传产生重复数据
  • 原理
    • 排序:接收缓冲区就是一个char类型的数组,数据包到达之后,内核会根据TCP报头的32位序列号,把它放到对应索引的位置上,即使后发的包先到了,也只能等着,直到前面的空缺被填上
    • 去重:如果内核发现收到的数据包序号在缓冲区里已经有了,就会直接丢弃这个重复的报文段,通常会回复一个ACK,告诉对方“不用发了,我已经有了”

确认应答(ACK):不仅仅是收到

  • 核心痛点:我发了一堆数据,怎么知道对方收到了哪部分?丢了哪部分?
  • 原理:累计确认
    • TCP不会为每一个字节单独发送一个ACK,串行效率太低了
    • 确认序号含义:我已经成功收到了该序号之前的所有字节,下一次发送请从该序号开始发送
    • 举例:
      • 发送方发了 1-1000, 1001-2000, 2001-3000 三个包
      • 接收方收到了 1-1000 和 2001-3000,中间的 1001-2000 丢了
      • 此时接收方不能回复 ACK 3001,只能回复 ACK 1001。
      • 含义: 1001 之前我都收到了,但我想要 1001,请你重传

兜底:超时重传机制

image-20260302175308665

因为网络拥堵问题,A给B发送失败或B给A的应答A没收到

这个超时的时间如何确定?

  • 绝对不能写死
    • 写长了业务层阻塞时间过长
    • 写短了稍微有点网络波动就会引起一大批主机重发
  • 正常情况:动态计算内核会动态采样,每次数据包一来一回,内核会测量真实的耗时,动态更新下一次的超时时间,做到网快就短,网慢就长
  • 极端情况:指数退避,真的发生超时,说明网络极度拥塞。TCP会把下次超时时间翻倍,如果连续重试15次依然失败,底层Socket就会抛出ETIMEDOUT错误,通知业务层连接断开

连接管理(超级重要)

首先建立一个认知:物理层面上,根本不存在所谓的连接,网络是一根根网线和路由先,它们只负责转发电信号。而所谓的“TCP”连接,本质上是通信双方的操作系统内核里,各自开辟了一块内存,维护了一个struct tcp_sock的状态机。建立连接,就是双方同步初始化这个状态机的过程;断开连接,就是双方销毁这块内存的过程

image-20260302213859445

三次握手

详细过程
  • 第一次握手(客户端发)

    • 报文:客户端发送一个SYN标志位为1的报文,说:我要建立连接,我的初始序列号是x
    • 内核:客户端调用connect(),触发底层发包,此时客户端TCP状态机处于SYN_SENT状态(发出就转换状态)

    image-20260302195955105

  • 第二次握手(服务器响应)

    • 报文:服务端收到后,恢复一个标志位SYN和ACK为1的报文,说:收到你的x了(ACK=x+1),我也要和你建立连接,我的初始序列号是y(SYN=y)
    • 内核:服务端收到SYN,TCP状态机处于SYN_RCVD状态

    image-20260302200543189

  • 第三次握手(客户端拍板)

    • 报文:客户端收到了服务端的SYN+ACK,最后回复一个ACK报文,说:收到你的y了(ACK=y+1)信道正式建成!
    • 内核:客户端状态机进入ESTABLISHED(发出就转换状态),同时,服务器进入ESTABLISHED状态(发出就转换状态),此时服务器阻塞在accept()的代码唤醒,返回一个全新的Socket文件描述符

    image-20260302201045373

注意:

  • 三次握手的过程是操作系统完成的,connect和accept不参与!
  • 三次握手本质就是赌,赌最后一个ACK对方收到了

再来一张图总结:

image-20260302202747655

为什么要三次握手?
  • 防范历史失效连接的内存泄漏:

    假设网络极度堵塞,客户端发了SYN1卡在半路,超时之后又发了SYN2,如果只有两次握手,服务器收到迟到的SYN1直接分配内存建立连接,但是客户端不认这个旧连接了,导致服务器的Socket资源浪费

    而三次握手的精髓就是客户端最后一次校验,服务器回复SYN1+ACK给客户端,客户端发现上下文不对,我应该收到SYN2+ACK啊,立刻发送一个RST标志位为1的报文,强制把服务器建立好的半成品连接销毁

  • 保证了全双工信道的通畅性:

    客户端发SYN,服务端收到发来SYN+ACK,这里验证了客户端O的能力正常,服务器的IO能力正常,客户端收到SYN+ACK,发一次ACK,验证了客户端的I能力正常,你来我往,同时也验证了网络的通畅性!

四次挥手

详细过程
  • 第一次挥手(客户端发起关闭)
    • 报文:客户端发送FIN报文,说:我的发送缓冲区空了,不再发数据了
    • 内核:调用close,进入FIN_WAIT_1状态
  • 第二次挥手(服务器确认,进入半关闭)
    • 报文:服务器回复ACK,说:知道了,你不会再发了
    • 内核:服务器收到FIN后,进入CLOSE_WAIT状态,客户端收到ACK,进入FIN_WAIT_2状态。此时是**半关闭状态:**客户端不能发,但是还能收,服务器应用层如果调用read回读到0,但是它底层发送缓冲区里的历史数据还会继续往外发
  • 第三次挥手(服务器彻底发完)
    • 报文:服务器把剩下的数据发完,也向客户端发送FIN报文,说:我的数据也发完了,我也关了
    • 内核:服务器应用层调用close,进入LAST_ACK状态
  • 第四次挥手(客户端最后确认)
    • 报文:客户端收到FIN,回复ACK,说:收到,就此别过
    • 内核:服务器收到ACK后,立刻进入CLOSED,销毁,而此时客户端绝对不能立刻销毁,必须进入TIME_WAIT状态,在内核中等待2MSL(60s)后,等游离的报文消散,再进入CLOSED

完整图解:

image-20260302213832499

双方FIN和ACK有来有回,连接断开成功

注意:主动关闭连接的一段,才有TIME_WAIT状态

为什么是四次挥手?

服务器收到客户端的FIN时,仅仅代表了客户端不发了,但服务器可能还有没发完的数据,所以服务器必须回一个ACK稳住客户端(防止客户端超时重传FIN),等服务器自己的应用层把数据全部发完之后,才能发出属于自己的FIN。ACK和FIN中间隔着业务处理的时间,通常无法合并,必须四步走,确保双方的读写缓冲区都关闭了

理解TIME_WAIT状态

未完待续~

赞(0)
未经允许不得转载:171主机测评 » 深度解读TCP原理
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址