[ 计算机网络 ] 深入理解TCP/IP协议

​🎉欢迎大家观看AUGENSTERN_dc的文章(o゜▽゜)o☆✨✨

🎉感谢各位读者在百忙之中抽出时间来垂阅我的文章,我会尽我所能向的大家分享我的知识和经验📖

🎉希望我们在一篇篇的文章中能够共同进步!!!

🌈个人主页:AUGENSTERN_dc

🔥个人专栏:C语言 Java | 数据结构 | 算法 | MySQL | RabbitMQ | Redis | 计算机网络  

⭐个人格言:

一重山有一重山的错落,我有我的平仄

一笔锋有一笔锋的着墨,我有我的舍得

目录

1. 协议栈

1.1 定义: 

1.2 协议的作用:

1.3 协议栈的工作原理: 

1.3.1 数据封装与解封装

1.3.2 层间交互

2. TCP/IP协议栈四层模型

2.1 应用层:

2.2 传输层:

2.2.1 有连接 VS 无连接

2.2.2 可靠传输 VS 不可靠传输

2.2.3 面向字节流 VS 面向数据报

2.2.4 全双工 VS 半双工

2.3 UDP协议

2.3.1 UDP长度: 

2.3.2 UDP校验和: 

注意事项

2.3.3 16位源端口号和目的端口号

2.3.4 数据

2.4 TCP协议

2.4.1 16位源端口号和目的端口号

2.4.2 32位序号和确认序号

2.4.3 16位校验和

2.4.4 16位紧急指针

2.4.5 16位窗口大小

2.4.6 4位首部长度

2.4.7 选项

2.4.8 六位标记位

2.4.8.1 URG 紧急指针

2.4.8.2 ACK 确认

2.4.8.3 PSH 推送

2.4.8.4 RST 重置

2.4.8.5 SYN 同步

2.4.8.6 FIN 结束

2.4.9 6位保留位

2.5 TCP十大核心机制:

2.5.1 确认应答机制

2.5.2 超时重传机制

2.5.3 连接管理机制

2.5.3.1 三次握手

2.5.3.2 四次挥手

2.5.3.3 连接管理中的状态转换

2.5.4 滑动窗口机制

2.5.5 流量控制机制

2.5.6 拥塞控制机制

2.5.7 延时应答机制

2.5.8 捎带应答机制

2.5.9 面向字节流

2.5.10 异常处理机制

2.6 网络层

2.7 IP协议

2.7.1 四位版本

2.7.2 4位首部长度

2.7.3 八位服务类型

2.7.4 16位总长度

2.7.5 16位标识

2.7.6 3位标志

2.7.7 13位片偏移

2.7.8 8位生存时间

2.7.9 8位协议

2.7.10 16位首部校验和

2.7.11 32位源IP地址 / 32位目的IP地址

2.7.12 选项

2.8 链路层


1. 协议栈

在了解TCP/IP协议之前,我们需要了解一个基本的概念,协议栈

1.1 定义: 

协议栈(Protocol Stack)是计算机网络中一个非常重要的概念,它是指一组协议的集合,这些协议按照层次结构组织,每一层负责特定的功能,并通过接口与相邻层进行交互。

协议栈的核心思想是分层,通过分层可以将复杂的网络通信问题分解为多个相对独立且易于管理的部分。

我们可以认为OSI七层模型是一种协议栈的规范或者标准,协议栈就是按照OSI七层模型来进行分层的,不过不同的协议栈在七层模型的基础上进行了不同的简化

像我们所说的TCP/IP协议四层模型,其实就是TCP/IP协议栈四层模型而不是TCP/IP协议有四层模型,

因为TCP协议是传输层的协议, IP协议是网络层的协议,这都是某一层中的某一个协议, 而我们所说的TCP/IP四层模型指的是TCP/IP协议栈的四层模型,其他层还会使用其他的协议, 只不过在这个协议栈中, TCP/IP协议是更为重要的一部分, 所以用TCP/IP协议来给这个协议栈命名

TCP/IP协议栈在OSI七层模型的基础上进行了简化,将OSI七层模型简化成了四层,这里后面会细说

1.2 协议的作用:

协议是协议栈中每一层的"规则", 他规定了数据在该层的格式、传输方式、差错控制等。

例如:

TCP协议规定了如何在不可靠的网络环境中实现可靠的数据传输,包括数据分段、序列号、确认应答、重传机制等;

IP协议规定了数据包的格式、路由选择等。

在协议栈中, 每一层的协议可以有多个, 这些协议可以根据不同的需求和应用场景来实现该层的功, 这种设计使得协议栈具有灵活性和可扩展性, 能够适应不同的网络环境和应用需求

例如: 

