《计算机网络:自顶向下方法》:TCP

1. 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 将这些数据引导到该连接的发送缓存(send buffer)里,发送缓存是在三次握手初期设置的缓存之一。接下来 TCP 就会不时从发送缓存里取出一块数据。TCP 可以从缓存中取出并放入报文段中的数据受限于 “最大报文段长度”(Maximum Segment Size,MSS)。MSS 通常根据最初确定的由本地发送主机发送的最大链路层帧长度(即所谓的最大传输单元(Maximum Transmission Unit,MTU))来设置。设置该 MSS 要保证一个 TCP 报文段加上 TCP/IP 首部长度(通常 40 个字节)将适合单个链路层帧。以太网和 PPP 链路层协议都具有 1500 字节的 MTU,因此 MSS 的典型值为 1460 字节。注意到 MSS 是指在报文段里应用层数据的最大长度,而不是指包括 TCP 首部的 TCP 报文段的最大长度。

TCP 为每块客户数据配上一个 TCP 首部,从而形成多个 TCP 报文段(TCP segment)。这些报文段被下传给网络层,网络层将其分别封装在网络层 IP 数据报中。然后这些 IP 数据报被发送到网络中。当 TCP 在另一端接收到一个报文段后,该报文段的数据就被放入该 TCP 连接的接收缓存中。应用程序从此缓存中读取数据流。TCP 连接的每一端都有各自的发送缓存和接收缓存。

可以看出,TCP 连接的组成包括:一台主机上的缓存、变量和与进程连接的套接字,以及另一台主机上的另一组缓存、变量和与进程连接的套接字。在这两台主机之间的网络元素(如路由器、交换机和中继器)中,没有为该连接分配任何缓存和变量。

2. TCP 报文段结构

image

TCP 把数据看成一个无结构的、有序的字节流。一个报文段的序号(sequence number for a segment)是该报文段首字节的字节流编号。确认号是希望从目标主机收到的下一字节的序号。

假定主机 A 已收到一个来自主机 B 的包含字节0-535的报文段,以及另一个包含字节900-1000的报文段。由于某种原因,A 还没有收到字节536-899的报文段。在这个例子中,主机 A 为了重新构建主机 B 的数据流,仍在等待字节536(和其后的字节)。因此,A 到 B 的下一个报文段将在确认号字段中包含536。因为 TCP 只确认该流中至第一个丢失字节为止的字节,所以TCP被称为提供累积确认(cumulative acknowledgment)。实践中,主机 A 将保留失序的字节900~1000,并等待缺少的字节以填补该间隔。

3. 可靠数据传输

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

TCP 发送方有三个与发送和重传有关的事件:

  • 从上层应用程序接收数据。TCP 从上层应用程序接受数据,将数据封装在一个报文段中,并把该报文段交给 IP。注意到每一个报文段都包含一个序号,这个序号就是该报文段第一个数据字节流编号。还要注意到如果定时器还没有为某些其他报文段而运行,则当报文段被传给 IP 时,TCP 就要启动该定时器。
  • 定时器超时。TCP 通过重传引起超时的报文段(具有最小序号的还未被确认的报文段)来响应超时事件。然后 TCP 重启定时器。
  • 收到 ACK。当该事件发生时,TCP 将 ACK 的值 y 与它的变量 SendBase 进行比较。TCP 状态变量 SendBase是最早未被确认的字节的序号。(因此 SendBase - 1 是指接收方已正确接收到的数据的最后一个字节的序号。)TCP 采用累积确认,所以 y 确认了字节编号在 y 之前的所有字节都已经收到。如果 y > SendBase,则该 ACK 是在确认一个或多个先前未被确认的报文段。因此发送方更新它的 SendBase 变量;如果当前有未被确认的报文段,TCP 还要重新启动定时器。

发送方伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/* 假设发送方不受 TCP 流量和拥塞控制的限制,来自上层的数据长度小于 MSS,且数据传送只在一个方向进行 */

NextSeqNum = InitialSeqNumber
SendBase = InitiSeqNumber

loop(forever){
switch(event){

event: 接收来自应用程序产生的数据data放入到发送缓存中
if(定时器没有开启)
start_timer
将包装后的报文段交付给网络层形成IP数据报
NextSeqNum = NextSeqNum + length(data)
break;

event:如果定时器超时
发送那个没有确认的序号最小的分组
start_timer
break;

event:接收到ACK,将ACK中的确认号赋给y
if(y > SendBase){
SendBase = y;
if(目前还有尚未确认的部分){
start_timer
}
}else {
开始对y进行计数
if(发送ACK的确认号为y的个数为3个的时候){
重新发送序号为y的哪一个分组
}
}
break;
}
}

超时时间加倍

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

