运输层
3.1概述和运输层服务
运输层为运行在不同端系统的进程之间提供了逻辑通信的功能(而非物理上的通信),从应用进程的角度来看这些端系统似乎直接相连,实际上他们经过了许多的路由器和链路相连。应用进程使用运输层的逻辑通信功能彼此发送报文,不需要考虑物理链路的细节。也就是说运输层规定了不同进程之间发送报文的方式,并为应用层提供了对应服务的接口,而不考虑底层的链路问题。
因为运输层的协议是为端系统上的进程提供服务,所以运输层的协议是运行在端系统上的,而并非路由器上。在运输层的发送端,会先将从应用层接收到的报文进行分组封装(分成运输层分组并加上运输层首部字段)封装为运输层报文段,并将其传到网络层,由网络层负责下一步的传输给接收端,接收端的运输层从网络层接收报文段,处理后传到应用层。
物理应用程序可以使用多种运输层协议,因特网有两种运输层协议:TCP和UDP
3.1.1运输层和网络层的关系
运输层和网络层都提供了逻辑通信的服务,但是运输层面向的是进程之间的通信,而网络层实现的是主机之间的通信。正如上面所说的,运输层负责接收要发送的报文并将其封装后发送给接收端,在这里运输层提供了一条从这个进程发送端到另一个进程接收端的逻辑通信服务,但是在这个通信中,信息的传输实际上是依赖主机之间的通信,主机接收所有进程要传输的报文段,通过网络层提供的逻辑服务将其传到另一个接收信息的主机。用收发邮件来解释,两个家庭(主机)之间的不同成员(进程)要进行通信,不同人(进程)的邮件(信息)放在邮箱(交付给运输层)里面就可以实现邮件(报文)的传输,而邮箱(运输层)里面邮件(报文,也可以说是报文段)的传输是依靠邮政车(路由器或者说是网络层)将一个邮箱里面的邮件运送到另一个邮箱。由此我们可以清楚运输层提供了进程的通信,而进程通信的实现是在网络层提供的主机之间的通信的基础上实现的。
运输层的功能实现是在网络层的基础上的,因此网络层出现问题也会影响运输层的服务,但是运输层也能提供网络层所不具备的一些服务如信息的冗余、加密、修复等的功能。
3.1.2因特网运输层概述
之前有说过因特网的运输层的两种协议:UDP(用户数据报协议)和TCP(传输控制协议)协议。UDP协议的特点是轻量级、无连接、不可靠、可操作性强;TCP则完全相反有连接、可靠、拥塞控制、流量控制。在使用运输层服务时,必须说明选择哪一种服务。
另外一提,在一些文献中,关于运输层、TCP或者UDP传输的数据分组的称呼其实不尽相同,比如有的说运输层传输报文段也有称为数据报的,甚至也有把TCP的叫做报文段把UDP的叫做数据报,这里运输层的分组就全部称作报文段了。
首先先概述网络层,网络层有一个重要的协议叫做IP协议,这是一个尽力而为交付服务(best-effort delivery)的协议,它会尽最大努力去实现数据报(这里指网络层的分组名)的传输的可靠性,但其实它不确保任何数据报的交付和传输的顺序,也就是说是不可靠的协议。基于此我们肯定会想起之前有讲到运输层的服务是基于网络层实现的,既然网络层不提供可靠的数据传输服务,那么TCP可靠信息的传输就是依赖于运输层的实现了。
接下来讲UDP和TCP,首先明晰他们的职责所在:将两个端系统的IP的交付服务扩展为进程之间的交付服务,人话就是把主机之间的通信细分为了进程之间的通信。这被称为运输层的多路复用与多路分解。UDP和TCP都能提供差错检查和数据传输的服务,这是运输层最基础要实现的服务(就是运输层至少要实现这两个功能,这也就是UDP的所有内容,值得一说的是,UDP只会检查,但没有纠错所以它还是一个不可靠的服务,也体现了它的可操作性)TCP在UDP的基础上主要实现了拥塞控制,可靠数据传输两个服务。(为了实现数据加密,还在TCP的基础上添加网络安全的服务演变出了TLS)
3.2多路复用和多路分解
首先我们知道在进程中一般都对应着多个套接字,是负责在网络和进程之间传输数据的接口。在进程向运输层传输数据时并不是直接交给了运输层,而是交付给了对应的接口,由接口传输给运输层,在主机上套接字往往不止一个,所以主机上的每个套接字都有一个标识符与其一一对应,而标识符的格式也与套接字采用UDP还是TCP有关。

首先,要使接收主机将一个要到达运输层的报文段定向到对应的套接字,我们在运输层报文具有几个字段。在接收端,运输层检查这些字段并标识出接收套接字从而定向到该套接字。将运输层报文段数据交付到正确的套接字叫做多路分解;从源主机不同套接字收集数据块,并为每个数据块封装上首部信息生成报文段,然后将报文段传递到网络层就是多路复用。就是说在发送数据时,因为不同套接字要先经过相同的路径,在运输层定向后会分化为多条路径向不同主机的进程发送报文段,就是多路分解(多路分用是将从一个信道接收到的数据流分发到多个目标进程的过程)。相反的在接收数据时,不同进程发送的数据先经过不同路径最后统一由运输层接收就是多路复用(多路复用是将多个数据流合并到一个信道(或连接)中进行传输的过程)
接下来讲解多路分用和多路复用的原理,首先我们知道运输层多路复用需要能定位套接字,也就是套接字有唯一标识符且每个报文段都有特殊的字段来指明它要交付给哪个套接字。这些特殊的字段包括有源端口号和目的端口号(UDP和TCP报文段还要一些别的字段 )端口号是一个16bit的数大小在0-65535之间,0-1023的端口号是周知端口号,是已经被占用的端口号,比如用于HTTP、FTP之类的应用层协议。在开发一个新的应用程序时,必须给其分配端口号。