假设用户使用浏览器访问一个安全的网页(https://www.example.com), 并进行登录操作, 在这个过程中, 协议栈的每一层都可能涉及多个协议的协同工作

在应用层,HTTP和HTTPS是两中不同的协议, 他们分别用于不同的场景:

HTTP: 用于普通的网页浏览, 数据以明文的方式传输

HTTPS: 是HTTP的加密版本, 通过SSL/TLS协议对数据进行加密, 确保数据传输的安全性

由于用户是通过域名进行访问的, 因此浏览器还会使用其他应用层协议 DNS

DNS: 用于将域名(如www.example.com)解析为IP地址

1.3 协议栈的工作原理: 

1.3.1 数据封装与解封装

  • 当数据从应用层向下传输到链路层时,每一层都会对数据进行封装。封装的过程是在数据前面添加本层的协议头部(有时还会在后面添加尾部),头部包含了该层协议需要的信息,例如源地址、目的地址、协议类型等。

  • 例如,在TCP/IP协议栈中,应用层数据首先被传输层封装,添加TCP头部(如果是TCP协议);然后被网络层封装,添加IP头部;最后被链路层封装,添加以太网帧头部(如果是以太网环境)。

  • 当数据从链路层向上传输到应用层时,每一层会解封装,去掉本层的头部(和尾部),并将数据向上层传递。解封装的过程是封装的逆过程,每一层只处理本层的头部信息,提取出需要的信息后,将剩余的数据向上层传递。

1.3.2 层间交互

  • 协议栈中各层之间通过接口进行交互。每一层都提供服务给上层,并使用下层提供的服务。接口是一组原语(操作)的集合,上层通过调用这些原语来请求下层的服务,下层通过执行原语来向上层提供服务。

  • 例如,应用层通过调用传输层的原语(如TCP的socket接口)来请求传输层建立连接、发送数据等服务;传输层通过调用网络层的原语(如IP层的发送数据包接口)来请求网络层将数据包发送到网络上。

2. TCP/IP协议栈四层模型

四层模型是哪四层呢???

在TCP/IP四层模型,将OIS七层模型中的应用层, 表示层, 会话层合并成了应用层, 将数据链路层和物理层合并成了链路层

2.1 应用层:

应用层和应用程序直接相关, 是和程序员打交道最多的一层

应用层协议, 里面描述的内容, 就是你写的程序, 通过网络具体按照啥样的方式来传输数据

不同的应用程序, 就可以使用不同的应用层协议

而实际开发过程中, 很多时候, 都是需要程序员自己定制应用层协议

自定义应用层协议,本质上就是对传输的数据做出约定

在我们自己写的程序中, 在服务器和客户端之间进行通信的时候, 约定好数据的格式、传输规则、错误处理方式等,就可以实现自定义协议的通信。

协议说白了就是通信双方的一种约定

2.2 传输层:

在传输层中, 我们主要讲解两个协议: UDP 和 TCP

TCP和UDP都是TCP/IP协议栈中传输层的协议

虽然都在同一层, 但是他们之间的差别还是很大的

UDP: 无连接, 不可靠传输, 面向数据报, 全双工

TCP: 有连接, 可靠传输, 面向字节流, 全双工

2.2.1 有连接 VS 无连接

有连接就好比打电话, 得电话先接通了, 才能说话

无连接就好比, 发短信, 不需要"先接通" , 直接就可以发信息

计算机中的连接, 是一个抽象的概念

计算机中的连接认为是, 要建立连接的双方,各自保存对方的信息

此时, 就认为是建立了一个"连接"

2.2.2 可靠传输 VS 不可靠传输

首先在这里要澄清一点:   < 可靠 != 安全 >

我们所说的可靠是指, 要传输的数据, 尽可能(而不是确保)的传输给对方

比如再网络通信的过程中, 可能会存在很多的意外情况, 比如丢包

丢包原因: 网络环境复杂, 可能是断网了, 或者网络波动了, 导致的丢包

A传输给B, 中间可能会经历很多的交换机和路由器进行转发

这些交换机和路由器, 也不只是转发你的数据, 要转发很多的数据

比如某个交换机/路由器非常繁忙, 繁忙到要处理的数据量已经超出了硬件水平的极限

此时,多出来的数据就无法被转发, 会被直接丢弃

我们无法预知丢包何时发生, 也无法预知丢包在哪台设备发生

为了对抗丢包, 我们就引入了"可靠传输"

TCP就具备可靠传输的特点, 内部就提供了一系列的机制来实现可靠传输

即使如此, TCP也只可能"尽可能", 无法保证100% 达到对端

极端情况下, 会出现网线断开的情况, 即使你在软件上使出浑身解数 ,也无济于事

UDP则是 不可靠传输, 传输数据的时候, 压根不关心对方是否收到了, 发了就完了

但是可靠传输是有代价的, 代价就是: 效率会大打折扣

所以在传输的效率上来看: UDP比TCP快

2.2.3 面向字节流 VS 面向数据报

文件操作, 就是可以是字节流的, 比喻成水流一样, 读写操作非常灵活

TCP 和文件操作具有类似的特点, 可以一点一点的读取, 而不是非要一次性读取一整段

面向数据报就不是了, 传输数据的基本单位, 是一个个的UDP数据报

一次读写, 只能读写一个完整的UDP数据报, 不能读写半个

网络传输数据的基本单位:

  1. 数据报 Datagram UDP
  2. 数据段 Segment TCP 
  3. 数据包 Packeg IP
  4. 数据帧 Frame 数据链路层

2.2.4 全双工 VS 半双工

全双工: 一条链路, 能够进行双向通信,(TCP, UDP) 创建socket对象, 既可以读,也可以写

半双工: 一条链路, 只能进行单向通信 (Linux中的 Pipe 管道)

2.3 UDP协议

学习一个网络协议, 最主要的就是学习报文格式

对于UDP协议来说, 应用层数据达到UDP之后, 就会给应用层数据报前面拼装上UDP报头

UDP数据报 = UDP报头 + UDP载荷

UDP协议端格式:

2.3.1 UDP长度: 

描述了整个UDP数据报, 占多少个字节

通过UDP长度, 就可以知道, 当前的载荷一共是多少个字节

UDP的头部包括: 源端口号, 目的端口号, UDP长度, 校验和一共八字节

所以载荷的大小 = UDP长度 - 8字节

使用UDP开发程序, 就会有很大的制约

确保传输的的那个数据报, 不能超过64kb

因为UDP长度一共16位, 16位能表示的最大数也就是2的16次方 = 64kb

2.3.2 UDP校验和: 

我们的数据在网络传输的过程中, 是可能会出错的

在网络中传输的时候,我们的数据是以比特流的方式进行传输的, 也就是0和1, 但是在传输的过程中,可能因为某些错误, 导致你原本传输的0到了对端实际上变成了1, 这就是比特翻转

在这种情况下,我们就需要能够有办法对传输的数据进行校验, 我们希望能够发现传输的数据是否出错了, 甚至能够发现传输的数据哪一个地方出错了, 前者实现代价较小, 但是传输的数据就不可用了, 后者实现代价较大, 但是能够对传输错误的数据进行纠错, 这两者实现的本质都是引入额外的冗余信息

在UDP中, 校验只能做前者, 也就是发现是否出错,但是不能知道哪里出错了

简单来说, 校验和就是拿着数据一部分进行一系列计算, 得到结果, 如果数据部分发生改变, 此时得到的结果也就不一样了

UDP中使用的一的补码加法算法作为校验和, 这是一个简单粗暴的计算校验和的方式

大概过程如下:

首先构造一个伪头部:

  1. 从IP数据报的头部拿到源IP和目的IP, 添加入伪头部
  2. 在伪头部中添加一个字节的填充字节, 值为0, 用于确保伪头部结构对齐
  3. 添加一个字节的字段, 表示UDP的协议号, 值为17, 用于IP层确认上层协议, 正确处理数据报
  4. 添加两个字节的字段,表示UDP长度

例如:

  1. 源IP地址:192.168.1.1(转换为32位二进制:11000000 10101000 00000001 00000001)

  2. 目的IP地址:192.168.1.2(转换为32位二进制:11000000 10101000 00000001 00000010)

  3. 填充字节:0x00

  4. 协议号:0x11(UDP协议号)

  5. UDP长度:500(转换为16位二进制:00000001 11110100)

将这些组合起来, 伪头部的二进制表示如下:

11000000 10101000 00000001 00000001  // 源IP地址
11000000 10101000 00000001 00000010  // 目的IP地址
00000000                                // 填充字节
00010001                                // 协议号(17)
00000001 11110100                      // UDP长度(500)

伪头部的主要作用是为UDP校验和的计算提供额外的上下文信息。通过将IP层的源地址、目的地址和协议号纳入校验和的计算范围,可以确保UDP数据报在传输过程中不仅数据部分没有被损坏,而且传输路径(IP地址)和协议类型也没有错误。

注意事项
  1. 伪头部仅用于校验和计算:伪头部不会实际传输,仅在发送方计算校验和和接收方验证校验和时使用。

  2. UDP长度字段:UDP长度字段包括UDP头部(8字节)和数据部分的总长度。

  3. IPv6的伪头部:如果使用IPv6,伪头部的结构会稍有不同,因为IPv6地址是128位的。

首先,将伪头部、UDP头部和数据部分按16位分组。如果数据部分的长度不是16位的整数倍,需要在最后一个分组的末尾用0填充到16位。

// 进行分组后的伪头部
11000000 10101000
00000001 00000001
11000000 10101000
00000001 00000010
00000000 00010001
00000001 11110100
// 假设这是UDP数据部分
00000000 00000000
00000000 00000000
...
00000000 00000000

然后对所有的16位分组进行一的补码加法

11000000 10101000 + 00000001 00000001 + 11000000 10101000 + 00000001 00000010 + 00000000 00010001 + 00000001 11110100 + ... + 00000000 00000000

如果加法结果产生进位,则将进位加到最低位(即“折叠”操作)

// 例如
1000 + 1000 = 1 0000
// 去掉最高位进位
1000 + 1000 = 0000
// 将最高位进位加到最低位
1000 + 1000 = 0001

假设最终的加法结果为:11111111 11111111(全1,表示校验和计算结果为0,需要取反)

取反(一的补码):00000000 00000000

如果校验和计算结果为全0,则认为校验和无效。

接下来就是比较计算出来的校验和 以及 UDP中的校验和, 若不相同, 则直接舍弃

2.3.3 16位源端口号和目的端口号

这两个字段就很简单了
源端口号: 就是发送方应用程序的端口号, 用于标识发送放的应用程序, 当接收方收到UDP数据报时, 可以通过源端口号知道数据是从哪个应用程序发送过来的

但是如果源端口号为0, 就是发送方不希望接收方发送任何响应

目的端口号: 是接收方应用程序的端口号,用于标识接收方的应用程序, 当发送方发送UDP数据报时,需要指定接收方的端口号,以便接收方能够将数据正确地传递给目标应用程序。

目的端口号不能为0, 因为0是一个无效的端口

2.3.4 数据

在UDP协议中, 数据部分是UDP数据报的核心内容, 他承载了用户需要传输的实际信息, UDP本身是一个简单的无连接协议, 他不保证数据的可靠传输, 也不提供错误恢复机制, 因此数据的完整性和可靠性需要由上层应用程序来保证

UDP数据部分的长度是可变的,从0字节到最大值(UDP数据报的最大长度为65535字节,因此数据部分的最大长度为65527字节)

UDP协议对数据部分的内容没有任何限制或结构要求。数据部分可以是任何类型的数据,例如文本、二进制数据、音频、视频等。

UDP不保证数据的可靠传输,也不提供错误恢复机制。如果数据在传输过程中丢失或损坏,UDP不会自动重传或修复。这种可靠性需要由上层应用程序来实现。

2.4 TCP协议

首先我们先看看TCP的报文格式:

2.4.1 16位源端口号和目的端口号

这一部分与UDP的是一样的

源端口号: 就是发送方应用程序的端口号, 用于标识发送放的应用程序, 当接收方收到UDP数据报时, 可以通过源端口号知道数据是从哪个应用程序发送过来的

但是如果源端口号为0, 就是发送方不希望接收方发送任何响应

目的端口号: 是接收方应用程序的端口号,用于标识接收方的应用程序, 当发送方发送UDP数据报时,需要指定接收方的端口号,以便接收方能够将数据正确地传递给目标应用程序。

目的端口号不能为0, 因为0是一个无效的端口

2.4.2 32位序号和确认序号

32位序号: 这是用来标识每个字节的位置, 确保数据的顺序性和完整性

32位确认序号: 用户告诉发送方接收方已经成功接收了哪些数据, 从而实现可靠传输

这里可能大家有点不懂, 32位序号是如何标识每一个字节的位置的呢?

首先, 32位序号只是用来标识数据中的每一个字节, 也就是说, TCP首部中的字节是不会被序号标记的,例如,我的TCP首部(包括选项)一共20字节, 数据10字节,那么32位序号只会标识数据中的10个字节

在TCP协议中, 连接建立的时候 客户端和服务器会各自随机选择一个初始序号,例如,客户端的初始序号为X,服务器的初始序号为Y。每个字节都会被分配一个序号。例如,客户端发送的第一个字节序号为X,第二个字节序号为X+1,以此类推。TCP的序号是32位的,最大值为2^32 - 1(即4294967295)。当序号达到最大值后,会回到0,继续递增。这种特性称为“序号循环”

但是TCP并不是真的将每一个字节的序号都记录下来, 而是只记录当前数据段的数据的第一个字节的序号, 当接收方收到了数据段后, 解析TCP首部, 获取到了序号字段, 假设是1000, 然后读取数据部分, 计算出数据部分有10个字节, 那么接收方就会计算确认序号, 也就是1000 + 10 = 1010, 表示接收方期望接收到的下一个字节的序号, 此时接收方就会发送一个确认报文(ACK, 这里稍后会说)给发送方, 发送方接收到了确认报文之后, 就会进行将下一次要发送的数据段的序号更新为1010, 以此类推

这里需要了解一下, TCP数据部分的大小是如何计算的:

TCP数据 = IP总长度 - IP首部 - TCP首部      (IP协议稍后会说)

2.4.3 16位校验和

校验和的作用主要是检测TCP报文在传输过程中是否发生了错误

这里的校验和其实和UDP的校验和几乎一致, 仅有的几个区别是

1. TCP的校验和中, 构造伪头部时, 协议号和UDP的协议号不一致(毕竟协议不一致嘛)

2. TCP协议若发现校验和不一致, 那么和UDP一样会舍弃该数据段,但是会要求发送发重新发送(UDP不要求发送方重新发送是因为UDP协议是无连接的)

2.4.4 16位紧急指针

紧急指针是用来标识紧急数据的, 紧急指针可以指示TCP数据流中紧急数据的结束位置, 接收方可以根据紧急指针快速定位并优先处理紧急数据,而不是按照普通数据的顺序处理

在TCP的数据部分中, 如果有紧急数据, 那么会优先存放紧急数据, 之后再存放普通数据, 16位紧急指针也就指向了紧急数据的末尾字节

例如: 有一段紧急数据: AAAAAA 一共6个字节, 那么此时的紧急指针的值就为6, 还有一段普通数据: BBBBBB, 那么TCP中的数据部分也就是 AAAAAABBBBBB, 紧急指针可以简单理解为用来分隔普通数据和紧急数据

2.4.5 16位窗口大小

16位窗口大小是TCP协议中用于实现流量控制和拥塞控制的关键字段, 它确保发送方不会发送超过接收方处理能力的数据量, 从而避免接收方缓冲区溢出.

这个地方我们暂且放下, 稍后在讲到拥塞控制时再细说

2.4.6 4位首部长度

4位首部长度是用来表示TCP首部的长度的(因为选项部分的大小是可变的)

这里需要注意一点, 4位首部长度并不是说只有4个比特位, 他的单位不是bit, 而是4byte, 也就是4字节, 所以4位首部长度最多能够表示(2*2*2*2 - 1) * 4 = 60个字节

所以选项部分最多也就可以有40个字节, TCP首部长度的范围就是20 ~ 60字节

2.4.7 选项

选项字段是TCP协议中一个非常强大和灵活的功能, 它允许在TCP首部中插入额外的信息,以扩展TCP的功能

以下是TCP协议中常见的选项:        

  1. 最大报文段长度(MSS,Maximum Segment Size)

    • 作用 :在建立 TCP 连接时,连接的双方通过这个选项来告诉对方自己能够接收的最大报文段长度。这有助于优化数据传输的效率。例如,如果一方的 MSS 是 1460 字节,那么另一方在发送数据时就会尽量以接近这个大小的报文段来发送,以减少分片和重组的次数,从而提高传输性能。

    • 格式 :它是一个 16 位的字段,紧跟在 TCP 首部的基本部分之后。当一个连接建立时,双方会在各自的 TCP 首部中包含 MSS 选项,告知对方自己的发送缓冲区能够接受的最大报文段长度。

    • 应用场景 :在不同网络环境和设备之间建立 TCP 连接时,由于设备的缓冲区大小和链路容量不同,通过 MSS 选项可以让双方协商出合适的报文段大小,避免数据包过大导致的网络拥塞或丢包等问题。

  2. 窗口扩大因子(Window Scale)

    • 作用 :随着网络带宽的增加,为了充分利用网络带宽,TCP 的接收窗口需要能够容纳更多的数据。窗口扩大因子选项允许接收方告诉发送方它可以将接收窗口的大小进行左移(乘以 2 的窗口扩大因子次方),从而有效地扩大接收窗口的大小。例如,如果窗口扩大因子是 2,那么实际的接收窗口大小就是通告的窗口大小乘以 2。

    • 格式 :这个选项是在 TCP 首部的选项部分,由一个 1 字节的选项种类(代表窗口扩大因子选项)、一个 1 字节的长度(通常是 3 字节,因为窗口扩大因子选项本身加上前面的选项种类和长度字段占 3 字节)和一个 1 字节的窗口扩大因子值组成。

    • 应用场景 :在高带宽高延迟的网络环境中,如长距离的高速光纤网络连接中,通过窗口扩大因子选项可以使得接收窗口足够大,让发送方能够持续发送足够多的数据,避免发送方频繁等待接收方的确认而导致带宽浪费。

  3. 时间戳(Timestamp)

    • 作用

      • 主要用于 RTT(往返时间)的测量。发送方在发送 TCP 报文段时会附带一个时间戳值,接收方收到后会将该时间戳值回显给发送方。发送方可以根据回显的时间戳计算出更精确的 RTT 估计值,这对于 TCP 的拥塞控制和流量控制非常重要。

      • 另一个作用是帮助解决序号 wrap - around(序号回绕)的问题。在高速网络中,序号可能会很快循环,时间戳可以区分出旧的重复报文段和新的报文段,避免接收方错误地接收旧的重复报文段。

    • 格式 :时间戳选项包括一个 32 位的时间戳值和一个 32 位的回显时间戳值。时间戳值是发送方当前的时钟值,回显时间戳值通常是之前收到的对方的时间戳值。

    • 应用场景 :在需要精确测量网络延迟和处理高速传输场景下可能出现的序号回绕问题时,时间戳选项就会发挥作用。例如,在数据中心内部的高速服务器间通信中,时间戳可以用于网络性能监控和数据包的正确性验证。

  4. 选择性确认(SACK,Selective Acknowledgment)

    • 作用 :当接收方收到乱序的 TCP 报文段时,传统的 TCP 确认方式只能确认到已经按顺序收到的最后一个字节。而选择性确认选项可以让接收方告诉发送方它已经收到的不连续的数据块,这样发送方就只需要重传那些丢失的数据块,而不是从丢失点开始重新发送所有后续的数据,大大提高了传输效率。例如,接收方已经收到了数据块 1 - 1000 字节和 2001 - 3000 字节,它可以发送一个 SACK 选项告诉发送方自己已经收到这些部分,发送方就可以只重传 1001 - 2000 字节的数据块。

    • 格式 :SACK 选项有一个选项种类标识,后面跟着一个长度字段(通常是 2 + 4 * n 字节,n 代表可以报告的不连续数据块的数量。每个数据块起始和结束序列号占 4 字节),然后是每个不连续数据块的起始和结束序列号。

    • 应用场景 :在网络环境复杂,容易出现数据包丢失和乱序的情况下,如无线网络或拥塞的广域网中,选择性确认选项可以有效地减少重传的数据量,提高 TCP 的吞吐量。

2.4.8 六位标记位

TCP中的6位标记位(也称为控制位或标志位)是TCP协议用于控制和管理数据传输的关键字段。这些标记位分别是:紧急指针(URG)、确认(ACK)、推送(PSH)、重置(RST)、同步(SYN)和结束(FIN), 他们用来标识本TCP数据段是哪一种类型的.

2.4.8.1 URG 紧急指针
  • 含义 :此位用于指示是否有紧急数据需要发送。当URG位被设置为1时,表示当前数据段包含紧急数据。

  • 作用 :紧急数据通常用于通知接收方需要立即处理某条信息,而不需要等待数据缓冲区被填满。例如,在网络管理中,可以通过发送紧急数据来通知接收方暂停或恢复数据传输。

  • 取值及说明 :URG位只有两种取值,即0和1。当URG位为1时,接收方会立即处理紧急数据,而不会等待缓冲区被填满。

2.4.8.2 ACK 确认
  • 含义 :确认字段用于指示当前数据段是否包含确认信息。

  • 作用 :用于向发送方确认已成功收到数据,确保数据传输的可靠性。例如,当接收方收到数据后,会立即发送一个带有ACK标记的确认消息给发送方,通知其数据已被成功接收。

  • 取值及说明 :ACK位只有两种取值,即0和1。当ACK位为1时,确认号字段有效;当ACK位为0时,确认号字段无效。

2.4.8.3 PSH 推送
  • 含义 :推送字段用于指示接收方收到数据后,应立即将数据推送给应用程序。

  • 作用 :避免数据在接收端的应用程序缓冲区中等待过长时间,保证数据的及时性。例如,在实时性要求较高的应用场景(如在线聊天、语音通话等),可以使用PSH标记来确保数据能够及时被应用程序处理。

  • 取值及说明 :PSH位只有两种取值,即0和1。当PSH位为1时,接收方会立即将数据推送给应用程序;当PSH位为0时,数据可能会在接收端的缓冲区中等待一段时间。

2.4.8.4 RST 重置
  • 含义 :重置字段用于指示当前连接需要被重置。

  • 作用 :用于异常情况下的连接恢复。例如,当一方检测到连接出现严重问题(如连接超时、协议错误等),可以通过发送带有RST标记的数据段来通知对方重置连接。

  • 取值及说明 :RST位只有两种取值,即0和1。当RST位为1时,表示当前连接需要被重置。

2.4.8.5 SYN 同步
  • 含义 :同步字段用于建立连接时的三次握手过程。

  • 作用 :用于在建立连接时同步双方的序列号。例如,在TCP连接建立的初始阶段,客户端和服务器通过发送带有SYN标记的数据段来进行初始序列号的协商。

  • 取值及说明 :SYN位只有两种取值,即0和1。当SYN位为1时,表示当前数据段用于建立连接。

2.4.8.6 FIN 结束
  • 含义 :结束字段用于表示数据传输结束。

  • 作用 :用于在连接终止时通知对方数据传输已完成。例如,当一方完成数据传输后,会发送一个带有FIN标记的数据段来通知对方关闭连接。

  • 取值及说明 :FIN位只有两种取值,即0和1。当FIN位为1时,表示当前数据段用于终止连接。

2.4.9 6位保留位

所谓的6位保留位, 其实就是目前还没有被使用的6个比特位, 他们都被设置为0, 保留位的主要作用是为了未来TCP协议的扩展保留空间, 当TCP需要新的标志位, 或者首部需要新的功能时, 则无需对TCP的协议结构进行较大规模的改动, 只需要将6为保留位进行使用即可

2.5 TCP十大核心机制:

2.5.1 确认应答机制

确认应答是确保数据可靠传输的重要手段, 它通过接收方对已成功接收的数据进行确认,使发送方能够得知哪些数据已被接收,从而重发未被确认的数据.

当发送方发送了很多数据给接收方的时候, 发送方要如何知道我所发送的数据是否成功到达了接收方呢? 此时就需要用到确认应答机制了

当接收方接收到了发送发传来的数据是, 接收方可以有两种选择

1. 如果接收方有数据要传输给发送方, 那么就在要传输的TCP段中, 将ACK标志位设置为1, 同时将32位确认序号设置为, 接收到的最后一个TCP段的32位序号 + 1, 这是捎带确认机制, 可以有效减少网络中的报文段数量, 防止网络资源的浪费

2. 如果接受方没有数据要传输给发送方, 那么就发送一个专门的确认应答段, 将ACK标志位设置为1, 同时将32位确认序号设置为, 接收到的最后一个TCP段的32位序号 + 1

当发送方接收到接收方发来的ack报文, 就能够获取到ack报文中的确认序号, 表示在该确认序号之前的所有数据, 都已经被成功接收了, 也就是,  确认序号之前的数据, 都被确认了

但是, 如果上述的过程没有顺利的出现, 反而发生了网络的丢包呢?

这里就要涉及到TCP协议的另外一个机制:      超时重传

2.5.2 超时重传机制

超时重传机制是用来应对网络出现丢包情况的策略

正常情况下, TCP是通过确认应答来判断数据是否被对端接收到了

发送方可以根据是否收到了ACK来区分是否出现了丢包, 在发送方发送出数据之后, 肯定是要经过一些时间才能正常收到ACK, 此时发送方就会进行判断, 如果等待的时间超过了某一个阈值, 还没有收到ACK, 此时, 就可以认为是出现了丢包.

发现了丢包, 就需要进行超时重传, 把刚才发送的数据在发送一遍

但是, 如果是数据报顺利到达了, 但是ACK丢包了呢?

那么就是TCP的接收方, 会针对收到的数据,按照序号进行去重

在接收数据的时候, 数据一开始都会被放入到缓冲区中, 放的过程中, 就会根据当前数据的序号, 在缓冲区中进行判断, 如果这个数据在缓冲区中存在或者曾经存在过, 那么这个数据就会被丢弃

由于接收方接收到了重复的数据, 此时接受方会认为自己的ACK丢失了, 从而重新发送ACK

如果网络一直波动, 数据一直丢包怎么办?

超时重传的超时时间并不是固定的, 而是会动态变化的, 通常, 超时时间会根据测量的往返时间(RTT)及其变化来设置, 如果网络上确实出现了严重故障了, 重传了若干次, 还是不成功, 达到一定次数与阈值, 就是尝试"重置连接", 发送一个"复位报文(RST)"尝试重置连接

重置就是通信双方, 清空之前的TCP传输过程的中间状态, 重新开始传输

如果网络出现严重故障, RST报文也是无法顺利完成的, 此时重置也就失败了, 就只能断开连接了

2.5.3 连接管理机制

连接管理机制, 是我们在学习TCP协议的时候, 最常听说的机制

2.5.3.1 三次握手

三次握手是通信双方连接建立的过程, 各自保存对端的信息

  1. 第一次握手:  第一次握手一定是客户端先发起的, 发送的数据报, 其中的SYN标记位为1, 表示此报文是一个同步报文, 不携带任何的业务数据, 载荷部分为空, 只有TCP报头, 这个报文段的主要目的是初始化连接, 核心任务是向对端表明自己想要建立连接的意图, 并告知初始序列号等控制信息, 而不是用于传输应用层的数据
  2. 第二次握手:  当服务器在收到客户端的同步报文之后, 会返回一个应答报文, 表示已经收到了客户端的同步报文, 同时若服务器能够和客户端同步, 则会返回一个同步报文给客户端, 随后服务器为连接分配必要的资源, 这里需要注意, 第二次握手不是返回两个报文给客户端, 而是一个报文中的ACK 和 SYN同时为1, 且确认序号为第一次握手时客户端传输的ACK的序号 + 1
  3. 第三次握手:  客户端收到服务器的同步报文之后, 会返回一个应答报文, 第三次握手的时候, 相当于双方各自让对方保存好自己的信息, 必须是双方都把对方的信息保存好才算是建立连接成功

三次握手的意义是什么呢, 要解决什么问题? 

  • 三次握手相当于"投石问路", 在正式传输业务数据之前, 先确认一下, 通信链路是是否正常
  • 通过三次握手, 来确认通信双方的发送能力和接收能力都是正常的
  • 通过三次握手, 来协商必要的参数, 例如各自的初始序号

为什么需要第三次握手, 而不是两次握手?

  • 三次握手的其中一个目的就是确认通信双方的发送能力和接收能力正常, 第一次握手可以确认客户端的发送能力正常, 第二次握手可以确定服务端的接收和发送能力正常, 但是如果缺少了第三次握手, 就无法确认客户端的接收能力正常, 所以需要三次握手
  • 假设TCP通信只有两次握手, 此时第二次握手时服务端的SYN + ACK由于网络波动导致丢包, 由于缺少第三次握手, 此时服务端就会认为连接已经建立, 此时服务端就会消耗本地的资源, 用来维持通信, 这样就会消耗服务器的资源
  • 假设TCP通信只有两次握手, 有这么一种极端情况, 第一次客户端发送的SYN报文由于网络波动, 一直没有传输到服务端, 但是并没有丢失, 由于客户端迟迟没有收到服务端的SYN + ACK, 此时的客户端认为第一次握手失败, 重新发送SYN, 服务端接收到SYN后, 返回SYN + ACK, 成功建立连接, 然后当数据传输完毕后, 连接断开, 此时第一次客户端发送的SYN传到了服务端, 服务端接收到后, 发送SYN + ACK, 由于只有两次握手, 服务端认为连接已经成功建立, 消耗本地资源来维持通信, 同样也会浪费服务器资源

三次握手的过程中, 可以携带数据吗, 什么是SYN攻击? 

  • 在三次握手的过程中, 是可以携带数据的, 但是一般情况下, 我们只允许第三次握手携带数据
  • TCP规定, SYN报文 (SYN = 1的报文段) 一般不携带数据, 但是需要消耗掉一个序号
  • TCP规定, ACK报文段可以携带数据, 但是如果不携带数据, 则不消耗序号
  • 那为什么要这么设计呢, 大家可以想一个问题:     假如第一次握手可以携带数据的话,  如果有人要恶意攻击服务器, 那他每次都在第一次握手的SYN报文中, 放入大量的数据, 因为攻击者根本就不理服务器的接收发送能力是否正常, 服务器忙于接收报文, 由于报文的数量多, 数据量大, 服务器要花费很多的时间和内存空间去接收这些报文, 且现有的Socket API中不支持在SYN中携带数据, 导致协议实现上也缺少支持
  • 第一, 二次握手不携带数据的另一个原因是, 我们只有经历了三次握手后, 通信双方才能确保, 双方的发送能力和接收能力正常, 同时, 前两次握手也是为了协商双方的序列号, 确认双方是否有足够的空间来维持通信
  • 第三次握手能够允许传输数据, 是因为, 对于客户端来说, 第三次握手表示客户端的连接已经建立了, 所以客户端发送数据也没什么问题

ISN 是固定的吗

  • ISN(Initial Sequence Number), 也就是在三次握手过程中, 通信双方的初始序号
  • 如果ISN是固定的, 攻击者很容易猜出后续的确认号, 容易仿造TCP报文进行攻击, 所以ISN一般是动态生成的.

什么是半连接队列, 什么是全连接队列? 

  • 半连接队列, 是用来存储已经完成两次握手, 但尚未完成三次握手的连接请求的, 服务端在接收到客户端的SYN报文之后, 会为该连接分配一个半连接项, 并将其放入半连接队列中, 这个队列允许服务器在三次握手的过程中暂时存储连接请求, 知道连接完全建立, 或者连接超时
  • 全连接队列, 是用来存储已经完成三次握手, 并处于ESTABLISHED状态的连接请求, 当三次握手完成后, 连接被转移到全连接队列, 这个队列的存在, 允许服务器在应用层接收和处理已经建立的连接
  • 这里需要注意, 半连接队列不是收到第一次握手后, 就允许连接加入队列, 而是第二次握手完成, 也就是服务器发送SYN + ACK后, 才允许加入队列, 这是因为, 当客户端的第一次握手传来时, 服务器不一定会有足够在资源用来分配连接, 例如半连接队列已满, 或者服务器内存资源不足等情况, 

2.5.3.2 四次挥手

四次挥手相对于三次握手来说, 要复杂一些

  1. 第一次挥手, 此时的客户端处于ESTABLISHED状态, 客户端向服务端发送FIN报文(TCP报文中FIN = 1的报文), 此时客户端进入FIN_WAIT_1状态
  2. 第二次挥手, 服务端接收到客户端的FIN报文, 向客户端发送ACK报文, 此时服务端的状态从ESTABLISHED  ->  CLOSE_WAIT
  3. 当客户端接收到服务端的ACK时, 客户端从FIN_WAIT_1状态进入FIN_WAIT_2状态, 等待服务端发送的FIN报文
  4. 第三次挥手, 服务端的数据发送完毕, 服务端发送FIN报文, 表示关闭连接, 此时服务端从CLOSE_WAIT状态 -> LAST_ACK状态, 表示等待客户端确认关闭
  5. 第四次挥手, 客户端收到服务端FIN报文, 从FIN_WAIT_2状态 -> TIME_WAIT状态, 等待2MSL后, 进入CLOSED状态, 关闭连接
  6. 服务端收到客户端发来的ACK, 从LAST_ACK状态 -> CLOSED状态, 连接关闭

为什么不是像三次握手那样, 将SYN于ACK两个报文合并为一个, 四次挥手需要将FIN 与 ACK分为两个报文分别发送?

  • 四次挥手不同于三次握手, 三次握手将SYN与ACK进行合并是为了提高连接的效率, 减少网络资源的消耗,, 同时SYN + ACK在同一个报文中发送, 并不会引起混淆或复杂的情况
  • 但是在四次挥手的过程中, 当一方完成数据发发送, 并发送FIN报文, 表示自己希望释放连接, 例如第一次挥手, 客户端发送FIN, 表示自己的数据发送完毕, 希望释放连接, 此时服务端收到FIN后, 不能将FIN 与 ACK合并为同一个报文发送, 因为客户端发送的FIN只是表示自己的数据发送完毕, 不代表服务端的数据已经成功传输完毕了, 所以此时服务端只能发送ACK表示自己收到了客户端发送的FIN, 同时, 当自己的数据发送完毕后, 才发送FIN报文, 表示自己发送完毕, 做好了断开连接的准备

为什么当服务端发送FIN, 完成第三次挥手之后, 服务端不能直接进入CLOSED状态, 而是进入LAST_ACK状态呢?

  • 虽然服务端FIN发送成功了, 但是不代表客户端成功接收到了FIN, 为了防止FIN丢包, 此时需要等待客户端接收到FIN, 同时返回ACK后, 才进入CLOSED状态, 防止客户端的资源浪费, 同时也保证了TCP的可靠传输

为什么客户端收到服务端的FIN并返回ACK后, 不能直接进入CLOSED状态, 而是要进入TIME_WAIT状态2MSL后再进入CLOSED状态呢, 为什么不是1MSL或者3MSL呢?

  • MSL(Maximum Segment Lifetime)是报文在网络上存活的最长时间, 也就是报文从一段到达另一端的理论最长时间
  • 我们假设一个极端情况, 当客户端接收到服务端的FIN时, 返回了服务端一个ACK, 此时ACK在网络中经历了1MSL的时间, 没有到达服务端, 此时服务端会认为自己的FIN丢包了, 从而重新发送FIN, 此时FIN正好又经过1MSL的时间才到达客户端, 这时所花的时间正好为2MSL
  • 如果只等待1MSL, 可能会导致由于网络延迟或者报文重传情况下的报文, 无法成功到达客户端, 导致报文丢失
  • 如果等待3MSL, 就会浪费服务器资源, 这完全没必要
  • Tips:  当客户端重新接收到FIN时, 会重新进入TIME_WAIT状态, 也就是说, 2MSL会重新开始计时

2.5.3.3 连接管理中的状态转换
  • LISTEN(监听状态) :服务器端调用 listen() 函数后进入监听状态,等待客户端的连接请求。

  • SYN_SEND(同步已发送状态) :客户端发送连接请求后进入该状态,等待服务器端的确认。

  • SYN_RECV(同步已收到状态) :服务器端收到客户端的连接请求后进入该状态,准备发送确认信息。

  • ESTABLISHED(已建立连接状态) :当连接建立成功后,双方都处于该状态,可以进行数据的收发。

  • FIN_WAIT1、FIN_WAIT2(连接释放等待状态) :在连接释放过程中,发送完 FIN 报文后依次进入这两个状态,等待对方的确认和释放请求。

  • CLOSE_WAIT(关闭等待状态) :对方发送 FIN 报文后,该端进入该状态,等待本地应用层关闭连接。

  • LAST_ACK(最后确认状态) :本地应用层关闭连接后,发送 FIN 报文,进入该状态,等待对方的最终确认。

  • TIME_WAIT(等待时间状态) :在发送完最后的 ACK 报文后,进入该状态,等待足够的时间以确保对方收到确认,然后完全关闭连接。

2.5.4 滑动窗口机制

当没有滑动窗口时, TCP发送一个数据, 就需要一个ACK, 才会发送下一个数据, 此时的效率有点低, 就好比于我们在微信上聊天, 我发送一条信息, 需要你回复我了, 我才会发送下一条一样

为了解决这个问题, TCP引入了滑动窗口机制, 他是操作系统开辟的一个缓存空间, 窗口大小值表示无需等待确认应答, 而可以继续发送数据的最大值

TCP的头部有16位窗口大小, 为了就是告诉对端, 我的接收缓冲区还能够容纳多少字节的数据, 这样, 对端就可以控制发送数据的速度, 从而达到流量控制的目的.

滑动窗口分为两种, 一种是发送窗口, 一种是接收窗口

  1. 发送窗口
    1. 发送窗口时发送缓冲区的一个逻辑子集, 也就是说, 发送窗口是发送缓冲区的一部分
    2. 当发送方没有被ACK确认的数据大小小于滑动窗口的大小时, 发送方可以不等待ACK确认, 而是继续发送数据
    3. 当发送窗口中发送的数据都没有被ACK确认时, 发送窗口将会等待ACK确认后, 再向后移动
    4. 当发送缓冲区的部分数据被ACK确认后, 滑动窗口就会向后移动, 如下图蓝色框向后移动
  2. 接收窗口
    1. 接收窗口是接收缓冲区的逻辑子集, 也就是说, 接收窗口是接收缓冲区的一部分
    2. 接收窗口表示接收方可以接收, 但是尚未确认的数据量
    3. 当接收方将接收窗口中的数据确认后, 即发送ACK后, 接收窗口向后移动

在滑动窗口中, 如果报文在传输的过程中丢包了, TCP会如何进行处理呢?

  • 当发送方的报文丢失时, 接受方缓冲区中的序号就是不连续的, 当窗口滑动到数据丢失的部分, 则会持续发送确认序号为丢失数据序号的ACK, 表示自己期望接收到丢失的数据包, 此时发送方会意识到, 自己的数据可能丢失了, 于是触发快速重传机制, 重新发送数据, 接收方收到数据后, 会将序号进行拼接, 确保数据的完整性
  • 当接收方的ACK丢失时, 此时发送方没有收到ACK, 会认为自己的数据丢失了, 从而重新发送数据, 当接收方接收到数据后, 发现是重复数据, 会丢弃数据, 并重新发送对应确认序号的ACK, 并不会影响接收方滑动窗口的正常运行

在TCP中, 窗口大小只有16位, 是不是意味着发送窗口最大只能为4kb呢?

  • 并不是的, 虽然在TCP报文格式中, 窗口大小确实只有16位, 但是在TCP的选项中, 包含了窗口扩展因子这一选项, 窗口扩展因子的大小远大于窗口大小, 所以发送窗口的大小可以不止4kb

2.5.5 流量控制机制

在接收方, 有一个区域叫, 接收缓冲区, 接收方能够根据接收缓冲区的大小, 来制约发送方的传输速度, 使双方的发送和接收速度, 尽量达到一个平衡

在TCP的首部, 有一个字段为16位窗口大小, 当发送方接收到ACK时, 会读取ACK中的窗口大小, 同时根据ACK中的窗口大小来调整自己的滑动窗口大小, 在三次握手的时候, 通信双方都会发送各自的窗口大小, 进行协商

我们假设一个极端情况, 当接受方的接收缓冲区满了, 此时接收方会发送一个窗口大小为0的ACK给发送方, 此时发送方就会调整自己的窗口大小为0, 那么发送方将不会发送数据, 当接收方的缓冲区有空闲的时候, 由于发送方没有发送数据给接收方, 那么也就没有办法调整发送方的窗口大小, 此时就会造成连接失效, 为了应对这种情况, TCP协议引入了零窗口探测机制:

零窗口探测机制:

当发送方接收到了窗口大小为0的数据包时, 此时发送方会调整自己的窗口大小, 同时触发一个定时器, 定时器超时后, 发送方会发送一小段数据作为探测报文, 即使当前没有数据要发送, 探测报文主要是为了触发接收方发送新的ACK报文, 告知发送方当前接收窗口的大小, 如果接受方的缓冲区已有空闲的空间, 那么新的ACK报文会携带更新后的窗口大小, 发送方收到后, 即可恢复数据的发送

2.5.6 拥塞控制机制

拥塞控制机制和流量控制机制类似, 都是和滑动窗口搭配的机制

流量控制是站在接收方的角度, 动态控制发送方的发送速度, 而拥塞控制是站在发送方的角度, 来调整发送方的发送速度

拥塞控制中, 有两个重要的值, 一个是ssthresh, 一个是窗口大小, 这两个值互相影响, 同时共同实现了拥塞控制机制

  • 在连接建立之初, ssthresh会有一个初始值, 这个初始值通常会被设置为一个较大的值, 如65535字节, 这在很多操作系统中是默认设置的, 但是也可以通过其他方式设置, 如在连接建立之前, 通过路由通告等
  • 在连接建立之初, 拥塞窗口的大小一般是一个很小的值, 初始中通常为1, 也就是2 ^ 0(二的零次方)
  • 当数据开始传输时, TCP将会比较拥塞窗口和ssthresh的大小, 此时ssthresh的值更大, 拥塞窗口的大小按照指数级增长, 进入慢启动阶段, 即每当发送方收到一个ACK, 拥塞窗口大小就从2^0 -> 2^1 -> 2^2.
  • 直到拥塞窗口的大小大于等于ssthresh的值时, 此时TCP进行判断, ssthresh的值比窗口大小要小, 拥塞窗口大小线性增长, 进入拥塞避免阶段, 此时拥塞窗口大小变为没收到一次ACK, 即每一次往返时间, 窗口大小+1
  • 当拥塞窗口增大到一定程度时, 达到网络承载上线, 会出现发送方的数据丢包的情况, 此时接收方会发送多个相同的ACK, 当发送方收到3个重复的ACK时, 触发快速重传机制, 即不需要等待重传定时器超时, 就可以快速重新发送数据包, 提高了传输效率
  • 此时, 由于收到了多个重复的ACK, TCP认为网络承载以达到上限, 将ssthresh的值设置为窗口大小的一半, 将窗口大小设置为ssthresh相同的值
  • 在后续的数据传输过程中, 窗口大小随着收到的ACK继续线性增加, 逐步恢复传输
  • 若在后续依然丢包, 触发快速重传, 则继续将 将ssthresh的值设置为窗口大小的一半, 同时将窗口大小设置为ssthresh相同的值, 以此往复

在TCP中, 拥塞控制机制和流量控制机制都能够控制发送窗口的大小, 那么这两种机制到底是如何控制发送窗口的大小的呢?

  • 在TCP中, 流量控制机制维护了一个发送窗口, 发送窗口是根据接收方发送的ACK中的窗口大小字段来进行调整的, 也就是根据接收方的接收窗口的大小来进行调整的
  • 而拥塞窗口机制是根据在发送方发送数据的过程中, 通过一系列算法来实现的对拥塞窗口的大小控制
  • 当接收方接受到ACK时, 拥塞窗口的大小会动态调整, 此时接收窗口会比较ACK中的发送方的窗口大小和拥塞窗口的大小, 发送窗口的值取两个窗口中的最小值

2.5.7 延时应答机制

延时应答机制较为简单

当接收方收到发送方的数据报文段后,并不立即发送 ACK 确认报文,而是等待一段时间,看是否还有后续的数据报文段到达。在这段时间内,如果有多个数据报文段连续到达,接收方会将它们的确认信息合并到一个 ACK 报文中发送回去,从而减少 ACK 报文的数量,提高网络的利用率。

  • 减少网络流量 :通过合并多个数据报文段的确认信息,减少了单独发送 ACK 报文的次数,降低了网络中的控制信息流量,提高了网络带宽的利用率。

  • 提高传输效率 :避免了频繁的 ACK 报文发送,使得发送方和接收方可以更专注于数据的传输,一定程度上提高了整个 TCP 传输的效率。

  • 增大发送窗口大小: 如果每当数据传输到接收缓冲区, 就立马发送ACK, 此时数据消耗的速度和数据接收的速度可能会达到一个平衡, 若将多个ACK合并发送, 此时接收窗口中的空余空间就会立马增大, 可以打破数据消耗和接收的平衡, 从而使得发送方的发送窗口变大, 提高数据传输的效率

2.5.8 捎带应答机制

捎带应答机制是建立在延时应答机制上的提升效率的机制

日常开发中, 客户端服务端之间的的通信经常是"一问一答"的形式, 正常来说, 当客户端发送请求之后, 服务器会立马回复一个ACK, 之后再发送一个响应(业务数据), 但是由于延时应答的存在, ACK不一定会立即返回, 而是会等待一会, 等消耗了更多的数据后, 统一返回一个ACK, 正好在等待的过程中, 就需要返回响应数据了, 此时, 就在响应数据中, 将ACK设置为1, 把两次传输, 合并成了一次, 提高了传输的效率

  • 减少报文数量 :避免了单独发送 ACK 报文所带来的额外开销,降低了网络中的报文数量,从而节省了网络带宽和资源。

  • 提高传输效率 :通过捎带应答,接收方可以在发送数据的同时完成确认工作,使得数据传输过程更加紧凑和高效。

  • 捎带时机 :接收方只有在需要向发送方发送数据时,才会考虑捎带 ACK。如果接收方没有数据要发送,仍需要单独发送 ACK 报文。

  • 捎带内容 :捎带的 ACK 信息包含了对接收到的数据报文段的确认序号,表示接收方已经成功接收了哪些数据

2.5.9 面向字节流

在数据传输的过程中, TCP将应用程序发送的数据视为一个无结构的, 首尾相连的字节序列, 而不是以报文为单位进行传输.

此时, 就会出现一个很严重的问题 : 粘包问题

  • 当发送方发送了多条数据量很小的小数据的时候, TCP为了节省网络资源, 减少报文数量, 就会将多条小数据合并为一整条数据, 一起发送给接收方, 接收方接收到这些数据后, 由于数据都是按照字节拼接在一起的, 接收方很可能无法区分出, 这是一整条数据, 还是多条数据的组合, 从而造成粘包问题
  • 当发送方发送了一条数据量很大的数据时, TCP由于窗口大小等因素, 无法一次性将这些数据发送出去, TCP会将,这些数据分割成多条TCP数据发送出去, 当接收方接收到这些数据的时候, 由于接受方无法区分出这些数据不同的数据还是一整条数据, 就会产生粘包问题

粘包问题的解决方式

  1. 使用分隔符作为数据包的间隔, 例如, 当发送方发送了hello和world两条数据时, TCP将两条数据合并成为了一条hellowrold, 此时是无法分辨的, 当我们加入了分隔符, 假设为#, 那么数据会编程hello#world#, 此时接收方就能够分辨出这是两条不同的数据
  2. 固定每一个消息的长度, 例如将每条消息固定为64字节, 发送方在发送数据时, 不足的消息用空格或其他填充字符补齐, 接收方在读取数据时, 按照固定长度来读取消息.
  3. 在消息的前面添加一个消息头, 消息头中包含消息的长度等信息. 接收方在收到数据后, 先解析消息头获取消息长度, 然后根据消息长度来读取消息内容.

通过以上三种方式, 能够很好的解决粘包问题

2.5.10 异常处理机制

当数据在网络中传输的过程中, 可能因为网络波动或其他原因, 导致数据丢失或者其他情况, TCP针对上述情况做了不同的应对措施

  1. 客户端或服务端进程奔溃: 进程奔溃或者应用程序正常结束, 本质都是操作系统释放掉对应的PCB, 也就可以释放掉文件描述符表, 相当于调用了操作系统的方法, 此时仍然能够正常和对端进行四次挥手操作
  2. 某一个主机正常关机: 这和第一点类似, 都是操作系统结束了进程, 同样可以正常的进行四次挥手
  3. 某主机掉电, 异常关机: 
    1. 如果是服务端主机异常关机, 此时由于服务端关机, 导致客户端一直收不到对应的ACK, 会触发超时重传机制, 重传多次后, 触发RST, 如果RST同样没有响应, 则认为连接中断了, 客户端会单方面的删除通信相关的信息
    2. 如果是客户端主机异常关机, 此时的服务端会一直接收不到客户端发送的数据, 当服务端在一定时间内没有收到对应客户端的数据时, 会周期性的给客户端发送一个心跳包, 如果客户端没有传输对应的ACK, 则认为客户端异常, 服务端单方面删除与对应客户端的通信信息
  4. 网络故障: 这个本质上和第三种没有什么区别

以上就是TCP相关的全部内容, 如果大家还想更加深入了解, 可以自行查阅资料

rfc文档: RFG Editor

2.6 网络层

主要功能

网络层的主要任务分为两点:  寻址和路由选择

  1. 寻址 :网络层为每个网络中的设备分配唯一的 IP 地址,这个地址就像是设备在网络世界中的身份证和家庭住址,让数据包能够准确地找到目标设备。例如,在互联网中,当你访问一个网站时,你的设备会先通过网络层的寻址机制找到该网站服务器的 IP 地址。

  2. 路由选择 :它负责确定数据包从源节点到目标节点的最佳传输路径。网络层会不断地分析网络的拓扑结构、链路状态、流量负载等信息,像一个聪明的交通指挥官一样,选择最优的路线,使数据包能够快速、可靠地到达目的地。

主要协议

  • IP(Internet Protocol)协议 :这是网络层最核心的协议,它的主要作用是实现网络层的寻址和数据包的传输。IP 协议定义了数据包的格式、IP 地址的分配和使用规则等。数据包在传输过程中,可能会经历分片和重组,这是因为在不同的物理网络中,对数据包的大小限制不同,IP 协议会根据这些限制对数据包进行处理,以确保数据包能够顺利地通过各种网络。

  • ICMP(Internet Control Message Protocol)协议 :主要用于在 IP 主机、路由器之间传递控制消息,这些消息包括网络是否可达、主机是否可达、路由是否可用等网络状态信息。例如,当你的设备无法访问某个网站时,ICMP 协议可能会发送一个 “目标不可达” 的消息,告诉你的设备这个网站可能暂时无法访问。

  • IGMP(Internet Group Management Protocol)协议 :用于在 IP 主机和路由器之间传递组播组成员资格信息。在网络中,有些数据可能需要同时发送给多个特定的设备,如视频直播、在线会议等,这就是组播。IGMP 协议可以让设备告诉路由器它想接收哪些组播组的数据,路由器根据这些信息来决定将组播数据发送给哪些设备。

数据包的分片与封装

  • 封装 :当上层协议(如传输层的 TCP 或 UDP)将数据交给网络层时,网络层会在数据前面添加网络层的头部信息,这个头部信息包含了源 IP 地址、目标 IP 地址、协议版本等重要信息,同时还会根据需要计算校验和,用于检测数据在传输过程中是否出错。

  • 分片 :由于不同网络对数据包的大小有不同的限制,当数据包的大小超过了某个网络所能处理的最大传输单元(MTU)时,网络层会对数据包进行分片。分片后的数据包在到达目的地后,再由网络层进行重组,恢复成原来的完整数据包。

网络层的设备

  • 路由器 :它是网络层的核心设备,主要用于连接不同的网络,并在这些网络之间转发数据包。路由器会根据数据包中的目标 IP 地址和自身的路由表来决定如何将数据包转发到下一个网络节点,直到数据包最终到达目标设备。

在网络层中, 我们主要讲解IP协议

2.7 IP协议

我们通过认识IP协议中的报文格式, 以及每个字段的含义, 来学习IP协议

2.7.1 四位版本

四位版本号可以让网络设备(如交换机, 路由器等) 知道如何正确的解释和处理该IP数据报, 不同的IP协议版本对应的IP数据报格式可能不同, 目前常见的IP版本有IPv4和IPv6

  • IPv4(Internet Protocol version 4)

    它是互联网协议(IP)的第四版,也是第一个被广泛使用的版本。IPv4 地址长度为 32 位,通常以 “点分十进制” 的形式表示,如 192.168.1.1。IPv4 地址数量有限,随着互联网的飞速发展,IPv4 地址逐渐面临枯竭的问题。
  • IPv6(Internet Protocol version 6)

    为了应对 IPv4 地址不足的问题而设计。IPv6 地址长度为 128 位,采用 “冒号十六进制” 表示法,如 2001:0db8:85a3:0000:0000:8a2e:0370:7334。IPv6 提供了几乎无限的地址空间,还具有更大的首部、改进的安全性(如内置 IPsec)、更高效的路由选择等优点。

但是由于IPv6的更换成本等原因, 目前最常使用的还是IPv4

2.7.2 4位首部长度

在IP协议中, 四位首部长度字段用来表示IP数据报首部的长度, 和TCP协议的首部长度一样, 他们的单位都不是bit, 而是4byte, 也就是(2 * 2 * 2 * 2 - 1) * 4 = 60字节

他告诉接收方, 首部在哪里结束, 数据在哪里开始, 这对于正确解析数据报至关重要, 因为IP(IPv4)数据报的首部长度是不固定的, 而IPv6的首部固定为40字节

2.7.3 八位服务类型

首先八位服务类型中的八位, 被分为了三个部分

  1. 3 位优先级子字段 :用于指定数据报的优先级,取值范围从 0(最低优先级)到 7(最高优先级)。例如,网络管理流量可能被赋予较高的优先级,而普通用户的数据流量可能被赋予较低的优先级。

  2. 4 位服务类型子字段 :用于描述数据报的服务需求,包括吞吐量、延迟、可靠性等方面。例如,对于实时性要求很高的视频会议应用,可能需要低延迟的服务;而对于文件传输应用,可能更关注高吞吐量。

  3. 1 位保留位 :用于将来扩展,目前该位必须设置为 0。

2.7.4 16位总长度

这个字段描述的是整个IP数据报的长度, 也就是报头 + 载荷的长度

IP的总长度和UDP的总长度一样, 都是16位, 那么是不是IP最多也只能携带64KB的数据呢?

  • 不能这么简单的理解, 我们上面说了, 网络层的一个作用就是对数据包进行封装和分片, 这里的分片其实就是将很大的数据包, 分成多个小的数据包, 然后分别存放到连续的IP中

2.7.5 16位标识

16位标识字段用于帮助接收方正确地重新组装分片后的数据报

在大多数的操作系统中, 每当发送一个新的IP数据报时, 标识值会递增或采用某种算法生成, 为了确保标识字段的唯一性, 操作系统可能会在发送数据之前检查当前使用的标识值, 并避免重复使用

但是, 当IP 数据报被分片时,每个分片的标识字段都与原始数据报的标识字段值相同。这使得接收方能够识别出哪些分片属于同一个原始数据报。

2.7.6 3位标志

3 位标志字段用于控制 IP 数据报的分片和重组过程。这 3 位标志字段由以下部分组成:

  1. 保留位(第0位):  目前尚未被使用, 保留为将来可能的功能扩展
  2. 未分片标志(DF, 第一位):  当DF位被设置为1时, 指示网络设备不要对该IP数据报进行分片, 如果数据报的大小超过了网络链路的MTU(最大传输单元), 而DF被设置为了1, 则数据报将会被丢弃
  3. 更多分片标志(MF, 第二位):  当MF 位被设置为 1 时, 表示该数据报是一个分片数据报, 并且后面还有更多的分片, 只有当 MF 位被设置为 0 时, 才表示这是最后一个分片

例如

  • 当MTU大小为1000字节, 整个IP大小为600字节时, DF可以设置为0或1, 而MF通常设置为0, 因为这是最后一个分片(也就是没有被分片)
  • 当MTU大小为1000字节, 整个IP大小为1500字节时, DF如果设置成1, 标识不允许对数据进行分片, 那么这个数据就会被丢弃
  • 当MTU大小为1000字节, 整个IP大小为1500字节时, DF如果设置成0, 标识允许对数据进行分片, 此时MF通常设置为1, 表示这是一个分片数据报, 第二个数据报的MF设置为0, 表示这是最后一个分片

2.7.7 13位片偏移

在IP协议中, 13位片偏移字段用于指示该分片的数据部分在原始IP数据报数据部分中的起点位置, 片偏移以8字节位单位, 当接收方收到多个分片的时候, 通过片偏移, 就能够知道这些分片的初始顺序, 防止因为网络波动造成的数据顺序错误

如果一个 IP 数据报需要携带 10000 字节的数据,且需要分片,那么片偏移的表示需要考虑以下情况:

  • 不分片的情况 :如果 IP 数据报不分片,那么片偏移字段的值为 0。在这种情况下,接收方知道该数据报没有被分片,所有的数据都在这个完整的数据报中。

  • 分片的情况 :假设我们把 10000 字节的数据分成多个分片来传输。例如,我们决定每个分片的数据部分为 1480 字节(这是一个常见的分片大小,用于适应以太网的 MTU),那么我们可以计算每个分片的片偏移值。需要注意的是,在计算片偏移时,是以 8 字节为单位来表示的。

    • 第一个分片:数据从原始数据报的起始位置开始,片偏移为 0,它包含前 1480 字节的数据。同时,MF(More Fragments)标志位设置为 1,表示后面还有分片。

    • 第二个分片:数据从第 1480 字节开始,片偏移值为 1480/8=185。这个分片包含接下来的 1480 字节的数据,即从第 1480 字节到第 2959 字节。同样,MF 标志位设置为 1。

    • 第三个分片:数据从第 2960 字节开始,片偏移值为 2960/8=370。它包含接下来的 1480 字节的数据,即从第 2960 字节到第 4439 字节。MF 标志位仍然是 1。

    • 第四个分片:数据从第 4440 字节开始,片偏移值为 4440/8=555。它包含接下来的 1480 字节的数据,即从第 4440 字节到第 5919 字节。MF 标志位还是 1。

    • 以此类推, 直到最后一个分片, MF标志位为1

2.7.8 8位生存时间

8位生存时间字段, 主要是为了防止IP数据报在网络中无限期地存在, 避免由于路由器配置错误或其它原因导致数据报在网络中循环转发而无法到达目的地, 通过设置生存时间, 可以确保数据报在一定范围内被转发, 超出该范围后会被丢弃

在实际应用中,TTL 的值一般不会设置得太大。常见的默认值可能在 32、64、128 等。例如,在局域网环境中,数据报通常不需要经过太多跳就能到达目的地,TTL 设置为 32 或 64 就足够了。而在广域网中,为了确保数据报能跨越多个网络和路由器到达较远的目的地,可能会将 TTL 设置得更大一些,如 128。

2.7.9 8位协议

8位协议描述的是, 当我IP协议收到数据后, 接下来需要用什么协议处理它, 例如当字段值为6, 则使用TCP处理, 如果字段值为17, 就用UDP协议处理, 实现数据准确解封装与处理

2.7.10 16位首部校验和

16位首部校验和是IP协议中用于确保数据传输完整性和可靠性的关键机制, 他通过特定的计算方法验证IP数据报首部的正确性, 帮助检测传输过程中的错误

16位首部校验和只会检验首部是否正确, 而不检测载荷, 当IP检测到收到的数据报首部异常, 则会丢弃异常数据报

2.7.11 32位源IP地址 / 32位目的IP地址

  • 源IP地址:源IP地址标识了发送数据的设备的网络接口。它告诉接收方数据是从哪里发送过来的。例如,当一台计算机向另一台计算机发送数据时,源IP地址就是发送计算机的IP地址。

  • 目的IP地址:目的IP地址标识了接收数据的设备的网络接口。它指导网络设备将数据报发送到正确的目的地。例如,当数据报在互联网上传输时,路由器会根据目的IP地址来决定数据报应该通过哪条路径转发。

2.7.12 选项

IP 数据报的选项字段用于提供一些可选的功能,这些功能不是每个数据报都需要,但可以在特定情况下使用。选项字段位于 IP 首部的固定部分之后,紧接在源 IP 地址和目的 IP 地址字段之后, 类似于TCP的选项

2.8 链路层

链路层是TCP/IP四层模型中的最底层, 是由OSI七层模型中的物理层和链路层合并而成的, 链路层主要关注如何在物理介质上实现数据的可靠传输, 他处理的数据是帧, 即在物理介质上传输的数据单元

  • 封装成帧:链路层将来自网络层的数据报封装成帧,添加帧头和帧尾,以确保数据在物理介质上的正确传输。

  • 透明传输:确保数据在传输过程中不会被物理介质的特性(如信号干扰、噪声等)所影响,保证数据的完整性。

  • 错误检测与纠正:通过校验和等机制检测传输错误,并采取措施纠正错误。

  • 介质访问控制(MAC):管理对共享物理介质的访问,避免数据冲突(如以太网中的CSMA/CD机制)。

以上就是本文章的全部内容, 希望对大家有所帮助. ovo~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值