美文网首页
TCP协议连接

TCP协议连接

作者: 明翼 | 来源:发表于2020-08-11 20:57 被阅读0次

    TCP 协议非常重要,怎么强调都不过分,所以需要扎实地学习。本文参考《计算机网络》和互联网上的相关资料,想以简单明了的方式来阐明一些 tcp 相关内容,行文尽量简单。

    一 TCP 协议特点

    TCP,全称传输控制协议(transmission Control Protocol)是位于传输层的重要协议,它最要的特点:
    面向连接 ,可靠面向字节流
    可靠: TCP 协议通过序号和确认序号,来保证即使在网络很差的环境,仍然可提供可靠传输。
    面向连接: TCP 协议是一对一的协议,每个连接用源 ip,源端口,目的 ip,目的端口四元组来确认一个连接,通过 socket,以及序号,窗口信息这些状态信息来表示连接。
    面向字节流: TCP 传输的是字节流,不是数据包,客户端发送 2 个 512 字节的数据块,在服务器端的应用层上,收到可能是 2 个 512 字节的数据块,也可能是 1024 字节的数据库,还可能是 4 个 128 字节的数据块。

    TCP 协议报文组成如下图:


    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
    
    1. 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

    注意在 LISTEN:
    Recv-Q 表示是当前全连接的大小即等待被 accpet 的连接;
    Send-Q 是最大的全连接队列大小。

    在非 LISTEN 状态上,Recv-Q 表示已经收到,但未被应用读取的字节数; Send-Q 表示已经发送但是没被对方确认的字节数,在网络有问题的时候,很有帮助。


    非LISTEN

    同样,我们可以通过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 原理

    syncookies示意图
    1. 当开启 syncookies 后,半连接队列满了,并不直接 drop 报文,而是根据客户端连接信息计算出一个 cookie 值,在给对方回复的 SYN+ACK 报文中一并带着。
    2. 客户端再次 ACK 回复服务器端的时候,会带着这个 cookie 信息,服务器端进行相关校验,如果校验通过,连接直接放入到全连接队列中。

    除了开启 cookie 功能外,防护 SYN 攻击还需要:

    1. 增大半连接队列长度,这个可以缓冲下攻击。
      增大半连接队列长度,需要同时增大全连接队列长度才可以生效:
    # 半连接数
    echo 2048 > /proc/sys/net/ipv4/tcp_max_syn_backlog
    #全连接内核控制参数
    echo 2048 > /proc/sys/net/core/somaxconn
    #全连接程序控制参数
    backlog
    
    1. 开启 syncookies 上面有讲到
    echo 1 > /proc/sys/net/ipv4/tcp_syncookies
    
    1. 减少 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 功能。

    三 诗词欣赏

    《水调歌头·重上井冈山》
    
    [ 现代 ] 毛泽东
    
    久有凌云志,重上井冈山。
    千里来寻故地,旧貌变新颜。
    到处莺歌燕舞,更有潺潺流水,高路入云端。
    过了黄洋界,险处不须看。
    
    风雷动,旌旗奋,是人寰。
    三十八年过去,弹指一挥间。
    可上九天揽月,可下五洋捉鳖,谈笑凯歌还。
    世上无难事,只要肯登攀。
    

    相关文章

      网友评论

          本文标题:TCP协议连接

          本文链接:https://www.haomeiwen.com/subject/xfhkfktx.html