一文带你搞定TCP挥手 tcpip握手挥手
摘要
- TCP断开连接
- TIME_WAIT
- TIME_WAIT优化
- TCP保活
- Sokcet编程
TCP断开连接
TCP断开连接,需要经历四次挥手,通信的双方都可主动断开连接,断开连接通信的双方占用的资源将会被释放。
- 客户端会发送一个FIN报文给服务端,然后进入FIN_WAIT_1状态
- 服务端在收到FIN报文后,会回复客户端段一个ACK报文,然后进入CLOSED_WAIT状态
- 客户端在收到服务端的ACK报文以后会进入FIN_WAIT_2状态
- 服务端在处理完历史数据以后会发送FIN报文给客户端,然后进入LAST_ACK状态
- 客户端在收到服务端的FIN报文以后,会发送一个ACK报文给服务端,然后进入TIME_WAIT状态
- 服务器在收到ACK报文以后,就会真正的关闭连接,进入CLOSED状态
- 客户端在经过2MSL时间后,也会自动关闭连接进入CLOSED状态
为什么回收需要四次
原因是客户端在主动发起FIN报文以后仅表示客户端不再主动发送数据了但是还可以接收数据。服务器在响应ACK报文以后,还有可能有数据还在处理且需要发送给客户端,因此当服务器处理完这些数据以后才能发送FIN报文表示同意关闭连接。
因此服务端的ACK和FIN报文需要分开发送,挥手也就变成了4次。
什么是MSL和TTL
TTL是IP头部中的一个字段,是指IP数据报可以经过的最大路由数,每经过一个路由器都需要减1,当TTL值为0时数据报就会被丢弃,同时发送ICMP报文给源主机。TTL的单位是路由跳数。
MSL是报文在网络中存在的最长时间,超过该时间就会被丢弃。
为什么TIME_WAIT需要经历2MSL后才可以变为CLOSED
网络中存在的发送方数据包,首先需要发送给服务端,服务端在处理完以后又会将相应发送给客户端,所以总共需要2个倍的时间。
2MSL的时间是从客户端接收到FIN报文并且发送ACK报文时开始的。如果此时ACK报文没有被服务端接收到触发了服务端的超时重传,客户端又再次收到了FIN报文,那么2MSL将重新开始计时。
LINUX中默认一个MSL是30s,也就是说TIME_WAIT的时间是60s。
TIME_WAIT
为什么需要TIME_WAIT状态
主动发起连接中断的一方需要有TIME_WAIT状态,主要是以下原因:
- 防止具有相同四元组的旧数据包被收到
- 保证最后一次ACK报文能被被动关闭连接的一方收到,也就是保证被动关闭连接的一方能被正确关闭。
防止旧连接的数据包被收到
假设没有TIME_WAIT状态,如果有相同的端口的TCP连接被服用后,上图中被延迟SEQ=301的数据包抵达了客户端,客户端是有可能正常接收该报文的,此时就会产生数据错乱现象。
但通过2MSL的等待时间,通信双方的数据包都可以在网络中消失,新的数据包一定是新连接的。
保证连接正确关闭
通过等待2MSL的时间确保最后一次ACK报文被被动断开连接的一方收到,从而正常关闭。
上图如果服务端没有收到最后一个ACK报文会处于LAST_ACK状态,如果此时客户端发起了一个新的SYN报文请求建立连接,服务端会发送RST报文给客户端,连接建立失败。
但是通过等待2MSL的时间会解决上述问题,因为假设服务端没有收到最后一次ACK报文,会触发超时重传重新发送FIN报文并等待新的ACK报文。客户端在收到新的FIN报文时会重新发送ACK报文并刷新2MSL的计时,最终能够保证服务端的连接能够正常关闭。
TIME_WAIT过多的弊端
服务器如果有TIME_WAIT状态的连接,说明TCP连接的断开是由服务端发起的,此时如果TIME_WAIT的连接过多,将会出现以下问题:
- 内存资源占用
- 端口资源占用,假设端口被占满,将无法建立新的连接
# 该参数用于指定开放的端口资源,默认是32768-61000
net.ipv4.ip_local_port_range
TIME_WAIT优化
TIME_WAIT的优化主要有以下几种方式,每种方式都有利有弊:
- 打开net.ipv4.tcp_tw_reuse和net.ipv4.tcp_timestamps选项
- net.ipv4.tcp_max_tw_buckets
- 应用程序使用SO_LINGER,应用强制使用RST关闭
打开net.ipv4.tcp_tw_reuse和net.ipv4.tcp_timestamps选项
参数开启以后,可以复用处于TIME_WAIT的Socket给新的连接使用。
tcp_tw_reuse的功能只能用于连接发起方,开启该参数以后,在调用connect函数时,内核会随机找一个time_wait超过1s的连接给新的连接复用。
net.ipv4.tcp_timestamp默认开启,表示打开对TCP时间戳的支持。时间戳字段存储在TCP头部的选项字段中,用于记录TCP发送方的时间戳和从对端接收到的最新时间戳。
net.ipv4.tcp_max_tw_buckets
当系统中的TIME_WAIT的连接数超过该项的值时,系统那个会将后面TIME_WAIT的连接重置,不推荐使用。
程序使用SO_LINGER
通过设置Sokcet的一些选项,来影响close方法的一些行为。
如果SO_LINGER中的onoff为非0,并且linger为0,调用close方法以后会立即发送一个RST报文给对方,TCP连接会直接跳过四次握手关闭。也过于暴力不推荐。
TCP保活机制
在某个时间段内,如果TCP连接上无任何活动,TCP保活机制开始生效,每隔一段时间就会发送一个探测报文,如果连续几个探测报文都没有收到响应,则认为TCP连接已死,系统内核会将错误信息通知给应用程序。
# 用于控制保活时间,如果7200s内没有活动,则会启动保活机制
net.ipv4.tcp_keepalive_time=7200
# 保活机制每次检测间隔为75s
net.ipv4.tcp_keepalive_intvl=75
# 如果9次探测无响应,则认为对端不可答,中断本次连接
net.ipv4.tcp_keepalive_probes=9
上述三个都是Linux中的默认值,也就是说Linux操作系统中至少经过2小时11分15秒才可以发现一个死亡连接。
Socket编程
public ServerSocket(int port, int backlog) throws IOException {
this(port, backlog, null);
}
Java中的ServerSokcet的初始化方法中有一个backlog参数,该参数在Linux2.2以前代表SYN队列大小,但是在Linux 2.2以后就是全连接队列的大小(accept队列的大小)。
- 半连接队列(SYN队列):接收SYN请求,处于SYN_RCVD状态的连接
- 全连接队列(Accept队列):完成三次握手处于ESTABLISHED状态的连接
Socket的一些连接操作对应的tcp连接步骤
- Socket在调用connect方法时,会发送SYN包给服务端,服务端会接收到到SYN报文,并且服务端会半连接队列里初始化一个连接。
- 服务端在处理完以后会发送ACK+SYN报文给客户端,客户端收到以后切实是就是connect方法的返回,同时客户端也需要对服务端的SYN报文进行应答。
- 服务端收到ACK报文以后,半连接队里的连接会被转移到全连接队列中,此时accept方法会成功拿到连接并生成一个Socket(这个就是传输时的Socket,不是监听Socket)。
close方法对应的TCP四次挥手
- 客户端调用close方法,会发送一个FIN报文给服务端
- 服务端收到FIN报文时,TCP协议栈会为该包插入一个文件结束符EOF到接收缓冲区,应用程序可以通过read方法获取到该文件结束符。**EOF会被放在所有的数据之后。**服务端会进入CLOSED_WAIT状态。
- 服务端处理完所有的数据以后,会读取到EOF,此时会调用close方法关闭Socket,然后发送一个FIN包进入LAST_ACK状态。
- 后面的其实就是TCP最终断开连接。