文章目录
- 一、三次握手流程
- 二、为什么握手是三次
- 三、关闭连接的情况
- 四、四次挥手流程
- 五、为什么挥手是四次
系列文章:
- 深入理解Linux网络——内核是如何接收到网络包的
- 深入理解Linux网络——内核与用户进程协作之同步阻塞方案(BIO)
- 深入理解Linux网络——内核与用户进程协作之多路复用方案(epoll)
- 深入理解Linux网络——内核是如何发送网络包的
- 深入理解Linux网络——本机网络IO
- 深入理解Linux网络——TCP连接建立过程(三次握手源码详解)
- 深入理解Linux网络——TCP连接的开销
一、三次握手流程
-
客户端调用connect系统调用,发出第一次握手
-
找到套接字:创建内核对象的时候,fd会跟file对象做通过fd_install关联起来,通过进程的fd_table就可以找到对应的file,而file的private指针就指向了socket对象,所以根据fd即可找到套接字
-
判断当前套接字的状态:只有SS_UNCONNECTED状态(刚创建的套接字就是该状态)才会继续,其他状态都会报错
- 注意此处是socket的状态,而不是sock的状态
- 会将socket状态更改为SS_CONNECTING
-
更改sock状态为TCP_SYN_SENT
-
绑定端口:如果当前套接没有bind端口(端口为0则表示没有绑定),则从ip_local_port_range的某一个随机位置开始循环遍历找到合适的端口,如果查询不到则抛出Cannot assign requested address的错误
-
申请skb加入发送队列并设置syn数据包:将SYN标志位置为1,随机生成一个序列号,并设置MSS等字段,随后将数据包发送出去
- 这里直接调用的是tcp_transmit_skb,而正常发送逻辑会从tcp_sendmsg开始,其中会检查当前套接字的状态,如果不是已建立或者CLOSE_WAIT,会等待连接建立
-
启动重传定时器:等到一定时间后收不到服务端的反馈的时候来开启重传。首次超时时间是在TCP_TIMEOUT_INIT宏中定义的,该值在Linux3.10版本是1秒, 在一些老版本中是3秒。每次超时时间为2的指数递增(1,2,4,8,16,32)
-
-
服务端收到SYN包之后,发出第二次握手
-
找到套接字:skb通过软中断不断上传到tcp协议栈,根据数据报首部的IP地址和端口号查找对应的socket对象
-
判断当前套接字的状态:这里会进入TCP_LISTEN的处理逻辑
-
判断连接是否已经建立:检查是否有与这个SYN包的四元组相同的连接已经存在
- 如果已经存在ESTABLISHED状态的连接,则丢弃该握手包
- 如果已经存在SYN_RCVD状态的连接,则可能是一个重传的SYN包,这个时候会继续一下逻辑
-
查找半连接队列:到套接字的半连接队列中查找是否存在对应的半连接对象,因为当前是第一次握手,所以显然队列中还不存在半连接对象
-
创建半连接对象加入队列:会先检查半连接队列和全连接队列是否已满,如果数据包没有被丢弃则创建request_sock对象,将状态设置为TCP_SYN_RCVD
- 如果半连接队列满了且还没有开启syn_cookies那么将直接把握手包丢弃
- 如果全连接队列已满且存在young ack则同样把握手包丢弃
-
构造synack包并发送:将ACK和SYN标志位都置为1,随机生成一个序列号,并将确认号设置为SYN包的序列号+1,同时设置MSS等字段,最后将数据包发送出去
-
半连接对象入队:将半连接对象request_sock加入半连接队列
-
开启重传定时器
-
-
客户端收到SYNACK包之后,发出第三次握手
- 找到套接字:skb通过软中断不断上传到tcp协议栈,根据数据报首部的IP地址和端口号查找对应的socket对象
- 判断当前套接字的状态:这里会进入TCP_SYN_SENT的处理逻辑
- 移除重传队列中的SYN包,停止计时器
- 更改sock状态为ESTABLISHED
- 初始化TCP连接的拥塞控制算法、接收缓存和发送缓存空间等信息
- 开启keep alive保活计时器
- 唤醒等待队列的进程
- 构造ACK包进行发送:判断是否满足TCP的延迟确认(Delayed ACK)机制,如果满足则和数据包一起发送
-
服务端收到ACK包之后,完成连接建立
-
找到套接字:skb通过软中断不断上传到tcp协议栈,根据数据报首部的IP地址和端口号查找对应的socket对象
-
判断当前套接字的状态:这里会进入TCP_LISTEN的处理逻辑(注意不是TCP_SYN_RCVD)
-
查找半连接队列:这次是响应第三次握手,在上一次已经创建了半连接对象放置在队列中,所以这次可以从队列中拿到半连接对象
-
创建sock对象:判断socket的全连接队列是否满了,没满则根据半连接对象创建子sock
- 会将sock状态设置为ESTABLISHED,并且关联到这个request_sock
- 随后将request_sock其从半连接队列移除,加入到全连接队列中
-
唤醒等待队列的进程:如果有进程调用accept等待连接的话,则会被唤醒
- 唤醒之后进程从全连接队列中拿到request_sock
- 之后就可以根据request_sock中保存的tcp_sock来创建socket对象
-
二、为什么握手是三次
如果不进行最后一次握手,即服务端返回synack报文之后就完成建立的话,那么在数据包延迟到达的情况下有可能出现问题。
- 第一次握手包延迟:假如说客户端发起的SYN数据包因为网络延迟没有到达服务端,那么这时候它就收不到服务端的SYNACK报文,那么此时它就会超时重传SYN数据包。如果这次服务端收到了并回复了SYNACK报文,那么连接就算建立成功了。而在连接建立成功并且通讯完成又释放了之后,第一次因为延迟而没有到达服务端的SYN数据包这时候到达了,这时候服务端会以为是一个新的连接到达,于是再次响应一个SYNACK报文,因为只有两次握手,所以就建立了一条本不应该存在的连接。而如果此时使用的是三次握手,那么客户端收到这条SYNACK报文后则会将其丢弃,不会完成连接的建立。
- 第二次握手包延迟:如果服务端收到了客户端的SYN报文,而回传SYNACK包的时候超时了,那么如果此时是两次握手,服务端直接认为连接建立成功,而客户端会以为是自己的SYN报文没有到达服务端而重传SYN数据包,那么服务端会因为已经建立连接(自己认为已经建立过了)所以丢弃重传的SYN包,所以客户端这边永远都连不上。而如果此时使用的是三次握手,那么在SYNACK包超时之后,除了客户端重发SYN包,服务端也会重发SYNACK包。那么客户端收到重发的SYNACK包之后会发出ACK包,而服务端收到重发的SYN包后会再次发出SYNACK包。这时这个新的SYNACK包到达之后会因为序列号对不上而被客户端丢弃,而服务端收到ACK包之后就完成连接的建立。建立完成之后即使延迟的SYNACK包到达了客户端,也会同样被丢弃。
- 第三次握手包延迟(当然只可能在三次握手时出现):客户端发出ACK报文之后是不会期待响应的,所以此时它会直接认为连接建立。而服务端会因为收不到ACK包而重传SYNACK包,那么客户端会再次发送ACK包,如果服务端收到则完成建立。如果重试多次后一直丢失,那么服务端会认为连接建立失败而关闭连接。后续如果客户端正常的发出数据包给服务端,则会收到RST包,从而意识到连接已经被关闭。也就是说没有必要有第四次握手,如果ACK包一直丢失不能建立连接,后续发送数据的时候就可以感知到。
三、关闭连接的情况
关闭连接有以下几种可能:
- 超时:如果在设定的超时时间内没有收到期望的ACK包或其他响应,TCP连接会被关闭。这是为了防止网络中的“僵尸连接”消耗系统资源。
- 错误或异常:如果发生了某些错误或异常,例如网络错误、对方突然断线或程序崩溃等,TCP连接也会被关闭。
- 主动关闭:如果应用程序调用了关闭连接的函数(例如close和shutdown),TCP连接也会被关闭。
- RST标志:如果收到一个带有RST(Reset)标志的TCP包,TCP连接也会被立即关闭。RST包通常在发生错误或异常时被发送,例如,收到了一个不应该收到的包,或者试图打开一个不存在的连接等。
如果是发生了如网络错误、断线或程序崩溃等错误或异常,那么自己这边的系统可能关闭所有的网络接口,释放所有的网络相关的内存等(取决于操作系统和协议栈的实现),而对端发送的保活数据包将接收不到ACK,重试几次后,就会进行连接的关闭,这个时候是不会进行四次挥手的。
而如果是发生以下情况,就会发送一个带有RST标志的TCP包。收到带有RST标志的包的一方会立即关闭连接,而不需要执行常规的四次挥手过程。这种情况下,连接的关闭是非正常的,因为它并没有经过正常的关闭过程就被终止了。
- 收到了一个错误的序列号的数据包:在TCP连接中,每个数据包都有一个序列号,用来保证数据包的有序接收。如果收到了一个序列号不正确的数据包(即这个数据包的序列号不在期望的序列号范围内),TCP会发送一个RST数据包来重置连接
- 应用程序强制关闭:应用程序在正常关闭一个TCP连接时,会通过操作系统发送一个FIN(Finish)标志的数据包,这将触发TCP的正常关闭流程,也就是所谓的"四次挥手"。然而,有一种特殊的情况,那就是"强制关闭"或"紧急关闭"。在某些情况下,例如,应用程序崩溃,或者用户想要立即关闭连接,而不等待四次挥手过程完成,操作系统会发送一个RST(Reset)标志的数据包来立即关闭连接。这种情况下,操作系统并不会等待对方的确认,连接会立即关闭。可以使用SO_LINGER选项来设置一个0延迟的linger时间以实现强制关闭
- 网络层错误或异常:在某些网络层的错误或异常情况下,例如,网络接口出错或者IP路由失败,TCP可能会发送一个RST数据包来关闭连接。
- TCP层错误或异常:在某些TCP层的错误或异常情况下,例如,内存不足,无法创建新的数据包,或者处理到一半的数据包被意外丢失,TCP可能会发送一个RST数据包来关闭连接。
也就是以上的情况其实都不会进行四次挥手,只有当正常进行连接的关闭才会进行四次挥手的逻辑。当应用程序A决定关闭一个TCP连接时,它会调用 close() 或 shutdown() 函数,这些函数在操作系统内部会发送一个FIN(Finish)标志的TCP数据包给对端B,这就开始了所谓的四次挥手过程。
四、四次挥手流程
-
主动方调用close或shutdown,发起第一次挥手
-
取消文件描述符和file对象指针的关联:即后续无法再使用这个文件描述符,shutdown不会关闭fd,所以仍需要调用close来关闭文件描述符
- lose在fd被多个进程持有时不会立马关闭连接,调用close只会让引用计数-1,需要等到socket的引用计数为0才会发送FIN报文
- 而shutdown会直接关闭连接
-
判断当前套接字状态:如果是LISTEN则直接设置为close,然后释放对象,结束流程
-
释放接收队列:如果不是LISTEN状态则循环遍历接收队列,释放队列中的skb
- 如果接收队列不为空,即释放了skb,则会发送一个RST来中断连接,然后更改套接字状态并释放相关资源,结束流程
-
判断是否设置了SO_LINGER:如果设置了该选项并且linger时间设置为0,那么也发送RST直接中断连接
- 默认是没有设置该选项的,close方法不会阻塞的,在后台进行处理
- 如果设置为0,则立即关闭连接,发送缓冲区有未发送的数据则直接丢弃,直接进入CLOSED
-
发送FIN数据包:只有没有设置SO_LINGER或者设置了非0的linger才会来到这里
- 更新自身状态为FIN_WAIT_1(状态机中ESTABLISH的下一位)
- 遍历发送队列,如果其中有数据包未发送就在最后一个数据包设置FIN标志位,然后将所有数据包发送出去。
- 如果设置了SO_LINGER,则进程进入阻塞,等待linger时间,如果超时仍然没有发完则会发送RST报文。
- 默认没有设置linger(不会阻塞)则会检测当前socket状态。如果是FIN_WAIT1(一般来说没有阻塞直接返回就是这个状态),就会查看孤儿socket数量是不是太多了,如果是则更改为CLOSE状态并发送RST直接关闭,不是则tcp_close函数到此基本结束
-
-
服务端收到FIN包之后,发起第二次挥手
-
找到套接字,检查到套接字状态:因为当前是ESTABLISHED状态,所以进入tcp_rcv_established,并在最终检测到数据报的FIN标志位为1而进入tcp_fin函数进行处理
-
更改套接字状态:对于ESTABLISHED状态的套接字,会将其更改为CLOSE_WAIT状态
-
内存清理:清空乱序队列中的数据包,并且根据当前的内存压力和套接字的内存使用情况来回收一部分内存
-
唤醒阻塞的进程:通知在recv上等待的进程有数据可读,此时读取的返回结果会是0
- 此时服务端已经知道对端已经关闭连接,然后就可以编写逻辑来决定何时调用close方法
-
发送ACK包给客户端
-
-
客户端收到ACK包
-
找到套接字,检查到套接字状态:因为当前的状态是TCP_FIN_WAIT1,所以会进去一个状态处理函数tcp_rcv_state_process(如果不是LISTEN或是ESTABLISHED就会进入这个函数)
-
更改套接字状态:将自身的状态更改为TCP_FIN_WAIT2
-
设置定时器:TMO+2MSL或者基于RTO计算超时
- 超时后会直接变迁到closed状态,然后将套接字的发送端设置为关闭
-
唤醒阻塞在close上的进程:针对于设置了SO_LINGER的情况,被唤醒后继续执行close后续逻辑
- 检测linger2是否大于等于0(TCP层面的,用于设定孤儿套接字在FIN_WAIT2状态的生存时间,如果没有配置则默认为tcp_fin_timeout,如果大于则等待一段时间来接收对端的FIN,如果小于0则立即关闭连接,并发送RST报文
-
-
服务端继续处理,发送数据包给客户端
-
服务端在知道客户端关闭连接后还可以继续发送数据包
-
如果客户端关闭了读通道(close会都关闭),那么客户端收到数据包后会发送RST数据包之后服务端直接进行关闭
-
如果客户端只是关闭了写通道(shutdown可以只关闭写),那么数据包会照常接收并返回ACK报文
-
-
当服务端处理完毕之后,调用close方法,发起第三次挥手
-
释放接收队列:如果有skb释放或者socket设置了SO_LINGER选项且linger时间为0,那么还是发送RST
-
更新套接字状态 :如果不是上面两种情况则继续更新到状态机的下一位,因为当前是CLOSE_WAIT,所以更新成LAST_ACK
- CLOSE_WAIT会在保活定时器超时后强行关闭连接,用于服务端一直没有主动关闭连接而客户端已经因为超时而关闭的情况。
-
发送FIN包给客户端
-
-
客户端收到FIN包之后,发起第四次挥手
- 发送ACK数据包:同样进入tcp_fin函数,发送ack包给服务端然
- 更改套接字状态:为当前状态是TCP_FIN_WAIT2,更改至TIME_WAIT
- 内存释放:time_wait状态时,原socket会被destroy,然后新创建一个inet_timewait_sock,在等待2MSL之后删除。
-
服务端收到客户端的ACK包之后,完成四次挥手:将LAST_ACK更改为CLOSED,并且释放对象
如果两边同时发送FIN,那么在FIN_WAIT_1时收到对方的FIN,会进入CLOSING,之后收到ACK变成TIME_WAIT
五、为什么挥手是四次
不同于握手,SYN和ACK可以同时发送。FIN表示的是自己没有数据要发了,而在客户端结束发送数据的时候,不一定服务端也结束了,所以没办法将FIN包和ACK包结合在一起发送。
对于可靠连接而言,ACK包是不可以省略的,每一个方向上的数据发送都应该得到对端的确认。并且假如说节省第二次挥手的ACK包,那么因为下一个FIN的时间是不确定的,有可能很久,那么实现的时候得让FIN_WAIT_1等待一个很久的时间。如果它是因为丢包了,那么重试也会需要一个很长的时间,这会导致close的时间非常的久。如果节省最后一个ACK包,也就是说被动方发出FIN之后就关闭,主动方收到FIN之后也直接关闭。那么有可能FIN包丢失了,所以导致被动方关闭了而主动方还在等待。
而最后需要进入TIME_WAIT状态等待2MSL的原因主要有两个:
- 保证老的重复报文在网络中消逝:如果说没有TIME_WAIT两个2MSL,而客户端和服务端又基于原本的端口建立了新的连接,那么旧连接中可能有数据包延迟,没达到最大生存时间,所以还没被丢弃,这个时候到达了新的连接,并且正好在接收窗口中,那么此时会被误以为是正常的数据包,从而导致新的连接数据错乱。序列号并不是无限递增的,会发生回绕为初始值的情况,这意味着无法根据序列号来判断新老数据。所以通过设置2MSL,保证新连接建立的时候,旧连接在网络中残留的数据包都已经死亡了
- 如果主动关闭方的ACK丢失,那么被动方会重发FIN包,以允许主动方重发ACK,那么此时如果没有TIMEWAIT,则主动方已经关闭了,无法重发ACK报文,TCP协议栈会返回 RST 报文,RST其实是出现异常的时候才发送的数据包,这对于可靠的TCP协议而言不是一个比较优雅的关闭方式。
如果出现过多的TIME_WAIT,想要缩短TIME_WAIT的时间,可以参考文章:深入TCP协议——tcp_tw_reuse和tcp_tw_recycle