Loading

计算机网络——传输层(下)

《计算机网络——自顶向下方法》的笔记。

这里所述的传输层(也称运输层)是TCP/IP分层架构中的传输层,处于应用层与网络层中间。

概述

本篇是传输层笔记的(下)部,因为篇幅实在太长,里面介绍的东西是在太多。本篇主要介绍:

  1. 面向连接的运输:TCP
  2. TCP序号和确认号
  3. TCP定时器的设定
  4. TCP流量控制
  5. TCP连接管理
  6. TCP拥塞控制

上篇中讲解了什么是传输层,它在分层网络中扮演了一个面向应用层提供端到端的直接连接以及在不可靠的网络层上提供各种(可靠和不可靠)服务的角色,还讲解了UDP协议以及有连接的可靠传输层协议的一些基础理论。详见上篇:计算机网络——传输层(上)

面向连接的运输:TCP

TCP是一个实际应用中的面向连接的运输层协议,它是一个点对点的协议,用于提供两个主机间进程的逻辑连接。

TCP连接

连接的发起方称为客户端,被连接的一方称为服务器。通信双方实际通信开始前需要进行三次握手来建立连接,客户端先发送一个特殊的TCP报文段,服务端也以一个特殊的TCP报文段进行响应,客户端再用第三个特殊报文段做响应,这才完成了三次握手。三次握手的前两次的报文段中不承载有效数据载荷,第三次才可以承载。注意前面说的是可以承载,而不是必须承载。

连接建立后,双方才可以发送数据。当一端的应用想要发送数据时,首先要将数据传递到套接字中,这是端系统中应用层将数据传递到传输层的门,一旦通过该门,数据就由系统中的TCP程序来控制了。这些数据将被保存到在三次握手期间建立的TCP发送缓存中,TCP会时不时的从中取出一块数据并送到网络层,TCP规范中没有对何时取出数据做要求。

不过TCP可以从缓存中取出的数据大小受限于最大报文段长度(MSS),MSS通常由主机中的最大链路层帧长度(最大传输单元MTU)来确定。MSS的设置要保证TCP/IP的首部长度加上TCP报文段的长度不超过MTU,通常该值是1500字节,而TCP首部字节通常是40字节,所以典型的MSS是1460字节。

TCP报文段结构

  1. 首部长度:TCP的首部长度是变长的,虽然大部分情况下是20字节
  2. 接收窗口:用于实现流量控制,它代表接收方愿意接受的字节数量
  3. 标志字段:ACK不说了,RST、SYN、FIN用于连接建立和拆除,CWR和ECE用于拥塞控制,PSH和URG代表数据是紧急数据

序号和确认号

在上篇笔记中,序号和确认号已经快说烂了,但这里还是提一下,序号是用于TCP保证有序传输的一个记录号,确认号是用于接收端通知发送端,具有某一个序号的报文已经被接收。

但是TCP中的序号和确认号有点不同,TCP是面向流的协议,所以它的序号代表它的数据载荷部分的首字节在流中的位置。比如发一个大文件,将这个大文件分割成1000字节的小块,每一个TCP报文中包含1000字节的用户数据载荷,那么第一个报文的序号是0,第二个报文的序号是1000。

TCP的确认号代表它期望接收发送端的下一个字节序号,假设接收端接收到了发送端的0~535字节的数据,它的下一个确认号应该是536对于丢失的分组,TCP的确认号是该流中第一个丢失的字节序号,比如接收端接收到了0~535900~1000的报文段,但是缺失了536~899,它的下一个确认号是536。由此,TCP被称为提供累计确认

对于失序报文,TCP标准并未规定该如何处理,TCP协议的实现可以缓存它或丢弃它。实践中大多都是缓存。

序号和确认号的一个案例

这里采用之前编写的一个回声服务器,回声服务器采用TCP连接,并且它会将客户端发送的字节原封不动的转发回客户端。

这里我们的客户端发送字符串Netty rocks!,这是我在学习Netty时的一个小案例。这里我们不观察三次握手,只观察数据传输阶段的序号和确认号。

首先看到了四个数据包,第一个是客户端发给服务器,第二个是服务器发送的确认,第三个是服务器发给客户端,第四个是客户端发送的确认。

