【Linux C++】网络编程:TCP的三次握手和四次挥手
日期:2025.2.6(凌晨)2025.2.7(凌晨)
学习内容:
-
TCP的三次握手和四次挥手
-
makefile入门
个人总结:
这篇文字敲得累,但是个人觉得很适合新手了解,对于我这种没学过计网的真的花了一段时间理解。
还有,本人对于三次握手四次挥手的理解也比较浅显,如果有不对的地方请指出。感激不尽。
劳烦看下去。
TCP 的三次握手和四次挥手:
我们要先来简单介绍几个包:
-
SYN包:
主要是用来申请连接请求和同步序列初始号的,(关于同步序列初始号的内容后面会说),也可以简单粗暴的理解成,SYN包就是请求连接的一个包,客户端和服务端最开始两个家伙都没有交集,客户端把他发送给服务端,服务端就知道客户端要来连接了。
-
ACK包:
就像刚才说的,服务端知道客户端要来连接了,需要向客户端发送自己已经知道了客户端的连接的请求,于是会回复一个ACK包,这个包是代表接收的意思,当客户端收到了ACK包,就知道了服务端收到了自己刚才发送的SYN包。
-
FIN包:
这个包就跟SYN包有点像,他们都是管理连接方面的包,SYN包是代表的请求开启连接,FIN包是代表的请求关闭连接。把这个包发送给对方(这里是对方,不一定是服务端,但是大多数都是服务端,原因等会会讲),对方就知道了自己想要关闭连接。
在 TCP 协议下,客户端和服务端之间进行的数据传输都是用的数据包传输,也就是 TCP 数据包。
而TCP数据包的开头一部分存有一些数据,我们称为TCP报头。
有关三次握手和四次挥手的内容与报头的关系性不是特别大,也不会多影响理解,所以想直接看握手挥手的也可以直接跳。
在介绍报头之前,先简单介绍一下序列号和确认号:
TCP 报头:
TCP(传输控制协议)报头是 TCP 数据包开头的一部分数据,用于管理 TCP 连接以及确保数据可靠传输。就像是包裹的标签,告诉接收方如何处理这个数据包。
每一个 TCP 传输的数据中当中都会含有序列号(就是按照顺序的编号),这个序列号主要是用来检测数据有没有丢失的。
我们直接看例子了解序列号和确认号是什么:
假设客户端发送了序列号为 100 - 109 的数据,服务器成功接收后,会发送一个确认报文,其中的确认号为 110,表示期望下一个收到的报文段的第一个字节序号为 110。我们就知道了,对方100-109号的数据都收到了,数据是正常传输的,我们就放心接着传输下去了。
如果这个过程出现了意外,105号数据丢失了。剩余的包都正常传递。接收方即使收到了106号以及之后的数据,也没有办法正常处理,因为它只有在受到105号数据之后才会接着继续正常进行。(因为在 TCP 协议的默认处理机制下,接收方通常会按序处理数据)。此时接收方会发送确认报文,确认号始终为 105,表示它还没有成功接收 105 号数据。发送方接收到了之后,会重新发送该数据。
这样可以保证数据传输的完整性,确保数据可靠传输。
另外这里有个细节是:
- 接收方会暂时先存储起来106号到109号的数据,然后重新接收到105号的数据,之后再自动的按顺序组合起来,不需要再重新传送106号之后的数据。
- 刚才我们提到SYN包主要是用来申请连接请求和同步序列初始号的,这里同步序列初始号就是在最开始连接的时候设置的。最开始发送SYN包不仅申请连接,顺便也使得客户端和服务端同步序列号,来确保数据一致。
三次握手:
先来简单介绍一下三次握手的流程是什么(抛开状态不谈):
- 客户端向服务器发送 SYN 包。
- 服务器接收到客户端的 SYN 包后,自动回复 SYN + ACK 包。
- 客户端收到 SYN + ACK 包后,发送 ACK 包。服务器收到客户端的 ACK 包。
刚好,总共三条。
这三条其实通过我们最开始介绍包的概念就可以简单的诠释了三次握手是干嘛的了。
首先客户端向服务端发送申请连接的请求,服务端收到了之后回复收到了,顺便也发送了自己也申请连接的请求,客户端收到了服务端的请求之后也向服务端发送自己接收到了的消息。
这是基础的一个大的框架,那讲细一些,我们将这中间接收到和发送包的各种时间阶段给服务端和客户端划分了各自的状态。有助于确保连接的有序性和可靠性。
服务端的状态的变化:
在此之前,要先说明一点,我们通常说的状态的转换是指的是套接字的状态的转换。
需要先了解之前说的网络编程写简单的服务端的内容:
整个过程大概如下:
-
调用了listen函数。我们的监听套接字。会变为LISTEN状态。
if (listen(listen_fd, 5) != 0) { perror("listen"); close(listen_fd); return -1; }
-
监听描述符处于
LISTEN
状态时,收到客户端的 SYN 包,已连接套接字的状态会转变为SYN - RECV
状态。 -
在
SYN - RECV
状态下发送 SYN + ACK 包,继续等待客户端的 ACK 包。 -
收到客户端的 ACK 包后,从
SYN - RECV
状态转变为ESTABLISHED
状态。
这里说明一点,使得套接字转换状态的最重要的函数就是listen函数,网上有人说执行accept函数,收到客户端的ACK包,从而转换成了ESTABLISHED状态,在这之前会处于SYN-RECV状态。但是这样说其实是不对的。
accept
函数是从已完成连接队列中取出一个已经完成三次握手的连接。返回一个新的用于和客户端进行数据通信的套接字描述符。具体而言:
- 当服务器调用
listen
函数后,内核会为该监听套接字维护一个已完成连接队列和一个未完成连接队列。 - 三次握手成功完成的连接会被放入已完成连接队列。
accept
函数的功能就是从这个已完成连接队列中取出一个连接。
另外关于这一点的证明:
if (listen(listen_fd, 2) != 0) {
perror("listen");
close(listen_fd);
return -1;
}
sleep(100000);
int client_fd = accept(listen_fd, 0, 0);
if (client_fd == -1) {
perror("accept");
close(listen_fd);
return -1;
}
在调用了listen函数之后sleep,之后才会调用accept函数。
运行代码,之后使用命令行,输入命令。
netstat -na|grep 5005
(5005)是我的服务端开放的端口。
终端会出现:
tcp 1 0 0.0.0.0:5005 0.0.0.0:* LISTEN
tcp 1 0 192.168.11.132:5005 192.168.11.132:50030 ESTABLISHED
tcp 0 0 192.168.11.132:50030 192.168.11.132:5005 ESTABLISHED
每一行代表的是监听套接字的listen状态,第二行代表的是已连接套接字的状态,第三行代表的是客户端套接字的状态,都是ESTABLISHED。
这说明了完成三次握手后接字的状态的转换与调用accept函数并没有直接关系。
TCP三次握手的状态的变化是由系统内核的TCP协议栈自动管理的。
客户端的状态的变化:
以下需要先了解之前说的网络编程写简单的客户端的内容:
最开始是套接字描述符的状态是CLOSED
状态 -> 之后调用 connect
发送 SYN 包,会进入 SYN - SENT
状态。
还记得之前讲的客户端的第三部分吗?connect,向服务端发送连接请求。
if (connect(sock_fd, (struct sockaddr*)&serv_addr, sizeof serv_addr) != 0) {
perror("connect");
close(sock_fd);
return -1;
}
SYN - SENT
状态下,主要的任务就是等待服务器对 SYN 包的回应。
再之后收到服务端的 SYN + ACK 包,发送 ACK 包,便会进入ESTABLISHED
状态,成功建立连接。
到此大概的讲述了三次握手。
如果是两次握手?
假如采用两次握手来建立连接,即客户端发送 SYN 包请求建立连接,服务器收到后发送 SYN + ACK 包表示同意连接,连接就建立成功。这种方式会有什么问题呢?
-
无法确保客户端接受能力正常
第一次发送 SYN 包,这个操作只能表明客户端当前能够将数据包发送到网络中并传向服务器,也就是体现了客户端的发送能力。但对于服务器来说,它无法通过这一个 SYN 包知晓客户端是否能够正常接收来自服务器的数据包。如果服务器发送 SYN + ACK 包后就认为连接已经建立,开始发送数据。如果客户端因为网络故障等原因没有收到服务器的 SYN + ACK 包,而服务器却已经开始发送数据,那么这些数据对于客户端来说是无效的,会造成资源浪费。
-
连接混乱或错误
考虑有延迟的影响,如客户端发的第一个SYN包有网络延迟。客户端在发送了这个包之后便放弃了,重新发送了一个新的SYN包。但服务端却接收到了一开始第一个发的SYN包开始传输数据,便会导致混乱和错误。
其实很像四次握手?
三次握手其实也可以理解成类似四次的交互过程。(以下结合四次挥手看会好一些)
客户端先发送 SYN 包发起连接请求,服务端收到后,理论上可以分两步回应:第一步发送 ACK 包确认收到客户端的 SYN 包;第二步发送 SYN 包发起自己的连接请求。最后客户端再发送 ACK 包确认收到服务端的 SYN 包。
但是在三次握手场景下,服务端没有数据残留需要处理的问题(不同于四次挥手的地方),所以可以将确认客户端 SYN 和发起自身连接请求这两个操作合并在一个数据包里发送。这样可以减少一次数据包的传输,更快地完成连接建立过程,尤其在网络状况不佳时,减少交互次数能显著提升性能。
四次挥手:
四次挥手的流程:
- 第一次挥手:客户端完成数据发送后,向服务器发送一个
FIN
包,表示自己没有数据要发送给服务器了,但仍可接收服务器的数据。此时客户端进入FIN - WAIT - 1
状态。 - 第二次挥手:服务器收到客户端的
FIN
包后,发送一个ACK
包进行确认。服务器进入CLOSE - WAIT
状态,意味着服务器知道客户端要关闭连接,但自身可能还有数据要发送。客户端收到ACK
包后进入FIN - WAIT - 2
状态,等待服务器的FIN
包。 - 第三次挥手:服务器完成自身数据发送后,向客户端发送
FIN
包,表示自己也没有数据要发送了。此时服务器进入LAST - ACK
状态。 - 第四次挥手:客户端收到服务器的
FIN
包后,发送ACK
包进行确认,然后进入TIME - WAIT
状态。服务器收到这个ACK
包后进入CLOSED
状态,客户端在TIME - WAIT
状态等待一段时间(通常是 2 倍的最大段生命周期,即 2MSL)后也进入CLOSED
状态,至此连接完全关闭。
另外我们在三次握手的时候提到了这种情况,大多数都是客户端作为最开始发FIN包的一方。
通过流程上我们也能够看出来最先发送FIN包要断开连接的一方,最后将会进入TIME-WAIT
的状态,需要等待2MSL时间才会close。
客户端和服务端连接的时候,服务端的端口是固定的,而客户端的端口是随机生成的。则服务端等待一段时间显然不宜,而客户端不会有太大影响。这也是为什么客户端作为发送的一方。
FIN - WAIT - 1
状态
当客户端(主动关闭方)发送 FIN
包给服务器(被动关闭方)后,客户端就进入 FIN - WAIT - 1
状态:
- 等待确认:客户端发送
FIN
包表明自己没有数据要发送给服务器了。因为数据包可能会丢失或延迟,所以客户端需要在这个状态停留,等待服务器对这个FIN
包的确认,以确保服务器收到了关闭请求。
FIN - WAIT - 2
状态
当客户端收到服务器针对其 FIN
包发送的 ACK
包后,就从 FIN - WAIT - 1
状态进入 FIN - WAIT - 2
:
- 等待服务器关闭:虽然客户端已经关闭了自己向服务器发送数据的通道,但服务器可能还有数据要发送给客户端。客户端在
FIN - WAIT - 2
状态等待服务器发送FIN
包,以表示服务器也完成了数据发送,准备关闭连接。
客户端为什么要在 TIME - WAIT
状态等待 2MSL :
-
确保最后一个ACK包能够被服务器收到
如果这个
ACK
包在传输过程中丢失,服务器没有收到确认,服务器会重传FIN
包。客户端在TIME - WAIT
状态等待 2MSL 时间,就有足够的时间来接收服务器可能重传的FIN
包,并再次发送ACK
包进行确认。如果客户端直接进入CLOSED
状态,服务器重传的FIN
包就会得不到响应,服务器将无法正常关闭连接。 -
防止旧的数据包干扰新的连接
如果客户端直接进入
CLOSED
状态并立即建立一个新的连接,而这个新连接使用了与旧连接相同的 IP 地址和端口号,那么旧的延迟数据包可能会被错误地当作新连接的数据包进行处理,从而干扰新连接的正常通信。等待 2MSL 时间可以确保网络中所有与该连接相关的延迟数据包都已经消失。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 推荐几款开源且免费的 .NET MAUI 组件库
· 实操Deepseek接入个人知识库
· 易语言 —— 开山篇
· Trae初体验