【网络篇】TCP协议的三次握手和四次挥手
TCP协议的三次握手和四次挥手
- 连接管理机制
- 建立连接
- 三次握手
- TCP三次握手的意义
- 确保路径畅通
- 验证双方能力
- 协商必要参数
- 断开连接
- 三次握手和四次挥手的异同点
- TCP连接管理中的状态转换
- 三次握手中涉及到的状态
- LISTEN
- ESTABLISHED
- 四次挥手中涉及到的状态
- CLOSE_WAIT
- TIME_WAIT
连接管理机制
定义:
连接管理机制的定义就是TCP如何建立连接,如何断开连接
建立连接
定义:
建立连接就是客户端主动给服务器发送一个syn
发送这个syn的目的就是为了表达客户端想要与服务器建立连接的意愿
这个syn就是一个特殊的数据报:
- 没有载荷,只有TCP报头,以太网数据帧,不带应用层数据
- TCP报头中的六个标志位的第五个标志位是1
- 带有TCP报头和IP报头,以太网数据帧,包括了客户端的端口和IP
此处这个syn的意思就是客户端在说 :“服务器,我想和你建立连接”
此时我们回到服务器的视角:
此时服务器有两个选择:
服务器的第一个选择:
服务器不愿意建立连接:很少见的情况,一般出现在服务器的负载太高了,想要与服务器建立连接的客户端太多了。
服务器的第二个选择:
服务器愿意建立连接:
此时服务器就会向客户端发送两个东西:
1.== ACK==,确认应答,向客户端表示已经收到
2. syn:此时的服务器返回的syn表示的就是服务器同意与客户端建立连接
TCP的三次握手,就是TCP在建立连接的过程中,需要通信双方先去“打三次招呼”才能完成连接的建立。
这个syn本质就是synchronized :同步的意思,
和之前学习过的多线程的知识里面的加锁一个词
但是在多线程里面的那个加锁synchronized是加锁的意思
目的是为了协调多个线程的执行顺序
但是此处的syn是同步的意思,是为了让客户端和服务器共同进入连接装态
建立连接的本质就是客户端和服务器各自向对方发送一个syn,然后再各自发送一个ack
当建立连接之后,客户端与服务器就是同进退,相互配合完成一系列的工作
这个syn也被称为握手:表示打招呼
syn(同步报文段),是一个特殊的TCP数据包,没有载荷,不携带业务数据的应用层数据包
如何区分这个数据包是一个普通的数据包,还是一个同步报文段(syn)呢?
在TCP报头中有一个标志位是S Y N:
如果这个标志位S Y N 为1,那么就是一个同步报文段,如果为0,那就不是同步报文段
三次握手
如下所示:
在第一次客户端向服务器发送syn的时候,在这个syn里面就包含了客户端的端口和IP
此时服务器在第一次握手的时候,就已经收到了客户端的信息了
但是此时服务器还没有建立连接,服务器也就还没有保存客户端的信息
是一直等到所有的握手都完成了之后,此时服务器才会完成建立连接的过程
此时的服务器才会去保存客户端的信息
此处虽然看上去是四次握手,但是其实在中间的服务器返回的两次都可以合并为一次的
将服务器返回的syn和ack都合并为一个数据报发送给客户端是可以的
为什么可以合并呢?
因为syn中的第五个标志位是1,ack中的第二个标志位是1
此时只需要让一个数据包中的第五个标志位(SYN)和第二个标志位(ACK)都为1
然后将这个标志位返回给服务器即可
为什么要将中间的两次交互合并为一次呢:
因为数据包在网络传输的过程当中,要经过路由器/交换机的封装分用
如果发送两个数据包(syn和ack分开发送),就要经历两次封装分用
如果只是发送一个数据包(syn和ack合并发送),就只需要经历一次封装分用,效率更高
如下就是TCP的三次握手示意图:
乱码出现的原因:
- 传输的二进制数据被破坏/错误,代码编写错误,可能下标是从1开始的,那就不是完整的UTF8字符了
- 传输的二进制数据是正确的,但是通信双方理解的编码格式不一样(传输方是UTF8,但是接收方是GBK)
如何解决:
将传输的数据使用十六进制打印一下,
printf(“%x”,buffer[i]);
将字节中的每一个数据进行打印,然后对照UFT8的码表看看传输的数据是不是正确的
TCP三次握手的意义
为什么要三次握手?
两次握手行不行?
四次握手行不行?
答:
两次握手不可以,四次握手可以,但是四次握手没必要
为什么要进行三次握手?
- 确保路径畅通
- 验证双方能力
- 协商必要参数
确保路径畅通
三次握手是为了确保数据传输的路径是畅通的(路径畅通是TCP可靠性的前提)
比如每天早上的地铁第一班车都是空车运一趟,就是为了去验证确保地铁的路线是畅通可传输的
注意:虽然TCP三次握手确实可以对TCP的可靠性传输起到一定的作用,但是TCP的可靠传输的核心机制是确认应答+超时重传机制
TCP三次握手只是在开始建立连接之前对于可靠性传输有用,但是在连接建立完成之后就与TCP的可靠连接无关了
验证双方能力
TCP的三次握手是为了去验证通信双方的发送能力和接收能力是否正常
为什么是三次?两次行不行?四次行不行?
TCP的三次握手每一次握手都起到了一定的验证通信双方的作用:
如下如所示:
如果只有两次握手,没有最后一次的握手,那么服务器并不知道自己的发送能力和客户端的接收能力是否正常,那么此时就缺少了“TCP可靠传输”的前提了,因此必须通过最后一次握手把上述信息同步给服务器。
协商必要参数
TCP的三次握手的过程中需要协商一些必要的参数
因为通信是客户端和服务器双方共同需要去完成的工作
双方要相互配合才能够去完成通信,所以在通信双方中的一些内容需要保持一致
同时TCP中有很多的参数需要进行协商,一般是以“选项”的方式来体现的:
这个选项存在于TCP的报头当中,
在选项这一部分中占的内存,最少0字节,最多40字节(TCP报头总长度60,去掉最前面固定的20,还剩下40字节)
在TCP三次握手的过程中,需要去确定下来一个重要的信息:TCP通信序号的起始值是多少?
需要去协商确定好序号的起始值是多少:
在TCP通信过程中,序号不是从0/1开始计算的,而是选择一个比较大的数字,从这个数字的开头开始计算
所以即使是同一个客户端和服务器,每次连接时,开始的序号都是不同的
这样做是为了防止出现一个问题: 前朝的剑,斩本朝的官
什么意思:
如下图所示:
当TCP的三次握手完成之后,第一次连接建立成功,通信双方就会开始进行数据传输
当连接断开之后,此时在第一次通信过程中,
可能会有一个数据包在中间堵住了,迟迟没有到达目的地。
但是此时已经开始第二次连接了,已经开始新的连接了,
但是上一次连接的数据包才刚刚到达对端,但是上一次的连接早就已经断开了,
早就物是人非了,那么此时再去处理这个前朝数据包就不合理,需要把这个前朝数据包给丢弃掉,
为什么必须要丢弃掉前朝数据包?
因为数据包是IP+端口号来进行传输的
第一次连接是客户端A进行连接,但是第二次是客户端B进行连接,此时端口号都不一样了
如果再去处理这个前朝数据包,就会发生错误,所以必须丢弃掉
既然必须要丢弃掉这种前朝数据包,那么在TCP中是如何识别这种前朝数据包的呢?
答案就是通过数据包中的序号来进行识别的
我们的TCP三次握手过程中会协商确定好数据报的序号是多少,而且这个序号不是随机分配的,而是具有一定的分配策略的
这个序号的分配策略就可以保证让前朝数据包和本朝数据包的序号之间的差异非常大
正常的数据包是从开始序号一直向后延续的,即使会偶尔出现丢包,序号不连续的情况,但是序号与序号之间的差异不会很大,差异很小
但是前朝数据包的序号和本朝数据包的序号的差异就会非常大了
TCP就很容易根据序号的差异分辨出前朝数据包,然后进行丢弃
断开连接
连接的本质: 通信双方都保存对端的信息
如果对端都有很多的信息,那么就需要通过数据结构来进行保存和组织
断开连接的本质:通信双方在数据结构中把保存的对端的所有信息全部进行删除/释放
通过四次挥手可以实现断开连接的目的
但是四次挥手只是适用于通信双方都想要去断开连接的情况
如果有一方不愿意断开连接,那么就不可以使用四次挥手的方式去断开连接
就需要去使用其他的方式去断开连接:
四次挥手的具体流程如下图所示:
第一次挥手:是向对方发送一个FIN(结束报文段)
这个结束报文段就是TCP报头中的标志位的第六个标志位,当第六个标志位是1的时候就表示为FIN(结束报文段)
这个FIN的发送顺序没有先后之分,谁先发,谁后发都可以,取决于代码的编写方式
但是TCP三次握手就一定是客户端先发送syn
当执行socket文件关闭的代码:socket.close(),就会触发这个FIN数据包,
所以FIN的操作也是在系统内核负责完成的
值得一提的是:不仅仅是代码scoket.close()会触发socket文件的关闭,进程的结束也会触发socket文件的关闭,因为当进程结束的时候,PCB也会被销毁,在PCB中的socket文件也就会被关闭
当客户端执行socket.close()代码之后,就会触发FIN数据包
客户端会将这个FIN数据包发送给服务器
服务器在接收到FIN数据包之后,就会立马返回一个ACK给客户端
然后服务器还会发送一个FIN给客户端
之后客户端会返回一个ACK给服务器
那么这个四次挥手中间的两次:服务器发给客户端的ACK和FIN这两次传输可以合并为一次传输吗:
四次挥手可以可以像三次握手那样,合并中间的两次传输吗:
答案是有时候可以合并,有时候不可以合并:
什么时候是不可以合并的:
当服务器发送的ACK与FIN的触发时机不一样时,两者之间的时间间隔很长的时候,就不可以合并:
因为ACK是服务器内核在接收到了客户端发来的FIN之后立即返回的
而服务器返回的FIN是在代码中的Socket.close()执行的时候触发的
如果此时ACK在接收到了客户端的FIN之后立即就返回了,但是此时的Socket.close()还迟迟没有执行,没有发送FIN,就会导致两个数据包之间的发送时间间隔很长,此时就不可以进行合并
而在三次握手中的中间那一次服务器发送的syn+ack都是在服务器内核中完成的,当接收到客户端的syn之后,这两个数据报(syn+ack)就会立即发送,这两个数据包的触发时机是一致的
什么时候是可以合并的?
就是当服务器发送的ACK与FIN之间的时间间隔很小的时候,就是可以合并的:
(可以通过TCP中的延时应答和捎带应答来实现)
三次握手和四次挥手的异同点
相同点:
- 都是需要通信双方互相发送一个syn/fin,然后双方再返回一个ack
- 数据报的发送顺序基本一致: syn/ack/syn/ack , fin/ack/fin/ack,而且中间的两次的两次数据报发送都是服务器发送的
不同点:
1.三次握手中中间的syn和ack可以合并为一次传输,但是四次挥手中间的fin和ack有时候能合并,有时候不能合并
2. 三次握手的最开始必须是客户端主动传输数据,四次挥手中客户端与服务器谁主动传输数据都可以
TCP连接管理中的状态转换
此处的“状态"和多线程中的线程状态类似
状态本质就是某一个实体现在正在干什么
因为在TCP中,服务器和客户端连接时,需要通过数据结构来保存通信双方连接的各种信息
在这个数据结构中就有一个属性叫做“状态”
操作系统内核就可以根据这个状态来明确确定当前要去干什么(保证不混乱,不迷茫)
我们只需要去学习几个重点的TCP状态即可,后续的各种状态当我们在开发/调试的时候再去学习也可以
三次握手中涉及到的状态
LISTEN
这个listen状态是服务器的状态: 表示当前的服务器已经创建好了ServerSocket,同时绑定好了端口,随时准备进行通信/连接
(手机开机了,信号畅通良好了,随时可以开始打电话了)
如何去观察这个listen状态?
1: 打开并且运行之前写过的TCP服务器代码:
2:接着打开命令行窗口:输入==netstat -ano | findstr 9090 ==
图片的解读如下所示:
ESTABLISHED
表示已确立的,当出现这个状态的时候,就说明客户端与服务器的连接已经建立好了,说明TCP的三次握手已经完成了。
当连接建立之后,服务器和客户端几乎会同时进行ESTABLISHED状态
当我们在代码中启动了服务器之后,立马启动客户端时,此时客户端与服务器就建立好了连接
我们在命令行输入刚刚的命令即可:==netstat -ano | findstr 9090 ==
四次挥手中涉及到的状态
CLOSE_WAIT
被动断开连接会进入CLOSE_WAIT状态:
当本端收到通信对端发送的FIN之后,会进入的状态:
当进入CLOSE_WAIT状态之后,就需要去代码中调用Socket.close()来主动发送FIN
注意:这个CLOSE_WAIT状态不容易观察到:
因为当代码中 调用了Socket.close()之后,会快速地关闭Socket,此时这个CLOSE_WAIT状态的持续时间很短,状态会马上从CLOSE_WAIT 转换为LAST_ACK
所以CLOSE_WAIT状态一般很难观察到
如果在代码调试阶段,在客户端/服务器中出现了大量的CLOSE_WAIT状态,说明代码出现了bug,一般是Socket文件没有关闭
TIME_WAIT
主动断开连接会进入TIME_WAIT状态
当本端给通信对端发送FIN之后,通信对端也回复了一个FIN之后,此时本端就会进入TIME_WAIT状态
TIME_WAIT更加容易观察到
进入TIME_WAIT状态就是为了给发送最后一个ACK重传保留时间:
什么意思?
如果TCP的四次挥手如下所示:
当服务器给客户端发送FIN之后,客户端收到FIN会返回最后一个ACK
但是如果最后一个ACK在传输过程中发生了丢包,无法到达服务器
服务器没有收到最后一个ACK ,就会重传一个FIN
但是由于在刚刚客户端收到第一次的FIN的时候就已经断开了连接
将TCP连接释放了,用来保存对端信息的数据结构也被销毁了,
我们的系统内核是通过这个保存对端信息的数据结构来进行各种操作的,没有这个数据结构了,那么也就无法返回ACK了,此时服务器即使重传FIN也无法收到ACK了:
如何解决这个问题的?
我们使用TIME_WAIT状态来表示:当本端给通信对端发送FIN,通信对端也回复了一个FIN之后
当进入TIME_WAIT状态之后不会立马断开连接,而是会等待一段时间
如果在等待时间过了之后,依旧没有重传过来的FIN
那么说明最后一个ACK成功到达,ACK没有丢包,此时才会断开连接
但是这个等待时间也不是一直等下去,这个等待时间最多等待2MSL,(MSL是一个系统内核的配置项,表示客户端到服务器之间消耗的最长时间,这个时间一般都是非常大的,常见的1MSL是1分钟
这个TIME_WAIT状态就表示客户端等待很长的时间,如果在这个很长的时间里面,服务器都没有重传FIN的话,说明服务器已经收到了最后一个ACK了,不会再再也不会重传FIN了