计算机网络:传输层协议TCP详解

概述

TCP协议作为传输层协议,它在网络层IP协议不可靠的尽力而为服务至上提供了一个可靠数据传输服务。TCP协议的数据传输确保了其上层协议读出的数据是无损坏、无间隔、按序、非冗余的二进制流

TCP是面向连接的,在两个进程通过TCP协议发送数据时,必须先要经过互相“握手”,来建立确保数据传输的参数。TCP连接不是在电路交换网络中的端到端TDM或者FDM电路,也不是构建在网络层上的虚电路,所以中间的网络元素并不会维持TCP的连接状态,对于路由器而言,它们所看到的只是一个IP数据报文,而不是连接。

TCP连接提供全双工服务,也就是说进程A与进程B存在一条TCP连接时,在同一时间内,它们都是可以互相发送数据的。并且,TCP是一个点对点数据传输的协议,即不支持多播


报文结构

TCP报文由首部字段数据字段组成:

TCP报文长度并不是无限的,它受限于所谓的最大报文段长度(Maximum Segment Size, MSS),简称MSS,MSS和什么有关呢?
一般来说,MSS根据最初确定的由本地发送主机的最大链路层帧长度(Maximum Transmission Unit, MTU),简称MTU来设置,设置MSS需要保证一个TCP报文加上网络层IP协议的首部长度(40字节)将适合底层的单个链路层帧。在链路层协议中,以太网和PPP协议都具有1500字节的MTU,所以用MTU减去IP协议首部长度字段就可以得到MSS的典型值1460字节

从上面这张图我们可以看到TCP首部字段至少有20个字节。在开头,和UDP协议类似,都有2个字节的源端口号字段和2字节的目的端口号字段,也包含用于校验数据正确性的检验和字段。除此之外,它还包含以下几个字段:

  • 4字节的序号字段和4字节的确认号字段:这些字段用于被TCP发送方和接收方实现可靠的数据传输。其中,序号字段用来解决包乱序的问题,确认号用于解决丢包问题。
  • 2字节的接收窗口字段:用于实现流量控制,该字段用于指示接收方愿意接收的字节数量,也就是可以控制发送窗口或者接收窗口的大小(我们待会会提到)。
  • 4比特的首部字段长度,该字段指示了以4字节为单位的TCP首部长度,因为TCP首部的长度是可变的(因为选项字段是可变的)
  • 6比特标志字段:用于操作TCP状态机。6个比特中每个比特依次是:URG、ACK、PSH、RST、SYN、FIN。ACK比特用于确认报文,即向报文发送方确认该报文段已经成功送达。RST、SYN、FIN用于TCP连接建立(三次握手)与TCP连接解除(四次挥手)。PSH比特用于指示接收方应立刻将数据交付上层协议。URG比特用来指示报文段里存在着被发送端上层实体置为紧急的数据,紧急数据的最后一个字节由2字节的紧急数据指针制定出(在实践中,URG、PSH很少被采用)

序号字段确认号是TCP协议实现可靠传输的关键字段。TCP把数据看成是一个无结构、有序的字节流(而不是像UDP那样把把数据看成是一整个包),序号字段也就是建立在传输的字节流上,即序号应当对应于该报文段首字节的字节流编号。确认号比序号稍复杂些,由于TCP是全双工协议,因此进程A在向进程B发送数据时也需要接收进程B传来的数据,进程A填充进报文段的确认号是进程A期望从进程B收到的下一个字节的序号。通过上述这个机制,TCP就可以建立起一个可靠的连接,我们举几个例子:

  • 假设进程A已经收到了进程B传来的编号为0~1000字节的数据,那么进程B会发送一个ACK,并将确认号设置为1001,表示期望接收到序号为1001的TCP数据包。
  • 假设进程A收到了进程B传来的编号为0~1000的数据,也收到了2000~3000的数据,但是1001~1999的数据在这两者之后才收到(也就是数据的乱序到达),这时,TCP的处理方式是保留失序的字节,等待缺少的字节到达后填补空隙。

