本文来探讨一下TCP协议方面的一些认识及实践
一、简介
引用《TCP/IP详解-卷1》中的介绍,TCP与UDP使用相同的网络层(IP层),TCP却向应用层提供了与UDP完全不同的服务。TCP提供一种面向连接的、可靠的字节流服务。
所谓面向连接的,是指在彼此通信前要先建立连接。同时这种点对点的连接表明了TCP不支持多播和广播。
所谓可靠的,是指TCP有一堆保证数据传输准确的机制。
所谓字节流,是指TCP接收端并不知道发送端每次向该连接写入了多少数据,只关心通过限制能从连接中最大字节数。
二、协议格式
图:TCP数据包结构
图:TCP包首部结构
字段说明:
16位源端口号:指建立连接(或发送数据)的端口号
16位目的端口号:指连接另一端(或接受数据)的端口号
32位序号:发送的字节序号,如果是新建立的连接,则第一个包的seq为0,否则为上一个数据包的确认序号。同一个包中的序号和确认序号是不同的。
32位确认序号:等于接收到数据包的序号seq+数据包的长度len。同时告诉对端,下一个数据包的开头字节序号。
4位首部长度:tcp包首部的长度。
URG:紧急指针有效。它使一端可以告诉另一端有些具有某种方式的“紧急数据”已经放置在普通的数据流中。另一端被通知这个紧急数据已被放置在普通数据流
中,由接收方决定如何处理。URG比特被置1,并且一个16bit的紧急指针被置为一个正的偏移量,该偏移量必须与TCP首部中的序号字段相加,以便得出紧急数据的最后一个字节的序号。注意:TCP的紧急方式不是带外数据(out-of-band data)。紧急方式有什么作用呢?两个最常见的例子是Telnet和Rlogin。当交互用户键入中断键时,另一个例子是FTP,当交互用户放弃一个文件的传输时。如果在接收方处理第一个紧急指针之前,发送方多次进入紧急方式会发生什么情况呢?在数据流中的紧急指针会向前移动,而其在接收方的前一个位置将丢失。接收方只有一个紧急指针,每当对方有新的值到达时它将被覆盖。这意味着如果发送方进入紧急方式时所写的内容对接收方非常重要,那么这些字节数据必须被发送方用某种方式特别标记。我们将看到Telnet通过在数据流中加入一个值为255的字节作为前缀来标记它所有的命令。
ACK:确认序号有效
PSH:接收方应尽快将这个报文段交给应用层。发送方使用PUSH标志通知接收方将所收到的数据全部提交给接收进程。这里的数据包括与PUSH一起传送的数据以及接收方TCP已经为接收进程收到的其他数据(还在TCP的缓冲区中)。
RST:重置连接
SYN:同步信号,用于发起一个连接
FIN:发端完成发送任务,关闭连接
16位窗口大小:TCP的流量控制由连接的每一端通过声明的窗口大小来提供。窗口大小为字节数,起始于确认序号字段指明的值,这个值是接收端正期望接收的字节。窗口大小是一个16 bit字段,因而窗口大小最大为65535字节。
16位校验和:检验和覆盖了整个的TCP报文段:TCP首部和TCP数据。
16位紧急指针:只有当URG标志置1时紧急指针才有效。紧急指针是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。TCP的紧急方式是发送端向另一端发送紧急数据的一种方式。
选项:部分可选配置,主要有以下几种:
每个选项的开始是1字节kind字段,说明选项的类型。kind字段为0和1的选项仅占1个字节。其他的选项在kind字节后还有len字节。它说明的长度是指总长度,包括kind字节和len字节。
三、TCP各种状态
1. 状态转移
A主动建立连接
图:主动建立连接
B被动建立连接
图:被动建立连接
C主动关闭连接
图:主动关闭连接
D被动关闭连接
图:被动关闭连接
2. 建立连接
图:TCP建立连接
3. 关闭连接
图:TCP关闭连接
4. 半连接
TCP连接是个全双工通道,所以可以同时支持发送和接收。可以认为将一个通道分成两部分,就像高速公路一样,一条大路中间分隔,两边的方向完全相反。
半连接是指连接的一端发送完本地数据后,将发送的那半个连接关闭,告诉对端不用再等待接收数据,而对端照样可以发送数据,本端可以读取数据。
再举高速公路的例子,假如连接是北京到天津的高速公路,如果某一天,北京到天津方向的没有车辆,则该方向高速公路可以封闭,北京这边的入口将关闭,同时告知天津出口不会有车辆通行,天津出口也没必要有人值班了。但是,有不少车辆要从天津开往北京方向,这样天津的入口没有关闭,北京的出口则需要有人收费抬杆。
5. 复位报文
无论何时一个报文段发往基准的连接(referenced connection)出现错误,TCP都会发出一个复位报文段(这里提到的“基准的连接”是指由目的I P地址和目的端口号以及源I P地址和源端口号指明的连接。)
产生复位的一种常见情况是当连接请求到达时,目的端口没有进程正在听。对于UDP,当一个数据报到达目的端口时,该端口没在使用,它将产生一个ICMP端口不可达的信息。而TCP则使用复位。
发送一个复位报文段而不是FIN来中途释放一个连接,这称为异常释放。异常终止一个连接对应用程序来说有两个优点:(1)丢弃任何待发数据并立即发送复位报文段;(2)RST的接收方会区分另一端执行的是异常关闭还是正常关闭。应用程序使用的API必须提供产生异常关闭而不是正常关闭的手段。Socket API通过“linger on close”选项(SO_LINGER)提供了这种异常关闭的能力。我们加上-L选项并将停留时间设为0。这将导致连接关闭时进行复位而不是正常的FIN。
四、TCP数据传输
1. 正常传输
图:正常的数据传输
2. 快发慢接
图:快发到慢收的数据传输
上图说明,在慢收设备接收数据时,由于没有及时的将TCP缓冲区的数据读到应用层而会返回给发送端一个通告窗口为0的ACK。
五、超时和重传机制
1. 往返时间RTT及重传超时间RTO(Retransmission TimeOut)
RTT估计器:R←aR+ ( 1-a )M
这里的a是一个推荐值为0.9的平滑因子。每次进行新测量的时候,这个被平滑的RTT将得到更新。每个新估计的90%来自前一个估计,而10 %则取自新的测量。
RTO公式:
E rr= M-A
A←A+ g E rr
D←D+ h( | E rr |-D)
RTO= A+ 4D
这里的A是被平滑的RTT(均值的估计器)而D则是被平滑的均值偏差。Err是刚得到的测量结果与当前的RTT估计器之差。A和D均被用于计算下一个重传时间(RTO)。增量g起平均作用,取为1/8(0.125)。偏差的增益是h,取值为0.25。当RTT变化时,较大的偏差增益将使RTO快速上升。
一个TCP连接只有一个RTT计时器,如果一个请求发送时计时器已经开始计时,则该请求不进行RTT计算。
图:RTT计时
上图中,第4个数据包在发送时因为RTT计时器已经开始计时(第3个数据包)而没有再次启动计时器。
2. 拥塞避免算法
有两种分组丢失的指示:发生超时和接收到重复的确认。
拥塞避免算法和慢启动算法是两个目的不同、独立的算法。但是当拥塞发生时,我们希望降低分组进入网络的传输速率,于是可以调用慢启动来作到这一点。
拥塞避免算法和慢启动算法需要对每个连接维持两个变量:一个拥塞窗口cwnd和一个慢启动门限ssthresh。算法如下:
1) 对一个给定的连接,初始化cwnd为1个报文段,ssthresh为65535个字节。
2) TCP输出例程的输出不能超过cwnd和接收方通告窗口的大小。拥塞避免是发送方使用的流量控制,而通告窗口则是接收方进行的流量控制。前者是发送方感受到的网络拥塞的估计,而后者则与接收方在该连接上的可用缓存大小有关。
3) 当拥塞发生时(超时或收到重复确认),ssthresh被设置为当前窗口大小的一半(cwnd和接收方通告窗口大小的最小值,但最少为2个报文段)。此外,如果是超时引起了拥塞,则cwnd被设置为1个报文段(这就是慢启动)。
4) 当新的数据被对方确认时,就增加cwnd,但增加的方法依赖于我们是否正在进行慢启动或拥塞避免。如果cwnd小于或等于ssthresh,则正在进行慢启动,否则正在进行拥塞避免。慢启动一直持续到我们回到当拥塞发生时所处位置的半时候才停止(因为我们记录了在步骤2中给我们制造麻烦的窗口大小的一半),然后转为执行拥塞避免。
慢启动算法初始设置cwnd为1个报文段,此后每收到一个确认就加1。
拥塞避免算法要求每次收到一个确认时将cwnd增加1/cwnd。与慢启动的指数增加比起来,这是一种加性增长(additive increase)。我们希望在一个往返时间内最多为cwnd增加1个报文段(不管在这个RTT中收到了多少个ACK),然而慢启动将根据这个往返时间中所收到的确认的个数增加cwnd。
图:慢启动和拥塞避免的可视化描述
3. 快速重传与快速恢复算法
在收到一个失序的报文之后,TCP需要立即产生一个ACK(一个重复的ACK)。这个ACK不应该被延迟。该重复的ACK是为了让对方知道收到一个失序的报文段,并告诉对方自己希望收到的报文序号。
问题出现了,有两种可能我们会收到重复的ACK:丢包和报文段失序。如果是报文段失序了,那么在发送一两个重复的ACK之后,基本上都能接收到失序的几个报文,然后在TCP缓冲区中重新排序,然后返回另一个ACK(希望收到的下一个报文的序号);而如果有三个或三个以上的重复ACK,那么认为该ACK对应的数据已经丢失,需要立即进行重传,无需等待超时定时器溢出。这就是快速重传算法。
图:TCP超时重传
上图可以看出,发送端在连续收到了3个重复的ACK报文后,并没有进入慢启动,而是执行了拥塞避免算法,这就是快速回复算法。之所以没有执行慢启动算法是因为,发送端收到了连续的重复的ACK报文段后,不仅判断出这个报文已经丢失,还能判断到接收端收到了后面的几个报文段(只有这样,接收端才连续的返回重复ACK报文),表明这时候的网络仍然可以传输报文,没有必要执行慢启动影响传输性能,所以,在接收到重传报文段的ACK之前,又发出了三个报文段67、69和71。
这个算法通常按如下过程进行实现:
1) 当收到第3个重复的ACK时,将ssthresh设置为当前拥塞窗口cwnd的一半。重传丢失的报文段。设置cwnd为ssthresh加上3倍的报文段大小。
2) 每次收到另一个重复的ACK时,cwnd增加1个报文段大小并发送1个分组(如果新的cwnd允许发送)。
3) 当下一个确认新数据的A C K到达时,设置cwnd为ssthresh(在第1步中设置的值)。这个ACK应该是在进行重传后的一个往返时间内对步骤1中重传的确认。另外,这个ACK也应该是对丢失的分组和收到的第1个重复的ACK之间的所有中间报文段的确认。这一步采用的是拥塞避免,因为当分组丢失时我们将当前的速率减半。
4. 拥塞算法例子
1) 初始SYN有一个超时重传的例子
图:SYN超时重传-拥塞避免例子
当SYN的超时发生时,sstresh被置为其最小取值(512字节,表示2个报文段)。为进入慢启动阶段,cwnd被置为1个报文段(256字节)
当收到SYN和ACK时,没有对这两个变量做任何修改,因为新的数据还没有被确认。
当ACK 257到达时,因为cwnd小于等于ssthresh,因此仍然处于慢启动阶段,于是将cwnd增加256字节。当收到ACK 513时,进行同样的处理。
当ACK 769到达时,我们不再处于慢启动状态,而是进入了拥塞避免状态。新的cwnd值按以下方法计算:
通过上面公式计算获得当前cwnd的大小为885字节:
当下一个ACK 1025到达时,我们计算cwnd为991字节:
2) 报文段丢失重传
图:报文段丢失重传-拥塞避免例子
当第3个重复的ACK到达时,ssthresh被置为cwnd的一半(四舍五入到报文段大小的下一个倍数),而cwnd被置为ssthresh加上所收到的重复的ACK数乘以报文段大小(也即1024加上3倍的256),然后发送重传数据。
又有5个重复的ACK到达(报文段64~66, 68和70),每次cwnd增加1个报文段长度。最后一个新的ACK(报文段72段)到达时,cwnd被置为ssthresh(1024)并进入正常的拥塞避免过程。由于cwnd小于等于ssthresh(现在相等),因此报文段的大小增加到cwnd,取值为1280。
当下一个新的ACK到达(没有在图中表示出来)时,cwnd大于ssthresh,取值为1363:
在快速重传和快速恢复阶段,我们收到报文段66、68和70中的重复的ACK后才发送新的数据,而不是在接收到报文段64和65中重复的ACK之后就发送。这是cwnd的取值与未被确认的数据大小比较的结果。当报文段65到达时,cwnd为2048,但未被确认的数据有2304字节(9个报文段:46, 48, 50, 52, 54, 55, 57, 59和63),因此不能发送任何数据。当报文段65到达后,cwnd被置为2304,此时我们仍不能进行发送。但是当报文段66到达时,cwnd为2560,所以我们可以发送1个新的数据报文段。类似地,当报文段68到达时,cwnd等于2816,该数值大于未被确认的2560字节的数据大小,因此我们可以发送另1个新的数据报文段。报文段70到达时也进行了类似的处理。
5. 重新分组
当TCP超时并重传时,它不一定要重传同样的报文段。相反,TCP允许进行重新分组而发送一个较大的报文段,这将有助于提高性能(当然,这个较大的报文段不能够超过接收方声明的MSS)。在协议中这是允许的,因为TCP是使用字节序号而不是报文段序号来进行识别它所要发送的数据和进行确认。
来源 http://www.cnblogs.com/geekma/archive/2012/10/23/2735944.html