总之,在主机上每个套接字可以分配一个端口号,报文段到达主机时,运输层会检查报文段中的端口号从而定向到目的套接字,然后通过这个套接字连入对应的进程。UDP大体是这么做的,TCP会更为复杂。
1.无连接的多路分用和多路复用
之前在2.7.1有说过,如何创建UDP套接字
clientSocket=socket(AF_INET,SOCK_DGRAM)
用这种方法创建套接字时,运输层会自动的为套接字分配一个端口号。特别的,运输层是从1024-65535内分配的未被使用的端口号,而且我们也可以再重新分配端口号,使用bind()方法
clientSocket.bind(('',19157))
如果是开发“周知协议”的应用程序,就需要为进程分配一个对应的周知的端口号,通常是用户端让运输层自动分配一个端口号,而服务器端则分配一个特定的端口号。在发送数据时会附带上源端口号,而在之前的代码中也有体现出来,这个源端口号的作用主要是方便回发数据。
2.面向连接的多路分用和多路复用
UDP中一个套接字是由IP+端口号的二元组来标识,而在TCP中一个套接字需要源IP+源端口号+目的IP+目的端口号的一个四元组来标识。因此当报文段到达一台主机时,主机会通过这个四元组定位对应的套接字。而且源IP和源端口号不同的TCP报文段会到达不同的套接字,除非是用于创建TCP连接的信息。这样TCP就可以支持多个并行的连接了,而且这样也便于维护TCP的连接状态和对连接过程中一些活动的实时监控(使用源IP和源端口号区分来自不同进程的并行连接)
3.Web服务器与TCP
Web服务器会为每条连接建立一个进程,每个进程都会有对应的套接字,通过这些套接字可以接收到HTTP请求和发送HTTP响应。但是其实上套接字和进程并不是完全的一一对应,现在的高性能Web服务器大多数只使用一个进程,而在进程中创立多个线程(类似一个轻量级的子进程),这样可能会有多个套接字共用这一个进程。

3.3无连接运输:UDP
UDP协议提供了最低限度的运输层服务,也就是提供了多路复用和多路分用的服务,以便可以满足传输数据的需要。同时还有一些差错检测,几乎就是UDP的全部内容了。DNS就是使用的UDP协议。
UDP只实现了最少的功能,但是很多情况下我们都会使用UDP而不是TCP,一是因为UDP对于信息传输的控制可以更精细,主要是因为TCP的拥塞控制协议,有时候会影响一些实时通信类的软件的信息传输速率,而这类软件对于最低信息传输速率的要求是很严格的,所以有的时候这类软件也会选择在UDP上实现,并加上一些额外的服务。
二是UDP是无连接的协议,传输数据时不需要花费时间去建立连接,时延也比较小。
三是UDP的无连接状态,相较于TCP需要预留一整个线路的资源去维护连接的状态,UDP的无连接状态往往更加节省资源与便于维护。
还有一个是UDP的首部字段小,对应的开销也极小。

其实大多数应用都能接受少量的分组丢失,所以选择TCP还是UDP的最主要原因并不是可靠的数据传输服务,而是对于流量的控制与资源分配的选择。TCP有流量控制协议和拥塞控制协议,所以它是不支持自主的控制流量和对网络拥塞的精细化处理的。相应的,如果想要自由的控制流量,就需要选择UDP,值得一提的是,虽然UDP没有拥塞控制协议,但是大多数情况下,还是需要为其添加应对网络拥塞的服务。如果UDP没有拥塞控制的话,很可能会因为网络拥塞造成UDP的高丢包率并挤垮TCP连接。
3.3.1UDP报文结构

UDP的首部报文一共4个字段,每个字段2字节。长度字段是UDP报文段的总字节数(包括首部)校验和主要是UDP用于检测报文段是否出现了差错的。
3.3.2UDP校验和
校验和就是把整个报文段每两个字节一累加,得出一个2字节的和,最后得出的结果求反之后就是校验和。校验和是为了提供差错检测功能的字段,虽然底层如链路层网络层可能都有差错检测的服务,但是在传输层是不能保证所有的底层链路都有差错检测的服务,所以需要再提供差错检测的服务,这也是所谓的端到端原则,也就是在与较高级别提供的这些功能的代价相比,在较低级别设置的功能可能是冗余或者没有价值的。
虽然UDP提供了差错检测,但是它并没有差错的恢复功能,只能发出警告。
3.4可靠数据传输原理
可靠数据传输其实就是说让数据通过一条可靠的信道进行传输,保证数据不会丢失,顺序不会错乱等,这也是TCP协议提供的服务。在链路层应用层运输层等都会有对于可靠数据传输如何实现的问题,通常可靠数据的传输依赖于可靠数据传输协议。
根据之前所讲的端到端的原则,底层的可靠数据传输在上层可能是不可靠的,所以实现可靠数据传输的会比较困难,为了方便,我们直接将底层视作不可靠的信道。(本节中rdt表示可靠的数据传输,udt表示不可靠,send和recv分别表示传输和接收,deliver是向上层交付)
本节只讲述了半双工(单向数据传输),全双工(双向)的情况与半双工类似。

3.4.1构造可靠数据传输协议
可靠数据传输协议分为多个版本,见下
1.经完全可靠信道的可靠数据传输:rdt1.0
rdt1.0考虑了最简单的情况,也就是底层信道完全可靠,不存在分组的丢失、错误等,这个模型中为发送方和接收方都创建了一个有限状态机(FSM)每个状态机都只有一个状态(图中的箭头表示从一个状态变为下一个状态,横线上面是会引起状态变化的事件,对应的操作在横线下方,如果对一个事件没有操作就是空白,或者写一个∧,FSM的初始状态用虚线来表示)rdt的发送端只通过rdt_send接收高层的data,make_pkt产生一个分组packet并将其发送到接收端,接收端使用rdt_rcv接收packet

2.具有比特差错信道的可靠数据传输:rdt2.0
在个模型中考虑到如果底层的链路不少完全的可靠,可能会有一些比特位发生错误或受损,所以加入了重传的机制,基于这个机制所实现的可靠数据传输协议叫做自动重传请求协议(ARQ)
ARQ对于比特差错有三种处理方式:差错检测,接收方反馈和重传。
差错检测和UDP的检验和类似,会有额外的bit随分组一起传输并汇集在检验和字段中
接收方反馈是为了让发送方知道接收方的情况,分别有两种反馈报文ACK(肯定确认)和NAK(否定确认),通常只有一个bit长,分别用1和0表示
重传就是如果有错误就重传文件


