深入理解TCP

深入理解TCP的原理

Posted by Ted on October 11, 2015

一、TCP VS UDP

TCP是面向连接的传输层层协议,可以为应用层提供可靠的数据传输服务。所谓的面向连接并不是真正意思上的连接,只不过是在发送数据之前,首先得相互握手,也就是说接收方知道你要发数据给它了。

UDP是面向无连接的传输层协议,并不提供可靠的数据传输。

UDP传输就类似于写信,接收方事先并不知道你要写信给他;而TCP传输就像是打电话,必须等对方按了接听键你才能更他通话。

二、TCP包的结构

  • 源端口号/目的端口号:应用层的每个网络服务都赌赢一个端口号,通过端口来标识对应的服务。端口号是将传输层绑定到应用层的粘合剂
  • 序号和确认号:用来实现可靠数据传输服务
  • 接受窗口:接收方缓冲区剩余大小,用于流量控制
  • RST、SYN、FIN:用于连接的建立和拆除
  • PSH:当PSH被设置时,接收方应该立即将数据交给上层
  • URG比特和紧急数据指针:URG比特指示报文段里存在着被发送端的上层实体置为“紧急”的数据;紧急数据的最后一个字节由16bit的紧急数据指针字段指出。当紧急数据存在并给出紧急数据尾的时候,TCP必须立即通知接收端的上层实体。
  • 检验和:用于差错检测

三、TCP如何保证数据传输可靠性

通过三点:

  1. 在发送数据之前,进行三次握手,建立连接,保证发送端与接收端相互可靠通信
  2. 通过确认和重传机制来保证数据的完整性和按顺序交付
  3. TCP提供了流量控制和拥塞控制

1、建立连接

为什么要进行三次握手而不是二次或者四次?

因特网中信道不可靠, 但是要在这个不可靠的信道上可靠地传输数据,三次握手是最小的理论值。

两次握手的情况会发送:

情况一:服务器接收到了这个SYN并返回ACK,无论客户端是否接收到了ACK,服务器都认为已经与客户端建立连接了,于是就开始向客户端发送数据。但是如果客户段没有收到ACK,那么客户端会认为与服务器没有建立连接,就不会接收服务器发来的数据,也就是说直接丢弃服务器发来的数据,服务器发出的消息超时了,就重复发送数据,这就产生了死锁。

情况二:客户端发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达服务器。本来这是一个早已失效的报文段。但服务器收到此失效的连接请求报文段后,就误认为是客户端再次发出的一个新的连接请求。于是就向客户端发送ACK,但是此时客户端没有发出请求,所以并不会理睬这个ACK,而服务器又开始发数据给客户端了,这时候,客户端又把这些数据都丢弃了,而服务器发出的消息超时了,就重复发送数据,也产生了死锁。

2、确认和重传机制

TCP把数据看成是无结构和有序的字节流,所以上面所说的报文段的序列号是该报文段首字节的字节流编号,而报文段中的确认号是主机期望从客户端收到的下一个字节的序号。我们来举个例子:

  假设TCP从应用层接收到3000个字节长度的数据,而TCP最大报文长度MSS为1460,那么就要对数据进行分段,第一段数据为0~1459字节,第二段为1460~2919字节,第三段为2920~2999字节,那么这三个报文段的序列号分别为0、1460、2920。

  假如服务器端接收到客户端发过来的第一个报文段0~1459字节,那么它期望收到的下一个字节的序列号为1460,那么在返回给客户端的ACK中确认号即为1460,然后服务器又收到客户端发来的2920~2999字节的报文,但是未收到1460~2919字节,那么服务器端继续期望下一个接收字节为1460,所以返回的ACK中的确认号依旧为1460。TCP只确认直到第一个未收到字节之前的字节,所以TCP提供的是累积确认接收方保留失序的字节,同时等待缺少的字节来填补间隔。

  当然在如此错综复杂的网络中,即使三次握手建立连接了,也不可能每次发送数据都能成功到达目的地。客户端每次向网络中发送一个报文号,其实还会继续缓存该报文,指导收到服务器端发过来的ACK确认服务器收到了该报文,然后才会丢弃。但是当发送的报文段在网络中发生丢包了或者产生了比特出错又或者服务器返回的ACK丢失了,那么客户端将都收不到ACK。那么怎么办?总不能一直等着吧?

  客户端通过一个定时器超时机制来保证客户端不会无限制地等待。也就是当发送一个报文段后,就启动定时器,当发生超时了还未收到服务器发来的ACK时,客户端就重新发送该报文段。但是设定多长时间呢??从客户端发送一个报文到接收到ACK相当于一个来回,我们用往返时间RTT来表示,设定的这个定时器时间至少得大于RTT吧。如果是ACK丢失了,那么服务器如果收到了这个重发的报文,那么数据不就重复了么??服务器通过序列号来保证数据的无冗余,当服务器收到了这个重复的数据包时,便知道客户端没有收到ACK超时了,就直接把它丢弃,然后返回给客户端一个最新的ACK。

