TCP/IP协议之TCP

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/freestyle4568/article/details/50642635


一. TCP简介及与UDP的比较

 TCP全名叫传输控制协议,由于其是面向连接的,可靠的字节流服务,所以该协议的复杂程度要远远大于之前的UDP(无连接,不可靠的)。

 TCP与UDP都是使用相同的网络层协议(IP)来传输数据的,但是与UDP明显不同的是,TCP仅使用在两方通信的前提下,真是因为这个原因,所以广播和多播是不能应用在tcp上面的。tcp连接可以通过以下几个方式来提供可靠性:

1.应用数据被分割成tcp认为最合适发送的数据块,如何分块与应用层无关,这个和udp不一样,udp是应用程序必须将数据块完整不变的提交给它,udp协议不具备自动分块的能力。

2.tcp发出一个数据段以后,它会开启一个超时定时器,如果在规定的时间之内没有收到接收端的应答,那么tcp将重新发送该段数据(可以包含该段即可)。

3.当tcp的接收端收到一个报文以后将发送ack应答,告诉发送端该报文已收到。

4.tcp是必须有首部和数据的检验和的,接收端如果校验和正确则发送ack收到应答,如果错误,则直接丢弃,等到发送端的超时重传。

5.tcp提供数据报的重排和一致性,保证发端的数据和收端的数据完全一样,不受ip层报文的到达顺序影响。

6.tcp可以提供强大的流量控制,对于发端,可以自适应的改变发送的速率,对于收端可以根据剩余的缓冲区大小来提供流量控制。


二. TCP数据报的格式


















因为tcp数据报是通过ip数据报来在网络层传输的,所以tcp报文封装在ip报文中。

tcp首部包括:

端口号(与ip地址组成socket)

序号:这里指的是发端发送的数据字节流的第一个字节的标识。

确认号:当收端接受到数据报后,发送该数据报的确认号,大小是收到数据报中数据的最后一个字节的下一个字节的序号。表示以收到该序号之前的所有字节,现在期待收到序号为确认号的数据。

数据偏移:这里指首部长度,从这里就可以确定下面的选项值为多少。

后面有6个bit位:分别为URG,ACK,PSH,RST,SYN,FIN。

URG表示紧急指针有效,一般用不到。

ACK表示确认序号有效。

PSH表示接受端应该尽快将这个报文交给应用层。

RST表示重建连接,也可以看作为异常终止连接。

SYN表示同步序号用来发起一个连接。

FIN表示该端准备停止发送数据。

窗口:用于流量控制,接收方指定的下次数据最大值。

检验和:首部与数据的检验和。

选项:该部分是用来提供可选功能的地方,最常见的可选字段是最长报文大小(MSS:maximum segment size),每个连接方通常都是通信的第一个报文段中指明这个选项。


三. TCP状态转换与分析

下面我们来看看tcp的连接过程,也就是最著名的三次握手过程:

1.请求端发送一个syn段指明客户打算连接的服务器的端口,以及初始序号。注意syn位在逻辑上占用一个序号,fin位也是。

2.服务器发回包含服务器的初始序号的syn报文段作为应答。同时,将确认序号设置为客户的isn(初始序号)加1作为对客户的syn报文段进行确认。

3.客户必须将确认序号设置为服务器的isn加1以对服务器的syn报文段进行确认。

 
























建立连接以后就可以正常的发送数据报了。

但是写到这里不禁想说,3次握手就一定能确保双方连接上了吗?

显然是不可以的,只能说有很大的概率双发建立连接,因为最后的一个ack数据报客户端无法确定服务端是否接受到。这里就存在一个无限循环的问题,这也是著名的红蓝军问题,双发永远无法保证对方准备就绪。

 建立一个连接需要三次握手,而终止一个连接需要经过4次握手。这是由于tcp的半关闭(half-close)造成的。当一方完成了它的数据发送任务时后,就能发送一个fin报文来终止这个方向上的连接。当另一端收到这个fin报文后,通知应用层tcp连接的另一端已经停止的数据发送,进入了半关闭状态。当然,这里的送个fin报文的一端终止连接并非说的是断开了连接,而是表示自己不再发送数据了(只能发送ack报文),但是可以接受数据报文。

 下面我们来看看连接终止的报文段交换过程:














结合上图,我们可以看到客户端发送fin报文段给服务器,引起服务器的被动关闭,服务器回应一个ack应答给客户端,这时客户端处于半关闭时期。在M+1序号的报文和N序号的报文中间,我们依然可以从服务器传输数据给客户端,但是此时客户端只接受数据并不发送数据(除了ack应答)。例如unix的rsh命令就是利用了半关闭状态,将服务器上执行的命令结果发送给rsh客户端。

 下面我们来看一下经典的tcp状态变迁图,这张图涵盖了所有的tcp状态,我们可以通过该图,非常清晰的看到tcp状态之间的相互转换。





































 图中虚线表示了一般服务器的tcp状态变迁过程,实线表示了客户端的tcp状态变迁过程。

 实线过程(客户端):

1.客户端要连接服务器,主动发送syn报文段,到达SYN_SENT状态,等待服务器端的回应。

2.正常情况下会收到来自服务器端的syn+ack报文,如果等待超时,则回到原始状态,关闭了tcp连接或者重发syn报文段。

3.收到正常的syn+ack报文后,客户端发送ack应答报文,进入ESTABLISHED状态,下面进入正常的数据传输,我们这里先忽略报文段的各种超时重传,阻塞等各种内部状态。

4.当客户端的应用程序准备closed的时候,发送FIN数据报,进入fin_wait_1状态,这个为什么要分成两个等待状态呢?因为客户端下一个接受到的报文段不确定,可能是ack,也可能是fin,还可能是fin+ack,这是由于tcp的半关闭特性引起的。

5.如果先收到等了fin报文,说明服务器端也同时准备closed,tcp两端恰巧都同时closed,发生同时关闭情况,进入closing状态。

  如果先收到了ack应答,则进入fin_wait2状态,半关闭,可以接受数据,但不能发送数据。

  如果收到fin+ack,则发送ack应答,然后直接进入TIME_WAIT状态,等待2msl时间,然后执行关闭。

 

 虚线过程(服务器):

1.当被动打开时,服务器进入listening状态,在此状态下,接受到来自客户端的syn报文段,然后发送syn+ack报文,进入SYN_RCVD状态。

2.在SYN_RCVD状态中,如果收到RST报文,则异常终止,回到listening状态; 如果收到ack回应,则进入ESTABLISHED状态,进行数据发送与接受,和客户端一样,我们这里先忽略各种内部状态。

3.如果是被动关闭,则收到fin报文,进入close_wait状态,同时向应用程序发送文件结束符,等待应用层动作,如果执行关闭,则发送fin数据报,进入last_ack状态。

4.进入last_ack状态,等待来自客户端的ack报文,接受这次tcp连接,注意在被动关闭中,没有time_wait状态。

 图中考虑到了很多种情况,下面我们来看看各个情况下tcp是怎么处理的。在讨论各个情况之前,我们先来看看2msl等待状态。

 1.2MSL等待状态

 每个具体tcp实现必须选择一个报文段最大生存时间msl。它是任何报文段被丢弃前在网络内的最长时间,这是通过定时器来实现的。类似的,在ip层中我们也又ttl字段表示ip数据报的生存周期。

 为什么要有2MSL等待时间呢?原因有:(1)为什么防止最后一个ack应答报文丢失,使得被动关闭端一直处于无限等待中,这样可以及时再次发送ack应答。

(2)为什么防止同样的socket在2msl时间内被重用,如果重用就可以出现上一个socket的报文在这个新开启的连接中被接受,从而引起各种错误。

 2.半打开状态

 如果一方已经关闭或异常终止连接而另一方却还不知道,则会发生tcp的半连接状态。在tcp连接建立好后,任何一端的主机异常都可能会导致发生这种情况。只要不打算在这种半连接状态下传输数据,仍处于连接状态的一方就不会检测另一方已经出现异常情况。

 那么tcp如何来正确的处理这种情况的呢?答案是用异常终止RST位。当服务器重启完成后,接受到来自客户端的一份数据报,但是服务器端并不知道数据报中提到的连接,那么服务器将以复位作为应答。接收方收到一份RST报文,则立刻终止此次tcp连接。

 3.同时打开

 两个应用程序同时彼此执行主动连接是有可能的,尽管发生的可能性非常小。

 tcp是特意设计为了可以处理同时打开,对于同时打开它仅建立一条连接而不是两条连接。我们结合状态图,可以看出,如果两端同时发送syn报文,当每一段收到syn时,状态变为syn_rcvd,同时都再发送syn并对收到的syn进行确认。当双发都收到syn及其ack时,进入established状态。