首先是发送端等待来自上层的调用,上层调用发送端时,发送端会向接收端发送报文(使用rdt_send)接着进入等待接收端反馈的状态,接收到反馈后,如果是ACK就回到等待调用的状态,如果是NAK就重传文件再等待反馈。接收端则是先等待调用,被调用时先接收报文段,接收到之后就反馈给发送端。
如果发送端发送了文件,在接收端没有反馈时,发送端是不会有任何反应的,因此rdt2.0也叫停等协议。这时如果反馈的报文出现了错误,数据的传输肯定也会出错,这也就是我们需要考虑的新的情况了,对此其实有三个解决思路,第一个是请求重传,但是请求重新反馈的报文也可能出错;第二是增加检验bit,对于只是出错但是没有数据丢失的线路是可以的;第三是如果反馈的分组模糊不清就重传,但是接收方或发送方并不知道对方是不是需要重传,对此我们也有解决方法(大部分的数据传输协议都是使用的这个方法)。
解决方法就是在分组中加入一个序号的字段,让发送方进行编号,接收方只需检测序号就能判断出接收到的分组是否为重传的,对于停等协议这种简单情况,1bit的字段就够了,让接收方知道发送方是不是在传输前一个分组还是一个新分组,因为停等协议是不考虑丢包的,接收方只需要知道所传回去的反馈报文有没有出错即可,如果出错就重新反馈。也就是在2.0的基础上加上对于反馈报文的检验与重传


rdt2.1将状态机的状态数量增加了1倍,主要是因为增加了序号段,需要区分发送的序号是0还是1两种情况。2.1版本首先是发送方发送序号为0的分组1,然后跳转到状态2;等待的反馈报文,然后准备下一次数据的发送(状态3);发送序号为1的分组2,进入状态4;等待反馈报文,接收到反馈后就回到状态1。
接收方会先接收数据,如果报文没有出错的话就检测序号,如果在等待序号0的状态1接收到了序号0,就返回ACK进入下一个状态,如果接收到了序号1就返回NAK继续等待重传,同理在状态2也是一样的。
接下来是rdt2.2,与2.1类似,但是为了减少信息冗余,就删去了NAK的报文


具体流程其实和2.1版本类似,但是如果接收方接收到的报文序号错误,说明上次发送的反馈报文出错了,就重传对应序号的ACK,发送端收到正确序号的ACK得知接收端已经收到了报文就发送下一个报文。还有一种情况就是序号正确但是校验和出错也就是单纯的分组出错了,这时就返回包含了错误序号的ACK即可,发送方接收到错误的序号就能得知文件出错就会进行重传。
3.具有比特差错的丢包信道的可靠数据传输:rdt3.0
在2.0的基础上rdt3.0又考虑了分组丢包的问题,也就是现在分组可能会丢失。
发送方得知分组丢失的解决办法就是加上了一个定时器,定时器的时间也是可以设置的,如果在定时器结束的时候仍然没有收到反馈报文就认为是分组丢失,就进行重传。这时会有四种情况,无丢包,分组丢失,反馈ACK分组丢失,或者分组没有丢失但是超时了。接收方的FSM图与rdt2.2一样





如果接收方收到重复的正确分组,说明可能是ACK丢失、ACK出错或过早超时了,这时会返回重复的正确的ACK,如果发送方接收到重复的正确的ACK说明是过早超时了,继续传输下一个分组。如果是发送方收到错误的ACK,则是报文出错了,对报文进行重传。(其实还有别的特殊情况,可以自己分析一下)
3.4.2流水线可靠数据传输协议
发送方(或信道)的利用率为发送方实际忙于将数据送入信道的时间比上发送时间
停等协议因为以停等的方式运行,不能发送多个协议,利用率和性能较低,因此产生了流水线协议,允许发送方发送多个分组且不许确认。
流水线协议因为要发送多个分组,所以要在rdt3.0的基础上进行修改,首先是增加序号的范围;然后是能让发送方对未确认的分组进行缓存,接收方对已经收到的分组进行缓存;还有对于丢失、损坏和时延过大(或丢失)的分组的处理方式(主要是回退N步和选择重传)。


3.4.3回退N步
在回退N步(GBN)协议中,允许发送方发送多个分组且不需确认,但未确认的分组数不能超过最大允许的值。该最大值由窗口大小N决定,发送方维护一个长度为N的滑动窗口,包含已发送但未确认的分组。当超时发生时,发送方将重传所有已发送但未被确认的分组,即使其中部分分组可能已被正确接收。接收方仅按序接受分组,若收到失序分组则直接丢弃并重复发送当前期望序号的ACK。这种机制虽提高了利用率,但在高误码率环境下会导致大量重复传输,影响整体效率。
下图是发送方在GBN协议中的序号范围,如果我们将base作为基序号(最早未被确认的分组序号)nextseqnum作为最小的未被使用的序号(下一个待发送的分组序号)则可将序号分为四段,[0,base-1]中是已经发送并确认的分组,[base,nextqnum-1]是已经发送未被确认的分组,[nextqnum,base+N-1]是将要被发送的分组,大于base+N的序号则是不可使用的。

N被称为是窗口长度,该协议也被称为是滑动窗口协议。
GBN接收方必须需要响应三种事件:
第一个是上层的调用,当上层调用服务时,发送方需要检查发送窗口是否已满,如果未满就创建新的分组并发送,然后更新变量;否则就将数据返回上层表示窗口已满了,稍等一会再进行尝试,不过实际大多数情况是进行缓存,或者使用信号量检测窗口,只允许上层在窗口不满时调用服务。
第二个是ACK。在GBN协议中对于一个序号为i的分组采取累计确认的方式,也就是只有当i之前的所以分组都被确认收到后才会确认i。
还有超时事件,如协议的名字所述,当发生丢失或超时等行为时,和停等协议的策略类似,GBN会对所有已发送但未收到确认的分组进行重传,如果收到任意一个ACK定时器都会被重置。
发送方的逻辑也简单,如果序号为i的分组被确认按序收到,接收方会反馈一个ACK,并将数据向上层交付,否则就会丢弃该分组并反馈最近的按序收到的一个分组的ACK表示错误。因此在GBN协议中,如果分组k被确认收到,那么k之前的所以分组都被确认收到。
对于接收方对一个错序的分组,假设预期收到分组n结果收到了n+1,可能直接丢弃分组n+1有些浪费,但是就算将其进行缓存,返回分组n的错误信息时,在发送方的窗口处,n+1也是未被收到的,还是要进行重传。因此接收方直接丢弃n+1即可。这种方法的有点是接收缓存简单,不需要缓存失序分组,但缺点也显著,随后对这个被丢弃的分组也可能会出错,就会导致一定的浪费。
GBN协议在协议栈中的实现,是一种基于事件的编程方式。即只需对特定的事件给出对应的响应即可。在发送方,这些事件包括:上层的调用;定时器的中断;报文到达时,来自下层的调用。
3.4.4选择重传
GBN协议允许发送方使用多个分组填充流水线,避免了停等协议中的信道利用率问题,但是GBN也存在性能问题。在一些情况下,如果GBN的窗口长度和带宽时延很大时,对窗口中的分组的大量的频繁的重传对于性能的消耗是巨大的,很多的分组可能都不需要重传,但是因为大量的重传,流水线往往会被这种不需要重传的分组充斥。(如果一篇作文,有一个字听不清楚就需要把全篇再读一遍,效率是极低的)
因此选择重传协议(SR)允许发送方只重传它认为需要重传的分组(丢失或损毁的分组),避免不必要的重传。