这种修改提供了一个形式受限的拥塞控制。定时器过期很可能是由网络拥塞引起的,即太多的分组到达源与目的地之间的路径上的一台(或多台)路由器的队列中,造成分组丢失或长时间的排队时延。在拥塞的时候,如果源持续重传分组,会使拥塞更加严重。相反,TCP 采用更文雅的方式,每个发送方的重传都是经过越来越长的时间间隔后进行的。

快速重传

超时触发重传存在的问题之一是超时周期可能相对较长。当一个报文段丢失时,这种长超时周期迫使发送方延迟重传丢失的分组,因而增加了端到端的时延。幸运的是,发送方通常可在超时事件发生之前通过注意所谓冗余 ACK 来较好地检测到丢包情况。冗余 ACK(duplicate ACK)就是再次确认某个报文段的 ACK,而发送方先前已经收到对该报文段的确认。下表总结了 TCP 接收方的 ACK 生成策略 [RFC 5681]。当 TCP 接收方收到一个具有这样序号的报文段时,即其序号大于下一个所期望的、按序的报文段,它检测到了数据流中的一个间隔,这就是说有报文段丢失。这个间隔可能是由于在网络中报文段丢失或重新排序造成的。因为 TCP 不使用否定确认,所以接收方不能向发送方发回一个显式的否定确认。相反,它只是对已经接收到的最后一个按序字节数据进行重复数据(即产生一个冗余 ACK)即可。(注意到表中允许接收方不丢弃失序报文段。)

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

因为发送方经常一个接一个地发送大量的报文段,如果一个报文段丢失,就很可能引起许多一个接一个的冗余 ACK。如果 TCP 发送方接收到对相同数据的3个冗余 ACK,它把这当作一种指示,说明跟在这个已被确认过3次的报文段之后的报文段已经丢失。一旦收到3个冗余 ACK,TCP 就执行快速重传(fast retransmit)[RFC 5681],即在该报文段的定时器过期之前重传丢失的报文段。

是回退 N 步还是选择重传

TCP是GBN协议和SR协议的混合体:

  • TCP 发送方仅需要维持已发送过但未被确认的最小序号和下一个要发送的字节的序号就可以了,这一点和 GBN 一致。
  • GBN 定时器过期之后发送方会重传未被确认的最小序号之后的数据段,这样很可能会造成重发大量分组,导致占用带宽,分组冗余。TCP 和 SR 相似,用的是选择重发,只发未被确认的最小序号的分组。

4. 流量控制

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

TCP 为它的应用程序提供了流量控制服务(flow-control service)以消除发送方使接收方缓存溢出的可能性。流量控制因此是一个速度匹配服务,即发送方的发送速率与接收方应用程序的读取速率相匹配。

TCP 通过让发送方维护一个称为接收窗口(receive window)的变量来提供流量控制。通俗地说,接收窗口用于给发送方一个指示——该接收方还有多少可用的缓存空间。因为 TCP 是全双工通信,在连接两端的发送方都各自维护一个接收窗口。现假设主机 A 通过一条 TCP 连接向主机 B 发送一个大文件。主机 B 为该连接分配了一个接收缓存,并用 RevBuffer 来表示其大小,主机 B 上的应用进程不时地从该缓存中读取数据。定义以下变量:

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

由于 TCP 不允许已分配的缓存溢出,下式必须成立

$$
LastByteRcvd - LastByteRead \leq RcvBuffer
$$

接收窗口用 rwnd 表示,根据缓存可用空间的数量来设置:

$$
rwnd = RcvBuffer - [LastByteRcvd - LastByteRead]
$$

由于该空间是随着时间变化的,所以 rwnd 是动态的。主机 B 通过把当前的 rwnd 值放入它发给主机 A 的报文段接口窗口字段中,通知主机 A 它在该连接的缓存中海有多少可用空间。开始时,主机 B 设定 rwnd = RcvBuffer。注意到为了实现这一点,主机 B 必须跟踪几个与连接有关的变量。

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

$$
LastByteSent - LastByteAcked \leq rwnd
$$

TCP 规范中要求:当主机 B 的接收窗口为 0 时,主机 A 继续发送只有一个字节数据的报文段。这些报文段将会被接收方确认。最终缓存将开始清空,并且确认报文段里将包含一个非 0 的 rwnd 值,以此通知主机 A 接收缓存有新的空间了。

UDP 并不提供流量控制。进程每次从缓存中读取一个完整的报文段。如果进程从缓存中读取报文段的速度不够快,那么缓存将会溢出,并且将丢失报文段。

image

5. TCP 连接管理

建立连接

image

第一步:客户端的 TCP 首先向服务端的 TCP 发送一个特殊的 TCP 报文段。报文段中不包含应用层数据。但是在报文段的首部中的一个标志位(即 SYN 比特)被值为 1。因此,这个特殊报文段通常被称为 SYN 报文段。另外,客户端会随机地选择一个初始序号(client_isn),并将此编号放置于该起始的 TCP SYN 报文段的序号字段中。该报文段会被封装在一个 IP 数据报中,并发送给服务器。

