“三次握手,四次挥手”你确定你了解么?(之三)
扫描二维码
随时随地手机看文章
连接队列
在外部请求到达时,被服务程序最终感知到前,连接可能处于SYN_RCVD状态或是ESTABLISHED状态,但还未被应用程序接受。
对应地,服务器端也会维护两种队列,处于SYN_RCVD状态的半连接队列,而处于ESTABLISHED状态但仍未被应用程序accept的为全连接队列。如果这两个队列满了之后,就会出现各种丢包的情形。
1 2 | 查看是否有连接溢出 netstat -s | grep LISTEN |
半连接队列满了
在三次握手协议中,服务器维护一个半连接队列,该队列为每个客户端的SYN包开设一个条目(服务端在接收到SYN包的时候,就已经创建了request_sock结构,存储在半连接队列中),该条目表明服务器已收到SYN包,并向客户发出确认,正在等待客户的确认包。这些条目所标识的连接在服务器处于Syn_RECV状态,当服务器收到客户的确认包时,删除该条目,服务器进入ESTABLISHED状态。
目前,Linux下默认会进行5次重发SYN-ACK包,重试的间隔时间从1s开始,下次的重试间隔时间是前一次的双倍,5次的重试时间间隔为1s, 2s, 4s, 8s, 16s, 总共31s, 称为
指数退避
,第5次发出后还要等32s才知道第5次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 63s, TCP才会把断开这个连接。由于,SYN超时需要63秒,那么就给攻击者一个攻击服务器的机会,攻击者在短时间内发送大量的SYN包给Server(俗称SYN flood攻击),用于耗尽Server的SYN队列。对于应对SYN 过多的问题,linux提供了几个TCP参数:tcp_syncookies、tcp_synack_retries、tcp_max_syn_backlog、tcp_abort_on_overflow 来调整应对。
参数 | 作用 |
---|---|
tcp_syncookies | SYNcookie将连接信息编码在ISN(initialsequencenumber)中返回给客户端,这时server不需要将半连接保存在队列中,而是利用客户端随后发来的ACK带回的ISN还原连接信息,以完成连接的建立,避免了半连接队列被攻击SYN包填满。 |
tcp_syncookies | 内核放弃建立连接之前发送SYN包的数量。 |
tcp_synack_retries | 内核放弃连接之前发送SYN+ACK包的数量 |
tcp_max_syn_backlog | 默认为1000. 这表示半连接队列的长度,如果超过则放弃当前连接。 |
tcp_abort_on_overflow | 如果设置了此项,则直接reset. 否则,不做任何操作,这样当服务器半连接队列有空了之后,会重新接受连接。Linux坚持在能力许可范围内不忽略进入的连接 。客户端在这期间会重复发送sys包,当重试次数到达上限之后,会得到connection time out 响应。 |
全连接队列满了
当第三次握手时,当server接收到ACK包之后,会进入一个新的叫 accept 的队列。
当accept队列满了之后,即使client继续向server发送ACK的包,也会不被响应,此时ListenOverflows+1,同时server通过tcp_abort_on_overflow来决定如何返回,0表示直接丢弃该ACK,1表示发送RST通知client;相应的,client则会分别返回read timeout
或者 connection reset by peer
。另外,tcp_abort_on_overflow是0的话,server过一段时间再次发送syn+ack给client(也就是重新走握手的第二步),如果client超时等待比较短,就很容易异常了。而客户端收到多个 SYN ACK 包,则会认为之前的 ACK 丢包了。于是促使客户端再次发送 ACK ,在 accept队列有空闲的时候最终完成连接。若 accept队列始终满员,则最终客户端收到 RST 包(此时服务端发送syn+ack的次数超出了tcp_synack_retries)。
服务端仅仅只是创建一个定时器,以固定间隔重传syn和ack到服务端
参数 | 作用 |
---|---|
tcp_abort_on_overflow | 如果设置了此项,则直接reset. 否则,不做任何操作,这样当服务器半连接队列有空了之后,会重新接受连接。Linux坚持在能力许可范围内不忽略进入的连接 。客户端在这期间会重复发送sys包,当重试次数到达上限之后,会得到connection time out 响应。 |
min(backlog, somaxconn) | 全连接队列的长度。 |
命令
netstat -s命令
1 2 3 | [root<a href="http://www.jobbole.com/members/server">@server</a> ~]# netstat -s | egrep "listen|LISTEN" 667399 times the listen queue of a socket overflowed 667399 SYNs to LISTEN sockets ignored |
上面看到的 667399 times ,表示全连接队列溢出的次数,隔几秒钟执行下,如果这个数字一直在增加的话肯定全连接队列偶尔满了。
1 | [root<a href="http://www.jobbole.com/members/server">@server</a> ~]# netstat -s | grep TCPBacklogDrop |
查看 Accept queue 是否有溢出
ss命令
1 2 3 4 | [root<a href="http://www.jobbole.com/members/server">@server</a> ~]# ss -lnt State Recv-Q Send-Q Local Address:Port Peer Address:Port LISTEN 0 128 *:6379 *:* LISTEN 0 128 *:22 *:* |
如果State是listen状态,Send-Q 表示第三列的listen端口上的全连接队列最大为50,第一列Recv-Q为全连接队列当前使用了多少。
非 LISTEN 状态中 Recv-Q 表示 receive queue 中的 bytes 数量;Send-Q 表示 send queue 中的 bytes 数值。
小结
当外部连接请求到来时,TCP模块会首先查看max_syn_backlog,如果处于SYN_RCVD状态的连接数目超过这一阈值,进入的连接会被拒绝。根据tcp_abort_on_overflow字段来决定是直接丢弃,还是直接reset.
从服务端来说,三次握手中,第一步server接受到client的syn后,把相关信息放到半连接队列中,同时回复syn+ack给client. 第三步当收到客户端的ack, 将连接加入到全连接队列。
一般,全连接队列比较小,会先满,此时半连接队列还没满。如果这时收到syn报文,则会进入半连接队列,没有问题。但是如果收到了三次握手中的第3步(ACK),则会根据tcp_abort_on_overflow字段来决定是直接丢弃,还是直接reset.此时,客户端发送了ACK, 那么客户端认为三次握手完成,它认为服务端已经准备好了接收数据的准备。但此时服务端可能因为全连接队列满了而无法将连接放入,会重新发送第2步的syn+ack, 如果这时有数据到来,服务器TCP模块会将数据存入队列中。一段时间后,client端没收到回复,超时,连接异常,client会主动关闭连接。
“三次握手,四次挥手”redis实例分析
我在dev机器上部署redis服务,端口号为6379,
通过tcpdump工具获取数据包,使用如下命令
1 2 | tcpdump -w /tmp/a.cap port 6379 -s0 -w把数据写入文件,-s0设置每个数据包的大小默认为68字节,如果用-S 0则会抓到完整数据包 |
在dev2机器上用redis-cli访问dev:6379, 发送一个ping, 得到回复pong
停止抓包,用tcpdump读取捕获到的数据包
1 2 | tcpdump -r /tmp/a.cap -n -nn -A -x| vim - (-x 以16进制形式展示,便于后面分析) |
共收到了7个包。
抓到的是IP数据包,IP数据包分为IP头部和IP数据部分,IP数据部分是TCP头部加TCP数据部分。
IP的数据格式为:
它由固定长度20B+可变长度构成。
1 2 3 4 5 | 10:55:45.662077 IP dev2.39070 > dev.6379: Flags [S], seq 4133153791, win 29200, options [mss 1460,sackOK,TS val 2959270704 ecr 0,nop,wscale 7], length 0 0x0000: 4500 003c 08cf 4000 3606 14a5 0ab3 b561 0x0010: 0a60 5cd4 989e 18eb f65a ebff 0000 0000 0x0020: a002 7210 872f 0000 0204 05b4 0402 080a 0x0030: b062 e330 0000 0000 0103 0307 |
对着IP头部格式,来拆解数据包的具体含义。
字节值 | 字节含义 |
---|---|
0x4 | IP版本为ipv4 |
0x5 | 首部长度为5 * 4字节=20B |
0x00 | 服务类型,现在基本都置为0 |
0x003c | 总长度为3*16+12=60字节,上面所有的长度就是60字节 |
0x08cf | 标识。同一个数据报的唯一标识。当IP数据报被拆分时,会复制到每一个数据中。 |
0x4000 | 3bit 标志 + 13bit 片偏移 。3bit 标志对应 R、DF、MF。目前只有后两位有效,DF位:为1表示不分片,为0表示分片。MF:为1表示“更多的片”,为0表示这是最后一片。13bit 片位移:本分片在原先数据报文中相对首位的偏移位。(需要再乘以8 ) |
0x36 | 生存时间TTL。IP报文所允许通过的路由器的最大数量。每经过一个路由器,TTL减1,当为 0 时,路由器将该数据报丢弃。TTL 字段是由发送端初始设置一个 8 bit字段.推荐的初始值由分配数字 RFC 指定。发送 ICMP 回显应答时经常把 TTL 设为最大值 255。TTL可以防止数据报陷入路由循环。 此处为54. |
0x06 | 协议类型。指出IP报文携带的数据使用的是哪种协议,以便目的主机的IP层能知道要将数据报上交到哪个进程。TCP 的协议号为6,UDP 的协议号为17。ICMP 的协议号为1,IGMP 的协议号为2。该 IP 报文携带的数据使用 TCP 协议,得到了验证。 |
0x14a5 | 16bitIP首部校验和。 |
0x0ab3 b561 | 32bit源ip地址。 |
0x0a60 5cd4 | 32bit目的ip地址。 |
剩余的数据部分即为TCP协议相关的。TCP也是20B固定长度+可变长度部分。
字节值 | 字节含义 |
---|---|
0x989e | 16bit源端口。1161616+81616+1416+11=39070 |
0x18eb | 16bit目的端口6379 |
0xf65a ebff | 32bit序列号。4133153791 |
0x0000 0000 | 32bit确认号。 |
0xa | 4bit首部长度,以4byte为单位。共10*4=40字节。因此TCP报文的可选长度为40-20=20 |
0b000000 | 6bit保留位。目前置为0. |
0b000010 | 6bitTCP标志位。从左到右依次是紧急 URG、确认 ACK、推送 PSH、复位 RST、同步 SYN 、终止 FIN。 |
0x7210 | 滑动窗口大小,滑动窗口即tcp接收缓冲区的大小,用于tcp拥塞控制。29200 |
0x872f | 16bit校验和。 |
0x0000 | 紧急指针。仅在 URG = 1时才有意义,它指出本报文段中的紧急数据的字节数。当 URG = 1 时,发送方 TCP 就把紧急数据插入到本报文段数据的最前面,而在紧急数据后面的数据仍是普通数据。 |
可变长度部分,协议如下:
字节值 | 字节含义 |
---|---|
0x0204 05b4 | 最大报文长度为,05b4=1460. 即可接收的最大包长度,通常为MTU减40字节,IP头和TCP头各20字节 |
0x0402 | 表示支持SACK |
0x080a b062 e330 0000 0000 | 时间戳。Ts val=b062 e330=2959270704, ecr=0 |
0x01 | 无操作 |
0x03 0307 | 窗口扩大因子为7. 移位7, 乘以128 |
这样第一个包分析完了。dev2向dev发送SYN请求。也就是三次握手中的第一次了。
SYN seq(c)=4133153791
第二个包,dev响应连接,ack=4133153792. 表明dev下次准备接收这个序号的包,用于tcp字节注的顺序控制。dev(也就是server端)的初始序号为seq=4264776963, syn=1.SYN ack=seq(c)+1 seq(s)=4264776963
第三个包,client包确认,这里使用了相对值应答。seq=4133153792, 等于第二个包的ack. ack=4264776964.ack=seq(s)+1, seq=seq(c)+1
至此,三次握手完成。接下来就是发送ping和pong的数据了。
接着第四个包。
1 2 3 4 5 6 | 10:55:48.090073 IP dev2.39070 > dev.6379: Flags [P.], seq 1:15, ack 1, win 229, options [nop,nop,TS val 2959273132 ecr 3132256230], length 14 0x0000: 4500 0042 08d1 4000 3606 149d 0ab3 b561 0x0010: 0a60 5cd4 989e 18eb f65a ec00 fe33 5504 0x0020: 8018 00e5 4b5f 0000 0101 080a b062 ecac 0x0030: bab2 6fe6 2a31 0d0a 2434 0d0a 7069 6e67 0x0040: 0d0a |
tcp首部长度为32B, 可选长度为12B. IP报文的总长度为66B, 首部长度为20B, 因此TCP数据部分长度为14B. seq=0xf65a ec00=4133153792
ACK, PSH. 数据部分为2a31 0d0a 2434 0d0a 7069 6e67 0d0a
1 2 3 4 5 6 | 0x2a31 -> *1 0x0d0a -> rn 0x2434 -> $4 0x0d0a -> rn 0x7069 0x6e67 -> ping 0x0d0a -> rn |
dev2向dev发送了ping数据,第四个包完毕。
第五个包,dev2向dev发送ack响应。
序列号为0xfe33 5504=4264776964, ack确认号为0xf65a ec0e=4133153806=(4133153792+14).
第六个包,dev向dev2响应pong消息。序列号fe33 5504,确认号f65a ec0e, TCP头部可选长度为12B, IP数据报总长度为59B, 首部长度为20B, 因此TCP数据长度为7B.
数据部分2b50 4f4e 470d 0a, 翻译过来就是+PONGrn
.
至此,Redis客户端和Server端的三次握手过程分析完毕。
总结
“三次握手,四次挥手”看似简单,但是深究进去,还是可以延伸出很多知识点的。比如半连接队列、全连接队列等等。以前关于TCP建立连接、关闭连接的过程很容易就会忘记,可能是因为只是死记硬背了几个过程,没有深入研究背后的原理。
所以,“三次握手,四次挥手”你真的懂了吗?
参考资料
【redis】https://segmentfault.com/a/1190000015044878
【tcp option】https://blog.csdn.net/wdscq1234/article/details/52423272
【滑动窗口】https://www.zhihu.com/question/32255109
【全连接队列】http://jm.taobao.org/2017/05/25/525-1/
【client fooling】 https://github.com/torvalds/linux/commit/5ea8ea2cb7f1d0db15762c9b0bb9e7330425a071
【backlog RECV_Q】http://blog.51cto.com/59090939/1947443
【定时器】https://www.cnblogs.com/menghuanbiao/p/5212131.html
【队列图示】https://www.itcodemonkey.com/article/5834.html
【tcp flood攻击】https://www.cnblogs.com/hubavyn/p/4477883.html
【MSS MTU】https://blog.csdn.net/LoseInVain/article/details/53694265