SR将确认一个已经接收的分组而不管其次序,失序的分组将会被缓存,知道所以失序分组被确认才会进行交付。
SR也是一个基于事件编程实现的协议,在发送方具有三个事件:
第一个是上层的调用,从上层接收到数据后,检测下一个可用序号,如果序号可用就打包发送,否则要么缓存要么返回上层。
第二个是超时,超时后会进行重传分组,和GBN不同的是,每个分组都有一个计时器,超时后只重传单个分组。
第三个是收到ACK,如果序号在窗口内就标记收到,如果等于base,base就移动至最小的未确认分组序号处(窗口前移)移动后如果有未发送分组加入窗口,就发送分组。
接收方具有三个事件
第一个是窗口内分组被正确接收,返回一个ACK,如果该分组未被接收到过,就缓存分组;如果分组的序号等于接收窗口的base,则将数据进行交付,然后接收窗口前移。
第二个是如果接收到上一个接收窗口的分组,也返回一个ACK即使该分组已经被收到。
第三是其他情况,丢弃分组。
接收方的第二个事件的处理十分重要,如果接收方不进行确认,发送方永远不知道分组已经被收到,发送方的窗口也就不能向前移动。因为SR协议中接收方和发送方的窗口中看到的情况并不一定相同。
如果序号数量有限,接收方和发送方的一致性缺失严重就会导致比较严重的后果。比如说如果窗口大小为3,可用序号为0 1 2 3,如果发送了分组0 1 2被正确接收并确认,接收方的窗口到达第4 5 6个分组,但是接收方反馈的ACK全部丢失,此时发送方就还停留在第1 2 3个分组,就会出现错误。

上面的两种情况都会引发错误,导致SR接收方无法确认是一个新分组还是重传,根本原因是窗口大小比可用序号的比值太大,对于SR协议,这个比值不能超过0.5。
3.5面向连接的运输:TCP
3.5.1TCP连接
TCP被称为面向连接的,因为在分组交换前,两个进程之间要交换预备报文段,也叫握手,以建立确保数据传输的参数。
TCP的连接建立并不是电路交换网络中的端到端的TDM或FDM电路,TCP只是逻辑上的连接,其状态只存储在两个进程当中,中间的网络元素并不会维护连接状态(中间路由器看不到连接,只能收到数据报)
TCP提供全双工服务(可以同时上传和下载)是点对点的连接,只在单个的发送方和接收方之间建立连接。譬如多播(在一次发送中向多个接收方同时发送)对于TCP是不能实现的。
如果一个客户端进程想和另一个服务器端进程建立TCP的连接,首先需要发送连接请求(一个特殊的TCP报文段),请求中要包含服务器端进程的IP和端口号,服务器端也会返回一个特殊的TCP的报文段作为响应,最后客户端再发送一个特殊的TCP报文段表示双方的TCP连接建立成功。前两次的报文段不包含应用层数据,第三个报文段可以承载有效载荷(如HTTP的请求报文)。这也被叫做三次握手。
连接建立之后就能进行数据的交换了,客户端通过套接字传递数据流,数据由套接字向下层交付之后,所有的行为都交由TCP连接管控。TCP会将数据引导至发送缓存中,这个缓存是在握手期间建立的缓存之一,TCP会时不时从发送缓存中取出部分数据发送到网络层(在TCP规范中并未定义应该何时发送,“TCP应该在其方便的时候以报文段的形式发送数据”)TCP可以从缓存中取出和放入报文段中的数据数量受限于最大报文段长度(MSS)MSS通常由主机发送的最大链路层帧长度(最大传输单元,MTU)设置。通常要保证一个TCP报文段加上TCP/IP的首部字段后适合单个链路层的帧,MSS的典型值为1460字节。
TCP为客户数据配上了一个TCP首部,从而形成TCP报文段,这些报文段被发送到网络层,网络层将其封装在网络层IP数据报中并将其发送到网络中。接收方接收到数据后就将其放置在接收缓存中,应用程序从此处读取数据流,TCP连接的两端都有接收和发送缓存。

如上所述,TCP连接的组成包括一台主机上的缓存,变量和与进程连接的套接字,以及另一台主机上的另一组缓存,变量和与进程连接的套接字,与两台主机之间的中间元素无关。
3.5.2 TCP报文段结构
TCP报文段由首部字段和数据字段组成。数据字段包括应用的数据,如前所述,MSS限制了报文段的最大长度。
当TCP发送较大的文件如图片视频等时,通常是划分为多个长度为MSS的TCP报文段进行传输的(最后一块通常小于MSS)

