TCP实现原理(报文段结构+可靠数据传输+流量控制)

TCP是因特网运输层的面向连接的可靠的运输协议。

一、TCP连接

TCP被称为是面向连接的(connection-oriented),这是因为在一个应用进程可以开始向另一个应用进程发送数据之前,这两个进程必须先相互“握手”,即它们必须相互发送某些预备报文段,以建立确保数据传输的参数。作为TCP连接建立的一部分,连接的双方都将初始化与TCP连接相关的许多TCP状态变量。

由于TCP协议只在端系统中运行,而不在中间的网络元素(路由器和链路层交换机)中运行,所以中间的网络元素不会维持TCP连接状态。事实上,中间路由器对TCP连接完全视而不见,它们看到的是数据报,而不是连接。

TCP连接提供的是全双工服务(full-duplex service):如果一台主机上的进程A与另一台主机上的进程B存在一条TCP连接,那么应用层数据就可在从进程B流向进程A的同时,也从进程A流向进程B。TCP连接也总是点对点(point-to-point)的,即在单个发送方与单个接收方之间的连接。

TCP的建立过程被称为三次握手

TCP从客户进程向服务器进程发送数据的情况如下所示:
在这里插入图片描述
整个TCP连接的组成包括:一台主机上的缓存、变量和进程连接的套接字,以及另一台主机上的另一组缓存、变量与进程连接的套接字。而这两台主机之间的网络元素(路由器、交换机和中继器)中,没有为该连接分配任何缓存和变量。

二、TCP报文段结构

在这里插入图片描述
TCP报文段由首部字段和数据字段组成。数据字段包含一块应用数据,而首部字段包含下列元素:

  • 16比特的源端口号和目的端口号:被用于多路复用/分解来自或送到上层应用的数据。
  • 32比特的序号字段(sequence number field)和32比特的确认号字段(acknowledgment number field)。这些字段被TCP发送方和接收方用来实现可靠数据传输
  • 16比特的接收窗口字段,该字段用于流量控制,指示接收方愿意接收的字节数量
  • 4比特的首部长度字段:该字段指示了以32比特的字为单位的TCP首部长度。由于TCP选项字段的原因,TCP首部的长度是可变的(通常选项字段为空,所以TCP首部的典型长度就是20字节)。
  • 可选与变长的选项字段:该字段用于发送方与接收方协商最大报文段长度,或在高速网络环境下用作窗口调节因子时使用。
  • 6比特的标志字段:ACK比特用于指示确认字段中的值是有效的,即该报文段包括一个对已被成功接收报文段的确认。RST、SYN和FIN比特用于连接建立和拆除。当PSH比特被设置的时候,就指示接收方应立即将数据交给上层。最后URG比特用来指示报文段里存在着被发送端的上层实体置为“紧急”的数据。紧急数据的最后一个字节由16比特的紧急数据指针字段指出。当紧急数据存在并给出指向紧急数据尾的指针的时候,TCP必须通知接收端的上层实体。
2.1 序号和确认号

TCP报文段首部中两个最总要的字段是序号字段和确认号字段。这两个字段是TCP可靠传输服务的关键部分,在讨论这两个字段是如何用于提供可靠数据传输之前,我们需要首先看下TCP在这两个字段中存放了什么。

