三次握手

四次挥手

TCP 连接异常断开分析
1. 进程 crash
当某个进程关闭时,内核会关闭由这个进程打开的套接字,也就是说 crash 掉的进程会下发 FIN 报文,进行四次挥手,因此能够关闭对端 TCP 连接。
2. 主机奔溃,或又迅速重启
- 1)非活跃的 TCP 连接且没有开启 keep_alive 保活机制:
当主机奔溃时是无法发出 FIN 报文的,而且没有保活机制和数据传入流动,因此该 TCP 会一直保存在对端 2)非活跃的 TCP 连接有开启 keep_alive 保活机制:
如果开启了保活机制,那么根据配置,在一定的时间后会自动断开 TCP 连接。
相关内核参数有: 因此,至少经过**2小时11分15秒**才能够发现一个“死亡连接”
keep-alive 保活机制是默认关闭的,需要在创建 socket 时设置**SO_KEEPALIVE**选项tcp_keepalive_time = 7200 // 当2个小时内没有数据流动时,启动保活机制tcp_keepalive_intvl = 75 // 当启动保活机制后,每次检测的间隔为75秒tcp_keepalive_probes = 9 // 总共检测9次
3)活跃的 TCP 连接且主机奔溃后无重启
当一端发送的报文没有得到 ACK 回复时,就会根据内核相关配置的参数触发重传,达到一定次数后(达到了限定的timeout)关闭 TCP 连接
相关内核参数有:tcp_retries1 = 3tcp_retries2 = 15// 以上两个参数并不是限定重传的次数,而是结合初始RTO,计算timeout时间上限。当重传的时间间隔(以2的倍数倍增)超过该timeout参数,则中断TCP连接
4)活跃的 TCP 连接且主机重启
重启后的主机如果没有监听对应的端口,那么由于找不到目标端口,内核会回复 RST 报文,重置该 TCP 连接
如果有监听该端口,由于之前的 Socket 数据结构已经丢失,无法查找到对应的数据,因此也会回复 RST 报文,重置连接- 5)重启后的主机向正处于ESTABLISH状态的对端发送SYN报文
当处于ESTABLISH状态的接收到SYN报文之后,会返回一条当前期望接收的正确的seq的ACK报文(非SYN-ACK报文)。客户端接收到该报文后,发现与自身期望的序列号不一致,因此回复RST报文,对端收到后进入重置。
TCP 三次握手、四次挥手异常分析及内核参数
1. 第一次握手丢失,会发生什么?
当客户端发送 SYN 报文之后,会进入 **SYS_SEND** 状态,在这之后如果迟迟收不到由服务端下发的 SYN-ACK 报文,会触发超时重传机制。内核参数:
tcp_syn_retries = 6 // 最大的重传次数,每次重传时间间隔倍增
如果在限定的重传次数内还是无法接收到回复,那么客户端就恢复为
**CLOSE**状态,如果**CLOSE**状态收到了第二次的握手包,会回复 RST 报文,否则则任由服务端达到重传次数后重置。
2. 第二次握手丢失,会发生什么?
由于客户端接收不到 SYN-ACK 报文,因此会触发重发 SYN 报文,当服务端收到 SYN 报文时会立即重发 SYN-ACK 报文,当 SYN 报文重发到一定次数之后,触发关闭。
服务端已接收到 SYN 报文,因此进入 **SYN_RECVED** 状态,因为收不到第三次握手的 ACK 报文,因此会进行 SYN-ACK 报文的重发。
内核参数:
tcp_synack_retries = 5 // 最大重传次数,每次重传时间间隔倍增
如果限定次数内无法收到回复,那么服务端就恢复为
**CLOSE**状态,此时如果接收到客户端的 SYN 报文,会再次进入**SYN_RECVED**状态,继续上述流程,而结果要么成功建立连接,要么客户端就已经由于达到了最大的重传次数而恢复为**CLOSE**状态,最终服务端也将进入**CLOSE**状态。
3. 第三次握手丢失,会发生什么?
由于客户端接收到了 SYN-ACK 报文,因此进入了 **ESTABLISH** 状态,会向服务端发送一个 ACK 报文。
由于 ACK 报文丢失,因此服务端会重发 SYN-ACK 报文(注意,客户端已不会重发 SYN 报文),直至达到最大重传次数,而转为
**CLOSE**状态。如果当服务端处于**SYN_RECVED**时接收到了客户端下发的数据包,则会直接根据数据包中携带的 ACK 标识,进入**ESTABLISH**状态。否则若客户端如果没有开启 keep-alive 机制或者触发某个数据包的发送的话,会一直保持该**ESTABLISH**状态,直至被关闭。
注意:ACK 报文是不会重发的,当 ACK 报文丢失,就由对方重发相应的报文
4. 第一次挥手丢失,会发生什么?
当客户端发出 FIN 报文之后,会进入 **FIN_WAIT1** 状态,由于没有接收到服务端的 ACK 报文,因此会进行重发。
内核参数:
tcp_orphan_retries = 0 // 虽然是0,但是实际上是8次
当达到限定的重发次数之后,转为
**CLOSE**状态。服务端如果尝试下发数据报文,若该报文被客户端接收到,则回复 RST 报文,否则会进入重发阶段,直到达到 tcp_retries1,tcp_retries2 参数决定的超时上限而转为**CLOSE**状态。如果服务端一直静默,则当有开启保活机制时,在超过保活机制时间后,会转为**CLOSE**状态,如果没有保活机制,则会一致保持**ESTABLISH**状态
5. 第二次挥手丢失,会发生什么?
当服务端接收到客户端下发的 FIN 报文时,会变为 **CLOSE_WAIT** 状态,且下发 ACK 报文。
如果 ACK 报文无法成功被客户端接收,而且由于 ACK 报文是不会重发的,因此客户端会一直尝试重发 FIN 报文,直至收到 ACK 报文或达到重试上限而转为
**CLOSE**状态。
6. 第三次挥手丢失,会发生什么?
当客户端接收到第二次挥手报文时,进入 **TIME_WAIT2** 状态,此状态最多维持时间由内核参数决定:
tcp_fin_timeout = 60 // 默认最多持续60秒
当服务端处于 **CLOSE_WAIT** 状态时,直到服务端进程发送完所要发送的数据后,显式调用 close() 函数,内核才会下发 FIN 报文,进而进入 **LAST_ACK** 状态,等待客户端的 ACK 回复。
如果一直没有收到 ACK 回复,会进行重试,重试次数的内核参数:
tcp_orphan_retries = 0 // 虽然是0,但是实际上是8次
7. 第四次挥手丢失,会发生什么?
当客户端接收到第三次挥手报文时,就会进入 **TIME_WAIT** 状态,此状态最多持续 2MSL 时长(60秒)。此时的服务端因为接收不到 ACK 报文,因此会进行 FIN 报文的重发,由 tcp_orphan_retries 参数控制。
TCP Time_WAIT状态等待2MSL的原因
客户端发出 ACK 报文时,最大化时间考虑的假设是 “该ACK报文经过了一个最大化的报文生存时间(MSL)才被服务端接收”,如果没有被接收到,那么服务端会重新下发 FIN 报文,那么假设也是 “经过了最大化的报文生成时间(MSL)才到达客户端”,那么这样一来一去,也就是需要 2MSL 了。
注意:当客户端重新接收到 FIN 报文时,2MSL 会重新计时
TCP TIME_WAIT状态过多
TIME_WAIT 状态的作用:
- 1 可靠的实现TCP全双工连接的终止
- 2 确保重用相同IP、端口的连接后,旧的数据包已经消亡
TIME_WAIT 状态过多的危害:
- 1 占用内存资源
- 2 占用端口资源,当端口资源被占用满了,会导致无法创建新的连接
TIME_WAIT 状态过多的产生原因:
- 1 高并发时产生过多的短连接
- 2 默认的2MSL时间太长
TIME_WAIT 状态的优化:
- 1 使用长连接,但是会增加资源占用
- 2 增加服务端对外服务端口,增加客户端机Ip,但是治标不治本
2 调整内核参数:
1)tcp_tw_reuse 和 tcp_timestamptcp_tw_reuse = 1 // 是否开启重用正处于TIME_WAIT状态的socket,默认为0,表示否tcp_timestamp = 1 // 开启TCP时间戳,默认开启
只能针对客户端有效,当开启该功能时,在调用 connect() 函数时,内核会随机找一个TIME_WAIT状态 超过1秒 的socket给新的连接复用。(谨记:服务端也是可以主动断开连接而进入TIME_WAIT状态的,当然一般设置不主动关闭连接,除非是HTTP请求。)
2)tcp_max_tw_buckets
tcp_max_tw_buckets = 18000 // 最大的TIME_WAIT数量
将最大的TIME_WAIT数量值调小,当检测到TIME_WAIT数量超过限定值之后,所有的TIME_WAIT状态都会被立即清除。但是做法粗暴,有造成异常的风险
3)修改等待2MSL的时间,需要重新编译内核
4)tcp_tw_recycle 和 tcp_timestamp
tcp_tw_recycle = 1 // 是否快速回收正处于TIME_WAIT状态的socket,默认为0,表示否tcp_timestamp = 1 // 开启TCP时间戳,默认开启
该参数在4.12版本后就被删除了,原因是当开启该参数后,虽然会对TIMEWAIT状态 **超过3.5_RTO 时间的socket进行回收。但是会丢弃所有来自远端的timestramp时间戳小于上次记录的时间戳(由同一个远端发送的)的任何数据包。也就是说要使用该选项,则必须保证数据包的时间戳是单调递增的。此时如果使用了NAT(内网到外网的端口映射机制)、LVS等就会有问题。
5)SO_REUSEADDR 参数
int on = 1;setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char*)&on, sizeof(on));setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, (char*)&on, sizeof(on));
开启该参数之后,即可重用处于 TIME_WAIT 状态的 socket,一般用在服务端(注意 SO_REUSEPORT是为了解决特定IP端口的复用问题,但是确无法绑定处于 TIME_WAIT 状态的 socket,因此一般同时使用这两个参数)
TCP CLOSE_WAIT状态过多
CLOSE_WAIT状态的危害:
- 占用fd套接字资源(可使用selelct_server.cpp测试,nc模拟客户端),如果是进入了LAST_ACK状态,则不会占用资源
CLOSE_WAIT产生的原因:
- 一般这种情况属于程序中未正确的调用close()函数
CLOSE_WAIT优化:
- 检查程序运行代码,特别是一些对端异常断开时,是否有在对应的try..catch中进行资源的释放
半连接和全连接
- 半连接队列是在当接收到 SYN 报文时,内核将该链接存储到半连接队列,后返回 SYN-ACK报文
- 全连接队列是在当接收到ACK时,将连接从半连接队列中取出,然后放到全连接队列,等待上层调用accept()时再取出。
示意图:
半连接队列满了
处于半连接队列的连接是使用 request_socket 数据结构,相较于完整的socket数据结构,内存更小。
当受到SYN攻击时,就可能导致半连接队列满了,从而导致后面的SYN报文被丢弃。
解决方法有:
1 开启syncookie
内核参数:
可选参数有:
0 :表示不开启
1 :表示只有当半连接队列满了之后才开启,默认项
2 :表示无条件开启
syncookie原理 TODOtcp_syncookie = 1 // 默认为1
2 增大半连接队列
涉及的内核参数有:
实际的半连接队列长度 = min(tcp_max_syn_backlog, somaxconn, backlog)
因此需要同时调整这三个参数tcp_max_syn_backlog // 半连接大小参数somaxconn // 这个是在tcp/core中的内核参数backlog // 由listen()函数指定
3 减小 SYN-ACK 重传次数
内核参数:
因此可以使用echo 2 > /proc/sys/net/ipv4/tcp_synack_retries调低重传次数tcp_synack_retries // 默认是5次
全连接队列满了
造成全连接队列满了的原因可能是本身队列设置过小,或者是程序调用accept()不及时。而且当全连接队列满了之后也会影响到半连接队列。
查看全连接队列的方法:
- 1 Recv-Q :当前全连接队列的大小
- 2 Send-Q :全连接队列的最大值
当Recv-Q > Send-Q时即说明全连接队列满了
解决方法有:
1 增大全连接队列
涉及内核参数有:
实际的全连接队列的长度 = min(somaxconn, backlog)somaxconn // 这个是在tcp/core中的内核参数backlog // 由listen()函数指定
2 检查程序为何 accept不及时
close() 和 shutdown()
shutdown()
shutdown函数可以指定函数行为,how的可选值有:
- SHUT_RD => 0 :关闭Read端,源码中不会发送FIN包,而只是单纯的修改tcp_shutdown标识,在 tcp_recvmsg() 内核函数内,会直接break跳出处理逻辑,并清理接收到的缓存数据,然后进行ACK回复,也就是说对端并不清楚当前端已经关闭读。 (实测没有用……..TODO)
如果本端在关闭了读操作之后,还调用 read() 函数,则会返回EOF(end of file) - SHUT_WR => 1 :关闭Write端,会发送FIN包,当上层调用 write() 函数,会报错
- SHUT_RDWR => 2 :同时关闭Read 和 Write端,上述二者的结合
close()
当调用close(),如果接受缓冲区还有数据,那么会清除数据,并向对端发送 RST 报文。如果发送队列中有未发送的数据,那么会把最后的数据加上FIN标记,否则插入一个FIN报文
发送0字长的数据报文
对于send 和 recv 函数来说,当返回0时代表的是对端连接关闭,但是当主动调用send发送0字节长的数据报文时,send函数会返回0,但是内核并不会把该数据报文发出,因此对端recv函数不会接收到。
三次握手时发送数据报文
第一二次握手是不可以发送数据报文的,只有第三次握手是可以带数据的。
- 当未带数据时,返回的ACK报文的Seq Number是0(从tcpdump上表现来看就是不带Seq Number),只有Ack Number。
- 当带数据时,Seq Number为上个SYN-ACK报文的Ack Number + 1。
TCP粘包问题
因为TCP是字节流传输,因为有 Nagle 算法的缘故,在发送时会进行组包
解决方法有:
- 固定发送、接收窗口大小的数据包:
游戏协议用的就是这种方式,通过协议文件定义了服务端和客户端之间的压包解包顺序和大小。当然对于变长的内容,可以通过先提前压入长度解决
```c // 服务端 pack(1) pack(4) packsend()
// 客户端 packrecv() unpack(1) unpack(4)
- 以指定的字符为包的结束标志:<br />这种方式一般是在要发送的字节流中加入特殊符号,如'/r/n'。但是要注意包体正文总不能出现相同的特殊符号,否则会引发异常,因此通常需要对包体征文进行压缩转换- 定长的包头 + 不定长的包体:<br />定长的包头包含本次包体的大小,如: <br />当接收buf中累计接收到超过 msg_header 大小的数据时,从中抽取包头信息,然后再根据包头信息读取包体信息,依次循环接收。```cstruct msg_header{int32_t bodysize;int32_t cmd; // 协议号}