我们待会会仔细介绍上述例子的具体实现原理。


TCP连接的建立、解除

连接的建立

TCP连接的建立需要经历三次握手的过程:

步骤如下:

  1. 客户端向服务器发送一个TCP报文:SYN比特标记为1,序号取一个任意值x。此时,客户端进入SYN_SENT状态。
  2. 服务器收到该SYN报文后,向客户端发送一个ACKTCP报文:SYN比特标记为1,序号再取一个任意值y,并将确认号设为x + 1,并将其发送给客户端。服务器进入SYN_RCVD状态。
  3. 客户端向服务器发送一个TCP报文:将确认号设为y + 1。然后客户端进入ESTABLISHED状态。
  4. 服务器收到该报文后,同样进入ESTABLISHED状态。

为什么TCP的连接建立过程需要三次握手?
因为服务器和客户端都需要知道对方的接收消息功能和发送消息功能是正常的

  • 如果连接建立只需要一次握手,那么客户端无法获知服务端是否能够接收消息或者发送分组。在这种情况下,客户端向服务器发送了一个SYN包,然后建立连接。如果仅仅是这样,客户端根本无法知道服务器是否收到了这个请求。

  • 如果连接建立需要两次握手,那么客户端可以获知服务端的接收和发送分组的功能是正常的,但是服务端只能够知道客户端发送分组的功能是正常的,无法获知接收报文的功能是否正常。

  • 当连接需要三次握手时,服务端就可以获知客户端发送和接收报文的功能是正常的。所以通过三次握手就可以建立连接了。

  • 至于为什么不是四次握手,因为没有必要,在三次握手的情况下就已经可以让服务器和客户端知道对方的接收消息功能和发送消息功能是正常的。

连接的解除

TCP连接的解除需要经历四次挥手过程:

我们设定:主动断开方为客户端,被动断开方为服务器
步骤如下:

  1. 客户端向服务器发送一个TCP报文:FIN比特设为1,序号设为x,确认号设为y(注意,这里的xy和连接建立时不同)。客户端进入FIN_WAIT_1状态。
  2. 服务器向客户端发送一个ACKTCP报文:确认号设置为x + 1。服务器进入CLOSE_WAIT状态,客户端收到报文后进入FIN_WAIT_2状态。
  3. 服务器向客户端发送一个TCP报文:FIN比特设为1,序号设为y。服务器进入LAST_ACK状态,客户端收到报文后进入TIME_WAIT状态。
  4. 客户端向服务器发送一个TCP报文:确认号设为y + 2

其实你仔细看就会发现,可以将四次挥手拆分为两个部分:客户端的挥手和服务端的挥手,因为它们都有个共同特征就是先发送了FIN报文然后再接收ACK报文。之所以这么设计是因为TCP是全双工的协议,因为FIN报文就代表自己的数据已经发送完毕了,但这个时候还是可以接收来自对方的数据的。当对方也没有数据要发送时,也需要主动发送一个FIN报文,然后再接收对方的ACK,最后愉快地断开连接。同时这也是为什么TCP挥手需要四次。

主动关闭TCP连接的端系统在完成四次挥手后需要进入TIME_WAIT状态,时长2MSLTIME_WAIT主要作用为:

  • 可靠地实现TCP连接的终止
    在四次挥手的最后阶段,无法保证ACK包一定发送到了被动关闭方,所以需要将该TCP连接维持一段时间,保证被动关闭方再次发送FIN包时能够将ACK包发送给被动关闭方。
  • 允许老的重复分组在网络中消逝
    TCP分组可能由于路由器异常而“迷途”,在迷途期间,TCP发送端可能因确认超时而重发这个分组,迷途的分组在路由器修复后也会被送到最终目的地。如果不维持TIME_WAIT状态,那么系统会复用这个端口,然后该分组发送到目的地后会被误解。

