TCP/IP学习笔记(七)四次挥手

前面说过,TCP是稳定可靠的传输层协议,稳定体现在需要先建立连接(三次握手)才可以进行通讯。但是当一方想要关闭连接时,如果它一走了之,另一端又怎么知道连接已经关闭了呢,这就会导致另一端仍然保持着维护连接所需要的一切资源而无法释放。

所以TCP在关闭连接时也需要进行类似三次握手之类的流程,以通知双方关闭连接,释放资源。称为四次挥手

四次挥手

关闭一个TCP连接需要进行四次报文交互,任意一端关闭连接时都需要向对端发送报文段。

关闭时发送的报文段中FIN位被置1,表示关闭连接

首先调用close关闭套接字描述符的一方称为主动关闭(以客户端为例),从图中可以看到

  • 客户端发送FIN位被置1的报文段,告知服务器自己已经将连接关闭。此时客户端状态变为FIN_WAIT1(表示发送了一个FIN报文段)
  • 服务器收到客户端发送的FIN报文段后将应答报文段发给客户端。此时服务器状态变为CLOSE_WAIT(表示等待服务器应用程序关闭连接)
  • 客户端收到服务器的应答报文段后状态变为FIN_WAIT2(表示等待服务器发送FIN报文段)
  • 一段时间后服务器应用程序调用read/recv时返回0,得知客户端已经关闭连接,随后向客户端发送FIN被置1的报文段。此时服务器状态变为LAST_ACK(表示等待客户端最后一个应答)
  • 客户端收到服务器的FIN报文段后返回应答报文段。此时客户端状态变为TIME_WAIT
  • 服务器收到客户端的应答报文段后,连接关闭流程结束

如果客户端执行close(全关闭),那么在FIN_WAIT2状态下需要等待对端发送FIN报文段才能够继续关闭,如果迟迟收不到对端的FIN报文段,将可能一直等下去。不过很多TCP实现上都会启动一个定时器,如果规定时间到达后仍然没有收到对端的FIN报文段,就继续关闭

从wireshark中也可以看到连接关闭时的报文段发送情况,为了使结果更为明显,执行的操作步骤为

  • 开启服务器客户端程序
  • 客户端等待0.5秒后关闭连接
  • 服务器应用程序发现客户端关闭后(read/recv返回0),等待1秒后关闭连接

等待0.5秒和1秒的原因是防止延迟ACK的存在使报文段一起发送,对观察结果有影响

从结果可以看出

  • 1-3行是建立连接时的三次握手
  • 4-7依次是客户端关闭连接,服务器发送应答,服务器关闭连接,客户端发送应答

另外,虽然客户端关闭连接后直接结束了进程,但是当服务器发送FIN时仍然能够响应ACK,可见TCP是独立于应用程序管理报文段的接收和发送的

TIME_WAIT状态

在上面连接关闭的状态转换图中可以看到,主动关闭的一方TCP会变为TIME_WAIT状态而不会立即关闭,在测试程序运行结束之后也能够看到(服务器监听端口为9999)

➜  client netstat -ant | grep 9999
tcp        0      0 127.0.0.1:45378         127.0.0.1:9999          TIME_WAIT 

由于主动关闭的是客户端,所以客户端使用的端口45378会变为TIME_WAIT状态。在该状态下的端口不能够被立即使用,这么做的好处是

  • 应对服务器重传的FIN报文段。如果服务器没有接收到最后一个ACK报文,就会重传FIN报文段,因为客户端TCP处在TIME_WAIT状态下,仍保留着连接信息,可以再次发送ACK报文给服务器。而如果客户端TCP直接关闭,那么对于服务器重发的FIN报文会认为是无效的连接,将返回RST报文段
  • 使仍在网络中的报文段消失。防止使用同样端口的新连接建立,使服务器误以为是上一个客户端。假设没有TIME_WAIT状态,上一个客户端关闭后立即有一个新客户端使用相同端口并且立刻向服务器发送报文段(SYN),此时服务器会认为这个报文段是上一个客户端发送的之前没到达但现在刚到达的报文段,由于连接已经关闭,这个报文段将被丢掉

TIME_WAIT时间一般是2倍的报文段最大生存时间(2MSL),MSL的时间根据实现互不相同,常见的有30秒,1分钟不等

此外,TCP也提供了可以复用TIME_WAIT端口的方法,为套接字设置SO_REUSEADDR选项即可

TCP状态变迁图

半关闭

由于TCP是全双工的协议,任意一端都既可写又可读,所以TCP允许某一端执行半关闭操作(shutdown函数),比如只关闭发送数据的通道而保留接收数据的通道。

客户端关闭发送通道实际上就是告诉服务器数据已经发送完了,接下来都不会再发送数据了,但是可以接收数据,服务器可以发过来

而当服务器数据发送完毕后就可以调用close执行全关闭操作了,因为双方都不再发送数据,就意味着连接可以终止了

接下来通过wireshark观察半关闭的报文段发送情况,执行流程为

  • 启动服务器客户端,建立连接
  • 客户端0.5秒后调用::shutdown(fd, ::SHUT_WR)关闭写通道,不再发送数据
  • 服务器得知后(read/recv返回0)等待0.5秒向客户端发送一条数据
  • 服务器1秒后调用close关闭连接
  • 客户端得知后关闭连接

从图中可以看到当一端发送FIN报文段后仍然可以进行数据交互

猜你喜欢

转载自blog.csdn.net/sinat_35261315/article/details/79405050