浅谈TCP(1):状态机与重传机制

TCP协议比较复杂,接下来分两篇文章浅要介绍TCP中的一些要点。html

本文介绍TCP的状态机与重传机制,下文讲解流量控制与拥塞控制。git

本文大部份内容基于TCP 的那些事儿(上)修改而来,部分观点与原文不一样,重要地方增长了解释。github

前置知识

一些网络基础

TCP在网络OSI的七层模型中的第四层——Transport层,IP在第三层——Network层,ARP在第二层——Data Link层,在第二层上的数据,咱们叫Frame,在第三层上的数据叫Packet,第四层的数据叫Segment。算法

应用层的数据首先会打到TCP的Segment中,而后TCP的Segment会打到IP的Packet中,而后再打到以太网Ethernet的Frame中,传到对端后,各个层解析本身的协议,而后把数据交给更高层的协议处理。shell

TCP头格式

在正式讨论以前,先来看一下TCP头的格式: [图片上传中...(image.png-eed30f-1522722733998-0)]浏览器

image.png

注意:安全

  • TCP的包是没有IP地址的,那是IP层上的事。可是有源端口和目标端口。
  • 一个TCP链接须要四个元组来表示是同一个链接(src_ip, src_port, dst_ip, dst_port)(准确说是五元组,还有一个是协议,但由于这里只是说TCP协议,因此,这里我只说四元组)。
  • 注意上图中的四个很是重要的东西:
    • Sequence Number,包的序号Seq,用于解决网络包乱序(reordering)。
    • Acknowledgement Number,Ack用于确认收到Seq(Ack = Seq + 1,表示收到了Seq及Seq以前的数据包,期待Seq + 1),用于解决丢包
    • Window,又叫Advertised Window,能够近似理解为滑动窗口(Sliding Window)的大小,用于流控
    • TCP Flag ,区分包的类型,如SYN包、FIN包、RST包等,主要_用于操控TCP状态机_。

其余字段参考下图:服务器

image.png

TCP的状态机

其实,网络传输是没有链接的——TCP所谓的“链接”,其实只不过是在通信的双方维护一个“链接状态”,让它看上去好像有链接同样。因此,TCP的状态转换很是重要。cookie

下面是简化的“TCP协议状态机” 和 “TCP三次握手建链接 + 传数据 + 四次挥手断链接” 的对照图,两张图本质上都描述了TCP协议状态机,但场景略有不一样。这两个图很是重要,必定要记牢网络

TCP协议状态机,不区分client、server:

image.png

下图是经典的“TCP三次握手建链接 + 传数据 + 四次挥手断链接”,client发起握手,向server传输数据(server不向client传),最后发起挥手:

image.png

三次握手与四次挥手

不少人会问,为何建链接要三次握手,断链接须要四次挥手?

三次握手建链接

主要是要_初始化Sequence Number 的初始值_。

通讯的双方要同步对方ISN(初始化序列号,Inital Sequence Number)——因此叫SYN(全称Synchronize Sequence Numbers)。也就是上图中的 x 和 y。这个号在之后的数据通讯中,在client端按发送顺序递增,在server端按递增顺序从新组织,以保证应用层接收到的数据不会由于网络问题乱序。

四次挥手断链接

实际上是_双方各自进行2次挥手_。

由于TCP是全双工的,client与server都占用各自的资源发送segment(同一通道,同时双向传输seq和ack),因此,双方都须要关闭本身的资源(向对方发送FIN)并确认对方资源已关闭(回复对方Ack);而双方能够同时主动关闭,也能够由一方主动关闭带动另外一方被动关闭。只不过,一般以一方主动另外一方被动举例(如图,client主动server被动),因此看上去是所谓的4次挥手。

若是两边同时主动断链接,那么双方都会进入CLOSING状态,而后到达TIME_WAIT状态,最后超时转到CLOSED状态。下图是双方同时主动断链接的示意图(对应TCP状态机中的Simultaneous Close分支):

image.png

握手过程当中的其余问题

建链接时SYN超时