一个同时打开的连接需要交换4个报文段,比正常的3次握手多一个。

 4.同时关闭

 如果双发同时发送一个fin报文,则进入同时关闭。双发收到一个fin报文后,状态由fin_wait_1变为closing状态,并发送最后的ack。当收到最后的ack时,状态变化为time_wait。下面是同时关闭时的报文交换情况图:












四.TCP数据流传输

 前面讲解了tcp连接的建立与释放,现在来看看tcp是如何进行数据传输的吧!

 这里我们需要了解经时延的确认是如何工作的,以及Nagle算法是怎样减少通过广域网传输的小分组数目的。

1.经时延的确认

 通常tcp在接受到数据时并不立刻发送ack应答,而是延迟一定的时间看看是否有数据需要一起发送出去的。通常各个操作系统实现中tcp将采用200ms的时延等待是否有数据一起被发送。但并不是说一定会出现经延时的ack应答,这个要看情况,所谓的200ms定时器是相对于内核引导的时间,在达到200ms时发生溢出,但是收到报文和发送数据之间的时间可能远远小于200ms,所以不一定能看到经延时的ack。但是当收到数据后,在接收端没有产生发送数据时,定时器就溢出了,那么将产生时延的ack报文发出。

2.Nagle算法

 在某些应用软件,比如rlogin,telnet上面,我们90%产生的数据都是小型数据。如果不经处理,连续发送,那么不仅会降低有效数据发送率,而且还会在广域网上引发阻塞等不良情况。这时我们就采用nagle算法来解决小数据的发送问题。

 该算法要求一个tcp连接上最多只能有一个未被确认的未完成的小分组,在该分组的确认到达之前不能发送其他的小分组。同时,在等待ack到来期间,tcp会收集这些要发送的多个数据段,组成一个小分组,以便在下一次一起发送出去。这个算法具有一定的自适应功能,即在网速快的网络中,发送数据的速率就快,分组数就多,在慢网速的情况下,会发送更少的分组。

 但是该nagle算法并不是任何时候都是有效的,有时我们必须无延时的发送小消息,以便为进行某种操作的交互用户提供实时的反馈。比如linux上面的X窗口系统服务器。

3.滑动窗口协议

 在发送多个小消息报文时,我们需要nagle算法。但是当我们发送大数据块时,显然nagle算法就不适用了,我们需要更快传输率的高效算法,能连续发送多个分组。而滑动窗口协议就是用来解决成块数据流的传输问题的。该协议允许发送方在停止并等待确认前可以连续的发送多个分组,不必每发一个分组就停下来等待确认,因此该协议可以加速数据的传输。

 该协议是这样来实现的,在连接过程中,双方都告诉对方自己的数据缓存区大小(也称为窗口)。一方连续发送的数据量必须在对方的窗口大小以下,不能超过对方窗口大小,否则会引起数据溢出,在接受到数据后,ack应答报文中包含可用的窗口大小,这也规定了下次发送方发送数据量的上限。可用窗口的大小是动态变化的。我们可以通过下图看出。

 







接受方缓冲区大小为6字节,发送方开始发送6个字节填满了窗口,然后接收方确认了开始的3个字节。窗口右滑动3字节,现在窗口中仍有3字节未被确认,可用窗口大小为3字节。

 通过下图,我们可以看到滑动窗口协议的工作过程:





















发送方建立好连接后得知对方窗口大小为4096字节,然后连续发送4分组给接受方。接收方确认收到数据,ack全部数据,窗口大小变为2048,发送方根据可用窗口大小发送2048字节的数据,然后以此类推。。。。为什么窗口大小变为2048呢?应该是接收方希望降低数据发送率。