在Linux系统中,解决大量TIME_WAIT的方法有:

  • 设置参数net.ipv4.tcp_tw_reuse = 1 表示允许将TIME_WAIT套接字用于新的连接。
  • net.ipv4.tcp_tw_recycle = 1 表示开启TCP中TIME_WAIT的快速回收。

重传机制

TCP若需要保证所有的数据包都能够正确地抵达,必须要有一个重传的机制。那么什么时候会发生重传呢,有以下几种情况:

1、数据包发生了丢包

在上图中,进程A在发送SEQ=21的数据包(数据长度为11字节)后,由于网络故障发生了掉包。此时对于进程A来说最直观的感受就是迟迟没有收到来自进程B的ACK包。当超过一定的时间后,进程A就会重新尝试发送SEQ=21的数据包,直到收到进程B的ACK包。

丢包重传的效率在于超时时间的设置是否合理,如果超时时间太长,那么显而易见的效果就是会严重影响传输性能。如果超时时间太短,可能会导致并没有发生丢包就进行了重传,严重的可能会产生网络拥塞,增加数据包的时延。

首先这个超时时间肯定不能设置为一个恒定的值,因为受限于物理、网络设备等影响,往返时间(RTT) 肯定是有所差异的。例如从上海到北京的RTT肯定要比从上海到美国的RTT短。

那么这个超时时间是如何设置的呢,这涉及到了TCP的RTT算法
RTT算法实现有很多种,例如RFC793定义的经典算法,Karn / Partridge算法、Jacobson / Karels算法等。这里我们只简单提下RFC793的经典算法,其它算法有兴趣的可以自行Google。
算法的实现步骤如下:

  1. 先进行RTT的采样,我们定义采样得到的值为Sample RTT。当然,任何一个采样获取的Sample RTT是非典型的,所以自然要采取一个对Sample RTT取平均值的一个算法,我们称这个平均值为Estimated RTT,一旦获取到一个新的Sample RTT,该算法就会通过以下计算方法来更新Estimated RTT的值:
               E s t i m a t e d R T T = ( 1 − α ) × ( E s t i m a t e d R T T ) + α × S a m p l e R T T EstimatedRTT=(1-α) \times (EstimatedRTT)+α \times SampleRTT EstimatedRTT=(1α)×(EstimatedRTT)+α×SampleRTT
    在RFC6298中,给出的α参考值是0.125。
  2. 初此之外,还需要计算RTT偏差,即Dev RTT,用于估算Sample RTTEstimated RTT的偏离程度:
               D e v R T T = ( 1 − β ) × D e v R T T + β × ∣ S a m p l e R T T − E s t i m a t e d R T T ∣ DevRTT=(1-β) \times DevRTT +β \times |SampleRTT-EstimatedRTT| DevRTT=(1β)×DevRTT+β×SampleRTTEstimatedRTT
    其中,β的推荐值为0.25。
  3. 得出Estimated RTTDev RTT的值后,首先需要肯定的是超时时间肯定需要大于Estimated RTT,否则将会造成不必要重传,然后肯定也不能大太多,否则当报文发生丢失时,TCP将不能及时重传报文段。因此需要将Estimated RTT加上一点余量,所以实际设置的超时时间TimeoutInterval为:
               T i m e o u t I n t e r v a l = E s t i m a t e d R T T + 4 × D e v R T T TimeoutInterval=EstimatedRTT+4 \times DevRTT TimeoutInterval=EstimatedRTT+4×DevRTT
    推荐的初始TimeoutInterval为1秒。并且只要Estimated RTT更新了,上述的TimeoutInterval也会立刻得到更新。
2、ACK包发生了丢包

还有一种情况可能就是进程A发送的SEQ=21的数据包(数据长度为10字节)已经成功到达进程B的TCP接收窗口,进程B在发送ACK包后,由于网络故障导致ACK包发生了丢包。此时进程A的直观感受等同于数据包发生了丢包,所以同样在超过一定时间后,重新发送SEQ=21的数据包。当进程B收到这个数据包后,会丢弃这段数据(因为事实上已经收到这个包了),然后再次发送一个ACK包,直到进程A收到这个包为止。如下图所述:

快速重传机制

在实际情况中,应用程序可能会在应用层协议中组装一个比较大的包,并发送给服务器(例如HTTP协议中的文件上传请求),这个时候TCP协议一般会将其拆分为多个TCP包并依次发送给服务器,现在的情况是,如果其中的一个数据包发生了丢包,而其它数据包成功抵达了服务器,那么TCP协议会如何处理?答案如下图:

TCP协议引入了一种快速重传机制。它允许在数据包没有连续到达时,就连续发送ACK包(其值为最后那个没有按顺序到达的包)。如果发送方连续三次收到了同样的ACK包,就认定这个包发生了丢包,那么会重传这个丢掉的数据包,当服务器收到后,由于后面的数据包已经正确收到,所以就会发送ACK=6000的包。这么做的好处在于不一定要等到超时再补发缺失的数据包。
但是快速重传机制依然存在一个问题,就是客户端无法获知这三个ACK=2000的包是不是连续的。在客户端收到三个连续的ACK之前,可能已经连续发送了SEQ=6000SEQ=7000······,也就是意味着客户端端可能需要从SEQ=2000开始,一直重传之后所有发送的数据包。为了解决这个问题,选择确认机制(Selective Acknowledgment)出现了。

选择确认机制

选择确认机制需要在TCP报文头(TCP选项中)中引入一个SACK字段,它使得接收方能告诉发送方哪些报文段丢失,哪些报文段重传了,哪些报文段已经提前收到等信息。根据这些信息TCP就可以只重传哪些真正丢失的报文段。需要注意的是只有收到失序的分组时才会可能会发送SACK,TCP的ACK还是建立在累积确认的基础上的。也就是说如果收到的报文段与期望收到的报文段的序号相同就会发送累积的ACK,SACK只是针对失序到达的报文段的。

此外,TCP还可以处理丢包现象。TCP协议采用了超时重传的机制来处理报文段的丢失。关于超时时间具体是如何计算的,可以参考这篇博客

需要注意的一点是,序号不一定非要以0开始,这么做可以减少仍在网络中存在的来自两台主机之间先前已经终止的连接报文段,被误认为师后来两台主机之间新建的TCP连接所产生的报文的可能性。

TCP发送方有三个与发送和重传相关的事件:

  • 从上层应用程序收到数据。该事件发生时,TCP会将应用程序报文封装到符合TCP协议的报文中,然后交付给网络层的IP协议。当报文段传送给IP协议时,TCP就启动定时器确保数据能够在超时后重传。
  • 超时。当定时器超过超时阈值的时候,就进行数据的重传。
  • 收到ACK。TCP将ACK报文中的确认号提取出来,如果确认号大于SendBase(即最早未被确认的字节的序号),则该ACK是在确认一个或者多个先前未被确认的报文段。

TCP缓冲区

之所以TCP协议能像上面例子那样能够解决数据的乱序到达和丢包问题,是因为在TCP连接中,两个进程都各自维护了一个发送缓冲区和接收缓冲区。
发送缓冲区
在这里插入图片描述
发送缓冲区将其中的数据分为了四种类型:

  • 已经发送并且成功收到的数据:这些数据都已经得到接收者的ACK应答。
  • 已经发送但未被确认的数据:该数据已经发送但尚未得到接收者的ACK应答,所以该数据仍在发送窗口中,一旦没有及时收到接收者的ACK确认就需要进行重发。
  • 需要尽快发送的数据:该数据已经被加载到窗口中,等待发送。
  • 未发送,也不允许发送数据:该数据没有被发送出去,并且接收端也不允许发送,因为这些数据已经超出了发送端所能够接收的范围。

接收缓冲区
在这里插入图片描述
接收缓冲区分为了三个部分:

  • 已经接受的数据但尚未被上层协议处理
  • 未接收,但准备接收的数据
  • 未被接收并且未准备接收的数据