3、TCP提供了流量控制和拥塞控制

  流量控制其实是一个速度匹配服务,也就是说发送方发送数据的速率要与接收方应用程序读取速率相匹配,以消除接收端缓冲区溢出的可能性。在TCP首部中有一个字段叫做接收窗口,它就是用来通知发送端服务器上剩余的缓冲区的大小(rwnd)的。

  TCP提供的拥塞控制并不是网络辅助的拥塞控制,而是端到端的拥塞控制,因为IP层并不向端系统提供显式的网络拥塞反馈。那么TCP发送方如何限制它的发送速率?发送方又如何知道路径上是否拥塞?

  上面提到当数据包在网络中丢失时就可能发生超时,而服务器段可能收到冗余的数据包,当然客户端也不例外,也可能收到冗余的ACK。所以我们把丢包事件定义为:要么出现超时,要么收到来自接收端的3个冗余的ACK。当丢包事件发生了,客户端就知道链路上存在拥塞。

  发送端维护着一个拥塞窗口(cwnd),一个发送方的缓冲区中未被求确认的数据量不会超过cwnd和rwnd(流量控制中接收窗口字段,服务器上剩余的缓冲区的大小)的最小值。这个约束限制了发送方未被确认的数据量,也就间接限制了发送速率。

  其实TCP是按照如下原则来设置发送速率:

  • 一个丢失的报文段意味着拥塞,因此当丢失报文段时应降低TCP发送方的速率。
  • 一个确认报文段指示该网络正在向接收方交付发送方的报文段,所以,当对先前未确认报文段的确认到达时,能够增加发送方的速率。
  • 因为IP层并不向上层提供显式的网络拥塞反馈,所以TCP是通过ACK和丢包事件来充当隐式信号进行带宽探测。

四、TCP拥塞控制算法

TCP跟踪一个拥塞窗口来(cwnd)提供拥塞控制服务,通过调节cwnd值以控制发送速率。那么TCP如何基于丢包事件来设置cwnd值?通过TCP拥塞控制算法来实现。TCP拥塞控制算法主要有三部分:慢启动、拥塞避免、快速恢复。

1、慢启动

  当一条TCP连接开始时,cwnd的值一般初始设置为MSS的较小值。因为只有当发送方接收到ACK,才会更新cwnd的值,所以在一个RTT时间内,发送方只能发送cwnd字节大小的数据,这就使得初始发送速率大约为MSS/RTT。这是一个较小的值,大部分的带宽还是空闲的,如何迅速更新cwnd以提高带宽利用率??

  慢启动状态:cwnd的值以1个MSS开始并且每当传输的报文段首次被确认就增加一个MSS。

  • 刚开始在第一个RTT中,cwnd只有1个MSS,发送一个报文;
  • 收到一个ACK,此时cwnd增加1个MSS,在第2个RTT,cwnd为2个MSS,发送两个报文;
  • 会收到两个ACK,此时cwnd增加2个MSS,所以在第3个RTT中,cwnd为4个MSS,发送4个报文;
  • 依次类推。

  从上面的过程,不难发现每过一个RTT,发送速率就会翻一倍。虽然起始速率只有MSS/RTT,但是在慢启动的过程中是以指数增长的。当然,带宽是有限的,发送速率不可能无限制地增长,那么问题来了,什么时候结束这种指数增长??

  (1) 当有一个超时引起的丢包事件(拥塞)时,TCP发送方将cwnd设置为1并重新开始慢启动过程;同时将慢启动阈值ssthresh设置为cwnd/2。

  (2) 在(1)重新慢启动过程中,当cwnd的值增长到大于等于慢启动阈值ssthresh时,慢启动结束,同时转移到拥塞避免模式

  (3) 如果在慢启动的过程中,发送方收到3个冗余的ACK,TCP就结束慢启动过程,执行快速重传并进入快速恢复模式