4.慢启动

 如果仅仅使用上面的滑动窗口协议的话,会一直发送多个连续的报文段,直到窗口被填满为止。这种方式在多路由和慢网速的网络中会出现一些问题。一些中间路由必须缓存这些分组,从而可能耗尽路由器中的存储器的空间。1988年有科学家证明了这种单纯的滑动窗口机制会严重的降低tcp连接的吞吐量。为了解决这个问题,有了tcp中著名的慢启动算法。该算法是尝试将数据发送速率等于另一端返回的数据确认速率,从而达到平衡。

 慢启动算法为发送方增加了一个叫拥塞窗口的概念,大体的算法思路是:在主机与另一端建立一个tcp连接时,拥塞窗口被初始化为1个报文段,然后没收到一个ack报文,拥塞窗口就增加一个报文段。发送方取拥塞窗口与通告窗口中的最小值作为发送上限。这里必须明确一下,拥塞窗口是发送方的流量控制,而通告窗口是接收方的流量控制。拥塞窗口以一种指数上升的方式增加,当到达一定容量的时候,就会控制拥塞窗口的继续增大,我们称作拥塞避免,具体的控制方式我们在tcp的超时和重传中会详细分析。

5.带宽时延乘积

 下面我们来关注一下我们的窗口大小应该设置多少最为合理。

 信道的容量计算公式:

 C(bit) = Bandwidth(b/s) * RTT(s)

 就是说链路最多可以存在发送方C个比特的未被确认的报文段。

 那么我们可以要求接受方窗口的容量必须大于这个C值才能最高效的利用当前信道。


五. TCP的超时与重传

 tcp与udp不一样,提供了可靠的数据传输,也就是说当数据传输过程中出现丢失和阻塞时,能有策略解决这些问题。

 对于每个tcp连接,tcp管理4个不同的定时器:

 (1)重传定时器:使用于当希望收到另一端的确认。

 (2)坚持定时器:使窗口大小信息保持不断流动,即使另一端关闭了其接受窗口。

 (3)保活定时器:可检测到一个空闲连接的另一端何时崩溃或重启。

 (4)2msl定时器:测量一个连接处于time_wait状态的时间。

1.超时与重传

 在发送端第一次发送数据报文出去以后会设置一个超时时间,一旦过了这个时间就会重传数据报,并且超时时间加倍。比如开始超时时间为2秒,此后每次超时都会发生时间加倍,为4,8,16,32,64。这个倍乘关系称为“指数退避”。






















2.RTT(往返时间)测量

 tcp超时重传机制中最重要的是能对给定连接的往返时间进行测量,并且能自动的根据往返时间来不断调整自己的超时时间。

 在最初的tcp规范中使用低通滤波器来更新一个被平滑的RTT估计器:

 R = a*R + (1-a)M

  R即为RTT的估计值,M为本次RTT的测量值,a为平滑因子(0<a<1)。

 一种老的计算RTO(超时重传时间)的方法是:RTO = R*2

  但是这种方法缺乏灵活性,在网络波动比较大的情况下,会导致在很短的时间能重传数据报,加重网络负担,所以这种方法已经被废弃了。取而代之的是Jacobson提出的基于均值和方差来计算RTO的方法。我们都知道均值偏差是标准偏差的一种好的逼近,但却更容易进行计算。下面是更新公式:

 err = M - A

  A = A + g*err

  D = D + h*(|err| - D)

  RTO = A + 4*D

  A是被平滑的RTT,而D则是被平滑的均值偏差。err是刚得到的测量结果与当前RTT估计器之差。增量g起平均作用,偏差的增益是h。当RTT变化时,较大的偏差增益将使RTO快速上升。

 这种方法正如计算机界评价的那样,“你不需要知道它是怎么得来的,它恰恰工作的很好大笑”。

 值得我们注意的是,在RTT的测量过程中,对于重传的数据报返回的ack应答,我们无法得知它是回应的哪次重传发出的数据报。所以在发生超时的数据报应答不参与测量RTT的值中。这正是Karn算法的内容。由于数据被重传,RTO已经得到了一个指数退避,我们在下一次传输时使用这个退避后的RTO。对于一个没有被重传的报文段而言,收到一个ack应答以后,重新计算更新RTO。 



