前言
hello hello💕,这里是洋不写bug~😄,欢迎大家点赞👍👍,关注😍😍,收藏🌹🌹
TCP是有连接的,这个连接是抽象的,通信双方各自保存对方的关键信息(IP,端口)
建立连接时需要通过三次握手,连接断开时需要通过四次挥手,这两个环节是整个网络部分面试最高频、最重要的问题,这篇博客就会详细的解析三次握手和四次挥手相关知识🐵
🎆个人主页:洋不写bug的博客
🎆所属专栏:JavaEE学习
🎆铁汁们对于JavaEE的各种常用核心语法,都可以在上面的JavaEE专栏学习,专栏正在持续更新中🏀,有问题可以写在评论区或者私信我哦~
1,三次握手
1,流程
TCP通信在建立连接时,需要通过三次握手(handshake),这三次握手传输的是“打招呼”数据包,这个数据包中不携带任何的业务数据,也就是TCP数据包没有载荷,只有报头
这个就类似于出去谈业务,发现客户对方是多年未见的老同学,约在一起吃饭时,就先不谈业务,先聊一些之前上学的事情以及近况,拉近一下感情,建立一下连接
三次挥手如下图,步骤如下:
- 客户端给服务器发送个同步报文段(syn),告诉服务器:我要和你建立连接,请你保存我的信息
synchronized英文翻译是同步,这个术语在不同对方有不同含义,在多线程中表示的是一种互斥锁,在网络编程中表示的就是同步报文段 - 服务器收到后,给客户端返回一个应答报文段(ack),告诉客户端:收到,我会保存信息
- 服务器也给客户端发送个syn,告诉客户端:我也要跟你建立连接,请你保存我的信息
- 客户端收到后,给服务器返回一个ack,告诉服务器:收到,我会保存信息

因为建立连接是一个相互的过程,通过这种方式能保证客户端保存了服务器连接,服务器也表示了客户端连接
这个过程在逻辑上是四次交互,但是中间两次,可以合成一个TCP数据包(如下图),网络上实际只传输了三个数据包,因此就称为三次握手

区分报文段的类型,是看标志位(如下图),ACK的值为1,就说明当前报文段是应答报文段,SYN的值为1,就说明当前报文段是同步报文段

前面提到“三次握手”逻辑上是四次交互,有的铁汁可能回想:一定要交互四次,才能确保建立连接了吗?
举个例子,博主和哥们在玩pubg,开始玩之前前需要验证自己的麦克风和耳机没问题,也需要验证对方麦克风和耳机是没问题的,如下图:
-
首先博主说“能听到我说话吗“,哥们听到后,就能推断出他自己的耳机没问题,也能推断出博主的麦克风没问题
-
博主听到哥们回复的“可以听到”,说明哥们收到了前面博主回复的消息,说明博主的麦克风和哥们的耳机都没有问题,这里博主又听到了哥们的声音,说明博主的耳机和哥们的麦克风都没问题

这里测试麦克风就相当于测试发送能力,测试耳机就相当于测试接收能力
如果像上面只进行两次交互的话,虽然博主可以推出我们两个的麦克风和耳机都没问题,但是哥们是没法确定自己的麦克风和博主的耳机没问题(也就是接收方无法确定自己的发送能力和发送方的接收能力没问题)
所以还要再加上两次交互逻辑,哥们问博主说:那你能听见我说话吗,博主回复说可以,哥们就能确定他自己的麦克风和博主的耳机没问题了
第二次交互和第三次交互可以合在一起,就构成了“三次握手”,如下图:

2,作用
三次握手主要有两个作用:
- 验证通信链路是否畅通
- 验证通信双方的发送能力和接收能力是否正常
- 通过三次握手,让通信双方协商关键信息
验证通信链路是否畅通就类似于地铁,在早上会先空车从头到尾跑一遍线路,中间不停,不载客,因为经过一晚上,可能会出现一些异常,比如轨道漏水/电路问题,先空车跑一遍验证下通行是否正常,如果不检测,带着一车乘客,中途遇到故障停下来,就比较麻烦了😅
光是通信链路畅通还不够,还需要验证通信双方的发送能力和接收能力是正常的,也就是第二点
三次握手时,通信双方也会协商一些通信过程中的关键信息,主要协商的就是TCP数据的起始序号,这个序号就是按照字节进行编号,方便接收方验证和拼接数据,保证数据传输的可靠性(网络编程(四)中有解析),如下图
每次建立连接时,协商出来的编号都是不同的,而且编号的差值还是很大的

有的铁汁可能会想:每次建立连接这个起始序号直接从0/1开始不就行了,为什么三次握手时双方要重新协商,而且每次建立连接时起始序号的差别值都很大呢?
这个主要是为了解决网络通信中的一种特殊情况下出现的问题,客户端和服务器先通过三次挥手建立了连接,接着传输了一段时间数据,服务器和客户端断开连接(通过四次挥手)
但是,这个断开连接不一定是不通信了,可能是客户端想重启一下,断开后再去建立连接,如下图