TCP把数据看成一个无结构的、有序的字节流,因为序号是建立在传送的字节流上,而不是建立在传送的报文段的序列之上。一个报文段的序号(sequence number for a segment)是该报文段首字节的字节流编号。以一个例子来说明(忽略首部长度),假设数据流由一个包含500 000字节的文件组成,其中MSS为1000字节,数据流的首字节编号是0,如下图所示,TCP将为该数据流构建500个报文段。给第一个报文段分配序号0,第二个报文段分配序号1000,第三个报文段分配序号2000,以此类推:
在这里插入图片描述
接下来看下确认号。TCP是全双工的,因此主机A在向主机B发送数据的同时,也许也接收来自主机B的数据(都是同一条TCP连接的一部分)。从主机B到达的每个报文段都有一个序号用于从B流向A的数据。主机A填充进报文段的确认号是主机A期望从主机B收到的下一字节的序号。以一个例子来说明,假设主机A已收到了来自主机B的编号为0-535的所有字节,同时假设它打算发送一个报文段给主机B。主机A等待主机B的数据流中字节536及之后的所有字节。所以主机A就会在它发往主机B的报文段的确认号字段中填上536。假设主机A此时还收到了包含字节900-1000的报文段,但是出于某种原因仍没收到字节536-899的报文段。此时A到B的下一个报文段的确认号字段仍然包含536。因为TCP只确认该流中至第一个丢失字节为止的字节,所以TCP被称为提供累积确认。

在上图中举的例子假设初始序号为0,事实上,一条TCP连接的双方会随机地选择初始序号。这样做可以减少将那些仍在网络中存在的来自两台主机之间先前已终止的连接的报文段,误认为是后来这两台主机之间新建连接所产生的有效报文段的可能性(碰巧与旧连接使用了相同的端口号)。

2.2 序号与确认号的使用例子

我们下面的例子是Telent,它由RFC 854定义,现在是一个用于远程登陆的流行应用层协议。Telnet运行在TCP上,被设计成可以在任意一对主机之间工作。

假设主机A与主机B已经建立了一个Telnet会话,A是客户,B是服务器。用户键入的每个字符都会被发送至远程主机;远程主机将回送每个字符的副本给客户,并将这些字符显示在Telnet用户的屏幕上。在从用户击键到字符被显示在用户屏幕上这段时间内,每个字符在网络中被传输了两次。

现在假设用户键入了一个字符‘C’,考虑在客户与服务器之间发送的TCP报文段(假设在TCP连接建立后但没有发送任何数据之前,客户正在等待字节79,而服务器正在等待字节42)。

整个过程如下所示:
在这里插入图片描述
如上图所示,共发送3个报文段,第一个报文段由客户发往服务器,在它的数据字段中包含有一字节的字符‘C’的ASCⅡ码,此时第一个报文段的序号字段里面是42,由于客户还没有接收到来自服务器的任何数据,因此该第一个报文段中的确认号字段中是79.

第二个报文段是由服务器发往客户的。首先它为服务器收到数据提供一个确认,通过在确认号字段中填入43,服务器告诉客户它已经成功地收到了字节42及以前的所有字节,现在正在等待字节43的出现。其次该报文段回显了字符‘C’,在第二报文段的数据字段中填入的是字符‘C’的ASCⅡ码。第二个报文段的序号为79,它是该TCP连接上从服务器到客户的数据流的起始序号,这也正是服务器要发送的第一个字节的数据。值得注意的是,对客户到服务器的数据的确认被装载在一个承载服务器到客户的数据的报文段中,这种确认被称为是被捎带在服务器到客户的数据报文段中的。

第三个报文段是客户发往服务器的。它的唯一的目的是确认已从服务器收到的数据。该报文段的数据字段为空(即确认信息没有被任何从客户到服务器的数据所捎带)。该报文段的确认号字段填入的是80,因为客户已经收到了字节流中序号为79及以前的字节,它现在正等待字节80的出现。

三、往返时间的估计与超时

TCP采用超时/重传机制来处理报文段的丢失问,这就需要我们设置超时间隔长度。怎么获取这个超时间隔长度呢?接下来我们将会讨论这一点。

3.1 估计往返时间

首先考虑TCP是如何估计发送方与接收方之间往返时间的。报文段的样本RTT(表示为SampleRTT)就是从某报文段被发出(即交给IP)到该报文段的确认被收到之间的时间量。大多数TCP的实现仅在某个时刻做一次SampleRTT测量,而不是为每个发送的报文段测量一个SampleRTT。这就是说,在任意时刻,仅为一个已发送的但目前尚未被确认的报文段估计SampleRTT,从而产生一个接近每个RTT的新SampleRTT值。另外,TCP决不为已被重传的报文段计算SampleRTT;它仅为传输一次的报文段测量SampleRTT。

