当前位置: 首页 > news >正文

传输层协议——TCP协议

目录

1.TCP 协议

1.1. 什么是 TCP 协议

1.2. 为什么 TCP 叫传输控制协议

1.3.TCP 是面向字节流的

2.TCP 协议段格式

2.1. 流量控制 —— 窗口大小(16 位)

2.2. 确认应答机制

2.2.1. 什么是确认应答机制

2.2.2. 推导确认应答机制

2.2.3. 确认应答机制的实现

2.2.4. 确认号和序列号的详细解析

2.3. 六位标志位

1. ACK

2. SYN

3. FIN

4. RST

5. PSH

6. URG​编辑

3. TCP 的可靠策略

3.1. 超时重传机制

3.2. 连接管理机制

3.2.1. 如何理解 TCP 连接

4.TCP 的 11 种核心状态

5、TCP 三次握手:建立可靠连接

5.1 三次握手的完整流程

5.2 Socket 编程与三次握手的关联​编辑

5.3 TCP 三次握手的优点

6. 理解四次挥手

6.1 四次挥手的深入理解

6.2 Socket编程与四次挥手的关系

6.2.1 测试CLOSE-WAIT

6.2.2 测试TIME-WAIT

6.2.3 解决TIME-WAIT问题

6.2.4 为什么客户端不受TIME-WAIT影响?

6.2.5 TIME-WAIT 状态的作用

7. TCP的效率策略

7.1 流量控制

7.2 滑动窗口

7.3 延迟应答

7.4 捎带应答

7.5 拥塞控制

7.5.1 拥塞控制的前提理解

7.5.2 拥塞窗口

7.5.3 拥塞控制算法

8.TCP的几个补充知识点

8.1 面向字节流

8.2 粘包问题

8.3 TCP连接异常问题

8.4 Linux文件和Socket的关系

1.TCP 协议

1.1. 什么是 TCP 协议

1.TCP 是面向连接的运输层协议。应用程序在使用 TCP 协议开展通信前,必须先建立起 TCP 连接。当数据传送完毕后,还需释放已建立的 TCP 连接。

2.每一条 TCP 连接仅能拥有两个端点,且每一条 TCP 连接只能是点对点的(一对一)通信模式。

3.TCP 提供可靠交付服务,通过 TCP 连接传送的数据,能够实现无差错、不丢失、不重复,并且按照发送顺序到达接收端。

4.TCP 支持全双工通信,允许通信双方的应用进程在任意时刻都能发送数据。TCP 连接的两端均设有发送缓存和接收缓存,用于临时存放双向通信过程中的数据。

5.TCP 具有面向字节流的特性,这里的 “流” 指的是流入到进程或者从进程流出的字节序列。

1.2. 为什么 TCP 叫传输控制协议

我们之前创建的 TCP 套接字,实际上 sockfd 会指向操作系统分配的 socket file control block(socket 文件控制块),而这个 socket 文件控制块内部会维护网络发送缓冲区和网络接收缓冲区。我们调用的所有网络发送函数,像 write、send、sendto 等,实际作用是将数据从应用层缓冲区拷贝到 TCP 协议层(也就是操作系统内部的发送缓冲区);而网络接收函数,如 read、recv、recvfrom 等,实际是将数据从 TCP 协议层的接收缓冲区拷贝到用户层的缓冲区中。

真正负责双方主机 TCP 协议层之间数据发送的过程,完全由 TCP 自主决定。比如什么时候发送数据、一次发送多少数据、发送过程中出现错误该如何处理等,这些都由 TCP 协议自身控制,属于操作系统内部的操作,与用户层没有关联。这正是 TCP 被称为传输控制协议的原因 —— 数据传输的整个过程由它自行控制和决定。

此外,客户端(c)向服务端(s)发送数据,与服务端(s)向客户端(c)发送数据,使用的是不同的发送缓冲区和接收缓冲区对。所以客户端给服务端发送数据时,并不会影响服务端给客户端发送数据,这也能说明 TCP 是全双工通信模式,一方在发送数据时,不影响另一方同时发送数据。由此可见,网络发送的本质就是数据在不同缓冲区之间的拷贝。

应用层缓冲区是什么?

提到应用层缓冲区,可能大家会觉得抽象。其实所谓的应用层缓冲区,就是我们自己在代码中定义的 buffer。从下面列出的 6 个网络发送和接收接口可以看到,它们都有对应的 buf 形参,我们在使用这些接口时,必须传入参数,而所传的参数就是我们在应用层定义的缓冲区。

这里额外说明一点,上述六个接口在进行网络数据发送和读取时,都会进行网络字节序和主机字节序之间的转换。其中 recvfrom 和 sendto 这两个接口,需要程序员手动显式进行转换;而其余四个接口,则由操作系统自动完成转换,这是确定无疑的事实!

1.3.TCP 是面向字节流的

为什么 TCP 是面向字节流的协议?

当用户消息通过 TCP 协议传输时,这些消息可能会被操作系统拆分成多个 TCP 报文。也就是说,一个完整的用户消息会被分割成多个 TCP 报文进行传输。

此时,如果接收方的程序不知道发送方发送的消息长度,也就是不清楚消息的边界,就无法读取到有效的用户消息。因为用户消息被拆分成多个 TCP 报文后,无法像 UDP 那样,一个 UDP 报文就对应一个完整的用户消息

我们通过一个实际例子来解释。

发送方准备发送「Hi.」和「I am Xiaolin」这两个消息。

在发送端,当我们调用 send 函数完成数据 “发送” 操作后,数据并没有真正通过网络发送出去,只是从应用程序拷贝到了操作系统内核协议栈中。

至于数据什么时候真正被发送,取决于发送窗口、拥塞窗口以及当前发送缓冲区的大小等多种条件。也就是说,我们不能认为每次调用 send 函数发送的数据,都会作为一个完整的消息被发送出去。

如果考虑实际网络传输中的各种影响因素,假设发送端陆续调用 send 函数,先后发送「Hi.」和「I am Xiaolin」报文,那么实际的发送情况可能有以下几种:

第一种情况,这两个消息被封装到同一个 TCP 报文中,形式如下:

[「Hi.」+「I am Xiaolin」](单个 TCP 报文)

第二种情况,「I am Xiaolin」的一部分内容与「Hi.」一起在一个 TCP 报文中发送出去,形式如下:

[「Hi.」+「I am X」](第一个 TCP 报文)、[「iaolin」](第二个 TCP 报文)

第三种情况,「Hi.」的一部分内容随一个 TCP 报文发送出去,另一部分内容和「I am Xiaolin」一起随另一个 TCP 报文发送出去,形式如下:

[「H」](第一个 TCP 报文)、[「i.」+「I am Xiaolin」](第二个 TCP 报文)

类似的情况还有很多,这里主要是为了说明,我们无法确定「Hi.」和「I am Xiaolin」这两个用户消息会以怎样的 TCP 分组方式进行传输。

因此,我们不能认为一个用户消息就对应一个 TCP 报文,正因为如此,TCP 才被称为面向字节流的协议。

2.TCP 协议段格式

图中每一行包含 4 个字节(32 位),TCP 报文的解包步骤如下:

提取报头

除了选项之外的报头被称为标准报头,固定为 20 字节。

提取选项:

根据 4 位首部长度(数据偏移)确定报头的整体大小,用报头整体大小减去 20 字节的标准报头(固定报头),得到的就是选项部分的内容。如果没有选项,那么直接就能得到有效载荷。

提取有效载荷:

有效载荷 = 报文总长度 - 报头长度(若有选项,需再减去选项长度)

字段长度说明
16 位源端口16 位标识发送方主机上应用程序的端口号
16 位目的端口16 位标识目的主机上应用程序的端口号
32 位 TCP 序列号32 位表示本报文段所发送数据的第一个字节的编号
32 位 TCP 确认序号32 位表示接收方期望收到发送方下一个报文段的第一个字节数据的编号
4 位 TCP 首部长度4 位也叫数据偏移,指数据段中 “数据” 部分起始处距离 TCP 报文段起始处的字节偏移量,用于确定 TCP 报文报头部分的长度,告知接收端应用程序数据(有效载荷)从何处开始
6 位保留字段6 位为 TCP 协议未来的功能扩展预留空间,目前这 6 位必须全部为 0
6 位标志位6 位共包含 6 个标志位,每个标志位占 1 个 bit,分别对应不同的控制功能
16 位窗口大小16 位表示发送该 TCP 报文的接收方,其接收窗口还能接受的字节数据量,用于 TCP 的流量控制
16 位校验和字段16 位用于验证传输的数据是否损坏。发送端根据数据内容计算生成校验和数值,接收端根据接收到的数据重新计算校验和。若两个数值相同,说明数据有效;反之则无效,接收端会丢弃该数据包。校验和的计算基于伪报头 + TCP 头 + TCP 数据三部分
16 位紧急指针字段16 位仅当标志位字段的 URG 标志位为 1 时才有意义,用于指出有效载荷中紧急数据的字节数。当所有紧急数据处理完毕后,TCP 会通知应用程序恢复正常操作。即使接收方窗口大小为 0,也可以发送紧急数据,因为紧急数据无需缓存
选项字段长度不定长度不固定,但必须是 32bits(4 字节)的整数倍,内容可根据需求变化,因此必须通过首部长度来区分选项的具体长度

TCP 的报头是变长的,由固定的 20 字节和变长的选项组成。其中 “数据偏移” 也称为 “首部长度”,它占用固定的 4 位,作用是保存报头整体的长度,方便接收端正确解析报文中的各个字段。

需要注意的是,虽然首部长度占 4 个比特位,4 个比特位能表示的数值范围是 0~15,但它的单位并不是 1 字节,而是 4 字节。所以首部长度实际能表示的范围是 0~60 字节。

学习每一层协议时,我们都应该掌握两个核心问题:

  1. 协议报头和有效载荷如何分离?
  2. 有效载荷如何向上交付?

第一个问题:协议报头和有效载荷如何分离

我们知道,TCP 首部包含 20 字节的固定长度首部和选项字段,同时固定首部中有一个首部长度字段(4bits)。

 

需要注意的是,虽然首部长度占 4 位,但它的单位是 4 个字节。4 个比特位能表示的数值范围是 0~15,所以对应的首部长度范围是 0~60 字节。

 

首部长度最大可表示 15(因为 4 位比特位最大是 1111,即 15),结合单位 4 字节,可知 TCP 首部最长为 15×4 = 60 字节。由于固定首部为 20 字节,所以选项字段最长为 40 字节。当没有选项字段时,首部长度字段的值为 5(20 = 5×4),二进制为 0101。

 

基于以上规则,提取报头、选项和有效载荷的步骤如下:

 
  • 提取报头:除选项外的标准报头固定为 20 字节。
  • 提取选项:根据 4 位首部长度计算报头整体大小,用整体大小减去 20 字节的标准报头(固定报头),得到选项内容。若没有选项,则直接获取有效载荷。
  • 提取有效载荷:有效载荷 = 报文总长度 - 报头长度(若有选项,需再减去选项长度)。
 

通过首部长度字段,我们就能实现 TCP 首部和有效载荷的分离。

第二个问题:有效载荷如何向上交付

TCP 属于传输层协议,其上层是应用层。应用层程序会绑定特定的端口号,而 TCP 首部中包含 16 位目的端口号。TCP 正是通过这个目的端口号,将有效载荷准确地向上交付给对应的应用层程序。

 

