三次握手和四次挥手
三次握手与四次挥手都是为了解决一个问题: 在不可靠的网络连接中建立可靠传输
TCP报文段部首格式
序号: 用于对字节流进行编号,例如序号为 301,表示第一个字节的编号为 301,如果携带的数据长度为 100字节,那么下一个报文段的序号应为 401,用来解决网络包乱序问题
确认号: 期望收到的下一个报文段的序号。例如 B 正确收到 A 发送来的一个报文段,序号为 501,携带的数据长度为 200 字节,因此 B 期望下一个报文段的序`号为 701,B 发送给 A 的确认报文段中确认号就为 701,用来解决丢包问题
数据偏移: 指的是数据部分距离报文段起始处的偏移量,实际上指的是首部的长度
确认 ACK: 当 ACK=1 时确认号字段有效否则无效。TCP 规定,在连接建立后所有传送的报文段都必须把ACK 置 1。同步 SYN: 在连接建立时用来同步序号。当 SYN=1,ACK=0 时表示这是一个连接请求报文段。若对方同意建立连接,则响应报文中 SYN=1,ACK=1
终止 FIN: 用来释放一个连接,当 FIN=1 时,表示此报文段的发送方的数据已发送完毕,并要求释放连接
窗口: 窗口值作为接收方让发送方设置其发送窗口的依据。之所以要有这个限制,是因为接收方的数据缓存空间是有限的
三次握手
为什么要第三次握手
避免历史连接
防止已经失效的连接请求到达服务器, 让服务器打开错误连接
客户端发送的连接请求如果在网络中滞留,那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后,就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器,如果不进行三次握手,那么服务器就会打开两个连接。如果有第三次握手,客户端会忽略服务器之后发送的对滞留连接请求的连接确认,不进行第三次握手,因此就不会再次打开连接
为什么两次握手不能阻止历史连接
主要是服务端不知道这是一个失效的请求连接,在收到SNK报文后立即发送数据,就会造成资源浪费
同步双方初始序列号
就是让双方都知道序列号已经同步了,维护TCP的可靠传输
避免资源浪费
如果没有第三次握手, 服务端无法确认自己的ACK_SYN报文是否正确到达客户端, 所以就只能收到SYN就建立连接,这样会造成资源浪费 (还是跟历史连接有关)
为什么第三次可以携带数据
因为当客户端收到服务器的回应时,客户端就可以确认可以正常接收和发送数据,但是服务器还不知道
所以客户端要告诉服务器这件事情,顺便可以带个数据就可以白嫖
为什么前两次不携带数据
因为服务器每次解析数据都需要消耗资源
前两次发送如果失败就会浪费资源
同时可能有坏蛋通过不断向服务器发送连接请求,导致服务器因资源殆尽挂掉
序列号与数据
TCP标准规定,ACK报文段可以携带数据,但是如果不携带数据就不消耗序号
SYN=1的报文段不可以携带数据,但是要消耗掉一个序号
FIN报文段也是即使不携带数据,它也消耗一个序号
如果第一次握手丢失会怎么样
客户端发完SYN报文后进入SYN_SENT状态
如果收不到服务端的SYN-ACK报文,就会触发超时重传机制,重传的SYN报文的序列号是一样的
超时多少算超时
这个跟内核有关
通常,第一次超时重传是在 1 秒后,第二次超时重传是在 2 秒,第三次超时重传是在 4 秒后,第四次超时重传是在 8 秒后,第五次是在超时重传 16 秒后。没错,每次超时的时间是上一次的 2 倍
当第五次超时重传后 (达到最大重传次数时),会继续等待 32 秒,如果服务端仍然没有回应 ACK,客户端就不再发送 SYN 包,然后断开 TCP 连接
所以,总耗时是 1+2+4+8+16+32=63 秒,大约 1 分钟左右
如果第二次握手丢失会怎么样
第二次SYN-ACK丢失,那么两边都会怀疑自己
客户端没收到第二次握手,他就会怀疑自己是不是第一次握手SYN报文丢失
服务器没收到第三次握手,他就会怀疑是不是第二次握手SYN-ACK报文丢失
而他们解决这个的办法,就是重传
所以两边都会重传
客户端重传第一次的报文
服务器重传第二次的报文
过程大概跟上面一样
如果大于最大重传次数他们两还没接上,就会断开TCP连接,等待下次有缘再见
如果第三次握手丢失会怎么样
当客户端收到SYN-ACK后,他就知道我们可以建立连接了,并且进入ESTABLISH状态
这个时候他会很自信 : 我他妈收发都没问题,所以一定不会是我的问题 ! 所以ACK报文不会重传
但是服务器他不知道啊,所以他就会怀疑自己,疯狂发送SYN-ACK报文
超过最大重传次数就会自己断开连接
至于客户端, 当他再次向服务器发送数据时,会收到RST=1 的报文,这个时候他就会知道服务器已经跑路了
ps:
由于客户端在发完ACk包后进入ESTABLISHED状态,如果后续客户端再向服务器发送数据包,服务器可以从里面得到与ACK包一样的数据(相当于第三次传输带了数据)
四次挥手
主动关闭连接的才有TIME_WAIT状态
为什么需要四次挥手
- 客户端主动调用关闭连接的函数,于是就会发送 FIN 报文,这个 FIN 报文代表客户端不会再发送数据了,进入 FIN_WAIT_1 状态;
- 服务端收到了 FIN 报文,然后马上回复一个 ACK 确认报文,此时服务端进入 CLOSE_WAIT 状态。在收到 FIN 报文的时候,TCP 协议栈会为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中,服务端应用程序可以通过 read 调用来感知这个 FIN 包,这个 EOF 会被放在已排队等候的其他已接收的数据之后,所以必须要得继续 read 接收缓冲区已接收的数据;
- 接着,当服务端在 read 数据的时候,最后自然就会读到 EOF,接着 read() 就会返回 0,这时服务端应用程序如果有数据要发送的话,就发完数据后才调用关闭连接的函数,如果服务端应用程序没有数据要发送的话,可以直接调用关闭连接的函数,这时服务端就会发一个 FIN 包,这个 FIN 报文代表服务端不会再发送数据了,之后处于 LAST_ACK 状态;
- 客户端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客户端将进入 TIME_WAIT 状态;
- 服务端收到 ACK 确认包后,就进入了最后的 CLOSE 状态;
- 客户端经过 2MSL 时间之后,也进入 CLOSE 状态;
每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手
是否要发送第三次挥手的控制权不在内核,而是在被动关闭方(上图的服务端)的应用程序,因为应用程序可能还有数据要发送,由应用程序决定什么时候调用关闭连接的函数,当调用了关闭连接的函数,内核就会发送 FIN 报文了,所以服务端的 ACK 和 FIN 一般都会分开发送
FIN 报文一定得调用关闭连接的函数,才会发送吗?
不一定
如果进程退出了,不管是不是正常退出,还是异常退出(如进程崩溃),内核都会发送 FIN 报文,与对方完成四次挥手
第一次挥手丢失了会怎么办
客户端会重传,等待服务端的ACK报文,当达到最大重传次数时,会直接断开连接
第二次挥手丢失了会怎么办
由于ACK报文不会重传,所以客户端会疯狂发送SIN报文,直到收到第二次挥手或者达到最大重传次数
如果达到最大重传次数,就会等一段时间然后关闭连接
close()和shutdown()
- close 函数,同时 socket 关闭发送方向和读取方向,也就是 socket 不再有发送和接收数据的能力。如果有多进程/多线程共享同一个 socket,如果有一个进程调用了 close 关闭只是让 socket 引用计数 -1,并不会导致 socket 不可用,同时也不会发出 FIN 报文,其他进程还是可以正常读写该 socket,直到引用计数变为 0,才会发出 FIN 报文。
- shutdown 函数,可以指定 socket 只关闭发送方向而不关闭读取方向,也就是 socket 不再有发送数据的能力,但是还是具有接收数据的能力。如果有多进程/多线程共享同一个 socket,shutdown 则不管引用计数,直接使得该 socket 不可用,然后发出 FIN 报文,如果有别的进程企图使用该 socket,将会受到影响
对于 close 函数关闭的连接,由于无法再发送和接收数据,所以FIN_WAIT2 状态不可以持续太久,而 tcp_fin_timeout 控制了这个状态下连接的持续时长,默认值是 60 秒
当收到客户端收到第二次挥手后会进入FIN_WAIT_2状态
如果第一次挥手是调用close() 关闭连接,会等待60s后关闭
如果主动关闭方使用 shutdown 函数关闭连接,指定了只关闭发送方向,而接收方向并没有关闭,那么意味着主动关闭方还是可以接收数据
如果主动关闭方一直没收到第三次挥手,那么主动关闭方的连接将会一直处于 FIN_WAIT2 状态(tcp_fin_timeout 无法控制 shutdown 关闭的连接)
|
第三次挥手丢失了会怎么办
当服务器收到客户端的FIN报文(第一次挥手) 后,内核会自动回复ACk,同时连接处于CLOSE_WAIT状态
但是此时内核没有权利代替进程关闭连接,必须由进程主动调用close函数来触发服务器发送FIN报文
服务器处于CLOSE_WAIT状态时, 调用了close() 函数,内核就会发出FIN报文(第三次挥手),同时连接进入LAST_ACK状态,等待客户端返回ACK来确认关闭连接
如果迟迟收不到这个 ACK,服务端就会重发 FIN 报文,重发次数仍然由 tcp_orphan_retries 参数控制,这与客户端重发 FIN 报文的重传次数控制方式是一样的
达到最大重传次数以后
如果还是没能收到客户端的第四次挥手(ACK报文),那么服务端就会断开连接。
客户端因为是通过 close 函数关闭连接的,处于 FIN_WAIT_2 状态是有时长限制的,如果 tcp_fin_timeout 时间内还是没能收到服务端的第三次挥手(FIN 报文),那么客户端就会断开连接
第四次挥手丢失了会怎么办
客户端收到服务端FIN报文后,会发送ACK,并等待2MSL后自动关闭
服务端(被动关闭方)没有收到 ACK 报文前,还是处于 LAST_ACK 状态
如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文,重发次数仍然由前面介绍过的 tcp_orphan_retries 参数控制
当达到最大重传次数后,服务端会自己关闭连接
关于TIME_WAIT
为什么TIME_WAIT等待时间是2MSL
MSL ( Maximum Segment Lifetime ), 报文最大生存时间, 是任何报文在网络上存在的最长时间, 超过这个时间的报文就会被丢弃
TCP报文头部有一个TTL, 每经过一个路由器就减一, 当TTL 为0时报文被丢弃,同时发送ICMP报文通知源主机
所以MSL要大于等于TTL消耗为0的是时间
2MSL时长这其实是相当于至少允许报文丢失一次
若 ACK 在一个 MSL 内丢失,这样被动方重发的 FIN 会在第 2 个 MSL 内到达,TIME_WAIT 状态的连接可以应对
我们假设连续两次丢包几乎不可能
2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时
为什么需要TIME_WAIT状态
防止历史连接中的数据,被后面相同四元组的连接错误的接收
序列号
为了保证消息的顺序性和可靠性,TCP 为每个传输方向上的每个字节都赋予了一个编号,以便于传输成功后确认、丢失后重传以及在接收端保证不会乱序
序列号是一个 32 位的无符号数,因此在到达 4G 之后再循环回到 0
初始序列号
在 TCP 建立连接的时候,客户端和服务端都会各自生成一个初始序列号,它是基于时钟生成的一个随机数,来保证每个连接都拥有不同的初始序列号
初始化序列号可被视为一个 32 位的计数器,该计数器的数值每 4 微秒加 1,循环一次需要 4.55 小时
序列号和初始化序列号并不是无限递增的,会发生回绕为初始值的情况,这意味着无法根据序列号来判断新老数据
为了防止历史连接中的数据,被后面相同四元组的连接错误的接收,因此 TCP 设计了 TIME_WAIT 状态,状态会持续 2MSL 时长,这个时间足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的
保证 被动关闭连接 的一方,能被正确的关闭
等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭
如果没有TIME_WT这个状态,当FIN到达时,主动方会用RST来响应,在被动关闭的一方看来似乎是一个错误,实际上是正常的连接释放过程
如果最后一次ACK报文丢掉了,服务器重传的FIN报文就不会被接受
ACK时间+重传FIN时间=2MSL
TIME_WAIT过多会怎么样
第一是占用系统资源
第二是占用端口资源
如果客户端(主动发起关闭连接方)的 TIME_WAIT 状态过多,占满了所有端口资源,那么就无法对「目的 IP+ 目的 PORT」都一样的服务器发起连接了,但是被使用的端口,还是可以继续对另外一个服务器发起连接的
如果服务端(主动发起关闭连接方)的 TIME_WAIT 状态过多,并不会导致端口资源受限,因为服务端只监听一个端口,而且由于一个四元组唯一确定一个 TCP 连接,因此理论上服务端可以建立很多连接,但是 TCP 连接过多,会占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等。