3.拥塞避免算法

 拥塞避免算法是在一种处理丢失分组的方法。丢失分组有2种指示,一种是发生超时,一种是接收到重复的确认。拥塞避免算法和慢启动算法是两个目的不同,独立的算法,经常有人会将它们搞混。当拥塞发生时,我们希望降低分组进入网络的传输速率,于是可以调用慢启动来做到这一点。在实际中,这两个算法经常同时使用。

 拥塞避免和慢启动需要对每个连接维持两个变量:一个是拥塞窗口cwnd和一个慢启动门限ssthresh。

 算法的工作流程如下:

 (1)对一个给定的连接,初始化cwnd为1个报文段,ssthresh为65535个字节。

 (2)tcp输出例程的输出不能超过cwnd和接受方通告窗口的大小。拥塞避免是发送方使用的流量控制,而通告窗口是接收方进行的流量控制。前者是发送方感受到网络拥塞的估计,而后者是与接收方连接上可用的缓存大小有关系。

 (3)当拥塞发生时(超时或者重复确认),ssthresh被设置为当前窗口大小的一半。此外,如果是由于超时引起的,则将cwnd设置为一个报文段。

 (4)当新的数据被对方确认时,就增加cwnd,但增加的方法依赖于我们是否进行慢启动还有拥塞避免。如果cwnd小于或等于ssthresh,则进行慢启动,否则进行拥塞避免。慢启动一直持续到ssthresh为止,则开始进行拥塞避免。

 拥塞避免算法要求每次收到一个确认时将cwnd增加1/cwnd。与慢启动的指数增加相比慢很多。这样可以保证在一个往返时间内最多为cwnd增加1个报文段。

 可以参考下图理解一下:

 












4.快速重传与快速恢复算法

 快速重传算法描述起来很简单,就是当收到3个或3个以上的重复ack,就表明报文段丢失,需要进行重传处理,无需等待超时定时器的溢出。

 在重传过程中,我们没有执行慢启动,而是进行拥塞避免算法,这就是快速恢复算法。

 整个算法实现过程如下:

 (1)当收到第3个重复的ACK时,将ssthresh设置为当前拥塞窗口cwnd的一半。重传丢失的报文段。设置cwnd为ssthresh加上3倍的报文段大小。

 (2)每次收到另一个重复的ack时,cwnd增加1个报文段大小并发送1个分组。

 (3)当下一个确认新数据的ack到达时,设置cwnd为ssthresh。这一步采用的是拥塞避免,因为当分组丢失时我们将当前的速率减半。

我们可以通过下面两图来看看超时和重复ack,cwnd的变化:




























 在bing上面拉的图片,文字不清晰,不过我们可以从曲线的变化上明显看到,超时时cwnd变为1开始慢启动过程;而收到重复ack时启动快速重传和快速恢复算法,cwnd变为ssthresh的一半进而进入拥塞避免算法。

 或许大家被慢启动和拥塞避免算法搞晕了,这里给强调一下,慢启动这个名字起的有点蛋疼,其实应该是快启动,因为增加的速率是指数性的。而拥塞避免算法的cwnd是线性增加的。

 

5.TCP坚持定时器

 tcp为了进行接收方的流量控制,通过传输通知窗口大小给发送方。当窗口大小为0时,则发送方停止发送,等待接收方再发送一个窗口大小不为0的ack应答。

 下面我们考虑这种情况:当接收方处理好缓存中的数据后,将窗口大小不为0的ack应答发送给另一端,但是不巧的是,ack应答在传输中丢失了,那么会怎么样呢?这会导致发送方依然在等在来自接收方的窗口变化报文,而接受方此刻在等待来自发送方的数据。这样就出现了类似死锁的情况。那么如果打破这种死锁呢?就用我们的坚持定时器,发送发使用一个坚持定时器来周期性的向接收方查询,以便发现窗口是否已增大。这些从发送方发出的报文段称为窗口探查。


6.TCP保活定时器

 保活定时器一般是为服务器应用程序提供的。我们设想一个没有数据传输的tcp连接,如果一端终止不会影响到另一端,这个连接似乎会一直持续下去。有时候服务器需要知道客户机的运行情况,比如是否死机,是否崩溃,是否重新启动等,因为服务器在处理每个连接时会占用一定的系统资源,如果资源都被无用的客户机占用,那么服务器很快就会崩溃。

 我们了解了tcp的半开放特性,在用一个半开放的客户机去发送数据给服务器的时候,会得到一个复位数据报。如果我们反过来,在正常连接的服务器与客户端中,我们断掉客户端,服务器会处于半开放状态,由于服务器是接受数据,不发送数据的,所以服务器会一直等待下去。保活功能就是试图在服务器端检测到这种半开放的连接的。所以一般保活选项是服务器端设置的,客户端一般不设置,但是并不是不可以设置。保活定时器简单了解一下即可。


OK!TCP篇终于大功告成,写了我一个星期有木有。。。最后我们以四档路飞霸气结束吧大笑






猜你喜欢

转载自blog.csdn.net/freestyle4568/article/details/50642635