TCP 的那些事儿(上)

        TCP是一个巨复杂的协议,由于他要解决不少问题,而这些问题又带出了不少子问题和阴暗面。因此学习TCP自己是个比较痛苦的过程,但对于学习的过程却能让人有不少收获。关于   TCP这个协议的细节,我仍是推荐你去看W.Richard Stevens的《TCP/IP 详解 卷1:协议》(固然,你也能够去读一下RFC793以及后面N多的RFC)。另外,本文我会使用英文术语,这样方便你经过这些英文关键词来查找相关的技术文档。html

  之因此想写这篇文章,目的有三个,linux

  • 一个是想锻炼一下本身是否能够用简单的篇幅把这么复杂的TCP协议描清楚的能力。
  • 另外一个是以为如今的好多程序员基本上不会认认真真地读本书,喜欢快餐文化,因此,但愿这篇快餐文章可让你对TCP这个古典技术有所了解,并能体会到软件设计中的种种难处。而且你能够从中有一些软件设计上的收获。
  • 最重要的但愿这些基础知识可让你搞清不少之前一些似是而非的东西,而且你能意识到基础的重要。

  因此,本文不会面面俱到,只是对TCP协议、算法和原理的科普。程序员

  我原本只想写一个篇幅的文章的,可是TCP真TMD的复杂,比C++复杂多了,这30多年来,各类优化变种争论和修改。因此,写着写着就发现只有砍成两篇。算法

  • 上篇中,主要向你介绍TCP协议的定义和丢包时的重传机制。
  • 下篇中,重点介绍TCP的流迭、拥塞处理。

  废话少说,首先,咱们须要知道TCP在网络OSI的七层模型中的第四层——Transport层,IP在第三层——Network层,ARP在 第二层——Data Link层,在第二层上的数据,咱们叫Frame,在第三层上的数据叫Packet,第四层的数据叫Segment。shell

  首先,咱们须要知道,咱们程序的数据首先会打到TCP的Segment中,而后TCP的Segment会打到IP的Packet中,而后再打到以太网Ethernet的Frame中,传到对端后,各个层解析本身的协议,而后把数据交给更高层的协议处理。segmentfault

  TCP头格式浏览器

  接下来,咱们来看一下TCP头的格式缓存

