TCP 协议非常重要,怎么强调都不过分,所以需要扎实地学习。本文参考《计算机网络》和互联网上的相关资料,想以简单明了的方式来阐明一些 tcp 相关内容,行文尽量简单。
一 TCP 协议特点
TCP,全称传输控制协议(transmission Control Protocol)是位于传输层的重要协议,它最要的特点:面向连接 ,可靠,面向字节流。
可靠:TCP 协议通过序号和确认序号,来保证即使在网络很差的环境,仍然可提供可靠传输。
面向连接:TCP 协议是一对一的协议,每个连接用源 ip,源端口,目的 ip,目的端口四元组来确认一个连接,通过 socket,以及序号,窗口信息这些状态信息来表示连接。
面向字节流: TCP 传输的是字节流,不是数据包,客户端发送 2 个 512 字节的数据块,在服务器端的应用层上,收到可能是 2 个 512 字节的数据块,也可能是 1024 字节的数据库,还可能是 4 个 128 字节的数据块。
TCP 协议报文组成如下图:
序列号: 每次连接随机生成,用来标识报文的顺序,防止乱序;TCP 服务器端会对收到的 TCP 报文进行重组。
确认序列号: 用来确认收到的数据,制定下一个期待的字节,而不是收到的最后一个字节,它是累计确认,不会超过丢失的数据。
首部长度:表示 TCP 头部包含多少个 32 位字节,TCP 头部有 20 个固定字节,还包含可选的头部,这个长度是可变的,所以用这个头部长度来区分下。
控制位:FIN: 用来释放连接,表示发送方不在发送数据了。
SYN:请求连接报文标识,用序列号字段的值来标识初始的序列号设定。
RST:TCP 连接中异常,重置终止。
ACK: 表示确认序列号字段有效,除 SYN 包外,几乎所有的报文 ACK 都是 1.
ECE 和 CWR:用于拥塞控制的时候,当接受端收到网络的拥塞控制指示后,通过设置 ECE 来通知另一端要放慢发送速率;TCP 一端通过设置 CWR 来通知另一端来告诉对方已经放慢速率,不用再进行 ECN-echo 通知(显示的拥塞控制通知)。
窗口大小:16 个字节,流量控制,发送端可以发送的最大字节数而不用确认,16 个字节位 64KB,在延迟比较大的网络,效率很低,在可选字段可以在 TCP 双方协商尺度因子,从而扩大窗口,最大可以支持的窗口位数位 30 位。
选项:
额外用途,比如指定每台主机可以支持的 MSS(最大段长度),还有上文说的,窗口尺寸因子;TCP 的时间戳选项,这个主要防止序列号过大之后,循环利用,从而导致的序号回绕问题。还有一个叫 SACK 选择确认,让对端可以了解接受端的接受报文情况,因为中间可能漏掉一个报文,如果对端将确认号后面报文都重发,就浪费了。
二 TCP 连接
上图就是著名的三次握手,如果每步骤的报文丢失有什么影响,另外我们对此如何优化?
2.1 SYN 包重传
SYN 包是客户端发起三次握手的时候第一包,如果包丢失了,客户端会进行重复,重发的次数由内核的参数:
cat /proc/sys/net/ipv4/tcp_syn_retries
或
sysctl -a|grep "net.ipv4.tcp_syn_retries"
默认值为 6,表示如果没有收到响应,会重发 6 次,6 次发送也是有讲究的,是每次超时后重发,每次的超时时间不同.
第一次 1s 超时后重发
第二次 3s 超时后重发
第三次 7s 超时后重发
第四次 15s 超时后重发
第五次 31s 后重发
第六次 63s 后重发 每次超时时间是指数级增加的超时时间为:2s, 4s,8s,16s,32s,64s,最后一次会等待 64s,所以一共等待的时间为: 1+2+4+8+16+32+64 = 127s 也就是 2 分钟多.
测试下:1) 在主机上通过 telnet 命令去访问不存在的主机端口。2) 通过 tcpdump 抓包看下重发的情况。
tcpdump -i eth0 -nn 'host 10.22.19.206' -w test.pcap
wireshark 打开观察 SYN 包重发情况。
优化: 对于内网通讯,或有明确任务的情况下,我们可以减少重试次数,快速失败,让错误快速暴露出来. 可以通过
sysctl -w net.ipv4.tcp_syncookies=1
sysctl -p
或者永久修改
vim /etc/sysctl.conf
net.ipv4.tcp_syncookies=1
sysctl -p
后面修改的方法都是类似的,不再赘述。
2.2 服务器端收到 SYN 报文后
如果服务器端正常的,可以收到 SYN 报文后,会如上面三次握手的示意图,回复 SYN+ACK,同时服务器端状态变成:SYN_RCVD 状态,当收到客户端 ACK 确认包后,会将连接从半连接队列中移除,放入到 accept 队列中去,等待应用层函数调用 accept 函数来把连接取出来。
由于三次握手并没有结束,所以服务器端有个队列存放这种未完成握手的连接,这个就是半连接队列。
这里面有个安全问题,就是 SYN 泛洪攻击,攻击者生成大量客户端 IP,给服务器端发起 SYN 连接,由于客户端的 IP 等信息是伪造的,服务器在收到 SYN 报文后,再发送 SYN+ACK,而由于客户端 IP 和端口都是伪造的,所以造成客户端一直没有回复,造成服务器端一直重发 SYN+ACK 报文,而且这未完全建立的连接会占用半连接队列,队列满了之后,服务器端就无法再接收到任何真正的请求了(如果没有开启 tcp_syncookies)。
开启 tcp_syncookies 可以对 SYN 泛洪攻击,有一定防护作用,设置:
sysctl -a |grep "net.ipv4.tcp_syncookies"
net.ipv4.tcp_syncookies = 1
值为 1 的时候,SYN 半队列放不下的时候,启用 syncookies; 2 表示无条件启用 syncookies;0 标识不启用。
sysctl -a |grep "net.ipv4.tcp_max_syn_backlog "
net.ipv4.tcp_max_syn_backlog = 1024
我们可以通过命令查看半连接的队列默认大小为 1024。这个值并不是完全确定了半连接队列的最大值。实际半连接最大值计算规则如下:
如果 tcp_max_syn_backlog > min(somaxconn,backlog) 即半连接队列最大值大于全连接队列最大值情况下,半连接最大值= min(somaxconn,backlog) *2. 如果 tcp_max_syn_backlog <= min(somaxconn,backlog) 半连接最大值设置为 tcp_max_syn_backlog *2
这个值我们可以在确定连接比较多的情况下扩大些,可以通过以下命令查看因半连接队列满而造成的报文丢失个数:
netstat -s | grep "SYNs to LISTEN"
28810 SYNs to LISTEN sockets dropped
现在的全连接队列的个数可以通过 ss 命令查看,如下:
注意在 LISTEN:Recv-Q 表示是当前全连接的大小即等待被 accpet 的连接;Send-Q 是最大的全连接队列大小。
在非 LISTEN 状态上,Recv-Q 表示已经收到,但未被应用读取的字节数;Send-Q 表示已经发送但是没被对方确认的字节数,在网络有问题的时候,很有帮助。
同样,我们可以通过netstat -s
命令来查看,因全连接队列满了之后,造成的丢包现象:
netstat -s |grep overflowed
28241 times the listen queue of a socket overflowed
全队列满了之后,除了丢包,还可以发 RST 包给客户端,可以通过内核参数来配置:
sysctl -a |grep "net.ipv4.tcp_abort_on_overflow"
net.ipv4.tcp_abort_on_overflow = 0
tcp_abort_on_overflow
值为 0,则服务器端在队列满了之后,直接丢到客户端的 ACK 报文。如果设置为 1,则服务器端在全连接队列满了之后,发送 RST 报文给客户端。设置为 0 ,可以提高连接的成功率,以为客户端可能以为 ACK 报文丢失,就重发,而重发后,如果服务器端的全连接队列空下来了,还可以正常建立连接。如果服务器端一直很忙的话,可以设置为 1,则客户端收到“connect reset by peer”的提示,这个提示是多么的眼熟啊!
全队列的大小除了我们在服务器端代码的 listen 进行监听时,设置 backlog,还受到系统的阀值限制(取两者的最小值)即:
sysctl -a |grep "net.core.somaxconn"
net.core.somaxconn = 1024
如果看上面的因为全连接队列满,造成的丢失数量越来越多,可以调大此值。
2.3 syncookies 原理
![](https://i-blog.csdnimg.cn/blog_migrate/c36e15bc5f60739e82b11d4819c608ce.png)
当开启 syncookies 后,半连接队列满了,并不直接 drop 报文,而是根据客户端连接信息计算出一个 cookie 值,在给对方回复的 SYN+ACK 报文中一并带着。
客户端再次 ACK 回复服务器端的时候,会带着这个 cookie 信息,服务器端进行相关校验,如果校验通过,连接直接放入到全连接队列中。
除了开启 cookie 功能外,防护 SYN 攻击还需要:
增大半连接队列长度,这个可以缓冲下攻击。增大半连接队列长度,需要同时增大全连接队列长度才可以生效:
# 半连接数
echo 2048 > /proc/sys/net/ipv4/tcp_max_syn_backlog
#全连接内核控制参数
echo 2048 > /proc/sys/net/core/somaxconn
#全连接程序控制参数
backlog
开启 syncookies 上面有讲到
echo 1 > /proc/sys/net/ipv4/tcp_syncookies
减少 SYN+ACK 重传次数 因为 SYN 攻击的时候,客户端的 IP 和端口是伪造的,服务器端收不到 ACK 报文的时候,会一直重发 SYN+ACK 报文,重发的次数由:
sysctl -a |grep "net.ipv4.tcp_synack_retries "
net.ipv4.tcp_synack_retries = 2
阿里云主机上默认是 2 次,一般 linux 上默认为 5 次,与客户端发送的 SYN 类似,会按照指数级递增超时时间,重试会经历 1,2,4,8,16 s 最后一次等待 32s 后超时,所以总共超时时间为 63s,如果配置为 2,则为 7s 超时,减少服务器端的重试次数。
2.4 如果 ACK 报文丢失
上文中,如果 SYN 报文丢失,默认重发 6 次,则 127 秒后显示超时,如果三次握手的第二个报文,即 SYN+ACK 报文丢失,则默认重发是 5 次,总共需要等待的时间为 63 秒显示超时。
那么如果三次握手中,客户端发送的 ACK 报文丢失后会发生什么?
首先服务器端收到了 SYN 报文处于 SYN_RECV 阶段,且发送 SYN+ACK 报文给客户端,没有收到对方的 ACK 报文,认为报文丢失,则按照 tcp_synack_retries 的配置重试,默认在 63 秒后显示超时。
对于客户端来说,客户端收到 SYN+ACK 报文后,回给服务器 ACK 报文,进入到 ESTABLISHED 状态,客户端认为自己已经建立了连接,所以可以发送数据给服务器端,但是服务器端连接并没有真正建立起来,所以客户端一直没有收到 ACK 报文,就重发,重发的次数默认为 15 次。
sysctl -a |grep "tcp_retries2"
net.ipv4.tcp_retries2 = 15
如果客户端一直没有发送数据,那么什么时候可以发现这个连接其实是有问题的那。可以在 linux 内核中设置 tcp 保活机制,意思就是在 tcp 连接一段时间内没发送报文后,内核自动发送保活报文,如果连接有问题,保活报文同样会重发,重发一定次数后,会中断此连接。
net.ipv4.tcp_keepalive_time=7200
net.ipv4.tcp_keepalive_intvl=75
net.ipv4.tcp_keepalive_probes=9
上面是保活相关配置,含义是如果 tcp 连接在 7200 秒的时间,没有发送任何数据,则会触发保活机制,进行探测保活。如果没有收到响应会在 75 秒后重新发送,一共发送 9 次。所以总共发现需要的时间为:7200+ (75*9)= 7875 秒 即 2 小时 11 分 15 秒才可以发现连接不可用。
2.5 Fast open
在 http 等协议中,发出第一次 GET 请求,到收到第一次请求数据回复,需要 2.5RTT,如果最后一次 ACK 带数据请求的话,也需要 2RTT,这样会影响 HTTP 等协议的性能。如果 HTTP 是短连接的,每次请求都要先经历三次握手,则消耗很长不必要的时间,如下图:
Linux 3.7 内核以后,支持 TCP fast open 功能,可以减少 tcp 连接的时延。
在第一次连接时候,服务器端在第二次握手时候产生一个 cookie,数据已经加密,并通过 SYN+ACK 包一起发送给客户端,由于客户端就缓存这个 cookie,第一次 HTTP GET 请求时延为 2RTT。
下次请求的时候,客户端 SYN 包会带上 Cookie 发送给服务器端,就可以跳过三次握手,服务器端从 cookie 中获取到必要的信息。
sysctl -a |grep "net.ipv4.tcp_fastopen"
net.ipv4.tcp_fastopen = 0
tcp_fastopen
设置按照比特来设置。
为 0 时候表示不开启
Fast open
;为 1 时候,表示作为客户端开启Fast open
;为 2 时候表示服务端开启Fastopen
; 为 3 的时候,表示客户端和服务器端都开启 Fast open 功能。
三 诗词欣赏
《水调歌头·重上井冈山》
[ 现代 ] 毛泽东
久有凌云志,重上井冈山。
千里来寻故地,旧貌变新颜。
到处莺歌燕舞,更有潺潺流水,高路入云端。
过了黄洋界,险处不须看。
风雷动,旌旗奋,是人寰。
三十八年过去,弹指一挥间。
可上九天揽月,可下五洋捉鳖,谈笑凯歌还。
世上无难事,只要肯登攀。