TCP协议结构
里面主要部分是包序号和确认序号,包序号是为了解决乱序的问题,知道那个包在前,哪个包在后。确认序号,发出去的包要有个确认,如果没有收到就要重新发送。
状态位:
- SYN:发起一个连接
- ACK:回复
- RST: 重新连接
- FIN:结束连接
窗口大小,TCP要做流量控制,通信双方各声明一个窗口,标识自己的处理能力,让对端别发送太快,撑死我,也别发送太慢,饿死我
TCP的3次握手
TCP建立连接时的3次握手也常称为“请求->应答->应答之应答”。为什么是3次呢,因为建连接的双方都要做一个发送和收到确认才能证明双方连接建立了。
3次握手除了建立连接之外,还为了沟通意见事情,就是TCP包序号的问题,这也是SYN的由来,全称Synchronize Sequence Number。双方要确定一个起始的序号,序号不能每次都从1开始,因为从1开始如果掉线又重连,上次的包可能还在路上,那序号就重复了。所以每个连接都要有个不同的起始序号。这个起始序号是随着时间变化的,可以看作一个32位的计数器,每4ms加1,这样到两个包重复,需要4个小时,早就超过上一个的生存时间了(TTL)。
Sequence Number如何增加
sequence number不是每次加1的,而是和传输的字节数有关。如果双方3次握手后seq都是1,这时候A给B发了一个长度1440的包,那seq就变成1440,。B给1440的ACK回1441,证明1440收到了。
状态演变
Server端监听某个端口,处于LISTEN状态。客户端主动发起连接发送SYN,之后处于SYN-SENT状态。服务端收到SYN之后,向客户端发送SYN和ACK,之后服务端处于SYN-RCVD状态。客户端收到服务端的反馈后再发送ACK,之后客户端处于ESTABLISHED状态,服务端收到后,也处于ESTABLISHED状态。
这里有一个问题,如果client发送SYN之后掉线了,服务端发送SYN-ACK后会一直保持住资源,等待客户端回来的ACK。如果没收到,就会重试SYN-ACK,默认重试5次,间隔从1s开始,每次翻倍。所以,总共需要1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s,Server端才会结束,释放资源。
SYN flood攻击
client利用server端会等待的特性(63s),不断给server发SYN,发完就下线,把服务端的SYN队列耗尽,让正常的链接请求不能处理。linux处理这个的方法是设置tcp_syncookies参数,当SYN队列满了以后,TCP发SYN-ACK的时候会带上cookie,然后正常的client再ACK的时候带上就可以建立链接,但是这种方式不是常规用法。更通用的方法是调整几个参数,第一个是
tcp_synack_retries
,用来减少Server端SYN-ACK重试次数;第二个是tcp_max_syn_backlog
,增大队列长度;第三个是tcp_abort_on_overflow
,处理不过来干脆拒绝连接。
TCP的四次挥手
一方发起关闭请求
发起端(A)想断开连接的时候,发送FIN,然后进入FIN_WAIT_1的状态。接收端(B)收到后,发送ACK,表示自己知道了,进入CLOSE_WAIT状态。
A收到B的确认后进入FIN_WAIT_2的状态,如果这个时候B跑路,A将永远处于这个状态。linux针对这种情况,支持tcp_fin_timeout参数,超时后A就算B跑路了。
如果B没跑路,会再发FIN和ACK,表示我也不玩了,然后B进入LAST-ACK状态。A收到后回复ACK,进入TIME-WAIT状态。而B收到A的ACK后进入CLOSED状态。A等待一段时间也会进入CLOSE就彻底结束了。
A为什么要等一段时间呢,一是为了防止A最后的ACK B没收到,B会要求重发,如果A直接CLOSE,就不会重发了。二是B在发送最后的FIN和ACK之前的数据包可能比这两个包晚到。所以A要等一段再关闭。
A的TIME_WAIT的时间是2个MSL(Max Segment Lifetime)报文最大生存时间。
还有一种情况是B一直没收到最后的ACK,于是要求重发,而A已经过了2MSL了,A会回一个RST表示自己跑路了。
双方都发起断链请求
因为TCP是全双工的,所以,发送方和接收方都需要Fin和Ack。只不过,有一方是被动的,所以看上去就成了所谓的4次挥手。如果两边同时断连接,那就会就进入到CLOSING状态,然后到达TIME_WAIT状态。
TCP状态机
扩展问题思考
日常维护中查看服务器所有连接的状态
$netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
#结果实例
TIME_WAIT 814
CLOSE_WAIT 1
FIN_WAIT1 1
ESTABLISHED 634
SYN_RECV 2
LAST_ACK 1
如果web服务器连接出异常,百分之八九十都是下面两种情况:
- 服务器保持了大量TIME_WAIT状态
- 服务器保持了大量CLOSE_WAIT状态
这个时候新的请求就无法被处理了,接着就是大量Too Many Open Files异常
服务器保持了大量TIME_WAIT怎么解决
这个是服务器主动发起关闭连接引起,所以要调整的是本端服务器的参数,修改/etc/sysctl.conf
:
#对于一个新建连接,内核要发送多少个 SYN 连接请求才决定放弃,不应该大于255,默认值是5,对应于180秒左右时间
net.ipv4.tcp_syn_retries=2
#net.ipv4.tcp_synack_retries=2
#表示当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时,改为300秒
net.ipv4.tcp_keepalive_time=1200
net.ipv4.tcp_orphan_retries=3
#表示如果套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2状态的时间
net.ipv4.tcp_fin_timeout=30
#表示SYN队列的长度,默认为1024,加大队列长度为8192,可以容纳更多等待连接的网络连接数。
net.ipv4.tcp_max_syn_backlog = 4096
#表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭
net.ipv4.tcp_syncookies = 1
#表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭
net.ipv4.tcp_tw_reuse = 1
#表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭
net.ipv4.tcp_tw_recycle = 1
##减少超时前的探测次数
net.ipv4.tcp_keepalive_probes=5
##优化网络设备接收队列
net.core.netdev_max_backlog=3000
这里头主要注意到的是net.ipv4.tcp_tw_reuse
net.ipv4.tcp_tw_recycle
net.ipv4.tcp_fin_timeout
net.ipv4.tcp_keepalive_*
这几个参数。
net.ipv4.tcp_tw_reuse和net.ipv4.tcp_tw_recycle的开启都是为了回收处于TIME_WAIT状态的资源。
net.ipv4.tcp_fin_timeout这个时间可以减少在异常情况下服务器从FIN-WAIT-2转到TIME_WAIT的时间。
net.ipv4.tcp_keepalive_*一系列参数,是用来设置服务器检测连接存活的相关配置。
注意tcp_tw_reuse,tcp_tw_recycle开启的风险,用这个是非常危险的:http://blog.csdn.net/wireless_tech/article/details/6405755
所以设置keepAlive是非常重要的。
服务器保持了大量TIME_WAIT怎么解决
如果一直保持在CLOSE_WAIT状态,那么只有一种情况,就是在对方关闭连接之后服务器程序自己没有进一步发出ack信号。换句话说,就是在对方连接关闭之后,程序里没有检测到,或者程序压根就忘记了这个时候需要关闭连接,于是这个资源就一直被程序占着。
如果你使用的是HttpClient并且你遇到了大量CLOSE_WAIT的情况,那么这篇日志也许对你有用:http://blog.csdn.net/shootyou/article/details/6615051
在那边日志里头我举了个场景,来说明CLOSE_WAIT和TIME_WAIT的区别,这里重新描述一下:
服务器A是一台爬虫服务器,它使用简单的HttpClient去请求资源服务器B上面的apache获取文件资源,正常情况下,如果请求成功,那么在抓取完资源后,服务器A会主动发出关闭连接的请求,这个时候就是主动关闭连接,服务器A的连接状态我们可以看到是TIME_WAIT。如果一旦发生异常呢?假设请求的资源服务器B上并不存在,那么这个时候就会由服务器B发出关闭连接的请求,服务器A就是被动的关闭了连接,如果服务器A被动关闭连接之后程序员忘了让HttpClient释放连接,那就会造成CLOSE_WAIT的状态了。
所以如果将大量CLOSE_WAIT的解决办法总结为一句话那就是:查代码。因为问题出在服务器程序里头啊。
参考文章:
[极客时间] 趣谈网络协议 --刘超
[CSDN] 再谈应用环境下的TIME_WAIT和CLOSE_WAIT