显然,由于路由器的拥塞和端系统负载的变化,这些报文段的SampleRTT值会随之波动。由于这种波动,任何给定的SampleRTT值也许都是非典型的。因此,为了估计一个典型的RTT,自然要采取某种对SampleRTT取平均的办法。TCP维持一个SampleRTT均值(称为EstimatedRTT)。一旦获得一个新的SampleRTT时,TCP就会根据下列公式来更新EstimatedRTT:

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-\alpha)\times EstimatedRTT+\alpha\times SampleRTT EstimatedRtt=(1α)×EstimatedRTT+α×SampleRTT

上面的公式中,在[RFC 6298]中给出的 α \alpha α的参考值是0.125,这时上面的公式变为:

E s t i m a t e d R t t = ( 0.875 ) × E s t i m a t e d R T T + 0.125 × S a m p l e R T T EstimatedRtt=(0.875)\times EstimatedRTT+0.125\times SampleRTT EstimatedRtt=(0.875)×EstimatedRTT+0.125×SampleRTT

可以注意到EstimatedRTT是一个移动加权平均值。这个加权平均值对最近的样本赋予的权值要大于对老样本赋予的权值,因为越近的样本越能更好地反映网络的当前拥塞情况。

除了估算RTT外,测量RTT的变化也是有价值的。[RFC 6298]定义了RTT偏差DevRTT,用于估算SampleRTT偏离EstimatedRTT的程度:

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-\beta)\times DevRTT+\beta\times|SampleRTT-EstimatedRTT| DevRTT=(1β)×DevRTT+β×SampleRTTEstimatedRTT

注意到DevRTT是一个SampleRTT与EstimatedRTT之间差值的加权移动平均数。如果SampleRTT值波动较小,那么DevRTT的值就会很小;另一方面,如果波动很大,那么DevRTT的值就会很大。 β \beta β的值推荐为0.25.

3.2 设置和管理重传时间间隔

假设已经给出了EstimatedRTT值和DevRTT值,那么TCP超时间隔应该用什么值呢?很明显,超时间隔应该大于等于EstimatedRTT,否则,将造成不必要的重传。但是超时间隔不应该比EstimatedRTT大太多,否则当报文段丢失时,TCP不能很快地重传该报文段,导致数据传输时延大。因此要求将时间间隔设为EstimatedRTT加上一定余量。当SampleRTT值波动较大时,这个余量就应该大些;当波动较小时,这个余量应该小些。因此,加入DevRTT可以解决这个问题:

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

TCP通过以下机制提供可靠数据传输:

  1. 定时器
  2. TCP校验出包有错时丢弃报文段,不给出响应,对于TCP发送数据端,超时后会重发数据
  3. TCP将对收到的数据进行重排序,将收到的数据以正确的顺序交给应用层
  4. IP数据报会发生重复,TCP的接收端必须丢弃重复的数据
  5. TCP还能提供流量控制。TCP连接的每一方都有固定大小的缓冲空间。TCP的接收端只允许另一端发送接收端缓冲区所能接纳的数据。这将防止较快主机使较慢主机的缓冲区溢出

因特网的网络层服务(IP服务)是不可靠的。IP不保证数据的交付,不保证数据的按序交付,也不保证数据报中的数据的完整性。对于IP服务,数据报能够溢出路由器缓存而永远不能到达目的地,数据报也可能是乱序到达,而且数据报中的比特可能损坏(由0变为1或者相反)。

TCP在IP不可靠的服务之上创建了一种可靠数据传输服务。TCP的可靠数据传输服务确保了一个进程从其接收缓存中读出的数据流是无损坏、无间隔、非冗余和按序的数据流;即该字节流与连接的另一方端系统发送出的字节流是完全相同。

