重学TCP

2019/11/02 Blogs

参考博文还有这篇

基础知识

TCP

研究完感觉在三次握手四次挥手中,这几个基础知识是很重要的:

  • SYN(1位):连接请求,只有在建立连接时才会设为1。

  • seq(4个字节):本端的发送编号,与另一端所对应的接收相呼应。初始是一个设定值,在WireShark中为了方便变成了一个从0开始的相对值。seq表示本端发送的第一个数据的序列号码。

  • ack(4个字节):接收端期望下次接收到的开始号码。通过他与seq是否相同也就保证了tcp的顺序。为什么是下一个期望的seq号码:因为TCP是以流水线发出的,比如发送端顺序的发出 Seq=1、Seq=2、Seq=3。那么如果ACK确认的序号和收到的包的序号一致的话,那么需要发回 ACK=1、ACK=2、ACK=3 共三个包。但是TCP协议对此进行了优化,只需要发送一个ACK包就能代表说自己已经收到了前面三个包,那就是发送ACK=4(期望收到Seq为4的包)。这样节省了ACK确认的数量。

  • ACK、ack、SEQ、seq:大写都是标志位,标志着这个对应的ack或seq是否是有效的。小写是具体的数值。

  • 客户端和服务器的seq和ack都是分别计算的。都是从1开始。每次发送的seq都是这一端的第一个号码,到达接收端时,接收端全部接收后ack是下一个期望开始的号码,也就会对应上发送端下一个seq的号码。如下图(来源)解释的很清楚:

    seq和ack
    上图客户端近向服务端发送725字节数据后,便一直等值服务端回数据,服务端每次回1448字节。

三次握手

三次握手

三次握手指的是客户端与服务端建立连接的过程,它主要的流程如下:

  1. 建立连接时,客户端发送SYN包到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。即请求服务器是否可以开始传送,seq=x 指一个设定初始的序列号。

  2. 服务器收到SYN包,必须确认客户的SYN(ack=x+1)下次接受的期望的就是x+1的序列号了,同时自己也发送一个SYN包(seq=y)也是一个设定的序号,即SYN+ACK包,此时服务器进入SYN_RECV状态;

  3. 客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1)下次期望接受的就是y+1的seq了,此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。

数据传送

开始传送数据。通过seq记录每次开始的数据,返回的ack确认回期望下次传的seq号。发送时为了保证有序还做了以下操作:

  • 发送方必须把已发送的数据包保留在缓冲区,并为每个已发送的数据设定一个定时器。
  • 如在定时器超时之前收到了对方发来的应答信息(可能是对本包的应答,也可以是对本包后续包的应答。是因为如果收到后续的,接收端会自动对排序后才发回ack的,收到后续的也就表明前面的收到了,有时tcp会故意少回ack减少网络开销即累计应答包),则释放该数据包占用的缓冲区;
  • 否则,重传该数据包,直到收到应答或重传次数超过规定的最大次数为止。
  • 接收方收到数据包后,先进行CRC循环冗余校验(Cyclic Redundancy Check, CRC)校验,如果正确则把数据交给上层协议,然后给发送方发送一个累计应答包,表明该数据已收到,如果接收方正好也有数据要发给发送方,应答包也可方在数据包中捎带过去。