接下来我们要进入 TCP 协议的深入学习,之后会遇到类似如下的示意图:

 

大家一定要注意:客户端和服务端基于 TCP 协议进行通信时,每次互相发送消息,传递的都是完整的 TCP 报文,也就是说,报文中一定会携带完整的 TCP 报头。

2.1. 流量控制 —— 窗口大小(16 位)

提到可靠传输,我们先明确一下不可靠传输的表现:比如数据传输过程中出现重复、乱序、丢包等情况,都属于不可靠传输。因此,可靠的数据传输必须规避这些问题。

在 TCP 协议段格式中,标准报头里有一个 16 位窗口大小字段,这个字段的作用是什么呢?

我们先补充一些相关知识:

什么是流量控制

我们知道,TCP 协议无论是在用户端还是服务端,都设有发送缓冲区和接收缓冲区。

假设某一时刻服务端任务繁忙,客户端正在向服务端发送数据,但服务端来不及调用 read 或 recv 这类接口读取数据。而客户端并不知晓服务端的情况,仍然持续向服务端发送数据。当服务端的接收缓冲区被写满后,如果客户端继续发送数据,就会导致大面积丢包现象。

为了规避这种情况,当服务端的接收缓冲区空间紧张时,需要让客户端降低发送数据的速度,甚至暂时停止发送。这种通过控制客户端发送数据的速度,确保服务端能够及时处理数据,从而避免大面积丢包的策略,就叫做流量控制。

另外需要说明的是,TCP 协议还有一种数据重传策略,即如果数据发生丢包,就重新传输一份。

那有人可能会问:丢包了重传不就行了吗?为什么还需要流量控制呢?

因为我们知道,一个 TCP 协议的报文,仅标准报头就有固定的 20 字节,再加上可能的选项和有效载荷,数据量并不小。如果出现大面积丢包,重传虽然能解决数据交付问题,但会浪费大量的网络资源,传输效率也会非常低。

大家还需要理解:双方通信时,发送的每一条消息中,都会包含完整的 TCP 协议报头。

如何实现流量控制?

答案就是服务端在给客户端返回的响应报文中,16 位窗口大小字段的值,代表服务端接收缓冲区当前剩余的空间大小。客户端可以根据这个剩余空间大小,合理调整自己发送数据的速度。

并且我们可以想到,服务端也可能会向客户端发送消息,此时客户端给服务端返回的响应报文中,16 位窗口大小字段的值,就是客户端接收缓冲区当前剩余的空间大小。

简单来说,这个 16 位窗口大小字段,本质上就是告知对方 “我的接收缓冲区还剩多少空间”。也就是说,通信双方都可以通过这个字段实现对对方的流量控制。

2.2. 确认应答机制

2.2.1. 什么是确认应答机制

TCP 保证数据安全传输的最基本特性之一,就是确认应答机制。

实际上,网络传输之所以存在不可靠问题,本质原因是传输距离过长。

比如我在内蒙给广东的网友发送消息,数据包需要经过多个路由器节点转发,穿过多个局域网,在局域网内部通过双绞线(以太网技术常用的物理介质)传输,还要经过运营商的基站。数据包在如此长的传输路径中,很可能出现丢失、数据比特位翻转、字节乱序,甚至重复发送给对方的情况(比如发送方误以为数据包丢失,再次发送)。

TCP 应该如何解决网络传输的不可靠问题呢?答案就是引入确认应答(acknowledgement)机制。

客户端向服务端发送数据时,每次发送完一段数据后,不会立即继续发送下一段数据,而是会等待服务端返回的应答。只有收到应答后,才会发送下一段数据,这样就能有效避免数据传输中的不可靠问题。

虽然数据包在网络中传输距离长、环节多,但只要我发送给网友的消息得到了回复,我就能确定发送的数据已经成功到达网友的主机。

比如我问网友:“你最近 TCP/IP 学得怎么样啊?” 网友回复:“我最近正在学 TCP 的确认应答机制呢!” 这时我就能立刻确定,我发送的数据经过网络传输后,网友一定收到了,因为网友对我的消息做出了回应。同样,如果我没有回复网友的消息,网友也无法确定他发送的内容我是否收到,因为他没有收到我的应答。这就是典型的确认应答机制。

但大家可能会发现,我和网友互相发送消息时,总会有最后一条消息没有被确认 —— 无论最后一条消息是对方发的还是我发的。所以我们可以得出结论:TCP 并没有绝对的可靠性,只有相对的可靠性!事实上,不只是 TCP,所有协议都不存在绝对的可靠性。

因此,TCP 讨论可靠性时,永远不涉及最新的消息,只讨论历史消息。因为必然存在最新的一条消息没有被应答的情况。

不过话说回来,我们实际上只需要保证客户端到服务端的数据完好即可,服务端并不需要知道自己的响应是否完好送达客户端。所以客户端发送数据后,如果在一段时间内没有收到服务端的响应,无论具体原因是什么,都会认为这次发送失败,进而进行重传。

总之,最新的一条消息(也就是应答消息)是没有应答的(这句话看似绕,但核心是 “应答本身不需要再被应答”)。

2.2.2. 推导确认应答机制

我们先看看最初设想的确认应答方式:

[客户端发送数据 1 → 服务端回复应答 1 → 客户端发送数据 2 → 服务端回复应答 2 → ...]

这种方式确实能保证可靠性,但效率非常低。

难道我说一句话,你都必须先回复 “我听到了”,然后才能说你想回应的话吗?显然这种方式效率太低。正确的做法应该是将应答和想要回复的消息一起发送回去,比如:

[客户端发送数据 1 → 服务端回复 “应答 1 + 服务端要发送的数据 A” → 客户端回复 “应答 A + 客户端要发送的数据 2” → ...]

我们不保证最新一条信息的可靠性,只保证历史消息的可靠性!

但新的问题又出现了:难道我发送信息前,必须等收到你的应答才能发吗?没有应答就不发送了吗?

显然不是。真实的 TCP 工作时,发送方会一次性发送一批数据段,接收方收到消息后,也会一次性返回一批确认数据段。

发送方可以同时发送多条数据,接收方根据收到的消息给出回复,但可能存在 “后发先至” 的问题 —— 接收方收到的数据顺序可能被打乱,这可能会引起严重的歧义。

例如下面这种情况,双方最终会造成误解:

客户端按顺序发送数据 1、数据 2,由于网络原因,数据 2 先到达服务端,服务端误以为数据 1 丢失,可能会请求重传数据 1,而客户端可能认为数据 2 丢失,造成混乱。

针对 “后发先至” 问题,TCP 的解决办法是给传输的数据和应答报文都进行编号

2.2.3. 确认应答机制的实现

确认应答机制是发送方确保自己的数据被对方成功接收的核心手段。

发送方发出数据后,会等待接收方返回的确认应答。如果收到确认应答,说明数据已成功到达接收方;反之,数据丢失的可能性很大。

如下所示,在一定时间内没有等到确认应答,发送端就会认为数据已经丢失,并进行重发。由此,即使产生了丢包,仍然能够保证数据到达对端,实现可靠传输。

未收到确认应答并不意味着数据一定丢失,也可能是数据已被对方接收,但返回的确认应答在传输途中丢失。这种情况也会导致发送端因未收到确认应答而认为数据未到达,从而进行重新发送。

此外,还可能因其他原因导致确认应答延迟到达,在发送端重发数据后才到达。此时,发送端只需按机制重发数据即可,但接收端会反复收到相同的数据。为了给上层应用提供可靠传输,必须丢弃重复的数据包。

为此,需要引入一种机制,既能识别是否已接收数据,又能判断是否需要接收。这些确认应答处理、重发控制及重复控制等功能,都可以通过序列号实现。序列号是按顺序给发送数据的每一个字节(8 位字节)标注的编号。接收端通过查询接收数据 TCP 首部中的序列号和数据长度,将自己下一步应接收的序号作为确认应答返回。就这样,通过序列号和确认应答号,TCP 实现了可靠传输。

TCP 是面向字节流传输的,编号时会给每个字节都编上序号。假设每次传输 1000 个字节,那么第一个字节的序号是 1,第二个是 2…… 最后一个是 1000。

由于这 1000 个字节属于同一个 TCP 数据报,TCP 报头只记录第一个字节的序号。

上述编号需要用到 TCP 报文格式中的 32 位序列号和 32 位确认序号:发送方利用 32 位序列号记录发送数据的第一个字节的序号;接收方读取数据后,返回 32 位确认序号,告知对方下一个数据的第一个字节应从哪个序号开始。

在 TCP 协议中,每一条数据都有序号,包括应答报文。判断一个报文是否是应答报文,依据 ACK 标志位:若 ACK 为 1,则是应答报文;若为 0,则不是。

TCP 的可靠传输主要通过确认应答机制保证,通过应答报文让发送方清楚传输是否成功,引入 32 位序列号和 32 位确认序号则进一步确保了多条数据的有效传输。

2.2.4. 确认号和序列号的详细解析

客户端在接收到响应之前,会把数据暂存在发送缓冲区中。

首先,客户端要发送的数据已存在 TCP 的发送缓冲区(内核中的缓冲区)中。由于 TCP 是面向字节流的,这个缓冲区可看作 char 类型的大数组,每个空间对应一个字节,且有对应的下标,即每个字节天然有自己的编号。

我们只需保证拷贝到缓冲区里的数据是按顺序存储的即可

序列号

  • 含义:序列号是一个 TCP 报文段中第一个字节的数据序列标识,用于表示在一个 TCP 连接中,该报文段所携带数据的起始位置,以保证数据传输的顺序性和完整性。
  • 作用:TCP 连接建立时,双方会各自随机选择一个初始序列号(ISN)。随后传输的每个报文段的序号基于这个初始值递增,增量为该报文段携带的数据量(字节数)。通过这种方式,接收方可以根据序号重组乱序到达的数据片段,确保数据的正确顺序和完整性。若接收到的报文段不连续,接收方可通过 TCP 的重传机制请求发送方重新发送缺失的数据。

例如,一个报文段的序号为 100,包含 100 字节的数据,则它代表序号 100 到 199 的数据。下一个报文段可能从序号 200 开始,若包含 50 字节的数据,则代表序号 200 到 249 的数据。

简单来说,数据中每个字节都有唯一的标识 —— 序列号。

补充知识

在 TCP 中,当发送端的数据到达接收主机时,接收主机会返回一个已收到消息的通知,这个通知叫做 ACK(确认应答,Positive Acknowledgement)。

每个 ACK 都带有对应的确认序列号,意思是告诉发送者:“我已经收到了确认号之前的所有数据,下一次请从确认号开始发送。”

服务器和客户端需要区分当前报文是普通报文还是确认应答报文,标志位中的 ACK 可以解决这个问题:ACK 为 0 时,是普通报文,此时只有 32 位序号有效;ACK 为 1 时,是应答报文,报文的序号和确认序号都有效。