这时候就会出现一个问题:假如有个数据包的传输路径有点拥堵,到达服务器的时候,这时候已经是其他的连接了,如果服务器接收这个数据包的话,就会收到一堆莫名其妙、不属于当前会话的脏数据,业务直接错乱
因此服务器遇到这种上次连接传过来的数据包时,就要直接丢弃,判断数据包是这次连接传过来的,还是上次连接传过来的,就是看这个数据包的序号是否跟本次连接的起始序号相接近(因此不同连接约定的起始序号的差值要很大)
三次握手的整套逻辑是在操作系统的内核中完成的,客户端在new Socket时,就已经触发三次握手了,服务器通过accept把已经建立好的连接拿到应用程序中时,三次握手就已经完成了
TCP通信过程中,服务器和客户端涉及到的状态还是比较多的,这里先介绍握手时的两种主要状态:
- LISTEN:服务器存在的状态,服务器new ServeSocket时就会进入LISTEN表示在“监听”这个端口,有客户端连接上这个端口,就能够accept了
- ESTABLISHED:表示客户端和服务器的连接已经建立好了,服务器和客户端就可以进行通信了
在命令行上输入netstat -ano,就能看到这些连接的状态,如下图:

2,四次挥手
四次挥手是断开连接时的流程,可能时客户端先发起的断开连接,也可能是服务器先发起的断开连接,而三次握手,则一定是客户端先发起的
四次挥手流程如下(拿客户端发起的举例):
- 客户端告诉服务器,我要和你断开连接,请你把我删了
- 服务器回复收到
- 服务器告诉客户端,我也要和你断开连接,请你也把我删除了
- 客户端回复收到

客户端和服务器把对方的信息(之前保存的对方的IP和接口)删除掉,删除后,连接就断开了,这个请求断开的报文的标志位中的FIN就是1,用来进行标注

有的铁汁可能会想:既然逻辑上都是四次交互,为什么三次握手能把中间两次交互合并成一次发送,四次挥手就不行呢?
三次握手的操作在内核中进行,客户端在new Socket时,就已经触发三次握手了,服务器通过accept把已经建立好的连接拿到应用程序中时,三次握手就已经完成了,是一定能把中间两次发送合并成一个数据包的
四次挥手就不一定能把中间两次发送合并成一个数据包了,这个铁汁们可以结合前面的网络编程(三)中的TCP客户端和服务器的代码来理解
当客户端进程退出或者手动调用socket.close(),都会触发向服务器发送FIN,服务器收到FIN后向客户端发送ACK
服务器的processConnection方法如下,服务器先用scanner.hasNext()感知到客户端断开连接了,退出while(true)循环,接着执行socket.close,断开跟这个客户端的连接,这时候才发送FIN
如果代码中在socket.close()前加上一些耗时逻辑,那服务器向客户端发送FIN需要等一段时间,而服务器回复ACK是在收到客户端的FIN后就立刻回复的,这两次逻辑发送是有时间差距的,因此没办法合并成一个数据包发送,
private void processConnection(Socket socket){
System.out.printf("[%s:%d] 客户端上线!\n",socket.getInetAddress().toString(),socket.getPort());
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scanner = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream,true);
while (true){
//1.读取请求并解析
if (!scanner.hasNext()){
System.out.printf("[%s:%d] 客户端下线!",socket.getInetAddress().toString(),socket.getPort());
break;
}
String request = scanner.next();
//2.根据请求计算响应
String response = process(request);
//3.把响应写回客户端
writer.println(response);
System.out.printf("[%s:%d] req:%s; resp: %s\n",socket.getInetAddress().toString(),socket.getPort(),request,response);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
//耗时逻辑
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
因为不一定每次都能触发合并,把两次发送逻辑合并成一个数据包,因此就叫做“四次挥手”
“四次挥手”过程中有两个状态比较重要:TIME_WAITED和CLOSE_WAIT

- CLOSE_WAIT:被动断开连接的一方收到FIN后,返回ACK,同时进入该状态,这个状态等待应用程序代码调用close,也就是等待关闭,这个状态的保持时间并不会太长,所以在cmd中用netstat -ano查看连接状态时,会发现处于这个状态的连接是很少的

- TIME_WAIT:类似于线程的TIME_WAITED,为有限制时间的等待,主动发起断开连接的一方,在收到对方发回的FIN后,返回ACK,并且进入TIME_WAIT状态
那为什么返回完ACK后,不直接close断开连接呢,还要先进入TIME_WAIT状态等一会?
因为发送个ACK可能会出现丢包情况,如下图,如果客户端没有等,直接释放连接了,后面服务器触发数据重传,重新传输FIN,不管触发几次数据重传,都不可能收到客户端ACK的回应,那这个四次握手过程就没有完成,因此客户端需要先等一段时间再断开连接

这个TIME_WAIT等待时间,就用2 * MSL(Maximum Segment Lifetime,报文段最大生存时间)来计算,MSL就可以理解为网络上任意两点之间,传输消耗的最大时间
但现实中,想要准确的测出MSL的值是不现实的,就会把MSL的值设置的宽裕一些,例如Linux上默认设置的是60s,这个时间是远远大于很多节点之间的传输时间的,多等一会,就是为了避免出现前面提到的ACK丢包后重传FIN无人应答问题
结语💕💕
TCP协议的的关键状态共有四个,简单总结如下:
- LISTEN:手机开机,信号良好,可以随时打电话
- ESTABLISHED:连接已经建立,电话接听,可以说话了
- CLOSE_WAIT:等待代码调用close
- TIME_WAIT:等待FIN重传,应对最后一个ACK丢包的情况
- 三次握手和四次挥手的过程很简单,重点是想清楚为什么一定要交互三次/四次,在交互时解决了什么样的问题,在面试时面试官问到相关知识时,可以在纸上简单的画个图,结合图来回答问题,就会更加清楚🐵
以上就是今天的所有内容啦~完结撒花~🥳🎉🎉

🎆个人主页:
2957



被折叠的 条评论
为什么被折叠?