拥塞控制机制(来源):

  • 慢启动:慢启动会在网络状态不好情况下使用,具体有几个点。
    • 拥塞窗口(cwnd):一个动态变化的窗口,慢启动的操作者。窗口值的大小就代表能够发送出去的但还没有收到ACK(Acknowledgement确认字符)的最大数据报文段。如果窗口太大可能造成网络堵塞,但如果窗口太小频繁传递效率低。所以提出了慢启动,用尝试的方法确定窗口大小。
    • 慢启动:建立的连接不能够一开始就大量发送数据包,而只能根据网络情况逐步增加每次发送的数据量。当新建连接时,cwnd初始化为1个最大报文段(MSS)大小,发送端开始按照拥塞窗口大小发送数据,每当有一个报文段被确认,cwnd就增加1个MSS大小。这样cwnd的值就随着网络往返时间(Round Trip Time,RTT)呈指数级增长,事实上,慢启动的速度一点也不慢,只是它的起点比较低一点而已。我们可以简单计算下:

      开始 —> cwnd = 1
      经过1个RTT后 —> cwnd = 21 = 2
      经过2个RTT后 —> cwnd = 2
      2= 4
      经过3个RTT后 —> cwnd = 42 = 8
      如果带宽为W,那么经过RTT
      log2W时间就可以占满带宽。

  • 拥塞避免

    从慢启动可以看到,cwnd可以很快的增长上来,从而最大程度利用网络带宽资源,但是cwnd不能一直这样无限增长下去,一定需要某个限制。TCP使用了一个叫慢启动门限(ssthresh 全称 slow start threshold)的变量,当cwnd超过该值后,慢启动过程结束,进入拥塞避免阶段。对于大多数TCP实现来说,ssthresh的值是65536(同样以字节计算)。拥塞避免的主要思想是加法增大,也就是cwnd的值不再指数级往上升,开始加法增加。此时当窗口中所有的报文段都被确认时,cwnd的大小加1,cwnd的值就随着RTT开始线性增加,这样就可以避免增长过快导致网络拥塞,慢慢的增加调整到网络的最佳值。(简单来讲就是大到一定程度由指数增长改为线性增长) 为了防止cwnd增加过快而导致网络拥塞,所以需要设置一个慢开始门限ssthresh状态变量(他是一个拥塞控制的标识),它的用法:

    • 当cwnd < ssthresh,使用慢启动算法 也就是一直*2
    • 当cwnd > ssthresh,使用拥塞控制算法,停用慢启动算法。也就是每次+1
    • 当cwnd = ssthresh,这两个算法都可以。
  • 出现拥堵后的 加法增大,乘法减小:

    当出现定时器内还没有得到对应大小的数据时候,立即重传,此时证明堵塞。具体方法是:

    只要判断网络出现拥塞,就要把慢启动开始门限(ssthresh)设置为设置为发送窗口的一半,cwnd(拥塞窗口)设置为 1(这个与 TCP 版本有关,TCP reno 使用快恢复 ,会将窗口设为新的 ssthresh 并执行拥塞避免也就是每次 +1,而最老的 Tahoe 版本则窗口再次设为 1 并重新执行慢启动,一般说的拥塞控制还都是说最老的 Tahoe 版本)。

    事例

快重传和快恢复也是两种方法,不用非生往拥塞避免中套,不同 TCP 版本使用不同的拥塞避免方案,一般说到的拥塞避免则是问的整体思路。

  • 快重传

    快重传算法要求首先接收方收到一个失序的报文段后就立刻发出重复确认,而不要等待自己发送数据时才进行捎带确认。接收方成功的接受了发送方发送来的M1、M2并且分别给发送了ACK,现在接收方没有收到M3,而接收到了M4,显然接收方不能确认M4,因为M4是失序的报文段。如果根据可靠性传输原理接收方什么都不做,但是按照快速重传算法,在收到M4、M5等报文段的时候,不断重复的向发送方发送M2的ACK,如果接收方一连收到三个重复的ACK,那么发送方不必等待重传计时器到期,由于发送方尽早重传未被确认的报文段。(即遇到失序,立刻要求重传)

    快重传

  • 快恢复

    • 当发送发连续接收到三个确认时,就执行乘法减小算法,把慢启动开始门限(ssthresh)减半,但是接下来并不执行慢开始算法。

    • 此时不执行慢启动算法,而是把cwnd设置为ssthresh的一半,然后执行拥塞避免算法,使拥塞窗口缓慢增大。

    快恢复

    与之前的对比 上图是快恢复与之前版本对比。虽然现在都过时了(现在使用的是Cubic)。

四次挥手

四次挥手

  1. 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(最后一个的seq了)(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。(证明client全部发完了,希望结束了)

  2. 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。(服务器还会继续发数据)

  3. 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。(这个阶段只是客户端发完了他的数据,服务器可能还有数据没发完,所以还要等,这也是为什么4次挥手的原因。)

  4. 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据(最后一条了),假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。(服务器也发完了)

  5. 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。(怕最后确认结束的ack服务器一直没有收到)

  6. 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。

总结

所有客户端存在的阶段有:CLOSED(结束) -> SYN-SENT(建立连接) -> ESTABLISHED(连接中) -> FIN-WAIT-1(客户端请求结束) -> FIN_WAIT-2(收到服务端的确认结束) -> TIME_WAIT(客户端发送确认服务端结束ACK后) -> CLOSED(结束)

服务端存在的阶段有:CLOSED -> LISTEN(等待客户端连接) -> SYN-RECVD(收到了客户端连接并返回了ack,等待确认ACK) -> ESTABLISHED(数据传输) -> CLOSE-WAIT(收到了客户端的已结束请求,还有数据没法完给客户端) -> LAST-ACK(发送了已发完ack给客户端,等待客户端的确认) -> CLOSED(结束)

常见面试题

  • 为什么连接的时候是三次握手,关闭的时候却是四次握手?

    答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,”你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。

  • 为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?

    答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。

  • 为什么不能用两次握手进行连接?

    答:3次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。 现在把三次握手改成仅需要两次握手,死锁是可能发生的。作为例子,考虑计算机S和C之间的通信,假定C给S发送一个连接请求分组,S收到了这个分组,并发 送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。可是,C在S的应答分组在传输中被丢失的情况下,将不知道S 是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分 组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。

  • 如果已经建立了连接,但是客户端突然出现故障了怎么办?

    TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。

Search

    Table of Contents