注意TCP在实现定时器时使用单一的重传定时器,这样可以减小开销。

在接下来的讨论中,我们将以两个递增的步骤来讨论TCP是如何提供可靠数据传输的。首先我们会给出一个TCP发送方的高度简化的描述,该发送方只用超时来恢复报文段的丢失;然后再给出一个更全面的描述,该描述除了使用超时机制外,还使用冗余确认技术。我们假定数据仅向一个方向发送,即从主机A到主机B,且主机A在发送一个大文件。

在下面的伪代码中给出了一个TCP发送方的高度简化的描述。在TCP发送方有3个与发送和重传有关的主要事件:

  • 从上层应用程序接收数据
  • 定时器超时
  • 收到ACK。
//假设发送方不受TCP流量和拥塞控制的限制,来自上层的数据长度小于MSS,且数据传送只在一个方向进行

NextSeqNum=InitialSeqNumber
SendBase=InitialSeqNumber

loop(forever){
	switch(event)
		event(data received from application above):
			create TCP segment with sequence number NextSeqNum
			if(timer currently not running)
				start timer
			pass segment to IP
			NextSeqNum=NextSeqNum+length(data)
			break;
		event(timer timeout)
			retransmit not-yet-acknowledged segment with smallese sequence number
			start timer
			break;
		event(ACK received, with ACK field value of y)
			if(y>SendBase){
				SendBase=y
				if(there are currently any not-yet-acknowledged segments)
					start timer
			}
}

一旦第一个主要事件发生,TCP从应用程序接收数据,将数据封装在一个报文段中,并把该报文段交给IP。注意到每一个报文段都包含一个序号,这个序号就是该报文段第一个数据字节的字节流编号。还要注意到如果定时器还没有为某些其他报文段而运行,则当报文段被传给IP时,TCP就启动该定时器(将定时器想象为与最早的未被确认的报文段相关联)。该定时器的过期间隔是TimeoutInterval,由EstimatedRTT和DevRTT计算得出。

第二个主要事件是超时。TCP通过重传引起超时的报文段来响应超时事件。然后TCP重启定时器。

第三个主要事件是一个来自接收方的确认报文段(ACK)的到达(更确切的说是一个包含了有效ACK字段值的报文段)。当该事件发生时,TCP将ACK的值y与它的变量SendBase进行比较。TCP状态变量SendBase是最早未被确认的字节的序号(因此SendBase-1是指接收方已正确按序号接收到的数据的最后一个字节的序号)。TCP采用累积确认,所以y确认了字节编号在y之前的所有字节都已经收到。如果y>SendBase,则该ACK是在确认一个或多个先前未被确认的报文段。因此发送方更新它的SendBase变量;如果当前有未被确认的报文段,TCP还要重启定时器。

4.1 一些有趣的情况

我们刚刚描述了一个关于TCP如何提供可靠数据传输的高度简化的版本。但即使这种高度简化的版本,仍然存在着许多微妙之处。为了较好的感受该协议的工作过程,我们来看几种简单情况:

下图描述了第一种情况,主机A向主机B发送一个报文段。假设该报文段的序号是92,而且包含8字节数据。在发出该报文段之后,主机A等待一个来自主机B的确认号为100的报文段。虽然A发出的报文段在主机B上被收到,但从主机B发往主机A的确认报文丢失了。在这种情况下,超时事件就会发生,主机A会重传相同的报文段。当然,当主机B收到该重传的报文段时,它将通过序号发现该报文段包含了早已收到的数据。因此,主机B的TCP将丢弃该重传的报文段中的这些字节。
在这里插入图片描述
下图描述了第二种情况,主机A连续发送了两个报文段。第一个报文段序号是92,包含8字节数据;第二个报文段序号是100,包含20字节数据。假设两个报文段都完好无损地到达主机B,并且主机B为每一个报文段分别发送一个确认。第一个确认报文段的确认号是100,第二个确认报文的确认号是120。现在假设在超时之前这两个报文段中没有一个确认报文到达主机A。当超时事件发生时,主机A重传序号92的第一个报文段,并重启定时器。只要第二个报文段的ACK在新的超时发生以前到达,则第二个报文段将不会被重传。
在这里插入图片描述
第三种也是最后一种情况,假设主机A与在第二种情况中完全一样,发送两个报文段。第一个报文段的确认报文在网络中丢失,但在超时事件发生之前主机A收到一个确认号为120的确认报文。主机A因而直到主机B已经收到了序号为119及之前的所有字节;所以主机A不会重传这两个报文段中的任何一个。
在这里插入图片描述

4.2 超时间隔加倍

在大多数TCP实现中都会加入一种叫做超时重传的机制:在这种机制下,每当超时事件发生时,TCP重传具有最小序号的还未被确认的报文段。但注意每次TCP重传时都会将下一次的超时间隔设为先前值的两倍,而不是用EstimatedRTT和DevRTT推算出的值。因此,超时间隔在每次重传后会呈指数型增长。然而,每当定时器在另两个事件(即收到上层应用的数据和收到ACK)中的任意一个启动时,TimeoutInterval由最近的EstimatedRTT值与DevRTT值推算得到。

这种修改提供了一个形式受限的拥塞控制。定时器过期很可能是由网络拥塞引起的,即太多的分组到达源与目的地之间路径上的一台(或多台)路由器的队列中,造成分组丢失或长时间的排队时延。在拥塞的时候,如果源持续重传分组,会使拥塞更加严重,所以TCP会采取这样的机制来减缓可能的拥塞。

4.3 快速重传

超时触发重传存在的问题之一是超时周期可能相对较长。当一个报文段丢失时,这种长超时周期迫使发送方延迟重传丢失的分组,因而增加了端到端时延。为了减少这种时延,TCP提供了另一种冗余ACK机制来触发重传,冗余ACK就是收到了某个先前已经确认收到了的报文段的ACK。要理解发送方对冗余ACK的响应,我们必须首先看一下接收方为什么会发送冗余ACK。

下表总结了TCP接收方的ACK生成策略:

事件TCP接收方动作
具有所期望序号的按序报文段到达。所有在期望序号及以前的数据都已经被确认延迟的ACK。对另一个按序报文段的到达事件最多等待500ms。如果下一个按序报文段在这个时间间隔内没有到达,则发送一个ACK
具有所期望序号的报文段到达。另一个按序报文段等待ACK运输立即发送单个累计ACK,以确认两个按序报文段
比期望序号大的失序报文段到达。检测出间隔立即发送冗余ACK,指示下一个期待字节的序号(其为间隔的低端的序号)
能部分或完全填充接收数据间隔的报文段到达倘若该报文段起始于间隔的低端,则立即发送ACK

当TCP接收方收到一个序号大于下一个所期望的、按序的报文段序号的报文段时,它就检测到了数据流中的一个间隙,这意味着数据丢失。这个间隔可能是由于在网络中报文段丢失或重新排序造成的。因为TCP不使用否定确认,所以接收方不能向发送方回一个显式的否定确认。相反,它只是对已经接收到的最后一个按序字节数据进行重复确认(即产生一个冗余ACK)。注意在上表中允许接收方不丢失失序报文段。

因为发送方经常一个接一个地发送大量的报文段,如果一个报文段丢失,就很可能引起许多一个接一个的冗余ACK。如果TCP发送方接收到对相同数据的三个冗余ACK,它把这当作一种指示,说明在这个已经被确认过3次的报文段之后的报文段已经丢失。一旦收到3个冗余ACK,TCP就执行快速重传,即在该报文段的定时器过期之前重传丢失的报文段。对于采用快速重传的TCP,可用下列代码片段代替上面简化的ACK收到事件。

