【Linux网络】传输层协议TCP(1)---TCP报文介绍

hello~ 很高兴见到大家! 这次带来的是Linux网络中关于网络基础这部分的一些知识点,如果对你有所帮助的话,可否留下你宝贵的三连呢?

个 人 主 页 : 默|笙


文章目录

  • [一、TCP 报文](#一、TCP 报文)
    • [1.1 4位首部长度](#1.1 4位首部长度)
    • [1.2 32位序号和32位确认序号](#1.2 32位序号和32位确认序号)
    • [1.3 16位窗口大小](#1.3 16位窗口大小)
    • [1.4 6个常见标志位](#1.4 6个常见标志位)
      • [1. ACK](#1. ACK)
      • [2. SYN 与三次握手(初识)](#2. SYN 与三次握手(初识))
      • [3. FIN 和四次挥手(初识)](#3. FIN 和四次挥手(初识))
      • [4. RST](#4. RST)
      • [5. PSH](#5. PSH)
      • [6. URG与16位紧急指针+序号本质](#6. URG与16位紧急指针+序号本质)

一、TCP 报文

接下来,我们将结合 TCP 报文结构,把传输层协议 TCP 的工作原理简单梳理一遍。

  1. 如果 TCP 报头不含选项(Options)字段,其长度固定为 20 字节。
    这 20 字节由以下字段组成:
    16 位源端口号、16 位目的端口号、32 位序号、32 位确认序号、4 位首部长度、6 位保留位、6 位标志位(如 SYN、ACK、FIN 等)、16 位窗口大小、16 位校验和,以及 16 位紧急指针。

1.1 4位首部长度

  1. 4 位首部长度用于记录 TCP 首部的总长度。由于该字段只有 4 个比特位(最大可表示 15),因此它的单位是 4 字节,计算时需将字段值乘以 4。基础报头为 20 字节(对应字段值 5),最大长度为 60 字节(对应字段值 15),所以 TCP 首部的长度范围是 20~60 字节。
  2. TCP 报文本质上是一个结构体,接收方分离报头和有效载荷的核心依据正是"4 位首部长度"。
    内核先读取基础的前 20 字节,从中提取出 4 位首部长度,将其乘以 4 得到 TCP 报头的总字节数(20~60 字节)。从这个总长度处切开,前面是 TCP 报头,后面则是交给应用层的有效载荷。不过,由于 TCP 是面向字节流的,应用层拿到有效载荷后,还需要自行处理粘包和拆包问题。
  3. 内核剥离掉 TCP 报头后,剩下的有效载荷(数据)应该交给谁?核心依据就是报头中的目的端口号。源端口号和目的端口号正是用来表明数据从哪里来、要到哪里去。

1.2 32位序号和32位确认序号

1. 对TCP可靠性的正确认识、重传机制与确认应答机制

  1. TCP 的可靠性是通过保证历史报文的可靠性来实现的 。要让发送方知道自己发出的消息被收到了,接收方就必须给发送方应答。对于这个应答,我们不需要再让发送方去回复一个"应答的应答";因为只要发送方收到了这个应答,就证明了在这条应答之前的历史消息双方一定都已经收到了,它们一定是有效的。由于 TCP 通信主要关注报文能否顺利传递,所以只要一问一答、完成一次应答的闭环就够了。

这就好比两个隔得很远的人互相说话,一个人说,另一个人应答。只要听到了对方的应答,你就能确信:在对方应答之前的那些话,他一定都已经听到了。你不需要再冲着对面大喊一句:"我知道你回答我了!"。

  1. TCP 的可靠性并不等同于"绝对能够送达",而是体现在"状态可知"与"有错能纠" 。
    也就是说,发送方发出的数据,TCP 必须能够明确感知到是否被对方成功接收。如果发送失败,TCP 需要知道是哪一种通信错误(比如是超时、连接被重置,还是目标不可达),并据此触发相应的重传机制;如果彻底失败,TCP 会向应用层报告具体的错误码,而不是让应用层对着黑洞盲目等待。
关于重传与序号作用之一:去重
  1. 假如 A 和 B 通信,A 向 B 发送数据,A 如何知道 B 收到了?A 必须通过 B 返回的应答来确认。但 A 也可能收不到应答:一方面,数据可能根本没到 B,B 自然不会应答;另一方面,B 收到了,但 B 返回的应答丢了,A 也收不到。A 不能一直等下去,因此超过一定时间仍未收到应答时(丢包),A 会认为数据没有送达并重传。A 实际上分不清是数据丢了还是应答丢了,它只知道自己没收到应答,于是统一按"需要重传"处理。
  2. 如果 A 没收到应答是因为应答丢了,而 B 其实已经收到数据,那么 A 重传后,B 会再次收到重复的数据段。TCP 接收端根据 32 位序号判断该段数据是否已经接收过:如果重复,就丢弃重复数据,保留原先已接收的数据,并再次向 A 发送应答。这样应用层不会读到重复字节。
  3. A 能够重传,是因为 TCP 有发送缓冲区:对于已发送但尚未收到确认应答的字节,TCP 会保留下来,直到收到确认后才释放;超时仍未确认就重传。UDP 则没有 TCP 这种可靠确认与重传机制,发送后数据交给内核排队发出,不提供 TCP 式的未确认数据保留和重传保证。
  4. 确认应答机制:确认应答机制(ACK)是 TCP 保证可靠传输的核心。当发送方发送 TCP 报文后,接收方必须返回一个带有确认序号的应答报文。发送方只有在收到这个应答后,才能确认数据已经安全到达,从而将这部分数据从自己的"发送缓冲区"中彻底清除(释放内存)。如果迟迟等不到应答,发送方就会认为数据丢失并触发重传。
  5. 没有任何协议能提供 100% 的绝对可靠。因为最新的报文永远在等待应答,只有已经被应答的'历史消息'才是可靠的。
序号作用:排序重组
  1. TCP 通信往往不是"一来一回"的,即不是每收到一个应答才发送下一个报文,这样效率太低。TCP 通常可以连续发送多个报文段,但连续发送后,报文段到达接收方的顺序可能因网络状况而乱序。此时就体现出序号的作用:接收方根据 TCP 报文段首部中的序号,对字节流进行重组和排序。
确认序号作用:支持累计确认
  1. 无论是数据还是应答,TCP 通信传递的都是 TCP 报文段,每个报文段由 TCP 首部和可选数据组成。应答通常是一个不带数据的 TCP 报文段,通过首部中的 32 位确认序号来表达确认信息。确认序号表示接收方期望收到的下一个字节序号,也就是"该序号之前的数据已经连续收到",这样可以有效减少报文重发次数(减少因为应答丢失而导致的重传)。例如接收方连续收到 1~3000,就设置确认序号为 3001(收到的序号+1),告诉对方:3000 及之前的数据已经连续收到,下一次期望从 3001 开始。

如果 A 只收到了确认序号为 3001 的确认报文段,而没有单独收到 1001、2001 的确认报文段(它们丢了),也能说明 B 已经收到了 1000、2000、3000 这些数据。因为 TCP 的确认序号是累积确认:确认序号 3001 表示 3000 及之前的字节都已经连续收到,A 不需要逐个收到中间的确认。当然,前提是 B 能合法地发出确认序号 3001,说明 B 确实已经连续收到了 3000 及之前的数据;如果中间缺了 2000 这一段,B 就不能用 3001 做累积确认。

如果接收方收到了 1000、3000、4000,但缺少 2000 这一段,并且假设 1000 及之前已经连续收到,那么接收方的累积确认序号应设置为 1001,表示"1000 及之前收到了,下一个期望 1001",告诉发送方下次从 1001 开始。3000、4000 可以先缓存在接收缓冲区里,等缺失的 2000 到达后再一起向上交付。不能因为收到了 3000、4000 就把累积确认序号设成 4001,因为中间还有空洞。现代 TCP 还可以通过选择确认额外告诉发送方"已经收到了 3000、4000",但基础累积确认仍然是 1001。

2. 捎带应答机制(发送的报文 = 应答 + 数据)

  1. 为什么报头里面有序号和确认序号两个?因为 TCP 是全双工的,A→B 和 B→A 是两个独立编号的方向:序号管本报文段自己发送方向的数据,确认序号用来告知对方发来的数据收到了多少 。

    发送方发送给接收方时,确认序号应该怎么填?A 发给 B 的报文段里,确认序号确认的是 B→A 方向的数据,填 A 期望 B 下一个发来的字节序号,也就是 A 已连续收到 B 的数据的最大字节序号 + 1。

  2. 接收方发送应答时,序号又该怎么填?B 给 A 发应答时,B 也是自己发送方向的发送方,所以序号填 B 自己发送方向的下一个字节序号;纯应答不携带数据,不消耗序号。

    这个时候就得介绍捎带应答机制了。TCP 通信是全双工的,也就是说通信双方地位相等 。A 和 B 通信,在 A 给 B 发送报文的时候,B 同时也可以给 A 发送报文;它们发送的报文里面也可以顺便带上发给对方的应答(确认序号)。这样一个报文段就同时完成了"发数据"和"确认对方数据",能够减少报文的发送次数,效率更高。

  3. 总结:TCP 的序号机制(序号 + 确认序号)有四大作用:重传与去重、支持排序重组、支持累积确认、支持捎带应答。

1.3 16位窗口大小

  1. 要知道,对于 TCP 通信,接收缓冲区都是有一个大小的。如果发送方发送数据太快,一下子把接收缓冲区填满了,之后来的报文数据怎么办?难道直接丢弃吗?这样还要重传,就太损失效率了。所以,首先接收方需要知道自己的接收能力,而发送方也要知道对方的接收能力。发送方如何知晓?接收方在应答报头中填入自己的接收能力,这就是 16 位窗口大小,表示接收方当前还能接收多少字节。
  2. 如果接收方告诉发送方缓冲区满了,那么发送方如何知道下一次什么时候能给对方发送报文?发送方可以定期发送探测报头(没有有效载荷),而由于确认应答机制,接收方必须应答,这个时候发送方就能看到现在接收方的剩余缓冲区大小了。这个机制叫零窗口探测;它不是无节制地一直发,而是由持续计时器定期触发。
  3. 16 位窗口大小是 TCP 流量控制的直接体现:接收方通过通告窗口告诉发送方自己还能收多少;零窗口探测则是窗口为 0 时的恢复机制,防止双方因为窗口更新丢失而一直等待。

1.4 6个常见标志位

6位标志位:

URG :紧急指针是否有效。

ACK :确认号是否有效。

PSH :提示接收端应用程序立刻从TCP缓冲区把数据读走。

RST :对方要求重新建立连接;我们把携带RST标识的称为复位报文段 。

SYN :请求建立连接;我们把携带SYN标识的称为同步报文段 。

FIN :通知对方,本端要关闭了,我们称携带FIN标识的为结束报文段。

  1. TCP 报文是分类型的,标志位就是做这个标识工作的;只是它的实现方式不是独立类型字段,而是位标志组合。

1. ACK

  1. 当 TCP 报文首部中的 ACK 标志位置为 1 时,表示该报文中的确认号字段有效,即该报文包含对已收到数据的确认。由于 TCP 支持捎带确认(捎带应答)机制,一个报文在发送数据的同时也可以携带确认信息,因此连接建立后,大多数 TCP 报文段的 ACK 标志位都会置为 1。

2. SYN 与三次握手(初识)

  1. 如果一个 TCP 报文段中的 SYN 标志位置为 1,表示该报文用于建立连接,即发起连接并同步初始序列号。双方通信第一次请求连接时,通常是 SYN=1、ACK=0,表示这是一个连接请求,确认号字段暂不生效。结合三次握手可以更清楚地理解 SYN 标志位的作用。
  2. TCP 通信双方建立连接需要进行三次握手。通常由客户端先发送连接请求报文,其中 SYN=1、ACK=0。服务端收到后,会发送一个 SYN=1、ACK=1 的报文:其中 ACK=1 表示确认客户端的 SYN,SYN=1 表示服务端也同步自己的初始序列号,发起自己方向上的连接建立。这个报文同时承担"确认客户端请求"和"发送服务端连接请求"两个语义,可理解为确认信息捎带在 SYN 报文中。最后,客户端再发送一个 ACK=1 的报文,对服务端的 SYN 进行确认。三次握手完成后,TCP 连接建立。

三次握手的本质可以理解为:双方都要发送一次连接请求,并分别对对方的连接请求给予确认。只不过中间服务端对客户端 SYN 的确认,和服务端自己发出的 SYN 连接请求,被合并在一个 SYN+ACK 报文中,因此原本可能需要的四次交互被压缩成了三次。

3. FIN 和四次挥手(初识)

  1. 如果通信双方中有一方想断开连接,可以将 TCP 报文首部中的 FIN 标志位置为 1。更准确地说,FIN=1 表示发送方已经没有更多数据要发送了,它要关闭"自己到对端"这个方向的数据发送能力,而不一定代表整个连接立刻完全关闭。可以结合四次挥手来理解 FIN 标志位的作用。
  2. 假设 A 和 B 正在通信,四次挥手由主动关闭方开始。若 A 想断开连接,A 先向 B 发送 FIN=1 的报文,表示 A 方向的数据发送结束,然后等待 B 的 ACK。B 收到 FIN 后先回复 ACK,表示已收到 A 的关闭请求;但此时 B 可能还有数据要发给 A,所以 B 可以继续发送数据。等 B 的数据也发送完毕、应用层准备关闭后,B 再向 A 发送 FIN=1 的报文,A 收到后回复 ACK 进行确认。
  3. 四次挥手与三次握手的流程大同小异,为什么通常是四次挥手而不是三次 ?因为当 A 发送 FIN 请求时,只代表 A 这边没有要传输的数据了,但 B 不一定没有数据要发,要等 B 把剩余数据传输完成之后,再向 A 发送断开请求,所以通常需要四次。
    而三次握手通常起初由客户端发送连接请求,正常通信中服务端一般都会接受连接请求;服务端收到客户端 SYN 后,可以把"确认客户端 SYN"和"发送自己的 SYN"合并在一个 SYN+ACK 报文中,因此三次握手通常只需要三次。当然,服务端也可能拒绝、重置或不响应,但正常路径下一般是接受连接的。

4. RST

  1. 在我们进行TCP三次握手的时候,如果最后一次应答(第三次握手的ACK)丢失了怎么办?此时,客户端那边会单方面认为连接已经建立好(发送完ACK报文之后),但是服务端这边并没有,因为它没有收到这个应答。

  2. 这个时候,如果客户端直接向服务端发送数据,服务端因为自身没有建立好连接(查无此连接上下文或状态不匹配),它就需要告诉客户端。因此,服务端会把RST标志位置为1,回复一个RST报文,表达的意思是:"我虽然收到了你的数据,但我这边的连接还没建立好,请立即重置。" 客户端收到这个RST后,会强制关闭连接,后续若需要通信,通常就需要应用层重新发起三次握手来建立连接了。

5. PSH

  1. PSH这个标志位就是让应用层快点把数据读上去。这通常用于发送端希望立即交付数据的场景,防止数据在接收缓冲区中滞留等待。 应用层读取数据不是无时无刻都在进行的,通常是通过调用read去读,如果此时内核接收缓冲区里没有数据,read就会堵塞等待。系统通常要等缓冲区积攒到一定量之后,才会唤醒应用层去读取,这就是为什么read有时候会堵塞的原因。 而PSH置为1,就是通知接收端的内核"不要等待积攒更多数据了",立刻将当前缓冲区的数据推给应用层,唤醒正在阻塞的read调用,直接读上去。

6. URG与16位紧急指针+序号本质

  1. 要知道URG置为1代表16位紧急指针生效。紧急指针是什么?首先我们要再对序号和确认序号的本质有一个认识:
  1. 我们可以将接收缓冲区和发送缓冲区看成一个巨大的字节数组,有数据自然就有数组对应的索引。TCP是面向字节流的,序号和确认序号的本质其实就是映射到这些字节的索引(因为32位序号到达最大值后会回绕,太长了就相当于模上数组大小)。指针狭义而言就是内存地址,而对于广泛意义上的指针,指针的作用就是"定位",所以这些索引也可以看作是一种广义上的指针。
  2. 明白了这一点,紧急指针就很好理解了:当URG置为1时,16位紧急指针本质上就是一个偏移量。它利用这种"广义指针"的定位能力,指出紧急数据在这个字节流"大数组"中的结束位置。它相当于在告诉接收端内核:"在这个位置之前有一小段紧急数据,不要按常规流程等缓冲区的数据排好队了,立刻把这个位置的信息通知给应用层优先处理。"
  3. 紧急指针本质上就是为接收缓冲区的某一段数据提供"插队"服务。 当URG置为1时,接收端内核会打破常规的队列顺序,优先把这段数据推给应用层处理。但一定要记住:紧急指针的索引是对应整个接收缓冲区的全局字节流,而不是它所在的当前报文段所传递的数据。
  4. 为什么紧急指针指出的是插队数据的结束位置?因为这段需要"插队"的数据,其"开始位置"已经由TCP报文头部的序号明确告诉接收端了(接收端以此定位到大数组的起始索引);而紧急指针作为一个相对偏移量,仅仅用来指出这段插队数据的"结束位置"。
  1. 应用层在需要优先处理紧急指令时,才会去接收这个"插队"数据,它的学术名称叫做带外数据(Out-of-Band, OOB)。应用层通常会有一层专门的逻辑来对带外数据进行读取。在Socket编程中,MSG_OOB 这个标志位就是应用层用来激活这项机制的:发送端调用 send 函数时带上它,用来"触发"内核发送紧急数据;接收端则调用 recv 函数时带上它,来"优先读取"这个插队的数据。
  2. 带外数据的读取不会干扰正常数据的读取。我们可以把TCP连接在逻辑上看作包含两个通道:一个是"紧急通道",一个是"普通通道",这两个通道可以同时工作。

今天的分享就到此结束啦,如果对读者朋友们有所帮助的话,可否留下宝贵的三连呢~~

让我们共同努力, 一起走下去!

相关推荐
相思难忘成疾2 小时前
从零搭建 Kubernetes 1.30 集群:kubeadm 部署一主多从实战(RHEL 9.3 + Docker + flannel)
linux·运维·kubernetes
用户4107192013252 小时前
用 systemd 从零做一个系统健康巡检服务:脚本、沙箱与磁盘满排查实战
linux·ubuntu
云飞云共享云桌面2 小时前
1台SolidWorks服务器如何承载10个设计并发?
运维·服务器·网络·3d·制造
GeW3 小时前
新质生产力路线图下,Linux 和数据库不能拆成两件事做
linux
云和数据.ChenGuang3 小时前
Python 闭包详解:从原理到实战
linux·开发语言·人工智能·python·kubernetes
Linux-lucky3 小时前
46-Linux学习之旅之LVS负载均衡与keepalived高可用
linux·运维·学习·ubuntu·负载均衡·lvs
Fruiticecake3 小时前
C 变量住在哪里,进程内存为什么越来越大:从 .data / .bss 到 smaps 排查内存增长
linux
盛世宏博北京4 小时前
以太网温湿度传感器协议选型实战:TCP、UDP、SNMP通信特性对比
网络协议·tcp/ip·udp
Escalating_xu4 小时前
【Linux 网络编程】NAT、代理服务与内网穿透:从一个公网 IP,到一条真正可达的连接
linux·网络·tcp/ip