TCP头格式(图片来源安全

  你须要注意这么几点:服务器

  • TCP的包是没有IP地址的,那是IP层上的事。可是有源端口和目标端口。
  • 一个TCP链接须要四个元组来表示是同一个链接(src_ip, src_port, dst_ip, dst_port)准确说是五元组,还有一个是协议。但由于这里只是说TCP协议,因此,这里我只说四元组。
  • 注意上图中的四个很是重要的东西:
    • Sequence Number是包的序号,用来解决网络包乱序(reordering)问题。
    • Acknowledgement Number就是ACK——用于确认收到,用来解决不丢包的问题
    • Window又叫Advertised-Window,也就是著名的滑动窗口(Sliding Window),用于解决流控的
    • TCP Flag ,也就是包的类型,主要是用于操控TCP的状态机的

  关于其它的东西,能够参看下面的图示

图片来源

  TCP的状态机

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

  下面是:“TCP协议的状态机”(图片来源) 和 “TCP建连接”、“TCP断连接”、“传数据” 的对照图,我把两个图并排放在一块儿,这样方便在你对照着看。另外,下面这两个图很是很是的重要,你必定要记牢。(吐个槽:看到这样复杂的状态机,就知道这个协议有多复杂,复杂的东西老是有不少坑爹的事情,因此TCP协议其实也挺坑爹的)

 

  不少人会问,为何建连接要3次握手,断连接须要4次挥手?

  • 对于建连接的3次握手,主要是要初始化Sequence Number 的初始值。通讯的双方要互相通知对方本身的初始化的Sequence Number(缩写为ISN:Inital Sequence Number)——因此叫SYN,全称Synchronize Sequence Numbers。也就上图中的 x 和 y。这个号要做为之后的数据通讯的序号,以保证应用层接收到的数据不会由于网络上的传输的问题而乱序(TCP会用这个序号来拼接数据)。
  • 对于4次挥手,其实你仔细看是2次,由于TCP是全双工的,因此,发送方和接收方都须要Fin和Ack。只不 过,有一方是被动的,因此看上去就成了所谓的4次挥手。若是两边同时断链接,那就会就进入到CLOSING状态,而后到达TIME_WAIT状态。下图是 双方同时断链接的示意图(你一样能够对照着TCP状态机看):


两端同时断链接(图片来源

  另外,有几个事情须要注意一下:

  • 关于建链接时SYN超时。试想一下,若是server端接到了clien发的SYN后回了SYN-ACK后 client掉线了,server端没有收到client回来的ACK,那么,这个链接处于一个中间状态,即没成功,也没失败。因而,server端若是 在必定时间内没有收到的TCP会重发SYN-ACK。在Linux下,默认重试次数为5次,重试的间隔时间从1s开始每次都翻售,5次的重试时间间隔为 1s, 2s, 4s, 8s, 16s,总共31s,第5次发出后还要等32s都知道第5次也超时了,因此,总共须要 1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s,TCP才会把断开这个链接。
  • 关于SYN Flood攻击。一些恶意的人就为此制造了SYN Flood攻击——给服务器发了一个SYN后,就下线了,因而服务器须要默认等63s才会断开链接,这样,攻击者就能够把服务器的syn链接的队列耗尽,让正常的链接请求不能处理。因而,Linux下给了一个叫tcp_syncookies的 参数来应对这个事——当SYN队列满了后,TCP会经过源地址端口、目标地址端口和时间戳打造出一个特别的Sequence Number发回去(又叫cookie),若是是攻击者则不会有响应,若是是正常链接,则会把这个 SYN Cookie发回来,而后服务端能够经过cookie建链接(即便你不在SYN队列中)。请注意,请先千万别用tcp_syncookies来处理正常的大负载的链接的状况。 由于,synccookies是妥协版的TCP协议,并不严谨。对于正常的请求,你应该调整三个TCP参数可供你选择,第一个 是:tcp_synack_retries 能够用他来减小重试次数;第二个是:tcp_max_syn_backlog,能够增大SYN链接数;第三个 是:tcp_abort_on_overflow 处理不过来干脆就直接拒绝链接了。
  • 关于ISN的初始化。ISN是不能hard code的,否则会出问题的——好比:若是链接建好后始终用1来作ISN,若是client发了30个segment过去,可是网络断了,因而 client重连,又用了1作ISN,可是以前链接的那些包到了,因而就被当成了新链接的包,此时,client的Sequence Number 多是3,而Server端认为client端的这个号是30了。全乱了。RFC793中 说,ISN会和一个假的时钟绑在一块儿,这个时钟会在每4微秒对ISN作加一操做,直到超过2^32,又从0开始。这样,一个ISN的周期大约是4.55个 小时。由于,咱们假设咱们的TCP Segment在网络上的存活时间不会超过Maximum Segment Lifetime(缩写为MSL - Wikipedia语条),因此,只要MSL的值小于4.55小时,那么,咱们就不会重用到ISN。
  • 关于 MSL 和 TIME_WAIT。经过上面的ISN的描述,相信你也知道MSL是怎么来的了。咱们注意到,在TCP的状态图中,从TIME_WAIT状态到CLOSED状态,有一个超时设置,这个超时设置是 2*MSL(RFC793定 义了MSL为2分钟,Linux设置成了30s)为何要这有TIME_WAIT?为何不直接给转成CLOSED状态呢?主要有两个原 因:1)TIME_WAIT确保有足够的时间让对端收到了ACK,若是被动关闭的那方没有收到Ack,就会触发被动端重发Fin,一来一去正好2个 MSL,2)有足够的时间让这个链接不会跟后面的链接混在一块儿(你要知道,有些自作主张的路由器会缓存IP数据包,若是链接被重用了,那么这些延迟收到的 包就有可能会跟新链接混在一块儿)。你能够看看这篇文章《TIME_WAIT and its design implications for protocols and scalable client server systems
  • 关于TIME_WAIT数量太多。从上面的描述咱们能够知道,TIME_WAIT是个很重要的状态,可是若是在大并发的短连接下,TIME_WAIT 就会太多,这也会消耗不少系统资源。只要搜一下,你就会发现,十有八九的处理方式都是教你设置两个参数,一个叫tcp_tw_reuse,另外一个叫tcp_tw_recycle的参数,这两个参数默认值都是被关闭的,后者recyle比前者resue更为激进,resue要温柔一些。另外,若是使用tcp_tw_reuse,必需设置tcp_timestamps=1,不然无效。这里,你必定要注意,打开这两个参数会有比较大的坑——可能会让TCP链接出一些诡异的问题(由于如上述同样,若是不等待超时重用链接的话,新的链接可能会建不上。正如官方文档上说的同样“It should not be changed without advice/request of technical experts”)。
    • 关于tcp_tw_reuse。官方文档上说tcp_tw_reuse 加上tcp_timestamps(又叫PAWS, for Protection Against Wrapped Sequence Numbers)能够保证协议的角度上的安全,可是你须要tcp_timestamps在两边都被打开(你能够读一下tcp_twsk_unique的源码 )。我我的估计仍是有一些场景会有问题。
    • 关于tcp_tw_recycle。若是是tcp_tw_recycle被打开了话,会假设对端开启了 tcp_timestamps,而后会去比较时间戳,若是时间戳变大了,就能够重用。可是,若是对端是一个NAT网络的话(如:一个公司只用一个IP出公 网)或是对端的IP被另外一台重用了,这个事就复杂了。建连接的SYN可能就被直接丢掉了(你可能会看到connection time out的错误)(若是你想观摩一下Linux的内核代码,请参看源码 tcp_timewait_state_process)。
    • 关于tcp_max_tw_buckets。这个是控制并发的TIME_WAIT的数量,默认值是 180000,若是超限,那么,系统会把多的给destory掉,而后在日志里打一个警告(如:time wait bucket table overflow),官网文档说这个参数是用来对抗DDoS攻击的。也说的默认值180000并不小。这个仍是须要根据实际状况考虑。

  Again,使用tcp_tw_reuse和tcp_tw_recycle来解决TIME_WAIT的问题是很是很是危险的,由于这两个参数违反了TCP协议(RFC 1122) 

  其实,TIME_WAIT表示的是你主动断链接,因此,这就是所谓的“不做死不会死”。试想,若是让对端断链接,那么这个破问题就是对方的了,呵呵。另外,若是你的服务器是于HTTP服务器,那么设置一个HTTP的KeepAlive有多重要(浏览器会重用一个TCP链接来处理多个HTTP请求),而后让客户端去断连接(你要当心,浏览器可能会很是贪婪,他们不到万不得已不会主动断链接)。

  数据传输中的Sequence Number

  下图是我从Wireshark中截了个我在访问coolshell.cn时的有数据传输的图给你看一下,SeqNum是怎么变的。(使用Wireshark菜单中的Statistics ->Flow Graph… )

  你能够看到,SeqNum的增长是和传输的字节数相关的。上图中,三次握手后,来了两个Len:1440的包,而第二个包的SeqNum就成了1441。而后第一个ACK回的是1441,表示第一个1440收到了。

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

  TCP重传机制

  TCP要保证全部的数据包均可以到达,因此,必须要有重传机制。

  注意,接收端给发送端的Ack确认只会确认最后一个连续的包,好比,发送端发了1,2,3,4,5一共五份数据,接收端收到了1,2,因而回ack 3,而后收到了4(注意此时3没收到),此时的TCP会怎么办?咱们要知道,由于正如前面所说的,SeqNum和Ack是以字节数为单位,因此ack的时候,不能跳着确认,只能确认最大的连续收到的包,否则,发送端就觉得以前的都收到了。

  超时重传机制

  一种是不回ack,死等3,当发送方发现收不到3的ack超时后,会重传3。一旦接收方收到3后,会ack 回 4——意味着3和4都收到了。

  可是,这种方式会有比较严重的问题,那就是由于要死等3,因此会致使4和5即使已经收到了,而发送方也彻底不知道发生了什么事,由于没有收到Ack,因此,发送方可能会悲观地认为也丢了,因此有可能也会致使4和5的重传。

  对此有两种选择:

  • 一种是仅重传timeout的包。也就是第3份数据。
  • 另外一种是重传timeout后全部的数据,也就是第3,4,5这三份数据。

  这两种方式有好也有很差。第一种会节省带宽,可是慢,第二种会快一点,可是会浪费带宽,也可能会有无用功。但整体来讲都很差。由于都在等timeout,timeout可能会很长(在下篇会说TCP是怎么动态地计算出timeout的)

  快速重传机制

  因而,TCP引入了一种叫Fast Retransmit 的算法,不以时间驱动,而以数据驱动重传。也就是说,若是,包没有连续到达,就ack最后那个可能被丢了的包,若是发送方连续收到3次相同的ack,就重传。Fast Retransmit的好处是不用等timeout了再重传。

  好比:若是发送方发出了1,2,3,4,5份数据,第一份先到送了,因而就ack回2,结果2由于某些缘由没收到,3到达了,因而仍是ack回 2,后面的4和5都到了,可是仍是ack回2,由于2仍是没有收到,因而发送端收到了三个ack=2的确认,知道了2尚未到,因而就立刻重转2。而后, 接收端收到了2,此时由于3,4,5都收到了,因而ack回6。示意图以下:

  Fast Retransmit只解决了一个问题,就是timeout的问题,它依然面临一个艰难的选择,就是重转以前的一个仍是重装全部的问 题。对于上面的示例来讲,是重传#2呢仍是重传#2,#3,#4,#5呢?由于发送端并不清楚这连续的3个ack(2)是谁传回来的?也许发送端发了20 份数据,是#6,#10,#20传来的呢。这样,发送端颇有可能要重传从2到20的这堆数据(这就是某些TCP的实际的实现)。可见,这是一把双刃剑。

  SACK 方法

  另一种更好的方式叫:Selective Acknowledgment (SACK)(参看RFC 2018),这种方式须要在TCP头里加一个SACK的东西,ACK仍是Fast Retransmit的ACK,SACK则是汇报收到的数据碎版。参看下图:

  这样,在发送端就能够根据回传的SACK来知道哪些数据到了,哪些没有到。因而就优化了Fast Retransmit的算法。固然,这个协议须要两边都支持。在 Linux下,能够经过tcp_sack参数打开这个功能(Linux 2.4后默认打开)。

  这里还须要注意一个问题——接收方Reneging,所谓Reneging的意思就是接收方有权把已经报给发送端SACK里的数据给丢了。这样干是不被鼓励的,由于这个事会把问题复杂化了,可是,接收方这么作可能会有些极端状况,好比要把内存给别的更重要的东西。因此,发送方也不能彻底依赖SACK,仍是要依赖ACK,并维护Time-Out,若是后续的ACK没有增加,那么仍是要把SACK的东西重传,另外,接收端这边永远不能把SACK的包标记为Ack。

  注意:SACK会消费发送方的资源,试想,若是一个攻击者给数据发送方发一堆SACK的选项,这会致使发送方开始要重传甚至遍历已经发出的数据,这会消耗不少发送端的资源。详细的东西请参看《TCP SACK的性能权衡

  Duplicate SACK – 重复收到数据的问题

  Duplicate SACK又称D-SACK,其主要使用了SACK来告诉发送方有哪些数据被重复接收了RFC-2833里有详细描述和示例。下面举几个例子(来源于RFC-2833

  D-SACK使用了SACK的第一个段来作标志,

  • 若是SACK的第一个段的范围被ACK所覆盖,那么就是D-SACK
  • 若是SACK的第一个段的范围被SACK的第二个段覆盖,那么就是D-SACK

  示例一:ACK丢包

  下面的示例中,丢了两个ACK,因此,发送端重传了第一个数据包(3000-3499),因而接收端发现重复收到,因而回了一个 SACK=3000-3500,由于ACK都到了4000意味着收到了4000以前的全部数据,因此这个SACK就是D-SACK——旨在告诉发送端我收 到了重复的数据,并且咱们的发送端还知道,数据包没有丢,丢的是ACK包。

Transmitted  Received    ACK Sent

Segment      Segment     (Including SACK Blocks)

3000-3499    3000-3499   3500 (ACK dropped)

3500-3999    3500-3999   4000 (ACK dropped)

3000-3499    3000-3499   4000, SACK=3000-3500

                                    ---------

   示例二,网络延误

  下面的示例中,网络包(1000-1499)被网络给延误了,致使发送方没有收到ACK,然后面到达的三个包触发了“Fast Retransmit算法”,因此重传,但重传时,被延误的包又到了,因此,回了一个SACK=1000-1500,由于ACK已到了3000,因此,这 个SACK是D-SACK——标识收到了重复的包。

  这个案例下,发送端知道以前由于“Fast Retransmit算法”触发的重传不是由于发出去的包丢了,也不是由于回应的ACK包丢了,而是由于网络延时了。

Transmitted    Received    ACK Sent

Segment        Segment     (Including SACK Blocks)

500-999        500-999     1000

1000-1499      (delayed)

1500-1999      1500-1999   1000, SACK=1500-2000

2000-2499      2000-2499   1000, SACK=1500-2500

2500-2999      2500-2999   1000, SACK=1500-3000

1000-1499      1000-1499   3000

               1000-1499   3000, SACK=1000-1500

                                      ---------

  可见,引入了D-SACK,有这么几个好处:

  1)可让发送方知道,是发出去的包丢了,仍是回来的ACK包丢了。

  2)是否是本身的timeout过小了,致使重传。

  3)网络上出现了先发的包后到的状况(又称reordering)

  4)网络上是否是把个人数据包给复制了。

  知道这些东西能够很好得帮助TCP了解网络状况,从而能够更好的作网络上的流控

  Linux下的tcp_dsack参数用于开启这个功能(Linux 2.4后默认打开)

  好了,上篇就到这里结束了。若是你以为我写得还比较浅显易懂,那么,欢迎移步看下篇《TCP的那些事(下)

 

参考博文:

http://kb.cnblogs.com/page/209100/

http://www.javashuo.com/article/p-pmqusnkf-hc.html

相关文章
相关标签/搜索