server收到client发的SYN并回复Ack(SYN)(此处称为Ack1)后,若是client掉线了(或网络超时),那么server将没法收到client回复的Ack(Ack(SYN))(此处称为Ack2),链接处于一个中间状态(非成功非失败)。

为了解决中间状态的问题,server若是在必定时间内没有收到Ack2,会重发Ack1(不一样于数据传输过程当中的重传机制)。Linux下,默认重试5次,加上第一次最多共发送6次;重试间隔从1s开始翻倍增加(一种指数回退策略,Exponential Backoff),5次的重试时间分别为1s, 2s, 4s, 8s, 16s,第5次发出后还要等待32s才能判断第5次也超时。因此,至多共发送6次,通过1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s,TCP才会认为SYN超时断开这个链接

SYN Flood攻击

能够利用建链接时的SYN超时机制发起SYN Flood攻击——给server发一个SYN就当即下线,因而服务器默认须要占用资源63s才会断开链接。发SYN的速度是很快的,这样,攻击者很容易将server的SYN队列资源耗尽,使server没法处理正常的新链接。

针对该问题,Linux提供了一个tcp_syncookies参数解决这个问题——当SYN队列满了后,TCP会经过源地址端口、目标地址端口和时间戳构造一个特别的Sequence Number发回去,称为SYN Cookie,若是是攻击者则不会有响应,若是是正常链接,则会把这个SYN Cookie发回来,而后server端能够经过SYN Cookie建链接(即便你不在SYN队列中)。至于SYN队列中的链接,则不作处理直至超时关闭。请注意,不要用tcp_syncookies参数来处理正常的大负载链接状况,由于SYN Cookie本质上也破坏了建链接的SYN超时机制,是妥协版的TCP协议。

对于正常的链接请求,有另外三个参数可供选择:

  • tcp_synack_retries参数设置SYN超时重试次数
  • tcp_max_syn_backlog参数设置最大SYN链接数(SYN队列容量)
  • tcp_abort_on_overflow参数使SYN请求处理不过来的时候拒绝链接

ISN的同步

  • 首先,不能选择静态的ISN。例如,若是链接建好后始终用1来作ISN,若是client发了30个segment(假设一个字节一个segment)过去,可是网络断了,因而 client重连,又用了1作ISN,可是旧链接的那些segment(称为“迷途的重复分组”)到了,因为区分链接的五元组相同(称该新链接为旧链接的“化身”),server会把它们当作新链接中的segment。
  • 而后,从上例还可以得知,须要使ISN随时钟动态增加,以保证新链接的ISN大于旧链接。
  • 最后,从安全等角度考虑,也不能使ISN的增加呈现规律性(如简单随时钟正比例增加)。这很容易理解,若是增加规律过于简单,则很容伪造ISN对网络两端发起攻击。

最终,设计了多种ISN增加算法,广泛_使ISN随时钟动态增加,并具备必定的随机性_。RFC793中描述了一种简单的ISN增加算法:ISN会和一个假的时钟绑在一块儿,这个时钟会在每4微秒对ISN作加一操做,直到超过2^32,又从0开始。这样,一个ISN的周期大约是4.55(我算的4.77???)个小时。定义segment在网络上的最大存活时间为MSL(Maximum Segment Lifetime),网络中存活时间超过MSL的分组将被丢弃。所以,若是使用RFC793中的ISN增加算法,则MSL的值必须小于4.55小时,以保证不会在相邻的链接中重用ISN(TIME_WAIT也有该做用)。同时,这间接限制了网络的大小(固然,4.55小时的MSL已经能构造很是大的网络了)。

MSL应大于IP协议TTL换算的时间,RFC793建议MSL设置为2分钟,Linux遵循伯克利习惯设置为30s。

挥手过程当中的其余问题

关于TIME_WAIT

为何须要TIME_WAIT

在TCP状态机中,从TIME_WAIT状态到CLOSED状态,有一个超时时间 2 * MSL。为何须要TIME_WAIT状态,且超时时间为2 * MSL?主要有两个缘由:

  • 2 * MSL确保有足够的时间让被动方收到了ACK或主动方收到了被动发超时重传的FIN。即,若是被动方没有收到Ack,就会触发被动方重传FIN,发送Ack+接收FIN正好2个MSL,TIME_WAIT状态的链接收到重传的FIN后,重传Ack,再等待2 * MSL时间。
  • 确保有足够的时间让“迷途的重复分组”过时丢弃。这只须要1 * MSL便可,超过MSL的分组将被丢弃,不然很容易同新链接的数据混在一块儿(仅仅依靠ISN是不行的)。