确认号确认号的值表示接收方已收到确认号之前的所有连续报文。例如,确认号为 11,代表接收方已收到 10 号及之前所有序号的报文,发送端下次从第 11 序号开始发送即可。从发送方的角度理解,确认号的值就是发送方下一次发送报文时,报文序号应有的值。

  • 含义:确认应答号是接收方期望从发送方接收到的下一个报文段的序号,实质是告诉发送方:“我已成功接收哪个序号之前的所有数据,请从这个序号开始发送后续数据。”
  • 作用:确认应答号用于实现可靠性传输。当一个报文段被接收方正确接收时,接收方会发送一个 ACK 报文,其中的确认应答号是接收到的数据最后一个字节的序号加 1(即接收方期望接收的下一个数据的序号)。发送方通过检查这个确认应答号,能知道发送的数据是否已被正确接收,并决定是否需要重传某些数据段。

2.3. 六位标志位

标志位字段共 6bit,包含 6 个标志位,每个标志位占 1bit,只有 0 和 1 两种状态。

TCP 标志位中文意思作用
SYN同步标志用于建立连接
ACK确认标志用于确认收到数据
FIN结束标志用于关闭连接
RST重连标志用于重置连接
PSH催促标志用于立即传输数据
URG紧急标志用于指示紧急数据
1. ACK

表示本报文前面的确认号字段是否有效:只有当 ACK=1 时,确认号字段才有效。

在 TCP 确认回复机制中,客户端和服务端任意一方发送数据后,另一方都需给予应答以表明已收到数据。应答报文中该标记位需置 1,且应答报文也可携带数据。

TCP 规定,建立连接后,ACK 必须为 1。

2. SYN

请求建立连接,携带 SYN 标识的报文称为同步报文段。

服务端如何区分客户端是想发送信息还是建立连接?这就需要 SYN 标志位。

SYN 表示同步标记位,即申请建立连接的标记位。

TCP 是面向连接的协议,双方正常通信前需先建立连接,这个过程是三次握手:

  • 第一次:Client 给 Server 发送连接请求,报文中携带 SYN 标记位,表明与建立连接相关。
  • 第二次:Server 接受 Client 的连接,询问何时建立连接,报文中携带 SYN 标记位,同时携带 ACK 回复上一条请求。
  • 第三次:Client 回复 Server “立即建立连接”,只需设置 ACK 即可。

那么,Client 何时认为连接已建立?答案是收到 Server 的第二次握手回复后。

而 Server 认为连接建立是在第三次握手,因为第二次握手只是 Server 单方面同意,Client 是否同意需通过第三次握手确认。

注意:TCP 虽保证可靠性,但允许连接建立失败。

  • client 向 server 请求建立连接:当 SYN=1,ACK=0 时,表示该报文为连接请求报文;
  • server 向 client 同意建立连接:当 SYN=1,ACK=1 时,表示同意建立连接;
  • 只有建立连接的前两次请求中 SYN 才为 1,这类报文称为同步报文段。
3. FIN

FIN 表示结束标记位(可理解为 Finish),即断开连接的标记位。

断开连接是四次挥手的过程:

为什么是四次?因为 Client 单方面断开连接需要两次,Server 单方面断开连接需要两次,合计四次。

谁先断开连接没有限制,下面假设 Client 先断开:

  • 第一次挥手:Client 通知 Server 单方面断开连接,发送的报文中携带 FIN 标记位。(注意:Client 单方面断开后,仅关闭 Client→Server 方向的数据传输,Server 仍可向 Client 发送数据,反之则不行。)
  • 第二次挥手:Server 收到 Client 的通知,给 Client 发送应答报文,携带 ACK 标记位。
  • 第三次挥手:Server 通知 Client 单方面断开连接,发送的报文中携带 FIN 标记位。
  • 第四次挥手:Client 收到 Server 的通知,给 Server 发送应答报文,携带 ACK 标记位。
4. RST

RST 表示复位标记位,代表重新建立连接。

三次握手不一定能成功建立连接,四次挥手也可能中断。例如,单方面拔掉服务器电源,连接会异常断开。

当服务器重启后,不认为连接已建立,但 client 仍认为连接存在,会持续给服务器发消息。服务器会觉得异常(连接已不存在却收到消息),于是给 client 发送 RST 标志位为 1 的复位报文段,告知 client“连接已异常断开,请重新发起三次握手建立连接”。

因此,复位标志位用于通信双方中任意一方认为连接不一致时,由认为连接异常的一方发送复位报文段,告知对方需要重新建立连接。

举例:

  1. 服务端过载导致重连:客户端与服务器通过三次握手成功建立连接,但通信时服务器操作系统资源满载,无法应答。为解决资源问题,服务器可能释放连接,此时必须向客户端发送 RST 标识为 1 的报文,请求重新建立连接才能继续通信。
  2. 三次握手时第三次握手失败导致重连:client 认为发送第三次握手报文后连接即建立,但如果第三次握手失败,server 不会认为连接成功。此时 client 发送数据,server 会因连接未建立而发送 RST 报文,要求重新握手。
5. PSH

PSH 表示催促标记位(可理解为 Push)。

Client 不断发送数据,Server 不断接收并回复,应答报文中的 16 位窗口大小字段告知 Client 自己接收缓冲区的剩余空间,以便 Client 调整发送速度。

当 Server 接收缓冲区快满或已满时,Client 可在发送的报文中设置 PSH 标记位,催促对方尽快取走缓冲区数据(让对方上层应用程序立即从 TCP 接收缓冲区读取数据,保证缓冲区有能力接收新数据或清空)。

补充:
(1) 缓冲区满的原因:可能是 Server 应用层还在处理上一条数据,没时间调用 read 接口读取缓冲区数据。
(2) “OS 催促上层尽快取走数据” 的理解:缓冲区有低水位和高水位标记,OS 以此为依据催促上层取数据 —— 低水位表示数据太少,暂不读取;高水位表示数据太多,需立即读取。

一般 OS 让上层每次读取一批数据,而非一个字节。因为上层通过 read 接口读取缓冲区数据,若每次只读一个字节,会频繁调用 read 函数,导致用户态和内核态频繁切换,降低效率。

6. URG

URG 表示紧急标记位,用于指示本报文的有效载荷是否包含紧急数据:URG=1 时表示有紧急数据,此时 16 位紧急指针字段有效。

当发送方希望某些数据尽快被接收方上层获取时,需用到该标记位,通常与 16 位紧急指针配合使用。紧急数据混在普通数据中,紧急指针指明其在普通数据中的具体位置。

16 位紧急指针表示紧急数据在有效载荷中的偏移量,TCP 规定紧急数据只能有 1 字节。当 URG 标志位有效时,接收方读取 TCP 报头后,会先从紧急指针指示的偏移量处读取 1 字节紧急数据,再从有效载荷起始位置读取剩余数据。这 1 字节紧急数据通常称为带外数据。

实际上,紧急指针和 URG 标志位在 99.99% 的场景中用不到。若需使用带外数据,可能是运维人员用于查询服务器状态(如服务器压力大时,发送 1 字节带外数据询问状态,服务器返回 1 字节状态码,带外数据无需经过冗长数据流,可直接在应用层读取)。

带外数据不在正常数据流中,UDP 和 TCP 协议均可能用到。若要读取带外数据,可将 recv 的 flags 标志位按位或上 MSG_OOB。

这些标志位的组合和状态变化规则,定义了 TCP 连接的建立、维护、关闭过程,以及数据传输中的特定行为,确保了 TCP 连接的可靠性和稳定性。


3. TCP 的可靠策略

除确认应答机制外,TCP 还有多种策略提高传输可靠性。

3.1. 超时重传机制

主机发送消息后,无法直接知晓是否发送成功,需通过对方的回应确认。

确认应答机制基于数据顺利传输的前提,而在丢包(对方未收到数据或未收到对方回复)时,需通过超时重传(隔一段时间重新发送)应对。

丢包有两种情况:发送的数据丢包,或返回的 ACK 丢包。无论哪种,只要未收到 ACK,到达某个时间阈值后就会重传。

补充知识 1—— 丢包是小概率事件

数据丢包是概率事件。假设一条数据传输丢包概率为 5%,成功概率为 95%,则第一次丢包且第二次重传也丢包的概率是 5%×5%=0.25%,三次重传均丢包的概率可忽略不计。实际中丢包概率极小,5% 已属较高,若连续多次重传仍丢包,需考虑网线断裂等物理问题。

补充知识 2—— 未收到应答的消息暂存于滑动窗口

已发送但未收到 ACK 的数据,不应立即从发送缓冲区移除(计算机中 “移除” 即数据覆盖),需保存一段时间,若丢包可重发。这些数据存放在滑动窗口中(后续详细讲解)。

ACK 丢包时接收端的处理

若 ACK 丢包,接收端已收到数据,发送端重传相同数据时,TCP 会进行去重处理。接收端有 “接收缓冲区”,会将数据按序号存放,根据序号识别重复数据并丢弃。

超时时间的设定与重传次数

超时时间需随网络情况动态调整:网络好时设长会浪费时间,网络差时设短会误判丢包。

理想情况是找到最短时间,保证绝大部分网络环境下,数据包及 ACK 能在此时限内往返。

在 Linux(Unix 和 Windows 类似)中,超时以 500ms 为基本单位控制。第一次重传后未收到确认,超时时间会以 2 的指数幂 ×500ms 的方式递增。累计重传次数达到一定值后,TCP 会强制关闭连接。

3.2. 连接管理机制

3.2.1. 如何理解 TCP 连接
  1. TCP 连接的本质:通常指客户端应用层到服务端应用层的端到端连接,若应用层未连接成功,则 TCP 连接失败。

当上层(如应用层)调用 connect 函数时,会请求传输层(TCP 协议层)建立连接,TCP 协议层通过三次握手完成连接建立,SYN 报文由客户端 TCP 协议层(传输层)发出

需注意,TCP 连接虽为端到端,但数据传输会经过多个网络设备和协议层,这些处理对上层应用透明,应用层只需关注连接是否成功及数据是否可靠传输。

因此,若看到某一方应用层断开连接后,仍能发送 SYN 等报文,均为传输层(操作系统)的操作。

  • TCP 连接的维护与资源消耗
  • TCP 为上层进程提供 “端对端” 信道服务,由客户端和服务端的套接字(socket)及交换的数据包(segment)组成,其建立、维持和终止需遵循特定协议和状态机制。

在 Client-Server 模式中,大量 Client 会与同一 Server 建立连接,Server 需管理这些连接,这涉及操作系统内核的数据结构维护(“先描述,后组织”)。

这些连接在操作系统中表现为内核中的数据结构(结构体),连接建立时创建对应的 “连接对象”,管理连接即对这些对象进行增删查改。

维护连接需消耗 CPU 和内存资源(如存储连接状态、序号、窗口大小等信息),这也是许多网络攻击的切入点(如 SYN 洪水攻击消耗 Server 资源)。

TCP 连接需维护的状态信息和参数(如序号、确认号、窗口大小、重传计时器等)存储在传输控制块(Transmission Control Block,TCB)中,每个 TCP 连接对应唯一的 TCB,操作系统用表存储所有 TCB,其信息随连接状态变化而更新。


4.TCP 的 11 种核心状态

TCP 连接从建立到关闭的全过程,会经历 11 种不同状态,每种状态对应特定的交互阶段。下表清晰梳理了各状态的含义、触发场景及核心作用:

状态名称核心含义触发场景(客户端 / 服务端)关键作用
CLOSED初始状态,连接未打开或已完全关闭客户端 / 服务端启动后默认状态,或连接关闭后最终状态标记连接生命周期的起点和终点
LISTEN服务端监听状态,等待客户端连接请求服务端调用listen()后进入表示服务端已就绪,可接收客户端的 SYN 报文(连接请求)
SYN_SENT客户端已发送 SYN 报文(连接请求),等待服务端的 SYN+ACK 报文客户端调用connect()后发送 SYN,进入此状态客户端主动发起连接的中间状态,等待服务端确认
SYN_RCVD服务端已接收客户端的 SYN 报文,已回复 SYN+ACK 报文,等待客户端的 ACK 报文服务端收到 SYN 后回复 SYN+ACK,进入此状态服务端被动响应连接的中间状态,正常情况下持续时间极短(需客户端快速回复 ACK)
ESTABLISHED连接已完全建立,双方可正常发送 / 接收数据客户端收到 SYN+ACK 后回复 ACK,或服务端收到客户端 ACK 后TCP 通信的核心状态,所有数据传输均在此状态下进行
FIN_WAIT_1主动关闭方已发送 FIN 报文(请求关闭连接),等待被动关闭方的 ACK 报文客户端 / 服务端调用close()后发送 FIN,进入此状态主动关闭连接的第一步,等待对方确认 “收到关闭请求”
FIN_WAIT_2主动关闭方已收到被动关闭方的 ACK,等待被动关闭方的 FIN 报文主动关闭方收到 ACK 后从 FIN_WAIT_1 切换至此状态半连接状态(主动方已无数据发送,但仍可接收被动方数据),无超时机制
CLOSE_WAIT被动关闭方已收到主动方的 FIN 报文,已回复 ACK,等待自身应用层调用close()被动关闭方收到 FIN 后回复 ACK,进入此状态需应用层主动调用close()才能发送 FIN,否则会长期占用连接资源
LAST_ACK被动关闭方已发送 FIN 报文,等待主动关闭方的 ACK 报文被动关闭方调用close()发送 FIN 后进入此状态被动关闭的最后一步,等待对方确认 “收到我方关闭请求”
TIME_WAIT主动关闭方已收到被动方的 FIN,已回复 ACK,等待 2*MSL 时间后关闭主动关闭方收到 FIN 并回复 ACK 后进入此状态确保最后一个 ACK 被对方收到,避免旧报文干扰新连接(仅主动关闭方会进入)
CLOSING双方同时发送 FIN,主动方未收到 ACK 却先收到对方的 FIN(罕见)客户端和服务端同时调用close()发送 FIN

5、TCP 三次握手:建立可靠连接

TCP 通过 “三次握手”(Three-Way Handshake)建立全双工连接,核心目标是同步双方初始序列号防止历史连接干扰,并确保双方都具备发送 / 接收能力。

5.1 三次握手的完整流程

假设客户端(A)主动发起连接,服务端(B)被动监听,流程如下:

  1. 第一次握手(A→B:SYN 报文)

    • 客户端 A 创建 TCB(传输控制块,存储连接信息如序列号、缓存指针),向服务端 B 发送SYN=1的报文,携带随机初始序列号seq=x(SYN 报文不携带数据,但消耗 1 个序列号)。
    • 客户端状态:从CLOSEDSYN_SENT
  2. 第二次握手(B→A:SYN+ACK 报文)

    • 服务端 B 收到 SYN 后,创建 TCB,回复SYN=1(自身同步请求)和ACK=1(确认客户端 SYN),携带自身初始序列号seq=y和确认号ack=x+1(表示 “已收到 x 及之前的所有字节,期待下一个字节是 x+1”)。
    • 服务端状态:从LISTENSYN_RCVD
  3. 第三次握手(A→B:ACK 报文)

    • 客户端 A 收到 SYN+ACK 后,回复ACK=1,携带确认号ack=y+1(确认服务端 SYN)和自身序列号seq=x+1(若不携带数据,不消耗序列号)。
    • 客户端状态:SYN_SENTESTABLISHED;服务端收到 ACK 后,状态从SYN_RCVDESTABLISHED

此时,双方均进入ESTABLISHED状态,连接正式建立,可开始数据传输。

 这样,服务端和客户端在 TCP 的连接成功的认知上存在着时间差,如果服务端并未收到第三次握手发送的 ACK 报文,会出现什么情况?

  • 如果服务端没有收到第三次握手发送的 ACK 报文,服务端的 TCP 连接状态会保持为 SYN_RECV,并且会根据 TCP 的『超时重传机制』,会等待 3 秒、6 秒、12 秒后重新发送 SYN+ACK 包,以便客户端重新发送 ACK 包。
  • 客户端在接收到 SYN+ACK 包后,就认为 TCP 连接已经建立,状态为 ESTABLISHED。如果此时客户端向服务端发送数据,服务端将以 RST 包响应,用于强制关闭 TCP 连接。
  • 如果服务端收到客户端重发的 ACK 包,会先判断全连接队列是否已满,如果未满则从半连接队列中拿出相关信息存放入全连接队列中,之后服务端 accept() 处理此请求。如果已满,则根据 tcp_abort_on_overflow 参数的值决定是扔掉 ACK 包还是发送 RST 包给客户端。

半连接和全连接队列
tcp_abort_on_overflow 是一个布尔型参数,当服务端的监听队列满时,新的连接请求会有两种处理方式,一是丢弃,二是拒绝连接(通过向服务端发送 RST 报文实现)。通过哪种方式处理,取决于这个参数:

tcp_abort_on_overflow 为 0,丢弃服务端发送的 ACK 报文,不建立连接。
tcp_abort_on_overflow 为 1,发送 RST 报文给客户端,拒绝连接。

另外, 服务端的监听队列有两种:

TCP 半连接队列和全连接队列是服务端在处理 TCP 连接时维护的两个队列,它们的含义如下:

半连接队列,也称SYN 队列,是存放已收到客户端的 SYN 报文,但还未收到客户端的 ACK 报文的连接请求的队列(即完成了前两次握手)。服务端会向客户端发送 SYN+ACK 报文,并等待客户端的回复。


全连接队列,也称accept 队列,是存放已完成三次握手,但还未被应用程序 accept 的连接请求的队列。服务端会从半连接队列中移除连接请求,并创建一个新的 socket,然后将其放入全连接队列。


半连接队列和全连接队列都有最大长度限制,如果超过限制,服务端会根据 tcp_abort_on_overflow 参数的值来决定是丢弃新的连接请求还是发送 RST 报文给客户端。

它们和 socket 的关系是:

服务端通过 socket 函数创建一个监听 socket,并通过 bind 函数绑定一个地址和端口,然后通过 listen 函数指定监听队列的大小。在listen中可以设置半连接的长度backlog的长度。
当客户端发起连接请求时,服务端会根据 TCP 三次握手的进度,将连接请求放入半连接队列或全连接队列。

  • 为什么要listen的这个第二个参数不能设置得太大?

 因为这个队列需要维护,会消耗资源,完全没有必要

  • 为什么要有这个listen这第二个参数?

为了提高效率!


当应用程序调用 accept 函数时,服务端会从全连接队列中取出一个连接请求,并返回一个新的 socket 给应用程序,用于和客户端通信。

5.2 Socket 编程与三次握手的关联

  1. 服务器调用listen函数,服务器进入LISTEN状态
  2. 客户端调用connect 函数,操作系统会开始三次握手,客户端发送完ACK之后,connect函数就会返回
  3. 服务端调用accept函数就会处理客户端发来的ACK,如果没有调用accept函数,就不会处理客户端发来的ACK函数
  4. 没有被accept的连接叫半连接,被accept的的连接叫全连接

三次握手由操作系统内核自动完成,Socket 函数仅控制连接的 “触发” 和 “获取”,对应关系如下:

Socket 函数作用对应 TCP 状态变化
socket()创建套接字(文件描述符),无实际连接动作客户端 / 服务端均处于CLOSED
bind()服务端将套接字绑定到指定 IP 和端口服务端仍处于CLOSED
listen()服务端将套接字设为监听状态,初始化半连接队列(SYN 队列)和全连接队列服务端从CLOSEDLISTEN
connect()客户端触发内核发送 SYN 报文,发起连接请求客户端从CLOSEDSYN_SENT
accept()服务端从全连接队列中取出已完成三次握手的连接,返回新套接字用于通信服务端从LISTENESTABLISHED(新连接)

关键注意点

  • 即使服务端未调用accept()三次握手仍会完成(内核自动处理),连接会暂存于全连接队列(accept 队列),accept不参与三次握手。
  • listen()的第二个参数backlog用于限制全连接队列的最大长度(实际受内核参数net.core.somaxconn限制,取两者最小值),若队列满,新连接会被丢弃或拒绝(取决于tcp_abort_on_overflow)。

5.3 TCP 三次握手的优点

在前面几个小节中,我们知道了什么是连接,也了解了 TCP 的三次握手过程和 TCP 状态的变化。在了解这些前提后,我们再来谈谈 TCP 为什么是三次握手。

TCP 连接除了要保证建立连接的效率、验证全双工之外,虽然它不保证 100% 的可靠性,但是它是用于保证可靠性和流量控制维护的某些状态信息(包括 Socket、序列号和窗口大小)的前提。

那么问题就转化为:为什么只有三次握手才可以初始化 Socket、序列号和窗口大小并建立 TCP 连接?

结论:

  • 1,2次连接会造成洪水危机,使得服务器被攻击而挂掉。
  • 阻止重复历史连接的初始化(主要)
  • 同步双方的初始序列号
  • 避免资源浪费

三次握手的首要原因是防止旧的重复连接初始化造成混乱。

首先谈谈什么是 “历史连接”。

有这样一个场景:假如客户端先发送了 SYN 报文(Seq=90),然后它突然关机了,好巧不巧,SYN(Seq=90)也被网络阻塞了,导致服务端并未收到。当客户端重启后,又向服务端发送了 SYN 报文(Seq=100)以重新发起连接。这里的 SYN(Seq=90)就被称为历史连接。

TCP 的三次握手通过序列号和确认号的机制来防止旧的重复连接初始化造成混乱。

TCP 的三次握手过程中,通过序列号和确认号的机制来确保连接的可靠性和防止旧的重复连接初始化造成的混乱。这一机制的核心在于序列号和确认号的形成与使用。

序列号的形成

序列号(seq)在 TCP 连接中扮演着至关重要的角色,它用于标记数据段的顺序,确保数据的正确传输和接收。序列号是一个占 4 个字节的字段,用来对 TCP 连接中发送的每一个字节进行编号。

  • 随机初始化:在 TCP 连接的建立过程中,每个端点(客户端和服务器)都会随机生成一个初始序列号。这个初始序列号用于标记该端点发送的第一个数据字节。例如,在三次握手的第一次中,客户端会随机生成一个序列号(seq=x),并将其置于 TCP 头部的 “序列号” 字段中,然后发送一个 SYN 报文给服务器。
  • 递增性:在数据传输过程中,每当发送方发送一个数据段时,它都会将其序列号增加该数据段的字节长度。这样,接收方就可以通过序列号来确定数据的顺序,并检查是否有数据丢失。接收方在接收到数据段后,会回复一个带有 “确认号(ack)” 的 ACK 报文,确认号字段表示接收方期望从发送方接收到的下一个字节的序列号。

确认号的形成