第二步:一旦包含 TCP SYN 报文段的 IP 数据报到达服务器主机,服务器会从该数据报中提取出 TCP SYN 报文段,为该 TCP 连接分配 TCP 缓存和变量,并向客户 TCP 发送允许连接的报文段。这个允许连接的报文段也不包含应用层数据。但是,在报文段的首部却包含 3 个重要的信息。首先,SYN 比特被置为 1。其次,该 TCP 报文段首部的确认号字段被置为 client_isn + 1。最后,服务器选择自己的初始序号(server_isn),并将其放置到 TCP 报文段首部的序号字段中。这个允许连接的报文段实际上表明了:“我收到了你发起建立连接的 SYN 分组,该分组带有初始序号 client_isn,我同意建立该连接。我自己的初始序号是server_isn。”该允许连接的报文段有时被称为 SYNACK 报文段(SYNACK segment)。

第三步:在接收到 SYNACK 报文段后,客户端也要给该连接分配缓存和变量。客户主机向服务器发送另一个报文段;这最后一个报文段对服务器的允许连接的报文段进行了确认(该客户通过将值 server_isn + 1 放置到 TCP 报文段首部的确认字段中来完成此项工作)。因为连接已经建立了,所以该 SYN 比特被置为0。该三次握手的第三个阶段可以在报文段负载中携带客户到服务器的数据。

一旦完成这个 3 个步骤,客户和服务器主机就可以相互发送包括数据的报文段了。在以后每一个报文段中,SYN 比特都将被置为 0。

关闭连接

参与一条 TCP 连接的两个进程中的任何一个都能终止该连接。当连接结束后,主机中的资源(缓存和变量)都将被释放。

image

第一步:客户应用进程发出一个关闭连接命令。这会引起客户 TCP 向服务器进程发送一个特殊的 TCP 报文段,其首部中的一个标志位即 FIN 比特被置为 1。

第二步:当服务器接收到该报文段后,就向发送方回送一个确认报文段。

第三步:服务器发送它自己的终止报文段,其 FIN 比特被置为 1。

第四步:该客户对这个服务器的终止报文段进行确认。

状态机

客户 TCP 开始时处于 CLOSED(关闭)状态。客户的应用程序发起一个新的 TCP 连接。这引起客户中的 TCP 向服务器中的 TCP 发送一个 SYN 报文段。在发送 SYN 报文段后,客户TCP进入了 SYN_SENT 状态。 当客户 TCP 处在 SYN_SENT状态时,它等待来自服务器 TCP 的对客户所发报文段进行确认且 SYN 比特被置为1的一个报文段。收到这样一个报文段之后,客户 TCP 进入 ESTABLISHED(已建立)状态。当处在 ESTABLISHED 状态时,TCP 客户就能发送和接收包含有效载荷数据的 TCP 报文段了。

假设客户应用程序决定要关闭该连接。(注意服务器也能选择关闭该连接。)这引起客户 TCP 发送一个带有 FIN 比特被置为1的 TCP 报文段,并进入 FIN_WAIT_1 状态。当处在 FIN_WAIT_1 状态时,客户TCP等待一个来自服务器的带有确认的 TCP 报文段。当它收该报文段时,客户 TCP 进入 FIN_WAIT_2 状态。当处在 FIN_WAIT_2 状态时,客户等待来自服务器的 FIN 比特被置为1的另一个报文段;当接收到该报文段后,客户 TCP 对服务器的报文段进行确认,并进入 TIME_WAIT 状态。假定 ACK 丢失,TIME_WAIT 状态使 TCP 客户重传最后的确认报文。TIME_WAIT 状态持续 2MSL(Maximum Segment Lifetime) 后,连接就正式关闭,客户端所有资源(包括端口号)将被释放。

客户 TCP 经历的典型的 TCP 状态序列
image

服务器端 TCP 经历的典型的 TCP 状态序列
image

向运行在本地8888端口的一个应用程序发送 HTTP 请求的 TCP 连接过程:
image

6. SYN 洪泛攻击

在 TCP 三次握手中,服务器为了响应一个收到的 SYN,分配并初始化连续变量和缓存。然后服务器发送一个 SYNACK 进行响应,并等待来自客户的 ACK 报文段。如果某客户不发送 ACK 来完成该三次握手的第三步,最终(通常在一分多钟之后)服务器将终止该半连接并回收资源。

这种 TCP 连接管理协议为经典的 DoS(deny of service)攻击即 SYN 洪泛攻击(SYN flood attack)提供了环境。在这种攻击中,攻击者发送大量的 TCP SYN 报文段,而不完成第三次握手的步骤。随着这种 SYN 报文段纷至踏来,服务器不断为这些半开连接分配资源(但从未使用),导致服务器的连接资源被消耗殆尽。

