介绍了可靠数据传输原理之后,再来介绍一下TCP (Transmission Control Protocol) 原理(基于rdt)。

1. 报文结构分析

  • 源端口号与目的端口号:用于多路分解与多路复用
  • 序号:与rdt不同,该分组序号为Data中第一个字节在字节流中的编号。可以指定任意初始值
  • 确认号:期望从对方接收到的下一个字节的序号
  • 接收窗口:用于表示接收方愿意接收的字节数量,用于流量控制
  • 首部长度:指示以32bits的字为单位的TCP首部长度。由于Options的存在,TCP是可变的。一般为20Bytes。
  • 可选与变长的选项字段(Options)。
  • 标志字段:
    • ACK:确认
    • RST,SYN,FIN:连接建立与拆除
    • PSH,URG:实践中未使用

2. 基本原理

  • 使用单一的重传定时器RFC 6298
  • 采用累积确认,(改进为选择确认RFC 2018
  • 超时时间加倍,重传之后,定时器超时时间加倍
  • 快速重传,接收到3个冗余的ACK之后,在该报文段的定时器过期之前,重传丢失的报文段。

3. 连接管理

考虑客户端与服务器通信:

3.1 建立连接

三次握手:

  1. 客户端向服务器发送 SYN报文。TCP报文首部的SYN比特被置为1,同时客户端随机选取一个初始序号(client_isn)
  2. 服务器收到SYN报文后,发送SYNACK报文。TCP报文首部的SYN比特被置为1,服务器随机选取一个初始序号(server_isn),并将确认号置为client_isn+1
  3. 客户端收到SYNACK报文后,向服务器发送一个确认报文,标志连接已经建立。SYN置为0,序号为client_isn+1,确认号为server_isn+1。此报文段可以携带要传输的数据。

3.2 撤销连接

四次挥手:

  1. 客户端发送FIN报文,将FIN比特置为1
  2. 服务器收到FIN报文后,发送ACK。之后释放与该连接相关的资源。
  3. 服务器完成资源释放后,向客户端发送FIN报文
  4. 客户端接收到FIN报文后,发送ACK。之后释放与该连接相关的资源。

4. 流量控制

由于接收端的接受缓存是有限的,接收方的上层应用程序不会以数据到达的速率读取缓存中的数据。当发送方发送速率过快时,会导致接收方缓存溢出,从而丢失数据。故要告知发送方,接收方还有多少缓存空间可以用于接收数据。流量控制使用TCP报文中Receive window进行控制。

4.1 接收方

在接收方定义以下变量:

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

使接受缓存不溢出的必要条件为:

LastByteRcvd - LastByteRead ≤ RcvBuffer

接受窗口记为rwnd

rwnd=RcvBuffer - (LastByteRcvd - LastByteRead)

接收方将rwnd值放入TCP报文首部中Receive window字段,用来告知发送端其接受窗口大小,也就是可用空间大小。

4.2 发送方

在发送方,要记录下面的变量

  • LastByteSent: 最后一个已发送字节的编号
  • LastByteAcked:最后一个已确认字节的编号

使接受端缓存不溢出的必要条件为:

LastByteSent - LastByteAcked ≤ rwnd

也就是保证,将未确认的数据量控制在接收方的rwnd内。

4.3 控制技巧

假设主机A,B二者相互通讯。由于TCP是全双工的,因此双方都可以发送数据给对方。

假设主机B缓存已满,将rwnd=0告知主机A之后,主机B并没有数据要发送给主机A(不能告诉A其新的窗口大小)。一段时间后,上层应用读取了主机B缓存中的所有数据,那么主机B又可以接收数据了。然而此时主机A并不知道B可以接受数据了,即主机A被阻塞而不能再发送数据给B了。

为了解决这个问题,TCP规范中要求,当主机B接受窗口为0时,主机A会持续向主机B发送一个只有一个字节的报文段,直到主机B的接受窗口非0

5. 拥塞控制

在实践中,丢包一般是当网络变得拥塞时由于路由器缓存溢出引起的。分组重传因此作为网络拥塞的前兆来对待,但是却无法处理导致网络拥塞的原因:因为有太多的源想以过高的速率发送数据。拥塞造成的代价有:

  • 发送方必须执行重传以补偿因为缓存溢出而丢弃的分组
  • 发送方在遇到大时延时所进行的不必要重传会引起路由器利用其链路带宽来转发不必要的分组
  • 当一个分组沿一条路径被丢弃时,每一个上游路由器用于转发该分组到丢弃该分组而使用的传输容量最终被浪费掉了

在发送方,会维护一个变量,拥塞窗口(cwnd)来进行拥塞控制。有以下不等式

LastByteSent - LastByteAcked ≤ min{ cwnd, rwnd }

cwnd的值过大,会造成拥塞,过小,又会降低传输速率。那么应当怎样调节cwnd,使之既不会导致拥塞,又能充分利用带宽呢?由于网络状态是动态变化的,那么又该怎么动态调节拥塞窗口呢?有以下几个原则:

  • 一个丢失的报文段意味着拥塞,因此当丢失报文段时应当降低发送方的速率。当定时器超时或收到3个冗余ACK(4个相同ACK)时,意味着拥塞发生,此时要调整拥塞窗口,降低发送速率。
  • 一个确认报文指示该网络正向接收方交付发送方的报文段,因此当对先前未确认报文段的确认到达时,能增加发送方的速率。也就是说,顺利交付后表示网络正常,可以增加拥塞窗口
  • 带宽检测。丢包表示拥塞,正确到达表示正常,因此可以通过调节发送速率来检测使拥塞发生的速率阈值

TCP拥塞控制算法中,有以下几个要点:

5.1 慢启动

初始时,将拥塞窗口设置为1个MSS(Maximum segment size),将阈值设为64 KB。有以下事件

  • 收到一个新的ACK。将拥塞窗口增加一个MSS,因此拥塞窗口会以指数增长。发送新的分组
  • 超时事件。说明此时拥塞窗口过大,将阈值设置为当前拥塞窗口的一半,然后将拥塞窗口重新设置为初始状态。重传丢失的分组。
  • 拥塞窗口超过阈值。进入拥塞避免状态,因为此时要是拥塞窗口再翻一倍,就跟之前引发超时的拥塞窗口长度一样了,因此要更改窗口增长策略。
  • 收到3个冗余ACK。窗口阈值改为当前窗口的一半,窗口设置为阈值+3MSS,然后快速重传丢失的分组,进入快速恢复状态。

5.2 拥塞避免

一旦进入了拥塞避免阶段,cwnd的值大约是上次遇到拥塞时的值的一半。故不能使用之前的窗口增长方法了,要适当地谨慎些:

  • 收到新的ACK。每个RTT只将cwnd的值增加一个MSS,而不是翻倍。
  • 发生超时。重传丢失的分组,重新回到慢启动状态,窗口阈值为当前窗口的一半,拥塞窗口从1个MSS开始;
  • 收到3个冗余ACK。窗口阈值改为当前窗口的一半,窗口设置为阈值+3MSS,然后快速重传丢失的分组,进入快速恢复状态。

5.3 快速恢复

由于超时比收到三个冗余ACK更严重。超时连ACK都收不到了,而冗余ACK只是丢包,故收到3个冗余ACK时会进入到此阶段,而不是重新启动慢启动。

  • 继续收到冗余ACK。这是慢启动阶段发送的分组到达接收端的表现,拥塞窗口增加1个MSS,发送新的分组
  • 收到新的ACK。表示快速重传的缺失分组已经被收到了,本阶段任务完成,进入拥塞避免状态。
  • 发生超时。表示重传的缺失分组仍然没有到达。与其它阶段相同,重传分组,重启慢启动

引入快速恢复阶段的目的是,避免一出现丢包就重启慢启动,这样会降低传输速率

5.4 总结

TCP的拥塞控制机制可以总结为,加性增,乘性减,即在拥塞避免时,线性增加拥塞窗口,一旦遇到拥塞,则将其减半。