首先,目前客户端和服务器的初始序号都是1,TCP规范未规定初始序号必须由某个数字开始,这个序号往往是随机的。客户端发送Seq=1,并且它期望接收服务器端的序号为1的字节,然后看看服务器端的ACK,它的Seq=1,并且Ack=13,因为客户端发送的字符串Netty rocks!占用了12个字节。然后服务器端将从客户端收到的内容转发回客户端,用的SeqAck号都没变,因为上一条确认消息并没有有效数据载荷,最后,客户端接收到服务器的消息并确认,它的Seq=13, Ack=13

往返时间估计与超时

在上一篇中,我们只说使用定时器的超时来判断丢包,可是这个超时具体是多长?我们永远不知道一个报文到达对方端并且接收到对方的确认ACK需要多久,这个值只能是一个估计值。

估计往返时间

SampleRTT是一个报文从交给网络层到接收到接收端的ACK消息之间的时间,TCP会在某一时刻测量一个已发送且尚未确认的报文段的RTT值,然后更新SampleRTT。TCP不会将重传的分组作为SampleRTT的测量依据。

任何时间网络可能会有波动,基于单次测量的SampleRTT会随之波动,所以它并不能作为定时器定时的依据,EstimatedRTT是根据每次SampleRTT的更新进行加权计算得到的一段时间内的平均RTT。

\[EstimatedRTT = (1-\alpha)\times EstimatedRTT + \alpha \times SampleRTT \]

\(\alpha\)的推荐值是0.125,根据下图,显然EstimatedRTT并不是很容易受波动影响

DevRTTSampleRTT偏离EstimatedRTT的程度:

\[DevRTT = (1-\beta)\times DevRTT + \beta \times \left| SimpleRTT - EstimatedRTT \right| \]

\(\beta\)的推荐值是0.25

超时时间应该大于EstimatedRTT,同时需要留一些余量应对网络波动,而且当网络波动大时余量应该大,网络波动小时余量应该小。

\[TimeInterval = EstimatedRTT + 4 \times DevRTT \]

初始时所有统计值都没有,我们必须给出一个初始的TimeInterval,推荐的值是1。

在超时发生后,TimeInterval加倍,而当EstimatedRTT更新,就用新值更新TimeInterval

可靠数据传输

下图是不考虑拥塞控制等细节,TCP的一个简单伪代码实现:

快速重传

当TCP的接收方接到一个报文段,它的序号大于它所期望的序号,这代表中间有可能产生了报文段丢失,它会重新发送一个具有已经收到的按序字节数据的最后一个字节的序号的ACK,也就是产生一个冗余ACK。

想象发送方发送大量报文段时,其中一个丢失那么将产生很多个冗余ACK。当发送端接到3个冗余ACK之后,TCP立即重传可能丢失的报文段,而不等到定时器结束。这种方式叫快速重传

TCP的差错控制协议是GBN和SR的结合体

流量控制

为了方便起见,这里假设接收方对于所有失序分组直接丢掉,不进行缓存

接收方具有接收缓存,发送方也有发送缓存。这里的缓存不是说接收方对于失序分组的那个缓存的概念,而是TCP规定的发送方在合适时从发送缓存中拿数据发送,接收方在合适时从缓存中取数据的那个缓存。

如果发送方发送速率太快会导致接收缓存溢出。TCP提供流量控制功能,会遏制这种情况的发生。它和术语——拥塞控制是不同的,流量控制是为了同步发送方与接收方的速率,而拥塞控制是为了在底层网络拥塞时遏制发送方的发送,虽然它们的目的都是控制发送方发送速率,但导致它们进行控制的原因不同。

接收方通过两个变量维护一个代表当前缓存中可用空间的窗口

  1. LastByteRead:从缓存中读出的最后一个字节的编号
  2. LastByteRcvd:从网络中到达并且已经放入缓存中的数据流的最后一个编号

满足以下条件,缓存不会溢出:

\[LastByteRcvd - LastByteRead \leq RcvBuffer \]

那么接收窗口rwnd就等于

\[rwnd = RcvBuffer - (LastByteRcvd - LastByteRead) \]