2、拥塞避免

  上面说到,只有当再次启动慢启动并且cwnd增长到大于等于慢启动阈值ssthresh时,才会进入拥塞避免模式,所以当进入到拥塞避免时,cwnd的值大概是在第一次遇到拥塞时的cwnd的值的一半。此时如果继续进行慢启动就会使cwnd的值翻倍从而可能导致拥塞。所以拥塞避免模式对于每次收到的ACK,并不像慢启动那样直接将cwnd增加一个MSS字节,而是增加MSSMSS/cwnd字节。也就是说当MSS=1460字节,cwnd为14600字节,那么每次收到一个ACK,cwnd增加14601/10字节,只有收到10个ACK,cwnd才会增加一个MSS。

  问题来了,与慢启动一样,难道cwnd的值也是这么一直增长下去么??什么时候结束呢?

  (1) 当有一个超时引起的丢包事件(拥塞)时,同慢启动一样,TCP发送方将cwnd设置为1并重新开始慢启动过程;同时将ssthresh设置为cwnd/2。

  (2) 当收到3个冗余的ACK时,ssthresh设置为cwnd/2,TCP将cwnd的值设置为ssthresh+3进入快速恢复模式

3、快速恢复

  对于引起TCP进入快速恢复状态的缺失报文段,对收到的每个冗余ACK,cwnd的值增加1个MSS,当收到新的ACK时TCP把cwnd设置为ssthresh的值进入拥塞避免状态

  当在快速恢复阶段出现超时事件,cwnd的值被设置为1个MSS,并且ssthresh的值设置为cwnd的一半,进入慢启动状态

  TCP的拥塞控制其实是加性增、乘性减(AIMD)的拥塞控制方式,当TCP连接的路径上没有拥塞(通过判断丢包事件)时,发送速率加性增;当出现丢包事件时,发送速率乘性递减。我们知道UDP本身是没有实现拥塞控制的,其实如果大量使用UDP而没有任何约束,那么网络就很容易出现死锁,使得端到端之间很少有数据能够被传输。

五、UDP

  • 源端口号/目的端口号:同TCP首部中端口号的作用相同
  • 首部长度:报文段中的字节数(首部加数据)。
  • 校验和:差错检测,用于确定当UDP报文段从源到达目地移动时,其中的比特是否发生了变化。

UDP是一个很简陋的传输层协议,只负责从发送端的应用层接收数据,封装层UDP报文段,然后交给下层发送到接收端;在接收端,UDP从下层接收数据,然后送达应用层。在该传输过程中,UDP之提供一个基本的差错检测服务,如果检测没有错误,就直接交给应用层;否则直接丢弃。

六、总结

当然,虽然UDP是不可靠、无连接的传输层协议,而TCP是面向连接的提供可靠数据传输的传输层协议。但是UDP的应用依旧很广,像DNS,QQ都是用的是UDP,很多人会奇怪,为什么TCP提供了那么多服务,为什么不用TCP而用这么不可靠的UDP呢??

  其实无论TCP还是UDP,在如此复杂的网络中,并不可能是完全可靠的。

  • TCP只是通过确认和重传机制来保证它的可靠性,而UDP并没有确认和重传机制。
  • TCP提供有序的数据流服务,UDP每个数据包是单独的,在接收方并不保证提交给应用层的数据包是有序的。
  • TCP提供流量控制和拥塞控制,通过流量控制,可以让发送方和接收方的应用层从接收缓冲区读取数据的速率匹配,从而不会因为接收缓冲区满而发生丢包;通过拥塞控制,每一个通过拥塞链路的TCP连接都能够平等地共享链路带宽。

  但是正因为TCP提供了这么多的服务,使得TCP变得很臃肿,很难发送大容量的数据;这时候,轻量的传输层协议UDP就站出来了。UDP很简单,不提供那么多的机制和服务,使得UDP的传输速率可以比TCP快很多。当然有人会说,UDP丢包率很高,UDP接收到无序的数据包,UDP没有拥塞,可能导致网络瘫痪等等一些问题。

 因为传输层及以上的层次都是只在端系统中实现的,在网络分组交换机中只有网络层一下的实现,也就是说TCP的所有这些服务都是基于端到端的服务。既然是端到端的服务,那么上述的所有问题都可以通过上层(也就是应用层)来实现,通过在应用层把发送的数据进行编号,就可以在接收端对接收的数据进行排序,从而的到有序的数据;通过在应用层添加确认和重传,就可以大大降低丢包率;通过在应用层加一个窗口,来达到流量控制和拥塞控制的目的。当然具体基于UDP的实现其实并不需要把所以TCP的服务都在应用层实现,否则还不如用TCP。我们只需要实现那些我们需要和关系的服务即可,比如说我们需要降低丢包率,我们就只实现重传机制。(当然我们也可以完全不用传输层,应用层直接通过网络层通信)

  UDP很自由,可以任由上层来实现;TCP很全面,可以给上层提高可靠的数据传输和各种机制。到底选择哪一种传输层协议,其实还要根据具体的应用来选择。存在既有价值,关键是各方面的权衡而已。

参考:http://www.cnblogs.com/whc-uestc/p/4715334.html