TCP报文段与UDP类似,首部包括源端口号和目的端口号,这通常被用作多路复用和多路分解。与UDP一样也具有校验和字段,同时还包括以下字段:
32bit的序号字段和32bit的确认号字段,用于被TCP发送方和接收方实现可靠数据传输服务。
16bit的接收窗口字段,用于流量控制。
4bit的首部长度字段,指示了以32bit的字为单位的TCP首部长度,由于TCP选项字段的原因,这部分的长度是可变的(通常选项字段为空,典型长度为20字节)
可选和可变的选项字段,该字段用于发送方和接收方协商MSS或者在高速网络环境下作为窗口调节因子,首部字段还有一个时间戳选项。
6bit的标志字段,ACK比特用于指示确认字段中的值是有效的,也就是说该报文段包括对一个成功接收的报文段的确认。RST、SYN、FIN比特用于连接建立和拆除,在明确拥塞通告中使用了CWR和ECE比特。当PSH比特被置位时,指示接收方要将数据传输给上层,URG比特用于指示该数据为紧急数据,紧急数据的最后一个字节由16bit的紧急数据指针字段指出,此时TCP必须接收端的上层实体。(不过PSH、URG和紧急数据指针在实际中并未被使用)
TCP报文段首部的一个很重要的组成部分就是序号和确认号字段。TCP把数据当作无结构的有顺序的字节流,一个报文段的序号就是这个报文段首字节的字节流编号,也就是说TCP发送数据时对每个字节隐式的进行编号,每一个报文段的首字节的编号被填入首部序号字段中。
因为TCP是全双工的,也就是说发送方也可能会接收数据,确认号就是用于填充接收方期望接收到的下一个字节的序号。TCP只会确认在接收数据的数据流中第一个丢失的字节的上一个字节,被称为是提供累积确认。值得一提的是TCP并未对失序报文段做任何的规定和处理,这需要负责实现TCP的编程人员实现,两个基础的选择是丢失失序报文段或者缓存失序报文段等待缺失的字节填补间隔。实践中一般采取后者。
3.5.3往返时间的估计与超时
1.往返时间(RTT)的估计
报文段的样本RTT(SampleRTT)是从某个报文段被发出到该报文段的确认被收到之间的时间。大多数TCP的实现时只会在某个时间段进行一次SampleRTT的测量,而不是为每个发送的报文段测量一个SampleRTT。所以只为一个已经发送但是为被确认收到的报文段估计SampleRTT,从而产生一个接近RTT的新的SampleRTT值。而且TCP不会为已经被重传的报文段估计SampleRTT,只为只传输一次的报文段估计SampleRTT。
随着路由器拥塞和端系统负载的变化,SampleRTT的大小会产生波动,正因如此任何的SampleRTT都是非典型的,为了估计一个典型的RTT,就需要对SampleRTT取平均的方法,TCP一旦获得了一个新的SampleRTT就会采取一个公式(见原书p155)更新SampleRTT的均值(EstimatedRTT)
除了估算RTT,测量RTT也有一定价值,RTT偏差(DevRTT)用于估算SampleRTT一般会偏离EstimatedRTT的程度,如果SampleRTT的波动小DevRTT就会很小,反之就会很大。
2.设置和管理重传超时间隔
假设给出了EstimatedRTT和DevRTT的值,那么TCP超时间隔就应该大于等于EstimatedRTT而且不应该超过太多。一般设置为EstimatedRTT加上一些余量,余量的大小根据DevRTT来设置,DevRTT越大余量就越大,一般采取DevRTT大小的四倍。
3.5.4可靠数据传输
前面说过,网络层的服务(IP服务)是不可靠的,IP不保证数据报的交付,不保证数据报的按序交付,也不保证数据报中数据的完整性。TCP在IP服务的基础上创建了一种可靠的数据传输服务。TCP的可靠数据传输服务确保一个进程从其接收缓存中读出的数据流是无损坏、无间隙、非冗余和按序的数据流;即该字节流与连接的另一方端系统发送出的字节流是完全相同的。
我们之前讲的可靠数据传输的过程很大的依赖了定时器,但是这需要很大的开销,RFC推荐的定时器管理过程的使用单一的重传定时器,即使有多个已发送但是未被确认的报文段。这里描述的TCP协议也采用了这种单一的定时器的方法。
TCP的一个简化的描述是:发送方只使用超时来恢复报文段的缺失。
在该描述中除了使用超时机制之外,还使用了冗余确认技术。

上图是一个简化的TCP发送方的描述,在发送方有3个与发送和重传相关的事件,
第一个是从上层接收数据,将数据封装在报文段里面,将报文段交付给IP。每个报文段中都有一个序号,正如前所述,这个序号就是第一个数据字节的字节流编号。定时器是与最早的未被确认的数据报关联的,如果定时器还没与其他的报文段运行,则当报文段交付给IP时定时器就会运行。
第二个是超时,TCP通过重传超时的报文段来响应超时事件,然后重启定时器。
第三个主要事件是接收到接收方的确认字段(ACK,其实是包含了ACK的报文段)。这是TCP会将ACK的值y与Sendbase比较,TCP的Sendbase变量是最早未被确认的字节的序号(这也就是前面所说的累积确认)所以y确认了y之前的字节都已经收到,如果y>Sendbase,那么ACK就确认一个或多个未确认的报文段,发送方会更新Sendbase,同时如果还有数据报未确认,还会刷新计时器。
TCP的一些特殊情况:
如果发送方发送的数据报被接收,但接收方的反馈ACK丢失,发送方会在超时后重传,接收方接收到重传的数据报时会将已经接收到的重复信息丢弃,并再次返回相同的ACK。
如果发送方发送了两个报文段到接收方,接收方正确接收了,但是由于ACK反馈时时延过高导致超时,在超时后发送方重传数据报,但是重传后接收到了接收方第一次反馈的ACK,如果第二个报文的ACK在第二个报文的重传之前到达了发送方,那么第二个报文的重传将不会发生。
与第二种情况一样,不同的是如果第一次反馈的两个ACK第一个ACK丢失,但是第二个ACK被接收到,第一个报文段就也会被确认收到。


超时间隔加倍
TCP在每次重传之后都会将超时时间设置为原来的两倍,间隔会在每次重传后指数级增长,但在接收上层数据或收到ACK时就会使用最近的由EstimatedRTT和DevRTT计算来的值。
快速重传
超时触发重传的问题就是超时周期过长,这会延长端到端的时延。冗余ACK就是再次确认某个报文段的ACK,而发送方之前已经收到了这个ACK。如下图,这说明有数据报丢失。因为TCP不使用否定确认,所以只能对接收到的最后一个字节进行重复确认产生一个冗余的ACK。因为发送方经常发送大量的报文段,如果报文段丢失,可能产生多个冗余的ACK,如果发送方收到相同数据报的3个冗余ACK就暗示之后的报文段已经丢失,这时就需要进行快速重传,即在定时器触发前进行重传。

回退N步还是选择重传
TCP是GNB协议还是SR协议?TCP是累积确认式的,所以它只需维持已发送过的未确认的字节序,这点比较类似GNB协议但又有所不同,比如缓存机制等。RFC 2018对TCP提出了一个修改意见,选择确认,类似SR的选择重传机制,允许TCP进行选择重传。因此TCP其实是一个GNB和SR协议的混合体。
3.5.5流量控制
TCP提供了流量控制的服务以消除发送方使接收方接收缓存溢出导致数据丢失的可能。因此这是一个速度匹配服务,即发送方发送速率和接收方读取速率匹配。TCP的发送方也可能会因为IP网络的拥塞被遏制,这种服务叫做拥塞控制。
TCP让发送方维护了一个接收窗口的变量来提供流量控制服务。它告诉发送方接收方还有多少缓存空间。因为TCP是全双工的,连接的两端都有接收窗口。
3.5.6TCP连接管理
首先我们来看TCP连接是如何建立的,首先假设客户端进程想与服务器端进程建立连接,首先客户端进程会通知客户TCP,告知连接请求。客户中的TCP会通过以下过程建立连接:
首先客户端TCP会向服务器端TCP发送一个特殊的TCP报文段,该报文段不包含应用层数据,但是在报文段首部的一个标志位SYN被置为1.这个报文段也被称为SYN报文段。客户端还会随机选择一个初始序号(cient_isn)将其置于TCP SYN报文段的序号字段中。该报文段会被封装进一个IP数据报中并发送给服务器。
然后,当报文段到达服务器端主机,服务器会提取出TCP SYN报文段,为TCP连接分配缓存和变量,并向客户端TCP发送允许连接的报文段,这个报文段中也不包含应用层数据。报文段首部的SYN被置为1,首部的确认号字段被置为client_isn+1。最后服务器端选择自己的初始序号(server_isn)将其放在TCP报文段的首部的序号字段中。这个报文段表面服务器端收到SYN报文段并同意连接请求,这个报文段也叫SYNACK报文段。
第三步,客户端收到SYNACK报文段之后,客户端也要为TCP分配缓存和变量。客户主机向服务器发送另一个报文段,这个报文段对服务器允许连接的报文段进行确认,因为此时连接已经建立了,所以SYN位为0.第三次握手允许负载客户到服务器端的数据(如http请求报文)

完成这三步之后连接就正式建立了,以后的报文段中,SYN位都被置为0。这个过程也被叫做三次握手。

TCP的连接断开也是通过握手来实现的,也叫四次握手,客户端进程打算关闭连接时,客户端TCP就会发送一个特殊的TCP报文段,其中的首部FIN标志位置为1,服务器接收到之后就会确认收到该报文段,并反馈一个ACK报文段,然后服务器再发送一个FIN报文段,客户端返回一个ACK就表示连接成功断开了,此时两台主机上为连接预留的资源也会被释放。
3.6拥塞控制原理
3.6.1拥塞原因和代价
情况1:两个发送方和一个无限大缓存的服务器
首先我们假设主机A和B都有一条连接,且都连接共享的源和目的地之间的单挑路由如下

假设主机A以λin的速率向连接中发送数据,每个数据单元只向套接字发送一次。下面的运输层协议也是简单协议,没有差错恢复流量控制等机制。忽略添加运输层和底层首部段产生的开销。主机B也以同样的方式和速率运行。这两台主机的分组通过一个路由器在共享容量为R的链路上传输,路由器带有缓存,可以在分组到达速率超过输出链路容量时将分组存储进“入分组”,我们假设路由器有无限大的缓存空间。
下图是这两台主机的性能

左边是没连接的吞吐量(接收方每秒接收到的字节数)与该连接的发送速率的函数关系,当发送速率在0-R/2之间时,接收方吞吐量和发送方发送速率相同,大于R/2时,吞吐量就无法超过R/2
虽然理论上来说发送速率为R/2时吞吐量最大,但是如右图,当发送速率接近R/2时,平均时延会越来越大,当超过R/2时路由器中的排队分组就会无限的增长,时延也会变成无穷大。因此吞吐量达到R/2(两台主机的总吞吐量是等于共享链路带宽R的)只是一个理论的理想值,但从时延角度来看却不是一个理想的状态。
情况2:两个发送方和一个有限缓存的路由器
和情况1类似,不过我们假定路由器缓存是有限的,当缓存区满时,新到达的分组就会被丢弃,其次我们假定连接是可靠的,如果分组被丢弃,就需要被重传。我们将应用层发送到套接字的数据的速率(应用层交付给TCP连接的数据)记作λin,运输层交付到网络的速率(连接发送给路由器的速率)记作λ’in(供给载荷)

此时性能强烈依赖于重传的方式,理想状态下,如果主机能通过某种方式获知到路由器缓冲区是否空闲,只在空闲时发送数据,此时λin=λ’in,就不产生数据的丢失,吞吐量也等于λin。
但是实际上发送方往往只会在丢包时进行重传,这时当供给载荷等于R/2时数据被交付给接收方的应用程序的速率假设为R/3,此时在发送的0.5R数据中只有0.333R是初始数据,剩下的是重传数据。这就是网络拥塞的一部分代价,发送方需要通过重传补偿因为溢出丢失的分组。
而且,发送方可能还会把未丢失但是已超时的分组进行重传,这时初始分组如果被推迟在缓冲中,初始分组和重传分组可能都会到达接收方但接收方只需一份副本即可了,重传分组就会被丢弃,这是重传的初始分组就是在做无用功,而路由器也本可以利用这份资源发送另一个有用的分组,这也是拥塞的代价,发送方在大时延下所进行的不必要重传会引起路由器利用其链路带宽转发不必要的分组副本。如下所示:

情况3:4个发送方和具有有限缓冲的多台多跳路由器
假设有四台主机,每台主机通过交叠的两跳路径传输,如图:

假设每个主机有相同的λin,所有链路带宽为R
我们考虑A到C的连接,该连接通过了R1和R2,A-C和B-D连接共享路由器R2,如果λin较小,路由器的溢出会很少见,吞吐量会大致接近供给载荷,此时λin增大会引发λout(吞吐量)增大
如果λin极大,不管其有多大到达路由器R2的流量都不能超过R,如果供给载荷对于所有连接都是极大的值,那么在R2上A-C和B-D连接要竞争路由器的缓冲,如果B-D的供给载荷极大,可能B-D的流量到达速率就会远超A-C,如果供给载荷无穷大,缓冲会被B-D流量占满,A-C的流量是无法进入缓冲的,此时A-C的吞吐量就会极小甚至为0.