接收方通过将该变量写入TCP报头的接收窗口字段中,将接收端缓存中可用空间大小反映给发送端。

发送方维护两个变量:

  1. LastByteSent:发送的最后一个字节的序号
  2. LastByteAcked:被确认的最后一个字节的序号

那么已经被发送到连接中,尚未确认的数据量就是\(LastByteSent - LastByteAcked\),发送方要保证:

\[LastByteSent - LastByteAcked \leq rwnd \]

当接收方缓存满了,接收方的应用会将其清空。当发送方发现rwnd=0时,它并非不发送数据,因为rwnd字段由接收端的ACK消息带回,如果发送端不发数据,将永远不知道当前rwnd是多少,它向接收方发送一个1字节的报文段,接收方会开始清空缓存,并确认这个报文段,确认报文段的rwnd不为0。

对于UDP,发生溢出时典型的实现是直接丢弃。

TCP连接管理

三次握手

上面已经知道了TCP建立连接需要三次握手,并且TCP的初始序号可能是一个随机值,那么通信双方如何知道对方的初始序号呢?

其实这个初始序号会在三次握手期间传递:

  1. 第一次握手,客户端将发送一个无数据报文段,这个报文段的SYN字段为1,并且客户端将选择一个随机的初始序号client_isn,将其设置为这个报文段的序号字段
  2. 第二次握手是服务器主机接收到这个报文段,它通过读取该报文的序号知道了客户端的client_isn,它将为该连接设置缓存和变量并回复一个ACK报文,这个ACK报文的SYN字段也是1,确认号字段为client_isn + 1,服务器随机选择一个初始序号server_isn,将其放到ACK报文的序号字段上。意思是:“我知道你的client_isn了,我的server_isn如下”。这个ACK报文称为SYNACK
  3. 第三次握手是客户端接到这个ACK,它为该连接分配缓存和变量,并回复服务端一个确认报文,将server_isn + 1设置为确认号,SYN被置零,并且这次可以携带数据。

四次挥手

客户端想要断开连接时,向服务端发送一个特殊的报文段,其中的FIN比特被设置为1,服务器接到后发送ACK确认,随后服务器向客户端发送FIN报文段,客户端接到后发送ACK确认。

当双向都发送了FIN和确认后,连接才算关闭。

一个TCP连接的生命周期图


拥塞控制原理

拥塞原因与代价

情况1:两个发送方和一台具有无限大缓存的路由器

如图,主机A和主机B都是发送方,它们在一条容量为\(R\)的共享式链路上工作,它们分别想和主机D和C通信,这需要通过中间的那个单跳路由器。

我们假设主机A和B的发送速率(通过套接字将数据传给运输层协议)都一样,都是\(\lambda_{in}\)字节每秒,并且该传输层协议没有差错控制、流量控制、拥塞控制等机制,而且不考虑每一层在原始数据上进行封装所产生的字节,那么当\(\lambda_{in} \leq R/2\)时,接收方的吞吐量等于发送方的发送速率,即发送方的数据经过有限延时后到达接收方,而当这个速率大于\(R/2\)时,由于链路容量限制,接收方吞吐量只能达到\(R/2\),如下面左图所示。

看起来是件好事,因为从表面上看,发送方的链路利用率极高,但是别忘了,发送方不管以多大的速率发送数据,接收方都只能以\(R/2\)的速率接收数据,而剩余的数据都保存在中间路由器的缓存中。现在假设这个缓存是无限大的,那么一旦发送方发送速率大于接收方的吞吐量,那么缓存将越来越长,这些数据到达接收方的时延越来越大。如上图右图所示。

拥塞会造成网络中数据到达的延时很长。。

情况2:两个发送方和一个具有有限缓存的路由器

现在假设路由器的缓存容量有限,那么当它的缓存满时,任何新到达的数据都将被丢弃,而且假设底层的运输层协议具有重传机制。\(\lambda_{in}\)继续表示发送方将数据通过套接字传递到运输层的速率,\(\lambda '_{in}\)表示发送方将数据从运输层推入网络层的速率,这个速率中包含重传数据,\(\lambda '_{in}\)又称供给载荷

下图显示了在三种供给载荷情况下,数据被交付给接收方应用程序的速率\(\lambda_{out}\)

图a显示了当\(\lambda '_{in} = \lambda_{in}\)的情况,即发送方永远不会让中间路由器的缓存出现溢出,那么接收方速率和发送方速率成正比,注意在这种情况下发送速率不会超过\(R/2\)

图b显示了假设发送方只重传丢失分组的情况(可能定时器设的足够长,不会误认为某个未到分组已经丢失),这种情况下,\(\lambda '_{in} > \lambda_{in}\),当\(\lambda '_{in}\)等于\(R/2\)时,接收方接收速率将是\(R/3\),也就是说在发送方发送的\(0.5R\)的数据中,平均\(0.333R\)是初始数据,\(0.166R\)是重传数据。发送方必须以重传来补偿因为缓存溢出而丢弃的分组

图c显示了除了数据真的被路由器丢弃外,还有可能因为发送方定时器超时而重新发送未丢失的分组,这种情况下,当供给载荷接近\(R/2\)时,接收速率接近\(R/4\)拥塞产生的延时可能会导致发送方进行不必要的重传,进一步降低接收速率。

情况3:四个发送方和具有有限缓存的多台路由器和多条路径

第三种情况,简单点儿说,主机A连接到主机C需要通过R1和R2,而主机B到主机D也需要通过R2,如果主机A和主机B的发送速率都很大,那么在R2的缓存上,B到D的分组可能比A到C的多的多,换句话说,因为它们之间的竞争关系导致A到C的分组在网络负载极大的情况下会经常被丢弃。

拥塞可能导致上游路由器用于转发一个分组所使用的容量被白白浪费

总结上面三种拥塞情况带来的四种问题:

  1. 拥塞会造成网络中的分组到达的时延很长
  2. 拥塞会造成分组丢失并重传
  3. 拥塞带来的大延时会导致发送方重传无用分组
  4. 拥塞可能导致上游路由器用于转发一个分组所做的努力白白浪费

拥塞控制方法

  1. 端到端拥塞控制:网络层没有对拥塞情况提供任何的显示支持,所以只能由运输层通过一些可能由拥塞而产生的现象(丢包,超时,分组时延)来猜测是否产生拥塞,从而进行控制。TCP采用这种办法。
  2. 网络辅助的拥塞控制:网络层对拥塞情况提供显示支持,比如在ATM可用比特率拥塞控制中,路由器显示通知发送方能输出在链路上的最大主机发送速率。

无疑,对于网络的拥塞情况,路由器显然比运输层协议更清楚,但IP层并没有提供有关网络拥塞的反馈信息,所以默认它只能使用端到端的拥塞控制。

TCP拥塞控制

TCP采用端到端的拥塞控制方法,当TCP感知到它和目的地之间的路径上没什么拥塞就加大发送速率,当它感知到有拥塞存在就减小发送速率。

如何限制速率

还记得之前的接收方的接收窗口rwnd吗?它是用来进行流量控制的,发送方为了控制拥塞,也维护一个窗口——cwnd拥塞窗口,并且限制发送方的速率:

\[LastByteSent - LastByteAcked \leq \min(rwnd, cwnd) \]

为了方便起见,后面不考虑流量控制,也就是说假设限制发送方发送速率的只有cwnd,这样限制了发送方的发送速率为cwnd/RTTRTT是一个报文段从交给网络层到接收到它的ACK消息的时间间隔。

如何感知拥塞

拥塞的一个最显著的特点就是丢包,而发送方在出现超时事件和冗余ACK情况下会认为丢包了。

何时/如何调节cwnd

cwnd和rwnd一样,会根据网络情况动态调节,那么何时调节?该调大还是调小?

  1. 发生丢包(超时或冗余ACK),此时可能产生拥塞,减小cwnd
  2. 正常的(非冗余)ACK到达,表示一切顺利,增加cwnd
  3. 使用带宽探测机制,增加速率以探测开始出现拥塞的速率,因为网络层不提供任何拥塞信息,所以一个TCP不知道另一个TCP的参与,所以这个拥塞开始速率可能会随事件改变,所以过一会需要再次探测。

下面真正的开始介绍TCP的拥塞控制算法

TCP拥塞控制算法

TCP拥塞控制算法分三个部分:慢启动、拥塞避免和快速恢复。其中快速恢复是推荐部分,不强制TCP发送方实现。

慢启动

说了那么半天cwnd是什么,怎么用它控制速率,怎么调节它,但是它要有个初始值啊!!没有初始值何谈增大和减小??

初始情况下慢启动通常把cwnd设置为一个MSS的大小,可用带宽通常比这个初始cwnd大很多。所以每当一个正常的ACK顺利到达,cwnd就会被增加一个MSS的大小。需要注意的是,这个增长阶段很快,因为这是一种指数级增长,不是常数增长

因为cwnd第一次增加从一个MSS变成两个,发送端可以同时发送两个最大报文段的长度,这样就会得到两个ACK,MSS会变成四个...

慢启动过程的几种状态转换:

  1. 当检测到拥塞时,设置ssthresh(慢启动阈值) = cwnd / 2,重置cwnd为1MSS,重启慢启动过程
  2. 当发送速率到达ssthresh时,结束慢启动模式并转移到拥塞避免模式
  3. 当检测到3个冗余ACK,TCP执行快速重传并进入快速恢复状态

TCP拥塞控制状态转换FSM:

拥塞避免(Congestion Avoidance)

一旦从慢启动状态进入到拥塞避免状态,cwnd就是上次发生拥塞时的一半,此时再像慢启动阶段将cwnd翻倍可能过于鲁莽。拥塞避免阶段不管接到了多少个正常ACK,每个RTT都只将cwnd增加一个MSS,比如一个RTT内发送10个报文段,那么每个报文段的ACK将增加1/10MSS。

一个通用方法是到达一个确认就将cwnd增加MSS/cwnd个字节。

拥塞避免状态的几种状态转换:

  1. 出现超时时,ssthresh=cwnd/2, cwnd=1MSS
  2. 出现3个冗余ACK,ssthresh=cwnd/2, cwnd=cwnd/2,进入快速恢复状态

快速恢复

总结

除了慢启动阶段,TCP的拥塞控制基本都是正常状态下cwnd线性增加1MSS,检测到拥塞后cwnd减半,TCP拥塞控制常被称为加性增,乘性减(AIMD),AIMD会导致下图中的锯齿状行为。

习题

  1. UDP通信双方不用缓冲数据
  2. UDP分组首部开销小
  3. UDP不受拥塞控制和流量控制的影响,使发送方可以更加精细的控制何时发送数据,而TCP协议就像,应用层一旦把数据推给TCP了,那么何时发送你就没法控制了
  4. UDP不用建立连接

所以追求实时性且偶尔丢失几个包也无伤大雅的服务比较喜欢使用UDP。

作者:Since most firewalls are configured to block UDP traffic, using TCP for video and voice traffic lets the traffic though the firewalls.

因为大多数防火墙被设置成阻止UDP...

能,TCP都能在不可靠的IP层上提供可靠服务,UDP只是对IP层的简单包装,你当然可以选择在应用层上提供类似TCP的各种机制了,只不过这样显得有点彪。。。


是的,但主机A和主机B的IP地址不同,所以可以区分


不是,A和B使用不同的套接字进行响应。这两个套接字都具有端口80。

rdt协议是一个停等协议,它里面的序号只是为了确定新来的报文是重传还是新数据。

接收端返回的用来告诉发送端分组丢失的NAK或冗余ACK消息也可能丢失,所以为了确保它们被正确的重传,需要引入定时器

需要啊,那不也得定一个固定时间,当这个时间内没有返回ACK就认为是丢包吗。但这样的好处就是发送方完全可以确认这个包(源包或者确认消息)就是丢了,而不会重传一个发送方已经确认但确认消息还没到达的包。

错,错,对,错,对,错,错

f:如果换成那个EstimatedRTT就是对了
g:万一丢失了这个报文段呢,此时确认号应该是38

a. 20字节
b. 90

R/2

否,其ssthresh被设置成发送方拥塞控制窗口cnwd的一半

posted @ 2022-04-12 10:11  yudoge  阅读(192)  评论(0编辑  收藏  举报