大规模出现TIME_WAIT

一个常见问题是大规模出现TIME_WAIT,一般是在高并发短链接的场景中,会消耗不少资源。

网上大部分文章都是教你打开两个参数,tcp_tw_reusetcp_tw_recycle。这两个参数默认都是关闭的,tcp_tw_recycletcp_tw_reuse更为激进;要想使用两者,还须要打开tcp_timestamps(默认打开),不然无效。不过,打开这两个参数可能会让TCP链接出现诡异的问题:如上所述,若是不等待超时就重用链接的话,新旧链接的数据可能会混在一块儿,好比新链接握手期间收到了旧链接的FIN,则新链接会被重置。所以,使用这两个参数时应格外当心

各参数详细以下:

  • tcp_tw_reuse:官方文档上说tcp_tw_reuse加上tcp_timestamps能够保证客户端(仅客户端)在协议角度的安全,可是须要在两端都打开tcp_timestamps
  • tcp_tw_recycle:若是是tcp_tw_recycle被打开了话,会假设对端开启了tcp_timestamps,而后会去比较时间戳,若是时间戳变大了,就能够重用链接(NAT网络有可能建链接失败,出现"connection time out"的错误)。

补充一个参数:

  • tcp_max_tw_buckets:控制并发的TIME_WAIT的数量(默认180000),若是超限,系统会把多余的TIME_WAIT链接destory掉,而后在日志里打一个警告(如“time wait bucket table overflow”)。官网文档说这个参数是用来对抗DDoS攻击的,须要根据实际状况考虑。

关于TIME_WAIT的建议

总之,TIME_WAIT出如今主动发起挥手的一方,即,谁发起挥手谁就要牺牲资源维护那些等待从TIME_WAIT转换到CLOSED状态的链接。TIME_WAIT的存在是必要的,所以,与其经过上述参数破协议来逃避TIME_WAIT,不如好好优化业务(如改用长链接等),针对不一样业务优化TIME_WAIT问题。

对于HTTP服务器,能够设置HTTP的KeepAlive参数,在应用层重用TCP链接来处理多个HTTP请求(须要浏览器配合),让client端(即浏览器)发起挥手,这样TIME_WAIT只会出如今client端。

示例

下图是我从Wireshark中截了个我在访问coolshell.cn时的有数据传输的图,能够参照理解Seq与Ack是怎么变的(使用Wireshark菜单中的Statistics ->Flow Graph… ):

image.png

能够看到,Seq与Ack的增长和传输的字节数相关。上图中,三次握手后,来了两个Len:1440的包,所以第一个包为Seq(1),第二个包为Seq(1441)。而后收到第一个Ack(1441),表示1~1440的数据已经收到了,期待Seq(1441)。另外,能够看到一个包能够同时充当Ack与Seq,在一次传输中携带数据与响应。

若是你用Wireshark抓包程序看3次握手,你会发现ISN老是为0。不是这样的,Wireshark为了显示更友好,使用了Relative Seq——相对序号。你只要在右键菜单中的protocol preference中取消掉就能够看到“Absolute Seq”了。

TCP重传机制

TCP协议经过重传机制保证全部的segment均可以到达对端,经过滑动窗口容许必定程度的乱序和丢包(滑动窗口还具备流量控制等做用,暂不讨论)。注意,此处重传机制特指数据传输阶段,握手、挥手阶段的传输机制与此不一样。

TCP是面向字节流的,Seq与Ack的增加均以字节为单位。在最朴素的实现中,为了减小网络传输,接收端只回复最后一个连续包的Ack,并相应移动窗口。好比,发送端发送1,2,3,4,5一共五份数据(假设一份数据一个字节),接收端快速收到了Seq 1, Seq 2,因而回Ack 3,并移动窗口;而后收到了Seq 4,因为在此以前未收到过Seq 3(乱序),若是仍在窗口内,则只填充窗口,但不发送Ack 5,不然丢弃Seq 3(与丢包的效果类似);假设在窗口内,则等之后收到Seq 3时,发现Seq 4及之前的数据包都收到了,则回Ack 5,并移动窗口。