而且,随着供给载荷的增加,当有一个分组在第二跳路由器被丢弃时,第一跳路由器转发这个分组的工作就是无效的。这也是网络拥塞的代价,当一个分组沿着一条路径被丢弃时,每个上游路由器转发该分组到丢弃该分组使用的传输容量就被浪费了。
3.6.2拥塞控制方法
端到端拥塞控制:网络层没有为运输层的拥塞控制提供显式支持。即使存在拥塞,端系统也需要对网络行为进行观察判断是否发生拥塞。比如前面提到的TCP报文段的丢失(3次冗余ACK),这时TCP就会减小窗口长度。一些最新建议还使用增加的往返时延来作为判断指标。
网络辅助的拥塞控制:路由器向发送方提供关于网络中的拥塞情况的显式反馈。比如在ATM可用比特率拥塞控制机制中,路由器显式的通知发送方它能在输出链路上支持的最大的主机发送速率。对于网络辅助的拥塞控制,一般采取两种反馈方式,第一种是直接反馈信息,通常采用发生阻塞分组的方式。另一个更通用的是,路由器标记或更新发送方流向接收方的分组中的某些字段来指示产生了拥塞,不过后一种方法起效至少需要一个往返时间。

3.7TCP拥塞控制
3.7.1经典的TCP拥塞控制
TCP所采用的方法是让每个接收方根据所感知到的网络拥塞程度来限制其发送流量的速率。
首先我们需要知道TCP发送方是如何限制发送流量的,TCP的连接的两端都是由变量,接收缓冲和发送缓冲组成的。拥塞控制机制主要是跟踪一个额外的变量,即拥塞窗口(cwnd)它对发送方发送流量的速率做了限制,在发送方,未被确认的数据量不能超过min{cwnd,rwnd(接收窗口)}。
上面的约束限制了未被确认的数据量,从而限制了发送方的发送速率。通过调整cwnd的大小,可以调y整发送速率。
TCP发送方的丢包被定义为是要么出现超时,要么收到接收方的三个冗余ACK。当出现过度拥塞时,路由器上的缓存会溢出,就会出现丢包,因此会触发发送方的丢包事件,使发送方得知发生了拥塞。
如果没有发生拥塞,接收方会以分组的正确到达反馈的ACK作为一切正常的指示,并增加窗口的长度,如果ACK的反馈速率较慢,窗口增长速率也会较慢。因为TCP使用确认来增加它的拥塞窗口长度,被说成是自计时的。
调节cwnd值可以控制发送速率,TCP发送方还需要确定应当以怎样的速率发送。TCP主要以以下原则来实现对速率的调节的一些问题:
一个丢失的分组表示拥塞,当发送拥塞,就需要适当降低发送速率。
如果一个分组正确到达,就表示一切正常,此时可以增加发送速率。
带宽探测:为探测拥塞发生时的速率,TCP会先逐步增加发送速率,当发生拥塞,就降低速率,然后再缓慢增加,直到探测到接近最佳的发送速率。
TCP拥塞控制的算法主要包括慢启动,拥塞避免和快速恢复,前两者是强制的部分,快速恢复则是推荐部分。
1.慢启动
TCP连接开始时,cwnd往往是一个MSS的较小值,当报文段被确认收到后,拥塞窗口大小就会增加一个MSS。假设cwnd为1个MSS,发送方发送一个报文段并收到确认,窗口变为2个MSS,发送方就发送两个报文段,收到确认后cwnd变为4个MSS。因此可以实现在慢启动阶段cwnd的指数级快速增长。但是在初始阶段cwnd的值是很小的,此时的数据传输速率极小。当发生一次丢包,慢启动就会停止,cwnd回到1个MSS,此时慢启动会设置一个拥塞阈值(ssthresh)设置为发生拥塞时的cwnd大小的一半(cwnd/2)当下一次慢启动超过这个阈值时就不会采取指数级的增长,而是更加谨慎的增长,慢启动也会结束。还有一种结束慢启动的方法是如果接收到3个冗余ACK,TCP会启动快速重传并进入快速恢复状态

2.拥塞避免
一旦进入拥塞状态,cwnd的值就大概是上次的一半,此时cwnd不能再采取指数级的增长(这很有可能让cwnd快速接近设置超过拥塞阈值)一种通用的方法是当收到一个ACK,发送方cwnd就增加一个MSS/cwnd,这样当整个窗口的所以分组都被收到确认后,cwnd只会增加一个MSS。
但是应该何时结束线性增长呢(每个RTT增加一个MSS)?当发生超时触发发送方的丢包事件时,TCP的应对策略就是结束慢启动并进入拥塞避免。但是如果是冗余ACK触发丢包,TCP的反应没有那么剧烈,首先将cwnd减半并设置为ssthresh,接下来进入快速恢复状态
3.快速恢复
首先当接收到冗余ACK时,每收到一个ACK,cwnd都会增加一个MSS,直到第三个相同的冗余ACK触发了丢包事件,TCP在降低cwnd后就会进入拥塞避免的状态
快速恢复是一个TCP推荐的非必要构件,一种早期的TCP版本TCP Tahoe是不包含快速恢复的,TCP的较新版本TCP Reno则包含快速恢复。
如下图

在前八个传输回合,两给版本的TCP采用相同的行为模式,从第9个回合开始接收到了3个冗余的ACK,此时,Tahoe的cwnd变为1个MSS并重新开始慢启动,而Reno采用快速恢复,cwnd被设置为9个MSS然后线性增长。
4.TCP拥塞控制:回顾
回顾TCP的拥塞控制的主要流程,忽略最初的慢启动阶段,如果发生了拥塞,而且是由冗余ACK引发的而不是超时指示,此时在每个RTT内cwnd线性增加1MSS,出现冗余ACK时,cwnd减半。由此可以看出TCP的拥塞控制的策略主要是加性增,乘性减(AIMD)的拥塞控制方式。它引发了下图的一个锯齿状的行为。

5.TCP CUBIC
TCP Reno对于拥塞情况的探测往往是当发生丢包时发送速率就下降为原来的一半,但如果拥塞的情况基本不变,这种方法可能就过于谨慎了,更好的方法是迅速接近每丢包的速率,然后在整个速率附近再进行小心的探测,这就是TCP CUBIC
TCP CUBIC首先令Wmax为最后检测到丢包时的cwnd大小,K为假设无丢包时当TCP CUBIC的窗口长度再次达到Wmax时的未来时间点,几个可调整的CUBIC参数决定K的值,也就是决定拥塞窗口长度如何快速达到Wmax。
CUBIC以当前时间t和K之间的距离的立方为函数增加拥塞窗口,也就是说t离K越远,cwnd增长越快,当cwnd达到Wmax时再进行谨慎的探测拥塞。
当t大于K时,立方规则意味着cwnd的增长很小,但随着t超过K,如果t离K很远,cwnd就会极快的增加。
下图就展示了Reno和CUBIC的性能

