TCP是一个巨复杂的协议,由于他要解决不少问题,而这些问题又带出了不少子问题和阴暗面。因此学习TCP自己是个比较痛苦的过程,但对于学习的过程却能让人有不少收获。关于 TCP这个协议的细节,我仍是推荐你去看W.Richard Stevens的《TCP/IP 详解 卷1:协议》(固然,你也能够去读一下RFC793以及后面N多的RFC)。另外,本文我会使用英文术语,这样方便你经过这些英文关键词来查找相关的技术文档。html
之因此想写这篇文章,目的有三个,linux
因此,本文不会面面俱到,只是对TCP协议、算法和原理的科普。程序员
我原本只想写一个篇幅的文章的,可是TCP真TMD的复杂,比C++复杂多了,这30多年来,各类优化变种争论和修改。因此,写着写着就发现只有砍成两篇。算法
废话少说,首先,咱们须要知道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的状态机
其实,网络上的传输是没有链接的,包括TCP也是同样的。而TCP所谓的“链接”,其实只不过是在通信的双方维护一个“链接状态”,让它看上去好像有链接同样。因此,TCP的状态变换是很是重要的。
下面是:“TCP协议的状态机”(图片来源) 和 “TCP建连接”、“TCP断连接”、“传数据” 的对照图,我把两个图并排放在一块儿,这样方便在你对照着看。另外,下面这两个图很是很是的重要,你必定要记牢。(吐个槽:看到这样复杂的状态机,就知道这个协议有多复杂,复杂的东西老是有不少坑爹的事情,因此TCP协议其实也挺坑爹的)
不少人会问,为何建连接要3次握手,断连接须要4次挥手?
两端同时断链接(图片来源)
另外,有几个事情须要注意一下:
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,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的实际的实现)。可见,这是一把双刃剑。
另一种更好的方式叫: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又称D-SACK,其主要使用了SACK来告诉发送方有哪些数据被重复接收了。RFC-2833里有详细描述和示例。下面举几个例子(来源于RFC-2833)
D-SACK使用了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的那些事(下)》
参考博文: