1 ARQ协议
ARQ协议(Automatic Repeat-reQuest,自动重传请求)是OSI模型中数据链路层和传输层的错误纠正协议之一。它通过使用确认和超时重传这两个机制,在不可靠服务的基础上实现可靠的信息传输。ARQ包括停止等待ARQ协议和连续ARQ协议,拥有错误检测(Error Detection)、正面确认(Positive Acknowledgment)、超时重传(Retransmission after Timeout)和 负面确认及重传(Negative Acknowledgment and Retransmission)等机制。
1.1 停止等待ARQ协议
数据包在网络上传输,存在2类可能。
- 其一左图,在无差错的理想情况下,发送方传输数据,接收方发送确认,发送方收到确认后进行下轮数据传输实现可靠传输;
- 其二右2图,因为拜占庭问题会导致发送方和确认发都没法保持可靠传输,那么确认和重传就能保存可靠传输。
如果客户端发送的包一段时间没有收到接收方的响应,则进行超时重传,这里又有3中情况:
- 服务端没有收到客户端的数据包 。(客户端因超时重新发送分组包)
- 服务端收到了客户端的分组包,发送的确认包丢失了导致客户端没有收到。(服务端收到客户端的重发包,直接丢弃,重传确认包)
- 服务端收到了客户端的分组包,发送的确认包超时到达客户端。(服务端收到客户端的重发包,直接丢弃)
信道利用率:
停止等待ARQ协议的优点是简单,但也有很严重的确定,就是信道利用率太低。
1.2. 连续ARQ协议
由于停止等待ARQ协议会发送一个分组数据包,再等待数据包的ACK导致信道的利用率低,所以定义了连续ARQ协议。即发送方一次连续发送多个分组,服务端在收到分组后在发送一个ACK。如下图所示:
连续ARQ协议通常是结合滑动窗口协议来使用的,发送方需要维持一个发送窗口,如下图所示
图(a)是发送方维持的发送窗口,它的意义是:位于发送窗口内的5个分组都可以连续发送出去,而不需要等待对方的确认,这样就提高了信道利用率。
连续ARQ协议规定,发送方每收到一个确认,就把发送窗口向前滑动一个分组的位置。例如上面的图(b),当发送方收到第一个分组的确认,就把发送窗口向前移动一个分组的位置。如果原来已经发送了前5个分组,则现在可以发送窗口内的第6个分组。滑动窗口向前滑动的分组数与收到的确认包有关,默认采用的是累计确认。
累积确认:对按序到达的最后一个分组发送确认。如果发送方发送了5个分组,接收方只收到了1,2,4,5,没有收到3分组,那么我的确认信息只会说我期望下一个收到的分组是第三个,此时发送方会将3,4,5,全部重发一次,当通信质量不是很好的时候,连续ARQ还是会带来负面影响。这种机制叫Go-back-N(回退N),表示需要再退回来重传已发送过的N个分组。
超时重传:在可靠的传输协议下,如果发送方只要超过了一段时间仍然没有收到确认,就认为刚才发送的分组丢失了,它会重传刚刚的发送过的分组。 超时重传的原理也很简单:发送方发送完一个分组后,就会设置一个超时计时器,如果超时计时器到期之前没有收到接收方发来的确认信息,则会重发刚发送过的分组;如果收到确认信息,则撤销该超时计时器。
超时重传考虑的三点:
- 既然发送方发送的分组可能丢失或者有差错,可能需要重传,那么它必须暂时保留已发送的分组副本,只有收到确认后,才清除这个副本。
- 分组和确认分组信息都应该有各自的编号,用来标示每一个分组和确认信息。(这样才知道需要发送哪个分组,收到了哪个分组的确认信息)
- 超时计时器设置的时间应该略长于分组传送往返时间
默认情况下TCP采取的是累积确认机制,这时如果发生了报文乱序到达,接收方只会重复确认最后一个按序到达的报文段,为此发送方的处理只能是重复按序到达接收方的报文段之后的那个报文段,因而它无法准确知道哪些报文段到达了,哪些没有到达。
SACK是TCP选项,它使得接收方能告诉发送方哪些报文段丢失,哪些报文段重传了,哪些报文段已经提前收到等信息。根据这些信息TCP就可以只重传哪些真正丢失的报文段。需要注意的是只有收到失序的分组时才会可能会发送SACK,TCP的ACK还是建立在累积确认的基础上的。也就是说如果收到的报文段与期望收到的报文段的序号相同就会发送累积的ACK,SACK只是针对失序到达的报文段的。
SACK包括了两个TCP选项,一个选项用于标识是否支持SACK,是在TCP连接建立时时发送;另一种选项则包含了具体的SACK信息。
SACK(Selective Acknowledgment)
选择性确认重传,ACK还是Fast Retransmit的ACK,SACK则是汇报收到的数据,在发送端就可以根据回传的SACK来知道哪些数据到了,哪些没有到。
选项字段
- PTR(Pointer Record):指针记录,PTR记录解析IP地址到域名
- TTL(Time to live):存活时间,限制数据包在网络中存在的时间,防止数据包不断的在IP互联网络上循环,初始值一般为64,每经过一个路由减去1。
- 通过TTL过滤运营商劫持包,假的包是抢先应答的,所以和真实包的TTL可能不同(例如ip.ttl == 54)
- Seq:数据段的序号,当接收端收到乱序的包,就能根据此序号重新排序,当前Seq等上一个Seq号与长度相加获取到
- Len:数据段的长度,这个长度不包括TCP头
- Ack:确认号,接收方向发送方确认已经收到了哪些字节
- RTT(Round Trip Time):也就是一个数据包从发出去到回来的时间
- RTO(Retransmission TimeOut):超时重传计数器,描述数据包从发送到失效的时间间隔,是判断数据包丢失与否及网络是否拥塞的重要参数
- MTU(Maximum Transmit Unit):最大传输单元
- MSS(Maximum Segment Size):最长报文段,TCP包所能携带的最大数据量,不包含TCP头和Option。一般为MTU值减去IPv4头部(至少20字节)和TCP头部(至少20字节)得到。
- Win(Window Size):声明自己的接收窗口
- TCP Window Scale:窗口扩张,放在TCP头之外的Option,向对方声明一个shift count,作为2的指数,再乘以TCP定义的接收窗口,得到真正的TCP窗口
- DF(Don’t fragment):在网络层中,如果带了就丢弃没带就分片
- MF(More fragments):0表示最后一个分片,1表示不是最后一片
过滤表达式
- 握手请求被对方拒绝:tcp.flags.reset === 1 && tcp.seq === 1
- 重传的握手请求:tcp.flags.syn === 1 && tcp.analysis.retransmission
- 过滤延迟确认:tcp.analysis.ack_rtt > 0.2 and tcp.len == 0
2. 滑动窗口协议
TCP 连接设有两个窗口:发送窗口和接收窗口。TCP 的可靠传输机制用字节的序号进行控制,它所有的确认都是基于序号而不是基于报文段。TCP协议通过使用连续ARQ协议和滑动窗口协议,来保证数据传输的正确性,从而提供可靠的传输。
TCP的滑动窗口分为接收窗口和发送窗口。TCP的滑动窗口主要有两个作用,一是提供TCP的可靠性,二是提供TCP的流控特性。同时滑动窗口机制还体现了TCP面向字节流的设计思路。TCP 段中窗口的相关字段。
TCP的Window是一个16bit位字段,它代表的是窗口的字节容量,也就是TCP的标准窗口最大为2^16-1=65535个字节。
另外在TCP的选项字段中还包含了一个TCP窗口扩大因子,option-kind为3,option-length为3个字节,option-data取值范围0-14。窗口扩大因子用来扩大TCP窗口,可把原来16bit的窗口,扩大为31bit。
滑动窗口协议在在发送方和接收方之间各自维持一个滑动窗口,发送发是发送窗口,接收方是接收窗口,而且这个窗口是随着时间变化可以向前滑动的。它允许发送方发送多个分组而不需等待确认。TCP的滑动窗口是以字节为单位的。
-
对于TCP会话的发送方,任何时候在其发送缓存内的数据都可以分为4类,“已经发送并得到对端ACK的”,“已经发送但还未收到对端ACK的”,“未发送但对端允许发送的”,“未发送且对端不允许发送”。“已经发送但还未收到对端ACK的”和“未发送但对端允许发送的”这两部分数据称之为发送窗口(中间两部分)。
-
对于TCP的接收方,在某一时刻在它的接收缓存内存在3种。“已接收”,“未接收准备接收”,“未接收并未准备接收”(由于ACK直接由TCP协议栈回复,默认无应用延迟,不存在“已接收未回复ACK”)。其中“未接收准备接收”称之为接收窗口。
规则:
- 凡是已经发送过的数据,在未收到确认之前,都必须暂时保留,以便在超时重传时使用。
- 只有当发送方收到了接收方的确认报文段时,发送方窗口才可以向前滑动几个序号。
- 当发送方A发送的数据经过一段时间没有收到确认(由超时计时器控制),就要使用回退N步协议,回到最后接收到确认号的地方,重新发送这部分数据。
3 TCP拥塞控制
拥塞避免( Congestion Avoidance)算法
当cwnd>=ssthresh,进入拥塞避免阶段,此时cwnd的增长不再像之前那样是指数增长,而是线性增长。
- 收到一个ACK时,cwnd = cwnd + 1/cwnd。
- 当每过一个RTT时,cwnd = cwnd + 1。
拥塞状态的算法
TCP拥塞控制认为网络丢包是由于网络拥塞造成的,有如下两种判定丢包的方式:
- 上面提到过的超时重传。
- 收到三个重复确认ACK包(duplicate ACK)。
超时重传的原理,在上面也简单提到过:在发送一个TCP报文之后,会启动一个计时器,该计时器的超时时间是根据之前预估的几个往返时间RTT相关的参数计算得到的,如果再这个计时器超时之前都没有收到对端的应答,那么就需要重传这个报文。
而如果发送端收到三个以上的重复ACK时,就认为数据已经丢失需要重传,此时会立即重传数据而不是等待前面的超时重传定时器超时,所以被称为“快速重传”。
最早的TCP Tohoe算法是这么处理拥塞状态的,当出现丢包时:
- 将慢启动阈值ssthresh变成当前cwnd的一半,即:ssthresh = cwnd / 2。
- cwnd = 1,从而直接回到原来的慢启动状态。
但是由于这个算法过于激进,每次一出现丢包cwnd就变成1,因此后来的TCP Reno算法进行了优化,其优化点在于,在收到三个重复确认ACK时,TCP开启快速重传Fast Retransmit算法:
- cwnd变成现在的一半,即:cwnd = cwnd / 2。
- ssthresh设置为缩小后的cwnd大小。
- 进入快速恢复阶段。
以下图来解释上面三种状态的处理:
上图中,横轴为传输轮次,纵轴为cwnd大小,按照时间顺序,其过程如下:
- 0-4:为慢启动阶段,在这个时间里,cwnd指数级增长,到时间4时等于初始的ssthresh值,此时进入拥塞避免状态。
- 4-12:为拥塞避免状态,此时cwnd值线性增长。
- 12:在时间点12,收到了三个重复ACK值,此时两个不同的拥塞控制算法的处理不同:
- TCP Tohoe算法:图中的点线部分,在时间点12,直接将cwnd变成1进入慢启动阶段,这个算法已经废弃。
- TCP Reno算法:cwnd变成原来的一半,ssthresh变成最新的cwnd值使用快速恢复算法。
这里提到了TCP Reno算法在收到三个重复ACK时,cwnd变成原来的一半并且使用快速恢复算法来处理拥塞,下面就接着分析快速恢复算法。
快速恢复(fast recovery)
再次说明:该算法只有TCP Reno版本才用,已经被废弃的TCP Tohoe算法并没有这部分。
在进入快速恢复以前,TCP Reno已经做了如下的事情:
- cwnd = cwnd / 2,即变成原来的一半。
- ssthresh = cwnd,即ssthresh变成新的cwnd值。
快速恢复算法的逻辑如下:
- cwnd = cwnd + 3 MSS,加3 MSS的原因是因为收到3个重复的ACK。
- 重传重复ACK(duplicate ACK)指定的数据包。
- 如果再收到重复ACK,cwnd递增1。
- 如果收到新的ACK,表明重传的报文已经收到。此时将cwnd设置为ssthresh值,进入拥塞避免状态。
如上图中:发送端的第五个包丢失,导致发送端收到三个重复的针对第五个包的ACK。此时将ssthresh值设置为当时cwnd的一半,即6/2=3,而cwnd设置为3+3=6。然后重传第五个包。当收到最新的ACK时,即ACK 11,此时将cnwd设置为当前的ssthresh,即3,然后退出快速恢复而进入拥塞避免状态。
拥塞控制算法的有限状态机
有了前面的解释,理解TCP拥塞控制算法的FSM就容易了:
下面对以上FSM进行简单的总结,每个状态转换箭头都做了数字标记,以数字标记为序来分别做解释:
- 这是拥塞控制算法的初始状态:
- cwnd = 1 MSS
- ssthresh = 64KB
- dupACKCount(重复ACK数量) = 0
- 此时进入慢启动状态。
- 慢启动状态下重传超时,则几个拥塞控制算法变量变为:
- ssthresh = cwnd / 2
- cwnd = 1 MSS
- dupACKCount = 0
- 同时重传丢失的报文段。
- 收到重复ACK时,递增dupACKCount数量。
- 当收到的重复ACK数量为3时,进入快速恢复状态:
- ssthresh = cwnd / 2
- cwnd = ssthresh + 3 MSS
- 同时重传丢失的报文段。
- 在慢启动状态下收到了新的ACK时,每收到一个ACK则递增cwnd一个MSS,即“指数增长”:
- cwnd = cwnd + MSS
- dupACKCount = 0
- 继续传输新的报文。
- 在慢启动状态下,cwnd>=ssthresh值时,进入拥塞避免状态:
- 慢启动状态下,每次收到一个ACK报文时,加法递增:
- cwnd = cwnd + MSS * (MSS/cwnd)
- dupACKCount = 0
- 继续传输新的报文
- 收到重复ACK时,递增dupACKCount数量。
- 当收到的重复ACK数量为3时,进入快速恢复状态,注意这里的处理跟慢启动状态下收到三个重复ACK的处理是一致的(见状态4):
- ssthresh = cwnd / 2
- cwnd = ssthresh + 3 MSS
- 同时重传丢失的报文段。
- 快速恢复状态下,收到重复ACK时:
- cwnd = cwnd + MSS
- 传输新的报文段。
- 快速恢复状态下重传超时,则几个拥塞控制算法变量变为:
- ssthresh = cwnd / 2
- cwnd = 1
- dupACKCount = 0
- 同时重传丢失的报文段。
- 在快速恢复状态下收到了新的ACK时:
- cwnd = ssthresh
- dupACKCount = 0
4. 流量控制
所谓流量控制就是让发送发送速率不要过快,让接收方来得及接收。
解法:利用滑动窗口机制就可以实施流量控制。
原理:运用TCP报文段中的窗口大小字段来控制,发送方的发送窗口不可以大于接收方发回的窗口大小。
考虑一种特殊的情况,就是接收方若没有缓存足够使用,就会发送零窗口大小的报文,此时发送放将发送窗口设置为0,停止发送数据。之后接收方有足够的缓存,发送了非零窗口大小的报文,但是这个报文在中途丢失的,那么发送方的发送窗口就一直为零导致死锁。
解决这个问题,TCP为每一个连接设置一个持续计时器(persistence timer)。只要TCP的一方收到对方的零窗口通知,就启动该计时器,周期性的发送一个零窗口探测报文段。对方就在确认这个报文的时候给出现在的窗口大小(注意:TCP规定,即使设置为零窗口,也必须接收以下几种报文段:零窗口探测报文段、确认报文段和携带紧急数据的报文段)。
TCP利用滑动窗口协议来进行流量控制
5. TCP三次握手和四次挥手的过程
5.1 三次握手
- 第一次握手:建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认;
- 第二次握手:服务器收到SYN报文段。服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1(Sequence Number+1);同时,自己自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RECV状态;
- 第三次握手:客户端收到服务器的SYN+ACK报文段。然后将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。
完成了三次握手,客户端和服务器端就可以开始传送数据。以上就是TCP三次握手的总体介绍。
5.2 四次分手
当客户端和服务器通过三次握手建立了TCP连接以后,当数据传送完毕,肯定是要断开TCP连接的啊。那对于TCP的断开连接,这里就有了神秘的“四次分手”。
- 第一次分手:主机1(可以使客户端,也可以是服务器端),设置Sequence Number和Acknowledgment Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;
- 第二次分手:主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number加1;主机1进入FIN_WAIT_2状态;主机2告诉主机1,我“同意”你的关闭请求;
- 第三次分手:主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK状态;
- 第四次分手:主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。
6. 深入讨论:
6.1. 为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?
建立连接时,ACK和SYN可以放在一个报文里来发送。而关闭连接时,被动关闭方可能还需要发送一些数据后,再发送FIN报文表示同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。
为什么要三次握手
在谢希仁著《计算机网络》第四版中讲“三次握手”的目的是“为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误”。在另一部经典的《计算机网络》一书中讲“三次握手”的目的是为了解决“网络中存在延迟的重复分组”的问题。
在谢希仁著《计算机网络》书中同时举了一个例子,如下:
“已失效的连接请求报文段”的产生在这样一种情况下:client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接。”
这就很明白了,防止了服务器端的一直等待而浪费资源。
为什么要四次分手
那四次分手又是为何呢?TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。TCP是全双工模式,这就意味着,当主机1发出FIN报文段时,只是表示主机1已经没有数据要发送了,主机1告诉主机2,它的数据已经全部发送完毕了;但是,这个时候主机1还是可以接受来自主机2的数据;当主机2返回ACK报文段时,表示它已经知道主机1没有数据发送了,但是主机2还是可以发送数据到主机1的;当主机2也发送了FIN报文段时,这个时候就表示主机2也没有数据要发送了,就会告诉主机1,我也没有数据要发送了,之后彼此就会愉快的中断这次TCP连接。如果要正确的理解四次分手的原理,就需要了解四次分手过程中的状态变化。
- FIN_WAIT_1: 这个状态要好好解释一下,其实FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况下,无论对方何种情况下,都应该马上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。(主动方)
- FIN_WAIT_2:上面已经详细解释了这种状态,实际上FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点数据需要传送给你(ACK信息),稍后再关闭连接。(主动方)
- CLOSE_WAIT:这种状态的含义其实是表示在等待关闭。怎么理解呢?当对方close一个SOCKET后发送FIN报文给自己,你系统毫无疑问地会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以 close这个SOCKET,发送FIN报文给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接。(被动方)
- LAST_ACK: 这个状态还是比较容易好理解的,它是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,也即可以进入到CLOSED可用状态了。(被动方)
- TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了。如果FINWAIT1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。(主动方)
- CLOSED: 表示连接中断。
6.2 为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?
两个存在的理由:1、无法保证最后发送的ACK报文会一定被对方收到,所以需要重发可能丢失的ACK报文。在TIME_WAIT状态时两端的端口不能使用,要等到2MSL时间结束才可继续使用。2、关闭链接一段时间后可能会在相同的IP地址和端口建立新的连接,为了防止旧连接的重复分组在新连接已经终止后再现。2MSL足以让分组最多存活msl秒被丢弃。
MSL是Maximum Segment Lifetime英文的缩写,中文可以译为“报文最大生存时间”,他是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为tcp报文(segment)是ip数据报(datagram)的数据部分,具体称谓请参见《数据在网络各层中的称呼》一文,而ip头中有一个TTL域,TTL是time to live的缩写,中文可以译为“生存时间”,这个生存时间是由源主机设置初始值但不是存的具体时间,而是存储了一个ip数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减1,当此值为0则数据报将被丢弃,同时发送ICMP报文通知源主机。RFC 793中规定MSL为2分钟,实际应用中常用的是30秒,1分钟和2分钟等。
6.3 为什么必须是三次握手,不能用两次握手进行连接?
记住服务器的资源宝贵不能浪费! 如果在断开连接后,第一次握手请求连接的包才到会使服务器打开连接,占用资源而且容易被恶意攻击!防止攻击的方法,缩短服务器等待时间。两次握手容易死锁。如果服务器的应答分组在传输中丢失,将不知道S建立什么样的序列号,C认为连接还未建立成功,将忽略S发来的任何数据分组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。