确认号(ack)是 TCP 头部中的另一个重要字段,用于表示接收方期望从发送方接收到的下一个字节的序列号。

  • 确认机制:在 TCP 连接中,每当接收方成功接收到一个数据段时,它都会发送一个 ACK 报文给发送方,其中确认号字段设置为接收到的数据段的最后一个字节的序列号加 1。例如,在三次握手的第二次中,服务器在收到客户端的 SYN 报文后,会发送一个 SYN+ACK 报文给客户端。在这个报文中,确认号字段被设置为客户端的初始序列号加 1(ack=x+1),表示服务器期望从客户端接收到的下一个字节的序列号是 x+1。
  • 重传机制:如果发送方在规定的超时时间内没有收到接收方的 ACK 报文,它会认为该数据段可能已丢失,并会重新发送该数据段。重传时,发送方会保持序列号不变,以便接收方能够识别出这是一个重传的数据段,并相应地更新其状态。

综上所述,TCP 通过序列号和确认号的机制来确保数据的顺序性和可靠性。在三次握手过程中,每个端点都会随机生成一个初始序列号,并在数据传输过程中递增地使用它。同时,接收方会通过发送带有确认号的 ACK 报文来告知发送方其期望接收的下一个字节的序列号。这种机制有效地防止了旧的重复连接初始化造成的混乱,确保了 TCP 连接的稳定性和可靠性。

具体来说:

  1. 在第一次握手中,客户端发送一个 SYN 报文,携带一个随机的初始序列 Seq=x,表示客户端想要建立连接,并告诉服务端自己的序列号。
  2. 在第二次握手中,服务端回复一个 SYN+ACK 报文,携带一个随机的初始序列号 Seq=y,表示服务端同意建立连接,并告诉客户端自己的序列号。同时,服务端也确认了客户端的序列号,将确认号 ack 设置为 x+1,表示期待收到客户端下一个字节的序列号。
  3. 在第三次握手中,客户端回复一个 ACK 报文,将确认号 ack 设置为 y+1,表示确认了服务端的序列号,并期待收到服务端下一个字节的序列号。至此,双方都同步了各自的初始序列号,并确认了对方的初始序列号,连接建立成功。

这样的过程可以防止旧的重复连接初始化造成混乱,因为:

  • 第一次握手:如果客户端发送的 SYN 报文是旧的重复报文,那么它携带的初始序列号 Seq=x 可能已经被服务端使用过或者超出了服务端期待的范围。这样,服务端收到这个旧的 SYN 报文后,会认为它是无效的或者已经过期的,不会回复 SYN+ACK 报文,也不会建立连接。
  • 第二次握手:如果服务端回复的 SYN+ACK 报文是旧的重复报文,那么它携带的初始序列号 Seq=y 可能已经被客户端使用过或者超出了客户端期待的范围。这样,客户端收到这个 SYN+ACK 报文后,会认为它是无效的或者已经过期的,不会回复 ACK 报文,也不会建立连接。
  • 第三次握手:如果客户端回复的 ACK 报文是旧的重复报文,那么它携带的确认号 ack 可能已经被服务端使用过或者超出了服务端期待的范围。这样,服务端收到这个 ACK 报文后,会认为它是无效的或者已经过期的,不会分配资源给这个连接,也不会进行数据传输。

代入上面假设的场景,如果在 SYN(Seq=100)正在发送的途中,原先 SYN(Seq=90)刚好被服务端接收,那么服务端会返回 ACK(Seq=91),客户端觉得自己应该收到的是 ACK(Seq=101)而不是 ACK(Seq=91),此时客户端就会发起 RST 报文以终止连接。服务端收到后,释放连接。

经过一段时间后,新的 SYN(Seq=100)被服务端接收,服务端返回 ACK(Seq=101),客户端检查确认应答号是正确的,就会发送自己的 ACK 报文,连接成功,且避免了旧的重复连接初始化造成混乱。

因此,通过序列号和确认号的机制,TCP 可以在三次握手中验证双方是否是当前有效的连接请求,并且同步双方的初始序列号。这样可以防止旧的重复连接初始化造成混乱。


6. 理解四次挥手

6.1 四次挥手的深入理解

四次挥手是TCP连接关闭的标准过程,虽然它看起来很简单,但实际过程需要仔细剖析。

四次挥手过程

在TCP连接的释放过程中,实际上就是通过四次挥手来实现的:

  1. 主动关闭方(比如客户端)会发送一个连接释放报文段(FIN报文),并停止发送数据,主动关闭TCP连接。此时,客户端的FIN报文段会将终止控制位(FIN)设置为1,序列号为u,它等于客户端之前发送的最后一个字节序列号加1。客户端进入FIN-WAIT-1状态,等待服务器的确认。

  2. 服务器收到FIN报文后会立即发送一个ACK确认报文,确认号为ack = u + 1,并且它的序列号为v,等于它之前发送的最后一个字节的序列号加1。此时,服务器进入CLOSE-WAIT状态。此时TCP连接处于半关闭状态,即客户端已经没有数据要发送,但服务器如果继续发送数据,客户端仍然能接收。

  3. 客户端收到来自服务器的确认后,进入FIN-WAIT-2状态,等待服务器发送连接释放报文段。

  4. 如果服务器已经没有数据需要发送,它会通知TCP层释放连接,此时服务器会发送一个FIN报文,设置FIN = 1,序列号为w(服务器可能已经发送了一些数据)。同时,服务器会再次发送ack = u + 1,并进入LAST-ACK状态,等待客户端的确认。

  5. 客户端收到服务器的FIN报文后会发送一个确认报文,设置ACK = 1,确认号ack = w + 1,序列号seq = u + 1,进入TIME-WAIT状态。此时TCP连接还没有完全关闭,客户端必须等待2倍的最大报文段生存时间(2MSL),之后才能进入CLOSED状态。最后,客户端会撤销相应的传输控制块(TCB),完成连接关闭。

TIME-WAIT与CLOSE-WAIT的状态

TIME-WAIT状态是由主动关闭连接的一方引起的,目的是为了确保最后的ACK包能够到达对方,并且确保旧的报文段不会干扰新的连接。CLOSE-WAIT状态则表示被动关闭连接的一方(通常是服务器)正在等待发送自己的FIN报文。

6.2 Socket编程与四次挥手的关系

在Socket编程中,当客户端主动调用close(fd)时,系统会开始四次挥手过程。客户端会发送FIN报文,进入FIN-WAIT-1状态,等待服务器的ACK报文。当服务器收到客户端的FIN后,会发送ACK并进入CLOSE-WAIT状态,等待自己发送FIN报文。这个过程的核心就是调用close(fd)来触发连接的释放。

6.2.1 测试CLOSE-WAIT

为了测试CLOSE-WAIT状态,可以通过去掉服务器端的close(sockfd),只保留客户端的close(sockfd)来模拟服务端不关闭连接的情况。这时,服务器将保持在CLOSE-WAIT状态,而客户端进入FIN-WAIT-2状态。

// 服务端的Socket编程示例
#include "Sock.hpp"int main() {Sock sock;int listensock = sock.Socket();sock.Bind(listensock, 8877);sock.Listen(listensock);while (true) {std::string clientip;uint16_t clientport;int sockfd = sock.Accept(listensock, &clientip, &clientport);if (sockfd > 0) {std::cout << "[" << clientip << ":" << clientport << "]# " << sockfd << std::endl;}}
}

在运行这段代码时,可以通过netstat命令查看服务端的连接状态,发现服务端进入CLOSE-WAIT状态,客户端进入FIN-WAIT-2状态。

6.2.2 测试TIME-WAIT

当服务端主动关闭连接时,连接会进入TIME-WAIT状态。TIME-WAIT状态的作用是等待2倍的最大报文段生存时间(2MSL),确保最后的ACK包可以到达,并且防止重传的旧数据影响新的连接

如果服务端在TIME-WAIT状态后重新启动,它会遇到端口号占用的问题,因为同一个端口在TIME-WAIT状态下仍然被占用,直到MSL时间过后才能释放。

1. TIME_WAIT 状态的定义

处于TIME_WAIT状态的一端,表示:

  • 它正在等待一段时间,确保对方成功收到了最后一个ACK包,或者确保可能出现的重复FIN包被处理。

  • 它处于一个半关闭状态。半关闭是指,它已经发送了FIN包,表示不再发送数据,但仍然可以接收对方的数据,直到对方也发送FIN包。

2. TIME_WAIT的作用

  1. 保证TCP连接可靠关闭:主动关闭连接的一方会进入TIME_WAIT状态,确保最后一个ACK报文能够到达对方,防止ACK丢失的情况导致对方无法正常关闭连接。

  2. 消除旧报文段的影响TIME_WAIT状态可以确保在网络中旧的重复报文段被丢弃。避免新的连接使用相同的端口号时,收到旧的连接报文,从而造成数据错乱。

3. 为什么是2倍MSL?

TIME_WAIT状态持续2倍MSL(最大报文段生存时间,Maximum Segment Lifetime),这是因为:

  • 保证ACK包能到达:客户端发送的最后一个ACK包可能会丢失,而丢失的ACK包可能导致处于LAST-ACK状态的服务器无法接收到确认。此时,服务器会超时并重发FIN-ACK报文,客户端需要再次确认,这样才会保证连接被正确关闭。

  • 确保重复报文段消失:如果不等待2倍MSL的时间,就有可能新连接的报文与老连接的重传报文段混淆,从而引发数据错乱。

4. 半关闭状态

半关闭状态是指:一方已经不再发送数据,但它仍然能够接收数据。在TCP连接中,一方发送FIN包后进入半关闭状态,允许另一方继续向其发送数据,直到对方也发送FIN包,才完全关闭连接。

5. TIME_WAIT的缺点

  1. 端口资源占用TIME_WAIT状态会占用端口资源,如果大量连接进入TIME_WAIT状态,会导致端口资源耗尽,无法建立新的连接。

  2. 延长连接的释放时间:在TIME_WAIT状态下,新的连接无法使用相同的端口号,因此如果有新的连接请求到来,必须等待现有连接的TIME_WAIT状态结束。

6. TIME_WAIT的持续时间

通常,TIME_WAIT状态的持续时间为2倍MSL,MSL值通常设置为2分钟或4分钟。具体取决于操作系统或网络环境。

6.2.3 解决TIME-WAIT问题

通过设置SO_REUSEADDR选项,可以解决TIME-WAIT状态占用端口的问题。这样,即使在TIME-WAIT状态下,服务器也能立即重用原来的端口号。

int Socket() {int listensock = socket(AF_INET, SOCK_STREAM, 0);int opt = 1;setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));return listensock;
}

这种方法常用于服务器需要高并发和快速重启的场景,尤其是在大规模分布式系统中,避免因TIME-WAIT而导致的连接延迟。

6.2.4 为什么客户端不受TIME-WAIT影响?

客户端在TCP连接中通常会使用动态端口,这些端口号属于动态端口范围(49152-65535),并且在连接关闭后会被释放。相反,服务端通常使用固定的端口号,这使得它更容易受到TIME-WAIT状态的影响。

由于客户端的端口是临时分配的,并且一旦连接关闭,这些端口会被释放,所以客户端不会像服务端那样受到TIME-WAIT状态的影响。

6.2.5 TIME-WAIT 状态的作用

TIME-WAIT状态的主要作用是保证连接的可靠关闭,并确保任何遗失的ACK报文能够被正确接收,从而避免重复的FIN报文造成混乱。它还能够确保任何旧的连接请求报文不会影响到新的连接。

但是,TIME-WAIT状态也有它的缺点,特别是在高并发的服务器中,它会占用端口资源,导致新的连接无法及时建立。因此,合理的配置和优化是非常必要的。

7. TCP的效率策略

7.1 流量控制

流量控制的概念

流量控制是为了防止发送方发送数据过快,超出接收方处理能力的一种机制。TCP协议通过接收和发送缓冲区来实现流量控制。发送方和接收方都维护缓冲区,在数据传输过程中,接收方会通过发送ACK报文告知发送方它的接收缓冲区还剩多少空间。通过这种方式,发送方能够调整数据发送的速率,确保不会导致接收方的缓冲区溢出。

假设服务端的接收缓冲区满了,而客户端并没有意识到这种情况,仍然不断向服务端发送数据,那么很容易出现丢包的现象。为了避免这种情况,流量控制机制就被引入了。它的作用是控制发送方的发送速度,以便接收方能够有足够的时间处理数据。

流量控制的实现方式

在TCP连接中,流量控制是通过16位窗口大小字段来实现的。这个字段告诉发送方接收方当前的接收缓冲区大小,也就是接收方的剩余接收空间。发送方根据这个信息调整其发送数据的速率,确保接收方能够处理数据。

连接建立时如何保证数据发送量合理

在TCP连接建立过程中,双方通过三次握手来协商彼此的接收能力。在第一次握手时,发送方会发送一个SYN报文,其中包含了自己的初始序列号(ISN)和最大报文段大小(MSS)。接收方会在第二次握手时回复一个SYN+ACK报文,其中包含接收方的ISN、MSS和窗口大小(即接收缓冲区的剩余空间)。这样,双方就能够知道彼此的接收能力,合理地控制数据发送量。

流量控制的工作过程

假设接收方B的接收缓冲区大小为4000字节:

  1. 主机A首先发送一个1000字节的数据段。

  2. 接收方B收到数据后,返回一个ACK,并告知A剩余的接收空间为3000字节。

  3. A根据B的窗口大小(3000字节)继续发送数据,直到B的缓冲区满。

  4. 如果B的接收缓冲区满了,B会返回一个窗口大小为0的ACK,告诉A暂停发送数据。

  5. A在没有收到B更新的窗口大小时,会通过发送一个窗口探测包来请求B重新更新接收缓冲区的剩余空间。

TCP窗口大小和扩展

TCP默认的16位窗口大小最大为65535字节,但是TCP还支持通过窗口缩放选项来扩大窗口的范围。窗口缩放选项允许在三次握手期间协商一个缩放因子,最大可以将窗口扩展到1GB。


7.2 滑动窗口

滑动窗口的由来

在没有滑动窗口协议之前,发送方和接收方发送一个数据包后就必须等待接收方的确认。这样,每发送一个包就要等待确认,吞吐量非常低。为了解决这个问题,滑动窗口机制被引入。

滑动窗口允许发送方连续发送多个数据包,并且不必等到前一个数据包的确认就能继续发送下一个包,从而提高了吞吐量和传输效率。

滑动窗口的工作原理

在滑动窗口协议中,发送方和接收方分别维护一个发送窗口接收窗口。窗口内的数据表示可以继续传输的数据范围。接收方通过ACK确认已经收到的数据,并通过窗口大小告知发送方可接收的数据量。

随着发送数据包的到达,发送窗口会向前滑动,表示已发送的数据不再需要重传。接收窗口也会随着ACK确认的到来而滑动,表示接收方已经成功接收的数据。

滑动窗口的工作示例

假设发送方需要发送400字节的数据,并且每个数据包大小为100字节:

  1. 发送方首先发送三个100字节的数据包(序号1-100、101-200、201-300)。

  2. 接收方收到第一个数据包(序号1-100),并返回ACK(ack=101),表示它准备接收序号101的下一个字节。

  3. 发送方收到ACK后,滑动窗口会向前滑动,继续发送剩余数据包。

丢包情况

如果某个数据包丢失,接收方不会确认丢失的数据包,而是继续确认已收到的数据。发送方会根据丢失数据包的ACK缺失情况触发超时重传机制,重新发送丢失的数据包。

在滑动窗口协议中,发送方会将数据分成多个段(数据包)并依次发送,每个数据包都有一个唯一的序列号。接收方根据这些序列号对收到的数据进行确认,并通过ACK报文通知发送方。如果某个数据包丢失了,接收方就无法确认该数据包,这时发送方需要重传丢失的数据。

1. 正常情况下滑动窗口的工作

假设发送方发送了多个数据包,每个数据包的序列号分别为1到100,101到200,201到300等。接收方收到数据包后,会按照顺序确认接收到的数据,并返回一个包含下一个期望字节的确认号(ACK)。例如,接收方收到1-100的数据段后,返回ACK=101,表示接收到了序号为100的字节,期望下一个字节的序号是101。

2. 丢包的情况

假设发送方发送了多个数据包,如下所示:

  • 第一个数据包:1到100

  • 第二个数据包:101到200

  • 第三个数据包:201到300

然而,在数据传输过程中,第二个数据包(序号101到200)丢失了,接收方并没有收到它。接收方会按照顺序继续接收其他数据包,例如序号为201到300的数据包。尽管接收方收到了序号为201到300的数据,但由于它没有收到序号为101到200的数据包,接收方无法确认该数据包。

3. 接收方的行为

接收方不会丢弃已经收到的数据,而是会将它们存储在接收缓冲区中,等待丢失的数据包的到来。接收方在确认数据时会发送一个重复的ACK,指示它仍然在等待丢失的数据包。例如,接收方已经收到了201到300的数据段,但它会发送ACK=101,因为它仍然在等待序号为101到200的数据包。

此时,接收方的窗口行为如下:

  • 它已成功接收了序号为1到100和201到300的数据,但它会继续返回ACK=101,以提醒发送方重传丢失的数据。

  • 接收方将不会对序号为201到300的数据段发送确认,直到它成功收到序号为101到200的包并能按顺序重组数据。

4. 发送方的行为

发送方会定期等待ACK确认,以便知道哪些数据包已成功到达接收方。如果发送方收到了重复的ACK(例如,ACK=101),这意味着接收方仍然在等待序号为101到200的数据包。因此,发送方会判断为丢包,并根据TCP的超时重传机制(Timeout Retransmission)重新发送丢失的包。

发送方的行为流程:

  • 发送方已经发送了序号1到300的三个数据包,但它只收到了序号为1到100和201到300的ACK。

  • 因为接收方没有收到101到200的数据包并且返回了重复的ACK=101,发送方会触发超时重传机制,并重新发送101到200的丢失数据包。

  • 一旦发送方重新发送了丢失的数据包(序号101到200),接收方就会再次确认并返回ACK=301,表示它已经成功接收到序号为1到300的数据。

5. TCP的快速重传(Fast Retransmit)

如果发送方连续收到3个重复的ACK(即重复ACK的次数大于等于3次),它会立即触发快速重传(Fast Retransmit),而不等待超时。快速重传的目的是尽早发现丢包并尽快恢复数据的传输。

快速重传的过程:

  • 在我们的例子中,如果发送方连续收到3个ACK=101的报文(即接收方重复确认它已经接收到的部分数据),发送方就会立即重传丢失的序号101到200的数据段,而不需要等待超时。

  • 通过快速重传,发送方可以减少丢包造成的延迟,提高数据传输的效率。

6. 滑动窗口中的丢包与流量控制的关系

丢包不仅影响数据的传输速度,还与TCP的流量控制机制密切相关。接收方根据滑动窗口大小来决定是否可以继续接收数据。如果接收方的缓冲区已经满了,它会通过返回一个窗口大小为0的ACK报文来告诉发送方暂停发送数据,直到接收方有足够的空间继续接收数据。

如果发生丢包,接收方会无法确认丢失的数据包。此时,滑动窗口会保持不变,直到丢失的数据包被重新发送并成功确认。

7. 丢包的窗口行为示例

  • 假设发送方正在发送数据包,接收方已确认序号1到100和201到300的数据包,但丢失了101到200的数据包。

  • 接收方会一直返回ACK=101,并不会确认序号201到300的数据,直到它收到序号101到200的数据。

  • 发送方会通过快速重传机制重新发送丢失的数据包,并在接收到新的ACK后,滑动窗口会向前移动,继续发送后续的数据包。

8. 总结

滑动窗口中的丢包处理是TCP协议确保可靠传输的重要机制之一。丢包会触发TCP的超时重传和快速重传机制,确保丢失的数据包能够尽快恢复传输。通过接收方发送重复ACK和发送方进行超时重传或快速重传,TCP能够有效地恢复丢失的数据,并确保数据按顺序正确到达接收方。

在滑动窗口协议中,丢包的发生并不会导致整个连接的中断,而是通过这种可靠的重传机制和滑动窗口的控制,保证数据最终能够完整并有序地传输。


7.3 延迟应答

延迟应答是一种提高传输效率的机制。它通过在发送确认应答时延迟一段时间来增加接收方缓冲区的剩余空间,从而能够返回一个更大的窗口值。通过这种方式,接收方能够一次性处理更多的数据。

例如,假设接收方的缓冲区为1MB,接收到500KB的数据。如果接收方立即返回ACK,窗口大小为500KB,但如果接收方等待一定时间,它可能会在这段时间内处理部分数据,将窗口大小扩大到更高的值,从而提高传输效率。


7.4 捎带应答

捎带应答是延迟应答的进一步优化。接收方在收到数据包后,不立即发送确认应答,而是等待一段时间,看看是否有更多数据到达。如果接收方在这个时间内收到更多数据,它会将确认应答和数据一起发送,从而减少网络上的小数据包和开销。


7.5 拥塞控制

7.5.1 拥塞控制的前提理解

除了流量控制,TCP还需要考虑中间网络的情况。网络可能由于拥堵导致丢包,这时TCP的拥塞控制机制就会启动,避免网络负载过大。拥塞控制的目标是调整发送速度,减少网络压力,确保数据能高效且可靠地传输。

7.5.2 拥塞窗口

拥塞窗口(cwnd)表示当前网络的拥塞情况,它是发送方用于控制发送速率的一个变量。拥塞窗口的大小随着网络状况的变化动态调整,当网络出现拥塞时,拥塞窗口会减小,避免发送过多数据给网络造成更大的压力。

拥塞窗口与发送窗口的关系

  • 拥塞窗口(cwnd):表示当前网络的拥塞情况,控制发送方可以发送的数据量。

  • 发送窗口(swnd):由接收方的接收窗口(rwnd)和拥塞窗口(cwnd)决定,表示发送方实际可以发送的数据范围。

拥塞控制机制:根据网络状态,发送方会动态调整拥塞窗口的大小,以优化传输效率。


7.5.3 拥塞控制算法

TCP的拥塞控制主要依赖拥塞窗口(cwnd),它控制着发送方可以发送的未确认数据量。TCP通过慢启动和拥塞避免算法来逐步增加传输速度,避免网络拥塞。

慢启动

  1. 起始窗口小:刚开始时,TCP设置一个非常小的窗口(通常为1个MSS),这样发送的数据量很小,避免一开始就给网络带来过大压力。

  2. 指数增长:每次发送的报文都得到ACK确认后,拥塞窗口会增加1个MSS,窗口会迅速增长。这样,TCP能尽快找到合适的传输速率。

  3. 风险:指数增长的速度非常快,如果不加限制,可能会导致网络出现拥堵和丢包。因此,当拥塞窗口达到一定大小时,增长速度会减慢。

拥塞避免

  1. 阈值设定:当拥塞窗口达到一个预设的阈值(ssthresh)后,窗口大小的增长从指数增长转为线性增长。每次确认后,窗口大小只增加1个MSS,这样可以避免网络拥塞。

  2. 丢包时回退:如果网络发生拥塞导致丢包,TCP会将慢启动阈值减半,拥塞窗口重置为1个MSS,重新进入慢启动阶段。

拥塞窗口的变化

  • 窗口增大:在正常的情况下,TCP通过指数增长来加速传输速率,直到达到阈值,之后会切换到线性增长。

  • 发生丢包时:如果丢包发生,说明网络出现了拥塞,TCP会减少窗口大小并重新开始慢启动。


8.TCP的几个补充知识点

8.1 面向字节流

在TCP中,数据是以字节流的形式进行传输的,TCP不关心字节流的内容,它只会按顺序将这些字节流从发送方传输到接收方。具体来说:

  • 发送缓冲区:当你调用write()函数写数据时,数据会进入TCP的发送缓冲区。如果缓冲区已满,write()会阻塞,直到缓冲区有足够空间。

  • 接收缓冲区:接收到的数据存放在接收缓冲区,调用read()时会读取这些数据。如果接收缓冲区为空,read()会阻塞,直到数据到达。

TCP会根据窗口大小拥塞控制流量控制等因素来动态调整数据的发送量,数据可能会被拆分成多个包发送,也可能会被合并在一起。

面向字节流的关键点:

  • TCP看不出数据的边界,只会按顺序传递字节流。

  • 你写100个字节的数据,可以通过1次write或者100次write来完成;同样的,读取100个字节的数据也可以通过1次read或者多次read来完成。

8.2 粘包问题

粘包问题发生在应用层,它通常是因为TCP作为面向字节流的协议,并不知道应用层的数据包的边界。结果,发送的多个数据包可能会被接收方合并成一个数据包,或者一个数据包被拆成多个部分。

  • 原因:TCP在传输时只是简单地发送字节流,没有像UDP那样有“报文长度”的字段,因此接收方无法准确区分数据包的边界。

  • 解决办法

    • 定长包:可以使用固定长度的包,每次都按固定大小读取。

    • 变长包:在包头约定包的总长度,接收方通过读取包头的长度信息,来确定包的边界。比如,HTTP协议中的Content-Length字段。

    • 分隔符:为每个包定义一个明确的分隔符,接收方根据分隔符来解析数据包。

UDP没有粘包问题:因为UDP是面向报文的协议,每个数据包是独立的,UDP不会拆分或合并数据包。它只是简单地将数据包从源端传输到目的端。

8.3 TCP连接异常问题

TCP连接过程中可能会出现各种异常,以下是常见的几种情况:

  1. 连接建立异常

    • 客户端发送SYN包后没有收到服务端的SYN+ACK,可能是网络不通或服务端未监听该端口。

    • 服务端拒绝连接,可能是服务端未启动或端口已关闭。

  2. 连接断开异常

    • 客户端或服务端发送FIN包后未收到对方的ACK包,可能是网络不稳定或对方宕机。

    • 如果连接已断开,服务端或客户端重新启动后可能会遇到连接建立失败的问题。

  3. 传输数据异常

    • 数据丢失、乱序、重复、校验和错误等都可能导致数据传输异常,通常通过超时重传、滑动窗口等机制进行修复。

  4. 客户端掉线

    • 当客户端掉线时,服务器端可能在短时间内不会知道客户端已掉线,连接会一直保持。TCP会使用心跳机制(基于保活定时器)来检查客户端是否还在线,如果多次未收到ACK,服务器会关闭连接。

8.4 Linux文件和Socket的关系

创建网络套接字的时候,操作系统会创建很多数据结构,其中有一个叫做: 

struct socket {socket_state		state;kmemcheck_bitfield_begin(type);short			type;kmemcheck_bitfield_end(type);unsigned long		flags;/** Please keep fasync_list & wait fields in the same cache line*/struct fasync_struct	*fasync_list;wait_queue_head_t	wait;struct file		*file;struct sock		*sk;const struct proto_ops	*ops;
};

struct socket 是 Linux 内核中用于表示和管理网络套接字的核心数据结构。套接字(socket)是网络编程中的重要概念,它提供了一个接口,允许进程间进行网络通信。下面是对 struct socket 结构体中各个成员的详细解释:

主要成员说明

  1. socket_state state

    • 该成员表示套接字的当前状态,例如是否已连接,是否正在监听等。socket_state 是一个枚举类型,用于定义套接字可能的各种状态。

  2. kmemcheck_bitfield_begin(type) 和 kmemcheck_bitfield_end(type)

    • 这两个宏用于标记 type 成员,以便在内存检查工具(如 kmemcheck)中对其进行特殊处理。这通常是为了提高内存检查的准确性或效率。

    • type 是一个 short 类型,指定套接字的类型(例如 SOCK_STREAM 表示面向连接的 TCP 套接字,SOCK_DGRAM 表示无连接的 UDP 套接字)。

  3. unsigned long flags

    • 该字段是一个标志位字段,用于存储与套接字相关的各种标志。可以表示套接字的当前配置或状态,例如是否启用了某些选项,是否正在被关闭等。

  4. *struct fasync_struct fasync_list

    • 这是一个指向 fasync_struct 结构体的指针,用于支持套接字的异步通知。

    • 当套接字上有数据可读、可写或发生错误时,可以通过这个机制来通知等待的进程或线程。

  5. wait_queue_head_t wait

    • 该成员表示一个等待队列的头部,管理那些等待套接字事件的进程或线程。

    • 当套接字处于某种需要等待的状态(例如等待数据可读)时,进程或线程可以将自己加入到这个等待队列中。

  6. *struct file file

    • 该成员是一个指向 file 结构体的指针,表示套接字与哪个文件(实际上是文件描述符)相关联。

    • 在 Linux 中,一切皆文件,因此套接字也通过文件描述符来进行访问。struct file 结构包含了文件的基本信息,套接字在操作系统中也是以文件形式进行管理的。

  7. *struct sock sk

    • 这是一个指向 sock 结构体的指针,sock 结构体包含了套接字的核心数据,例如协议相关的状态、接收和发送缓冲区等。

    • struct socketstruct sock 之间的关系可以看作是接口(struct socket)和实现(struct sock)之间的关系。struct socket 提供了对外暴露的操作接口,而 struct sock 则处理实际的协议和数据传输。

  8. *const struct proto_ops ops

    • 该成员是一个指向 proto_ops 结构体的指针,proto_ops 结构体包含了一系列操作函数指针,这些函数用于操作套接字,包括创建、连接、接收、发送等。

    • 根据协议类型的不同(例如 TCP 或 UDP),proto_ops 结构体会有所不同,确保每种协议的套接字操作能够得到支持。

补充:const struct proto_ops *ops字段就是我们所调用的方法:

​
struct proto_ops {int		family;struct module	*owner;int		(*release)   (struct socket *sock);int		(*bind)	     (struct socket *sock,struct sockaddr *myaddr,int sockaddr_len);int		(*connect)   (struct socket *sock,struct sockaddr *vaddr,int sockaddr_len, int flags);int		(*socketpair)(struct socket *sock1,struct socket *sock2);int		(*accept)    (struct socket *sock,struct socket *newsock, int flags);int		(*getname)   (struct socket *sock,struct sockaddr *addr,int *sockaddr_len, int peer);unsigned int	(*poll)	     (struct file *file, struct socket *sock,struct poll_table_struct *wait);int		(*ioctl)     (struct socket *sock, unsigned int cmd,unsigned long arg);int	 	(*compat_ioctl) (struct socket *sock, unsigned int cmd,unsigned long arg);int		(*listen)    (struct socket *sock, int len);int		(*shutdown)  (struct socket *sock, int flags);int		(*setsockopt)(struct socket *sock, int level,int optname, char __user *optval, unsigned int optlen);int		(*getsockopt)(struct socket *sock, int level,int optname, char __user *optval, int __user *optlen);int		(*compat_setsockopt)(struct socket *sock, int level,int optname, char __user *optval, unsigned int optlen);int		(*compat_getsockopt)(struct socket *sock, int level,int optname, char __user *optval, int __user *optlen);int		(*sendmsg)   (struct kiocb *iocb, struct socket *sock,struct msghdr *m, size_t total_len);int		(*recvmsg)   (struct kiocb *iocb, struct socket *sock,struct msghdr *m, size_t total_len,int flags);int		(*mmap)	     (struct file *file, struct socket *sock,struct vm_area_struct * vma);ssize_t		(*sendpage)  (struct socket *sock, struct page *page,int offset, size_t size, int flags);ssize_t 	(*splice_read)(struct socket *sock,  loff_t *ppos,struct pipe_inode_info *pipe, size_t len, unsigned int flags);
};​

而网络服务的本质是进程,在内核当中叫做struct task_strcut,Linux一切皆文件,每个文件有自己的文件描述符struct files_struct,里面包含了struct file*fd_array[],当文件被打开之后会创建strcut file,里面有一个字段叫:

void*   private_data;

当是网络服务的时候,它就会指向struct socket,而struct socket里面的有一个字段struct file *file;,它会回指向文件

所以最终网络文件挂接到struct file之下,在应用层通过文件描述符就能找到网络文件。

struct socket结构体里面有一个wait_queue_head_t wait;,它是一个自定义类型,转到定义:

struct __wait_queue_head {spinlock_t lock;struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;

里面有一个进程队列,进程在阻塞等待的时候,实际上要将自己的pcb链入指定的数据结构里面

所以,当网络数据不就绪的时候,将指定的进程挂接到这个wait里面等待即可

        ​从操作系统到网络struct sock *sk(struct socket的一个成员);,它里面有接收队列和发送队列:

  struct sk_buff_head    sk_receive_queue;struct sk_buff_head    sk_write_queue;

在创建套接字的时候传入SOCK_STREAM或者SOCK_DGRAM,这就表明是面向字节流的还是面向数据报的,sk就指向struct udp_sock或者struct tcp_sock的开头

struct udp_sock

struct udp_sock {/* inet_sock has to be the first member */struct inet_sock inet;int		 pending;	/* Any pending frames ? */unsigned int	 corkflag;	/* Cork is required */__u16		 encap_type;	/* Is this an Encapsulation socket? *//** Following member retains the information to create a UDP header* when the socket is uncorked.*/__u16		 len;		/* total length of pending frames *//** Fields specific to UDP-Lite.*/__u16		 pcslen;__u16		 pcrlen;
/* indicator bits used by pcflag: */
#define UDPLITE_BIT      0x1  		/* set by udplite proto init function */
#define UDPLITE_SEND_CC  0x2  		/* set via udplite setsockopt         */
#define UDPLITE_RECV_CC  0x4		/* set via udplite setsocktopt        */__u8		 pcflag;        /* marks socket as UDP-Lite if > 0    */__u8		 unused[3];/** For encapsulation sockets.*/int (*encap_rcv)(struct sock *sk, struct sk_buff *skb);
};

struct tcp_sock

struct tcp_sock {/* inet_connection_sock has to be the first member of tcp_sock */struct inet_connection_sock	inet_conn;u16	tcp_header_len;	/* Bytes of tcp header to send		*/u16	xmit_size_goal_segs; /* Goal for segmenting output packets *//**	Header prediction flags*	0x5?10 << 16 + snd_wnd in net byte order*/__be32	pred_flags;/**	RFC793 variables by their proper names. This means you can*	read the code and the spec side by side (and laugh ...)*	See RFC793 and RFC1122. The RFC writes these in capitals.*/u32	rcv_nxt;	/* What we want to receive next 	*/u32	copied_seq;	/* Head of yet unread data		*/u32	rcv_wup;	/* rcv_nxt on last window update sent	*/u32	snd_nxt;	/* Next sequence we send		*/u32	snd_una;	/* First byte we want an ack for	*/u32	snd_sml;	/* Last byte of the most recently transmitted small packet */u32	rcv_tstamp;	/* timestamp of last received ACK (for keepalives) */u32	lsndtime;	/* timestamp of last sent data packet (for restart window) *//* Data for direct copy to user */struct {struct sk_buff_head	prequeue;struct task_struct	*task;struct iovec		*iov;int			memory;int			len;
#ifdef CONFIG_NET_DMA/* members for async copy */struct dma_chan		*dma_chan;int			wakeup;struct dma_pinned_list	*pinned_list;dma_cookie_t		dma_cookie;
#endif} ucopy;u32	snd_wl1;	/* Sequence for window update		*/u32	snd_wnd;	/* The window we expect to receive	*/u32	max_window;	/* Maximal window ever seen from peer	*/u32	mss_cache;	/* Cached effective mss, not including SACKS */u32	window_clamp;	/* Maximal window to advertise		*/u32	rcv_ssthresh;	/* Current window clamp			*/u32	frto_highmark;	/* snd_nxt when RTO occurred */u16	advmss;		/* Advertised MSS			*/u8	frto_counter;	/* Number of new acks after RTO */u8	nonagle;	/* Disable Nagle algorithm?             *//* RTT measurement */u32	srtt;		/* smoothed round trip time << 3	*/u32	mdev;		/* medium deviation			*/u32	mdev_max;	/* maximal mdev for the last rtt period	*/u32	rttvar;		/* smoothed mdev_max			*/u32	rtt_seq;	/* sequence number to update rttvar	*/u32	packets_out;	/* Packets which are "in flight"	*/u32	retrans_out;	/* Retransmitted packets out		*/u16	urg_data;	/* Saved octet of OOB data and control flags */u8	ecn_flags;	/* ECN status bits.			*/u8	reordering;	/* Packet reordering metric.		*/u32	snd_up;		/* Urgent pointer		*/u8	keepalive_probes; /* num of allowed keep alive probes	*/
/**      Options received (usually on last packet, some only on SYN packets).*/struct tcp_options_received rx_opt;/**	Slow start and congestion control (see also Nagle, and Karn & Partridge)*/u32	snd_ssthresh;	/* Slow start size threshold		*/u32	snd_cwnd;	/* Sending congestion window		*/u32	snd_cwnd_cnt;	/* Linear increase counter		*/u32	snd_cwnd_clamp; /* Do not allow snd_cwnd to grow above this */u32	snd_cwnd_used;u32	snd_cwnd_stamp;u32	rcv_wnd;	/* Current receiver window		*/u32	write_seq;	/* Tail(+1) of data held in tcp send buffer */u32	pushed_seq;	/* Last pushed seq, required to talk to windows */u32	lost_out;	/* Lost packets			*/u32	sacked_out;	/* SACK'd packets			*/u32	fackets_out;	/* FACK'd packets			*/u32	tso_deferred;u32	bytes_acked;	/* Appropriate Byte Counting - RFC3465 *//* from STCP, retrans queue hinting */struct sk_buff* lost_skb_hint;struct sk_buff *scoreboard_skb_hint;struct sk_buff *retransmit_skb_hint;struct sk_buff_head	out_of_order_queue; /* Out of order segments go here *//* SACKs data, these 2 need to be together (see tcp_build_and_update_options) */struct tcp_sack_block duplicate_sack[1]; /* D-SACK block */struct tcp_sack_block selective_acks[4]; /* The SACKS themselves*/struct tcp_sack_block recv_sack_cache[4];struct sk_buff *highest_sack;   /* highest skb with SACK received* (validity guaranteed only if* sacked_out > 0)*/int     lost_cnt_hint;u32     retransmit_high;	/* L-bits may be on up to this seqno */u32	lost_retrans_low;	/* Sent seq after any rxmit (lowest) */u32	prior_ssthresh; /* ssthresh saved at recovery start	*/u32	high_seq;	/* snd_nxt at onset of congestion	*/u32	retrans_stamp;	/* Timestamp of the last retransmit,* also used in SYN-SENT to remember stamp of* the first SYN. */u32	undo_marker;	/* tracking retrans started here. */int	undo_retrans;	/* number of undoable retransmissions. */u32	total_retrans;	/* Total retransmits for entire connection */u32	urg_seq;	/* Seq of received urgent pointer */unsigned int		keepalive_time;	  /* time before keep alive takes place */unsigned int		keepalive_intvl;  /* time interval between keep alive probes */int			linger2;/* Receiver side RTT estimation */struct {u32	rtt;u32	seq;u32	time;} rcv_rtt_est;/* Receiver queue space */struct {int	space;u32	seq;u32	time;} rcvq_space;/* TCP-specific MTU probe information. */struct {u32		  probe_seq_start;u32		  probe_seq_end;} mtu_probe;#ifdef CONFIG_TCP_MD5SIG
/* TCP AF-Specific parts; only used by MD5 Signature support so far */const struct tcp_sock_af_ops	*af_specific;/* TCP MD5 Signature Option information */struct tcp_md5sig_info	*md5sig_info;
#endif
};

之后要访问其他属性内容,只需强转就能访问(struct tcp_sock*)sk,本质就是C语言的多态

我们的网络协议栈的本质就是:

用特定数据结构表述的协议
和特定协议匹配的方法集

如果是网络文件,struct file里面的const struct file_operations *f_op;有指向网络的方法;而struct sock里面的const struct proto_ops *ops;也有指向网络的方法。

        前者解决的是对上的,后者是解决对下交付的

操作系统内会同时收到很多,如果这些报文上层来不及处理,那么操作系统内就会存在很多报文,对应这些报文,操作系统是需要管理起来的——先描述,再组织

struct sk_buff_head {/* These two members must be first. */struct sk_buff    *next;struct sk_buff    *prev;__u32        qlen;spinlock_t    lock;
};

这个sk_buff也是自定义类型:

struct sk_buff {/* These two members must be first. */struct sk_buff        *next;struct sk_buff        *prev;//...//...sk_buff_data_t        transport_header;sk_buff_data_t        network_header;sk_buff_data_t        mac_header;/* These elements must be at the end, see alloc_skb() for details.  */sk_buff_data_t        tail;sk_buff_data_t        end;unsigned char        *head,*data;    unsigned int        truesize;atomic_t        users;
};

将报文交给每层,实际上就是将sk_buff在层和层之间流动,加报头就是头指针向上移动(封装),去掉报头就是头指针向下移动(解包) ,这就是先描述;

报文到了传输层之后,将报文分发给不同的文件描述符,实际上就是将sk_buff组织到对应的缓冲区当中。

所以说建立连接和维护连接是有成本的,因为要在内核当中创建大量的数据结构。

简单示意图:


文章转载自:

http://J5nH1TJC.pfggj.cn
http://upZcE2Qy.pfggj.cn
http://1wnx9PN0.pfggj.cn
http://UYqtURFQ.pfggj.cn
http://1eKSutge.pfggj.cn
http://ejQlA25k.pfggj.cn
http://uQIZTkak.pfggj.cn
http://5z89SaM9.pfggj.cn
http://ShiJGfF4.pfggj.cn
http://gtPKvR2T.pfggj.cn
http://qjcBOrRa.pfggj.cn
http://hr11HT7A.pfggj.cn
http://pOIBrApz.pfggj.cn
http://uaMRLJYX.pfggj.cn
http://Iv7bOXrg.pfggj.cn
http://lYNPIo4g.pfggj.cn
http://EAipSLcW.pfggj.cn
http://27XMGfQe.pfggj.cn
http://xRN2s4GZ.pfggj.cn
http://REWrSBBj.pfggj.cn
http://x3Wc2kn8.pfggj.cn
http://pzL3FKDv.pfggj.cn
http://cQpgXT2r.pfggj.cn
http://8qLuxfKm.pfggj.cn
http://JSxsHitV.pfggj.cn
http://DMpZEEJ6.pfggj.cn
http://gQKd81yZ.pfggj.cn
http://krLPTcgF.pfggj.cn
http://OBKDjggL.pfggj.cn
http://umLZc9IE.pfggj.cn
http://www.dtcms.com/a/386108.html

相关文章:

  • ChromaDB探索
  • 无人设备遥控器之帧同步技术篇
  • redis如何搭建哨兵集群(docker,不同机器部署的redis和哨兵)
  • C#之开放泛型和闭合泛型
  • typescript+vue+node项目打包部署
  • Python/JS/Go/Java同步学习(第十五篇)四语言“字符串去重“对照表: 财务“小南“纸式去重术处理凭证内容崩溃(附源码/截图/参数表/避坑指南)
  • 数据库基础知识入门:从概念到架构的全面解析
  • 负载均衡器和CDN层面保护敏感文件:防止直接访问.git等敏感目录
  • 微算法科技(NASDAQ: MLGO)研究隐私计算区块链框架,赋能敏感数据流通
  • 分析并预测糖尿病患者 R
  • 【Cesium 开发实战教程】第四篇:动态数据可视化:实时 GPS 轨迹与时间轴控制
  • 大数据毕业设计选题推荐-基于大数据的快手平台用户活跃度分析系统-Spark-Hadoop-Bigdata
  • HTML打包EXE工具中的WebView2内核更新指南
  • 固定资产管理软件是什么?哪家好?对比分析10款产品
  • gdb-dashboard使用
  • 【脑电分析系列】第13篇:脑电源定位:从头皮到大脑深处,EEG源定位的原理、算法与可视化
  • 【51单片机】【protues仿真】基于51单片机SHT11温湿度系统
  • 【Vue3 ✨】Vue3 入门之旅 · 第二篇:安装与配置开发环境
  • 【30】C# WinForm入门到精通 ——字体控件FontDialog 【属性、方法、事件、实例、源码】
  • 使用Nginx+uWSGI部署Django项目
  • 芯伯乐低噪声轨到轨运放芯片XAD8605/8606/8608系列,11MHz带宽高精度信号调理
  • FPGA硬件设计6 ZYNQ外围-HDMI、PCIE、SFP、SATA、FMC
  • FPGA硬件设计5 ZYNQ外围-USB、SD、EMMC、FLASH、JTAG
  • 知识图谱中:基于神经网络的知识推理解析~
  • 深度学习面试题:请介绍梯度优化的各种算法
  • python资源释放问题
  • ATR网格---ATR计算原理研究运用
  • 用Postman实现自动化接口测试
  • Hyper Rust HTTP 库入门教程
  • 软考系统架构设计师之软件架构评估法-ATAM