超时重传机制

当发送方发现等待Seq 3的Ack(即Ack 4)超时后,会认为Seq 3发送“失败”,重传Seq 3。一旦接收方收到Seq 3,会当即回Ack 4。

发送方没法区分是Seq 3丢包、接收方故障、仍是Ack 4丢包,本文统一表述为Seq发送“失败”。

这种方式有些问题:假设目前已收到了Seq 4;因为未收到Seq 3,致使发送方重传Seq 3,在收到重传的Seq 3以前,包括新收到的Seq 5和刚才收到的Seq 4都不能回复Ack,很容易引起发送方重传Seq 四、Seq5。接收方以前已经将Seq 四、Seq 5保存到窗口中,此时重传Seq 四、Seq 5明显形成浪费。

也就是说,超时重传机制面临“重传一个仍是重传全部”的问题,即:

  • 重传一个:仅重传timeout的包(即Seq 3),后续包等超时后再重传。节省资源,但效率略低。
  • 重传全部:每次都重传timeout包及以后全部的数据(即Seq 三、四、5)。效率更高(若是带宽未打满),但浪费资源。

可知,两种方法都属于超时重传机制,各有利弊,但两者都须要等待timeout,是基于时间驱动的,性能与timeout的长度密切相关。若是timeout很长(广泛状况),则两种方法的性能都会受到较大影响。

快速重传机制

最理想的方案是:在超时以前,经过某种机制要求发送方尽快重传timeout的包(即Seq 3),如快速重传机制(Fast Retransmit)。这种方案浪费资源(浪费多少取决于“重传一个仍是重传全部”,见下),但效率很是高(由于不须要等待timeout了)。

快速重传机制不基于时间驱动,而基于数据驱动若是包没有连续到达,就Ack最后那个可能被丢了的包;若是发送方连续收到3次相同的Ack,就重传对应的Seq

好比:假设发送方仍然发送1,2,3,4,5共5份数据;接收方先收到Seq 1,回Ack 2;而后Seq 2因网络缘由丢失了,正常收到Seq 3,继续回Ack 2;后面Seq 4和Seq 5都到了,最后一个可能被丢了的包仍是Seq 2,继续回Ack 2;如今,发送方已经连续收到4次(大于等于3次)相同的Ack(即Ack 2),知道最大序号的未收到包是Seq 2,因而重传Seq 2,并清空Ack 2的计数器;最后,接收方收到了Seq 2,查看窗口发现Seq 三、四、5都收到了,回Ack 6。示意图以下:

image.png

快速重传解决了timeout的问题,但依然面临“重传一个仍是重传全部”的问题。对于上面的示例来讲,是只重传Seq 2呢仍是重传Seq 二、三、四、5呢?

若是只使用快速重传,则必须重传全部:由于发送方并不清楚上述连续的4次Ack 2是由于哪些Seq传回来的。假设发送方发出了Seq 1到Seq 20供20份数据,只有Seq 一、六、十、20到达了接收方,触发重传Ack 2;而后发送方重传Seq 2,接收方收到,回复Ack 3;接下来,发送方与接收方都不会再发送任何数据,两端陷入等待。所以,发送方只能选择“重传全部”,这也是某些TCP协议的实际实现,对于带宽未满时重传效率的提高很是明显。

一个更完美的设计是:将超时重传与快速重传结合起来,触发快速重传时,只重传局部的一小段Seq(局部性原理,甚至只重传一个Seq),其余Seq超时后重传


参考:


本文连接:浅谈TCP(1):状态机与重传机制
做者:猴子007
出处:monkeysayhi.github.io
本文基于 知识共享署名-相同方式共享 4.0 国际许可协议发布,欢迎转载,演绎或用于商业目的,可是必须保留本文的署名及连接。

相关文章
相关标签/搜索