可以看到当发生丢包后CUBIC可以迅速的接近Wmax并开始更谨慎的探测,而且在拥塞结束后也可以更快的增长发送速率。
6.对TCP吞吐量的宏观描述
在给出TCP的锯齿状行为后,我们仍然需要对TCP的平均的吞吐量进行分析(忽略短暂的慢启动阶段)。假设在一个特定的往返时间内,TCP的发送速率是cwnd和当前RTT的一个函数。当cwnd为w字节,往返时间是RTT秒时,发送速率约为w/RTT。又因为TCP速率的线性增长,我们取平均值(具体可以参考上图,图3-54中的TCP发送速率)可以得到TCP的平均吞吐量约为0.75W/RTT
3.7.2网络辅助明确拥塞通告和基于时延的拥塞控制
TCP拥塞控制协议还有一些变种,我们主要考察明确拥塞通告和基于时延的拥塞控制
1.明确拥塞通告(ECN)
MCN是一种网络辅助的拥塞控制形式,在因特网中执行如下图。它涉及了TCP和IP。在网络层有两个bit被用于ECN,这两个bit位于IP数据报的服务类型字段中。路由器通过这种比特指示的方式告诉主机自己正在经历拥塞。RFC中没有规定路由器拥塞时刻的定义,这个配置是由路由器厂商决定的。根据ECN的拥塞指示,发送方可以对网络拥塞做出更好的反应。

如上图,当接收方接收到了ECN拥塞指示,就会将其中的ECE(明确拥塞通告回显)比特置位,告知路由器已经收到了拥塞通告,接下来发送方会通过减半拥塞窗口来对拥塞做出响应,并在下一个报文段首部对CWR(拥塞窗口减半)比特置位。
除了TCP以外的其他的运输层协议也可以利用网络层发生ECN信号,数据报拥塞控制协议(DCCP)提供了一种低开销的类UDP不可靠服务就利用的ECN。DCTCP(数据中心TCP)和DCQCN(数据中心拥塞通告)就利用了ECN(专门为数据中心设计)一些流行的路由器也对ECN做了部署。
2.基于时延的拥塞控制
ECN可以通过IP协议直接传输拥塞通告的形式来避免费时费力的分组重传和丢包,从而使发送方更快的做出响应,第二种方法就是基于时延的方法,目的是在拥塞之前主动检测时延
在TCP Vegas中,发送方会对所有应答分组测量源到目的的RTT,RTTmin是所有测量得到的最小值,这些值出现在没有经历拥塞且分组经历最小排队时延值时。TCP的未拥塞吞吐量就会是cwnd/RTTmin。TCP Vegas设计的初衷是,如果测量到发送方的时延接近这个值,那么TCP的传输速率可以增加,因为测量该路径未发生拥塞。这个协议的想法是保持TCP的管道充满但不溢出。
BBR拥塞控制协议是在Vegas的基础上设计的,综合了允许其与TCP非BBR发送方公平竞争的机制,现在一些网络已经开始使用。
3.7.3公平性
所谓TCP的公平性,就是在多个连接共用一条链路时,TCP需要尽可能的使得这几个连接平均的占用带宽,而不会出现某个进程占用大部分或极少部分带宽的情况(不考虑UDP流量)。

如果两台主机共用一条链路,共享链路带宽为R,假设两条链接拥有相同的RTT和MSS,不考虑其他流量并忽略慢启动,假设TCP按照AIMD(CA模式,加性增,乘性减)运行。如果TCP连接是公平的那么两条连接的吞吐量的斜率都应该是1,且最理想的情况下,两条链路的吞吐量之和刚好等于R,如下图。

上图是一个对于TCP连接公平性的一个直观描述,假设最开始两条链路的吞吐量位于A点,此时由于没有发生丢包,会沿着斜率为1的直线向右上方移动,随后在B点发生丢包,两个连接的发送速率同时减半,到达C点...循环往复,经过验证这条速率线是收敛与平等带宽共享的直线的,由此我们说TCP是公平的。
不过现实中TCP的公平性很难保证,我们假设的具有相同RTT,不考虑UDP流量等条件是难以满足的,已经表明,多条链路共享带宽时,RTT更小的连接能获取更多带宽。
1.公平性和UDP
TCP会在拥塞时控制发送速率,所以很多实时通信之类的软件会选择使用不限制流量的UDP。UDP是没有内置的拥塞控制机制的,当在UDP上运行时,可能会发生丢包。从TCP角度来看,UDP是不公平的,而且UDP可能会压制TCP流量(极端情况下可能会挤掉TCP连接)对UDP流量的控制也是一个主流的研究方向。
2.公平性和并行TCP连接
串行的TCP显然是公平的,但并行的TCP的公平性仍然无法保证,如果一个进程同时开启大量的并行TCP连接传输数据,所占用的其实是多个TCP连接应得的资源和带宽。
3.8运输层功能的演化
除了前面讲到的经典的TCP和UDP版本,还有许多最新研究和部署的TCP和UDP版本。
QUIC
如果某个应用不完全适合UDP也不适合TCP,那么就可以采用QUIC,这是应该需要在应用层自己定义的一个协议,也叫快速UDP互联网连接协议。
QUIC的主要特征包括:
面向连接和安全:QUIC和TCP类似,是面向连接的协议,因此要求双端之间通过一次握手建立QUIC连接,所有的分组都是加密的,QUIC会进行建立连接到握手和用于鉴别和加密的握手,会比传统TCP建立安全连接(在TCP基础上建立TLS连接,安全的TCP连接)更快
数据流:QUIC支持不同应用的数据流在同一个连接上复用,而且也可以迅速添加新数据流。流是连接两端可靠、按序的双向数据交付的抽象。
可靠的、TCP友好的拥塞控制数传输:QUIC提供可靠的类似TCP应对策略的数据传输服务
QUIC的拥塞控制是基于TCP NewReno的,仅有少量区别,可自行阅读。