event(ACK received, with ACK field value of y):
	if(y>SendBase){
		SendBase=y
		if(there are currently any not yet acknowledged segments)
			start timer
		else{//a duplicate ACK for already ACKed segment
			increment number of duplicate ACKs received for y
			if(number of duplicate ACKs received for y==3)
				//TCP fast retransmit
				resend segment with sequence number y
		}
	break;

五、流量控制(TCP窗口)

一条TCP连接每一侧主机都为该连接设置了接收缓存。当该TCP连接收到正确、按序的字节后,他就将数据放入接收缓存。相关联的应用进程会从该缓存中读取数据,但不必是数据刚一到达就立即读取。事实上,接收方应用也许正忙于其他任务,甚至要过很长事件后才去读取该数据。如果某应用程序读取数据时相对缓慢,而发送方发送得太多、太快,发送的数据就会容易地使该连接的接收缓存溢出。

TCP为它的应用程序提供了流量控制服务以消除发送法使接收方缓存溢出的可能性。流量控制因此是一个速度匹配服务,即发送方的发送速率与接收方应用程序的读取速率相匹配。(注意TCP发送方因IP网络的拥塞而被遏制的情况属于拥塞控制,要区分开来)我们在下面假设TCP是这样实现的,即TCP接收方丢弃失序的报文段。

TCP通过让发送方维护一个称为接收窗口的变量来提供流量控制。通俗的说,接收窗口用于给发送方一个提示——该接收方还有多少可用的缓存空间。因为TCP是全双工通心,在连接两端的发送方都各自维护一个接收窗口。接下来在文件传输的情况下研究接收窗口:

假设主机A通过一条TCP连接向主机B发送一个大文件。主机B为该连接分配了一个接收缓存,并用RevBuffer来表示其大小。主机B上的应用进程不时地从该缓存中读取数据。我们定义如下变量:

  • LastByteRead:主机B上的应用进程从缓存读出的数据流的最后一个字节的编号。
  • LastByteRcvd:从网络中到达的并且已放入主机B接收缓存中的数据流的最后一个字节的编号。

由于TCP不允许已分配的缓存溢出,下式必须成立:
LastByteRcvd-LastByteRead<=RvcBuffer

接收窗口用rwnd表示,根据缓存可用空间的数量来设置:
rwnd=RevGbuufer-[LastByteRcvd-LastByteRead]

由于该空间是随着时间变化的,所以rwnd是动态的,下图对rwnd进行了图示:
在这里插入图片描述
连接是如何使用变量rwnd提供流量控制服务的呢?主机B通过把当前的rwnd值放入它发给主机A的报文段接收窗口字段中,通知主机A它在该连接的缓存中还有多少可用空间。开始时,主机B设定rwnd=RcvBuffer。注意到为了实现这一点,主机A必须跟踪几个与连接有关的变量。

主机A轮流跟踪两个变量,LastByteSent和LastByteAcked,这两个变量的意义很明显。注意到这两个变量之间的差LastBYteSent-LastByteAcked,就是主机A发送到连接但未被确认的数据量。通过将未确认的数据量控制在值rwnd以内,就可以保证主机A不会使主机B的接收缓存溢出。因此,主机A在该连接的整个声明周期必须保证:
LastByteSent-LastByteAcked<=rwnd

对于这个方案还存在一个小小的技术问题。为了理解这一点,假设主机B的接收缓存已经存满,使得rwnd=0.在将rwnd=0通告给主机A之后,还要假设主机B没有任何数据要发给主机A。此时,考虑会发生什么情况。因为主机B上的应用进程将缓存清空,TCP并不向主机A发送带有rwnd新值的新报文段;事实上,TCP仅当在它有书记或有确认要发时才会发送报文段给主机A。这样,主机A不可能直到主机B的接收缓存已经有新的空间了,即主机A被阻塞而不能再发送数据!为了解决这个问题,TCP规范中要求:当主机B的接收窗口为0时,主机A继续发送只有一个字节数据的报文段。这些报文段将会被接收方确认。最终缓存将开始清空,并且确认报文里将包含一个非0的rwnd值。

(待补充/180)

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值