现在有一种有效的防御系统,称为 SYN cookie,它们被部署在大多数主流操作系统中。SYN cookie 以下列方式工作:

  1. 当服务器接收到一个 SYN 报文段时,它并不知道该报文段是来自一个合法的用户,还是一个 SYN 洪泛攻击的一部分。因此服务器不会为该报文段生成一个半开连接。相反,服务器生成一个初始 TCP 序列号,该序列号是 SYN 报文段的源和目的 IP 地址与端口号以及仅有该服务器知道的秘密数的一个复杂函数(散列函数)。这种精心制作的初始序列号被称为 “cookie”,服务器则发送具有这种特殊初始序列号的 SYNACK 分组。重要的是,服务器并不记忆该 cookie 或任何对应于 SYN 的其他状态信息。
  2. 如果客户是合法的,则它将返回一个 ACK 报文段。当服务器收到该 ACK,需要验证该 ACK 是与前面发送的某些 SYN 相对应的。服务器将使用在 SYNACK 报文段中的源和目的地 IP 地址与端口号(它们与初始的 SYN 中的相同)以及秘密数运行相同的散列函数。如果该函数的结果加 1 与在客户的 SYNACK 中的确认(cookie)值相同的话,服务器认为该 ACK 对应于较早的 SYN 报文段,因此它是合法的。服务器则生成一个具有套接字的全开的连接。
  3. 如果客户没有返回一个 ACK 报文段,则初始的 SYN 并没有对服务器产生危害,因为服务器没有为它分配任何资源。

7. TCP 拥塞控制

TCP 必须使用端到端拥塞控制而不是使用网络辅助的拥塞控制,因为 IP 层不向端系统提供显式的网络拥塞反馈。TCP 采用的方法是让每一个发送方根据所感知到的网络拥塞程度来限制其能向连接发送流量的速率。

TCP 连接的每一端都是由一个接收缓存、一个发送缓存和几个变量组成。运行在发送方的 TCP 拥塞控制机制跟踪一个额外的变量,即拥塞窗口(congestion window),拥塞窗口表示为 cwnd,它对一个 TCP 发送方能向网络中发送流量的速率进行了限制:在一个发送方中未被确认的数据量不会超过cwnd和rwnd中的最小值。特别是,在一个发送方中未被确认的数据量不会超过 cwnd 与 rwnd 中的最小值,即

$$
LastByteSent - LastByteAcked <= min{cwnd, rwnd}
$$

通过约束发送方中未被确认的数据量,间接限制了发送方的发送速率。考虑一个丢包和发送时延均可以忽略不计的连接。在每个往返时间 (RTT)的起始点,上面的限制条件允许发送方向该连接发送 cwnd 个字节的数据,在该 RTT 结束时发送方接收对数据的确认报文。因此,该发送方的发送速率大概为 cwnd/RTT 字节/秒。通过 调节 cwnd 的值,发送方因此能调整它向连接发送数据的速率。

TCP 拥塞控制算法包括 3 个主要部分:(1)慢启动(slow-start);(2)拥塞避免;(3)快速恢复。慢启动和拥塞避免是 TCP 的强制部分,两者的差异在于对收到的 ACK 做出反应时增加 cwnd 长度的方式。

7.1 慢启动

在慢启动状态,cwnd 的值以 1 个 MSS 开始并且每当传输的报文段首次被确认就增加一倍 MSS。这个过程每过一个 RTT,发送速率就翻番。因此,TCP 发送速率起始慢,但在慢启动阶段以指数增长。

指数增长的结束:首先,如果存在一个由超时指示的丢包事件(即拥塞),TCP 发送方将 cwnd 设置为 1 并重新开始慢启动过程。它还将第二个状态变量的值 ssthread(慢启动阈值)设置为 cwnd/2,即当检测到拥塞时将 ssthread 置为拥塞窗口值的一半。其次,当达到或超过 ssthread 的值时,进入拥塞避免模式。最后,如果检测到 3 个冗余 ACK,执行快速重传并进入快速恢复状态。

image

7.2 拥塞避免

一旦进入拥塞避免模式,cwnd 的值大约是上次遇到拥塞时的值的一半。此后,每个 RTT 只将 cwnd 的值增加一个 MSS。

image

7.3 快速恢复

对于引起进入快速恢复的每个冗余 ACK,cwnd 增加一个MSS。当最后一个 ACK 到达时,进入拥塞避免。如果出现超时事件,快速恢复在执行如同在慢启动和拥塞避免中相同的动作后,迁移到慢启动状态:当丢包事件发生后,cwnd 被设置为一个MSS,并且 ssthread 的值被设置为 cwnd 的一半。

image