计算机网络——运输层③
TCP连接管理
TCP使用报文段首部中特殊的比特来进行连接管理。
建立连接
建立连接时,客户主机和服务器主机使用三个特殊的报文段,称为三次握手。
- 客户主机选择一个初始序列号,记为
client_isn
,然后向服务器主机发送一个SYN
比特置为一、无应用层数据的特殊报文段,称为SYN报文段(来自SYNchronize
,意为“同步”)。 - 服务端主机收到SYN报文段后,选择一个初始序列号,记为
server_isn
,然后向客户主机发送一个SYN
和ACK
比特置为一、无应用层数据、ACK序号为client_isn+1
的特殊报文段,称为SYNACK报文段。同时服务端还要为连接分配资源(初始化各种变量并分配缓存等)。 - 客户主机收到SYNACK报文段后,为也为连接分配资源,然后发送一个
SYN
比特置零,ACK序号为server_isn+1
的可以含有载荷的报文段,此时连接正式建立。之后两端的所有报文中,SYN
比特都置零。
服务器一收到SYN报文段就会为连接分配资源,这一特性使得TCP服务端易受SYN洪泛(SYN flood)的攻击。
为了缓解这种攻击,服务端通常将分配资源这一步推迟至收到客户的ACK之后进行。
回忆一下,为了避免序号与之前的连接的序号混淆,这个server_isn
是随机选择的。
如果不分配资源,服务器就无法记住server_isn
,这会导致其难以识别第三步的ACK。
因此,服务器不选择随机生成server_isn
,而是利用哈希函数,从客户的IP地址和端口等固定的信息以及一个秘密数(即“盐”)产生一个server_isn
,这样就可以在不分配资源的前提下验证客户端的ACK报文段了。
用这种方式生成的初始序列号称为cookie。
如果服务器并不打算进行这一次连接(比如在某端口上并未开启TCP套接字),那么它会返回一个RST
比特置一的报文段。
相对地,由于UDP并不维护连接状态,如果其不打算接受报文段,那么会发送一个特殊的ICMP数据报(网络层而非连接层)。
关闭连接
相对于建立连接,关闭连接相当简单。
我们假设仍然是客户主机希望关闭连接,实际上两端中任意一端皆可。
- 客户首先发送一个特殊的报文段,其
FIN
比特置一; - 服务器接收到这个报文段,并返回一个
ACK
报文段确认,然后立刻发送一个FIN
比特置一的报文段; - 客户接受到这个报文段,然后返回一个
ACK
报文段确认,等待一段时间(通常为两倍最大报文段存活时间,2MSL,原则上120秒,实际上通常只有30-60秒),然后关闭连接并释放资源。 - 服务器收到
ACK
报文段,关闭连接并释放资源。
根据RFC 793,TCP的状态转移图如下所示:
+---------+ ---------\ active OPEN
| CLOSED | \ -----------
+---------+<---------\ \ create TCB
| ^ \ \ snd SYN
passive OPEN | | CLOSE \ \
------------ | | ---------- \ \
create TCB | | delete TCB \ \
V | \ \
+---------+ CLOSE | \
| LISTEN | ---------- | |
+---------+ delete TCB | |
rcv SYN | | SEND | |
----------- | | ------- | V
+---------+ snd SYN,ACK / \ snd SYN +---------+
| |<----------------- ------------------>| |
| SYN | rcv SYN | SYN |
| RCVD |<-----------------------------------------------| SENT |
| | snd ACK | |
| |------------------ -------------------| |
+---------+ rcv ACK of SYN \ / rcv SYN,ACK +---------+
| -------------- | | -----------
| x | | snd ACK
| V V
| CLOSE +---------+
| ------- | ESTAB |
| snd FIN +---------+
| CLOSE | | rcv FIN
V ------- | | -------
+---------+ snd FIN / \ snd ACK +---------+
| FIN |<----------------- ------------------>| CLOSE |
| WAIT-1 |------------------ | WAIT |
+---------+ rcv FIN \ +---------+
| rcv ACK of FIN ------- | CLOSE |
| -------------- snd ACK | ------- |
V x V snd FIN V
+---------+ +---------+ +---------+
|FINWAIT-2| | CLOSING | | LAST-ACK|
+---------+ +---------+ +---------+
| rcv ACK of FIN | rcv ACK of FIN |
| rcv FIN -------------- | Timeout=2MSL -------------- |
| ------- x V ------------ x V
\ snd ACK +---------+delete TCB +---------+
------------------------>|TIME WAIT|------------------>| CLOSED |
+---------+ +---------+
我们之前考虑了这个状态转移图中的最重要的部分:
- 对客户,其状态为
CLOSED -> SYNSENT -> ESTAB
; - 对服务端,其状态为
CLOSED -> LISTEN -> SYNRCVD -> ESTAB
; - 对发出结束请求一端,其状态为
ESTAB -> FINWAIT-1 -> FINWAIT-2 -> TIME WAIT -> CLOSED
; - 对收到结束请求一端,其状态为
ESTAB -> CLOSE WAIT -> LAST-ACK -> CLOSED
。
拥塞控制原理
拥塞的原因和代价
考虑只有一跳且缓存无限的网络,当发送分组的速率大于链路的速率时,分组会在队列之中无限堆积,从而导致排队时延急剧上升。
现在,假设这个网络的缓存有限,那么显然有分组会丢失,因此网络的效率很大程度上取决于重传的方式。 我们首先使用一种不可能的模型来进行研究,即发送者仅当路由器的缓存有空闲时才发送分组,以此确保不会发生分组丢失。 此时运输层向网络发送数据的速率被限制在其最大速率。 接下来我们假设发送者仅当确定分组丢失时才进行重传,那么拥塞会导致发送端不得不重发因溢出而丢失的分组。 最后我们考虑发送方使用定时器来进行重传的模型,此时拥塞会导致时延上升,从而导致发送方重发许多分组,即使这些分组只是在队列中而并未丢失。 因此,拥塞会导致路由器利用其带宽来重传不必要的分组。
最后考虑有限缓存的多跳网络。 如果多台主机的连接共享一个路由器,那么拥塞的显然的后果就是其中一个连接的分组会抢占另一个连接的分组,从而导致另一条连接的吞吐量趋于零。 除此之外,如果连接经过多个路由器,如果靠后的路由器因阻塞而丢弃分组,那么其上游的路由器的带宽就被浪费了。
拥塞控制的方法
用于拥塞控制的方法大体上可以分为两类:
- 端到端的拥塞控制:网络层不为拥塞控制提供支持,而端系统必须通过观察而猜测是否发生拥塞并进行处理;
- 网络辅助的拥塞控制:路由器向端系统提供关于拥塞的显式反馈信息,根据提供信息的方式不同可分为两类。
- 路由器直接向发送端发送特殊的拥塞分组以提示发送端;
- 路由器修改转发的分组以向接收方提供拥塞的信息,然后接收方通知发送端。
TCP使用端到端的拥塞控制,因为IP协议并不对其提供任何支持。
TCP拥塞控制
TCP通过限制发送方的速率实现拥塞控制,为此需要解决三个问题:
- 发送端用什么方法限制发送速率;
- 发送端如何感知拥塞;
- 发送端用什么算法决定发送速率。
第一个问题相对容易解答:和流量控制相同,TCP也维护拥塞窗口(Congestion WiNDow),然后要求发送方未确认的数据量不超过两个窗口的最小值: \(\mathit{LastByteSent} - \mathit{LastByteAcked} \le \min \left( \mathit{cwnd}, \mathit{rwnd} \right)\) 为使本章中研究的问题更加简单化,我们假设接收方的缓存无限大,从而$\mathit{rwnd}$无限大,此时发送方仅收到拥塞控制的限制。
发送方使用丢包事件来感知网络拥塞。 丢包事件即发送端发生定时器超时或连续收到三个冗余ACK。 以上两个事件通常是分组因路由器缓存溢出而被丢弃的表征,而路由器缓存溢出又是网络拥塞的表征。 因此,出现丢包事件被发送方认为是网络拥塞的指示,从而减小拥塞窗口。
相对地,发送方使用接收到ACK这一事件来作为增大拥塞窗口的指示。 通过计算从发送分组到收集到ACK的时间差,TCP发送端可以估计网络的带宽和时延,从而针对性地增加拥塞窗口。
解决了前两个问题,我们现在来考虑如何决定拥塞窗口的大小,从而控制发送速率。
TCP拥塞控制算法含有三个状态:慢启动、拥塞避免和快速恢复,其中快速恢复是可选项。
TCP发送端首先位于慢启动状态,然后根据网络情况在三个状态之间转移,根据不同的状态采用不同的的方法计算cwnd
。
慢启动
首先考察慢启动(Slow-Start)。
在初始时,TCP设置cwnd = MSS
,其中MSS
就是最大报文段长度。
这就使得初始发送速率大约为MSS / RTT
,这个值通常远远小于带宽限制,因此称为慢启动。
除此之外,发送端还要维护一个慢启动阈值(Slow-Start THRESHold),用来决定何时退出慢启动状态,这个值通常记为ssthresh
。
在慢启动状态,发送端有几个行动:
- 每当收到一个ACK,发送端就令
cwnd = cwnd + MSS
,然后如果新的cwnd
允许,发送新的报文段; - 当拥塞窗口大于等于慢启动阈值后,进入拥塞避免状态,降低
cwnd
增长的速率; - 发生超时后,设置
ssthresh = cwnd/2
,然后重置cwnd = MSS
,并重传报文段; - 连续收到三个冗余ACK,设置
ssthresh = cwnd/2, cwnd = cwnd/2 + 3MSS
,执行快速重传然后转移到快速恢复状态。
如果不使用快速重传,那么就没有第四项行动。 第二项和第四项订单将使TCP发送端离开慢启动状态。
虽然每收到一个ACK,cwnd
只增加一个MSS
,但是增加的MSS会导致发送额外的报文段,从而每次发送会收到更多ACK,因此这个增加实际上是指数级的。
如果收到三个冗余ACK,那么重新设置的拥塞窗口中的3MSS
对应三个ACK报文段。
拥塞避免
我们注意到在慢启动时,发生超时后ssthresh
为cwnd
的一半。
实际上研究完所有状态后,任何状态的超时都会使用类似的行动。
因此,当拥塞窗口大于等于阈值时,拥塞窗口已经超过上次拥塞的值的一半,如果继续翻倍,那么很快就会发生拥塞。
所以,当拥塞窗口大于等于阈值时,TCP发送端进入拥塞避免(Congestion Avoidance)状态,这个状态下cwnd
从指数增长变成线性增长。
在这个状态下,发送端有三个行动:
- 收到ACK,发送端继续增加拥塞窗口,
cwnd = cwnd + MSS * MSS / cwnd
; - 发生超时,令
ssthresh = cwnd/2, cwnd = MSS
,重传分组然后转移到慢启动状态; - 收到三个冗余ACK,执行快速重传,然后取
ssthresh = cwnd/2, cwnd = cwnd/2 + 3MSS
。
出于和慢启动相同的原因,尽管从公式上看每一个ACK只会导致次线性的增长,但是一次发送正好会发送cwnd / MSS
个报文段,因此也收到这么多ACK
,从而这个增长实际上是线性的。
快速恢复
接下来我们研究和快速重传对应的快速恢复(Fast Retransmit),在这个状态,其也有三个动作:
- 收到ACK,那么令
cwnd = ssthresh
,然后进入拥塞避免状态; - 继续收到冗余ACK,则令
cwnd = cwnd + MSS
,重传,然后如果新的cwnd
允许,发送新的报文段; - 发生超时,则令
ssthresh = cwnd / 2, cwnd = MSS
,然后重传报文段并进入慢启动状态。
考虑到进入快速恢复之前已经进行了快速重传,如果在快速恢复状态还收到冗余ACK,那么说明有报文段成功到达了接收方。
这个时候的冗余ACK可能是因为分组损坏、分组失序或是网络拥塞。
我们假设其总是因为前两种原因导致的,这可以使cwnd
以线性方式增长,从而起到快速恢复的作用。
虽然从公式上看,快速恢复和慢启动的增长相同,但是快速恢复时一次至多只产生一个冗余ACK,因此其增长实际上是线性而非指数的。
总述
回顾一下整个拥塞控制算法,其在增加cwnd
时总是线性的,而减少时总是乘性的,因此这个算法也叫加性增、乘性减(Additive Increase, Multiplicative Decrease, AIMD)控制模式,这个算法也叫TCP Reno算法。
使用这个算法,拥塞窗口随时间会出现锯齿状图案。
针对这个算法,我们有以下分析:
- 假定发生丢包事件时的拥塞窗口长度不变,记为$W$,那么连接的平均吞吐量可写成: \(\mathit{Throughput} = \frac{3}{4} \frac{W}{\mathrm{RTT}}\) 这可以很容易地计算出来;
- 按照相同的假定,其丢包率为: \(\mathit{Loss Rate} = \frac{1}{\frac{3W^2}{8} + \frac{3W}{4}}\) 每一次发生丢包事件后到下一层窗口到达$W$之间只会丢弃一个分组,因此用等差数列求和在取倒数即可;
- 按照相同的假定,其平均吞吐量,关于丢包率的函数为: \(\mathit{Throughput} = \frac{1.22 \times \mathit{MSS}}{\mathit{RTT} \sqrt{L}}\) 将丢包率公式略去高阶无穷并代入,然后乘以$MSS$即可。
从最后一个公式不难看出,如果要求吞吐量极高,那么丢包率必须极少。 这种观察启发研究人员为高带宽情况设计更特别的TCP。