上述两个缓冲区还可以用于TCP的拥塞控制


拥塞控制

TCP协议除了提供可靠的传输外,也提供了拥塞控制功能。

何为网络拥塞

网络拥塞(Network Congestion)是指在分组交换网络中传送分组的数目太多时,由于存储转发节点的资源有限而造成网络传输性能下降的情况。

为了更加形象的概括,我们假设进程A和进程B对应为城市A,城市B,那么连接两个城市的高速公路就相当于网络链路,数据包就相当于高速公路上的汽车。当城市A有很多汽车需要前往城市B时,高速公路就有很大概率发生堵车,汽车行驶速度下降,也就是数据包的时延增大了。然而在网络传输中,由于IP协议的TTL限制,超过TTL的数据包会被路由器丢弃,也就是说汽车无论如何也到达不了城市B了。所以拥塞控制的关键就是要把握好什么时候要增加、减少TCP数据包发送的大小或者其次数。

拥塞控制的方法

因为网络层IP协议是不会向上层协议提供网络拥塞反馈的,所以TCP作为其可靠的传输层协议必须要使用端到端的拥塞控制方法。
TCP所采用的方法就是让每个发送方根据所感知到的网络拥塞程度来限制其能向连接发送流量的速率。也就是说,如果发送方能够感知到路径上没什么拥塞,那么就应当加快传输速率,如果能够感知到有拥塞,那么就应当减少其传输速率。

那么TCP是如何感知有拥塞呢?当TCP发生了“丢包事件”时,我们就可以认定发生了拥塞。丢包事件包含:

  • 超时的发生
  • 连续收到来自接收方的三个冗余ACK包。

TCP控制流量的方式是通过连接建立时产生的变量cwnd(拥塞窗口)来计算。
拥塞控制包含四个部分:慢启动拥塞避免快速重传快速恢复。快速重传刚才已经提到过,这里就不细说了。

1、慢启动

慢启动通俗点讲就是在TCP连接刚刚建立时,一步步地提高速度。具体实现步骤如下:

  1. 首先将cwnd设为MSS的一个较小值,也就是说初始发送速率为MSS/RTT
  2. cwnd以1个MSS开始并且每当传输的报文段首次被确认就增加1个MSS。每经过一次RTT,就将发送速率翻一倍。

那么何时结束这种指数级别的增长呢?有三种方法:

  • 如果存在一个由超时指示的丢包事件,TCP发送方就将cwnd设为1并重新开始慢启动过程。它还将第二个状态变量的值ssthresh(慢启动阈值)设为cwnd的一半,即检测到拥塞时将ssthresh设为拥塞窗口值的一半。
  • 第二种方式是直接与ssthresh相关联,当cwnd等于ssthresh时,结束慢启动并将其转换为拥塞避免模式
  • 第三种方式是如果检测到3个冗余的ACK时,就进行一次快速重传并进入到快速恢复状态。
2、拥塞避免

一旦进入到拥塞避免状态,cwnd的值大约是上次遇到拥塞时的一半。TCP此时不会直接将cwnd乘以2去加快速度,而是通过一种线性的方式增加cwnd,即将cwnd的值每次只增加一个MSS。当出现超时后,TCP的拥塞避免算法行为和慢启动一样,将cwnd的值设为一个MSS,当丢包事件出现时,sshresh被更新为cwnd的一半。接下来就是快速恢复阶段。

3、快速恢复

快速恢复算法是认为,当发生了三个冗余ACK时说明网络状况并不算那么糟,所以无需像超时那样那么强烈。快速恢复步骤如下:

  1. cwnd设置为sshthresh加上3倍的MSS
  2. 重传冗余ACK指定的数据包。
  3. 如果再次收到冗余ACK,那么将cwnd加上1
  4. 如果收到了新的ACK,那么将cwnd设为sshthresh,然后进入拥塞避免算法。
总结

TCP的拥塞控制可以形容为加性增,乘性减的拥塞控制方式。

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值