文章目录
- 前言
- 运输层
-
- [面向连接的运输: TCP](#面向连接的运输: TCP)
-
- [TCP 连接](#TCP 连接)
- [TCP 报文段结构](#TCP 报文段结构)
-
- 序号和确认号
- [Telnet: 序号和确认号的一个学习案例](#Telnet: 序号和确认号的一个学习案例)
- 往返时间的估计与超时
- 可靠数据传输
- 流量控制
- [TCP 连接管理](#TCP 连接管理)
- 参考目录
前言
阅读本文前请注意最后编辑时间,文章内容可能与目前最新的技术发展情况相去甚远。欢迎各位评论与私信,指出错误或是进行交流等。
本文是关于《计算机网络:自顶向下方法(第七版)》的学习分享,内容书写顺序也是按照书中的顺序。本文并不会提及书中的所有内容,主要写重点的知识,以及自己感兴趣的内容。会对原文中的内容进行一定的精简,或者加上个人的理解。
运输层
面向连接的运输: TCP
既然我们已经学习了可靠数据传输的基本原理,我们就可以转而学习TCP了。 TCP是因特网运输层的面向连接的可靠的运输协议。
TCP 连接
TCP 被称为是面向连接的,这是因为在一个应用进程可以开始向另一个应用进程发送数据之前,这两个进程必须先相互握手,即它们必须相互发送某些预备报文段,以建立确保数据传输。作为TCP连接建立的一部分,连接的双方都将初始化与TCP连接相关的许多TCP状态变量。这种连接是逻辑上的连接,其共同状态仅保留在两个通信端系统的TCP程序中。由于TCP协议只在端系统中运行,而不在中间的网络元素(路由器和链路层交换机)中运行,所以中间的网络元素不会维持TCP连接状态。 中间路由器对TCP连接完全视而不见,它们看到的是数据报,而不是连接。
我们现在来看看TCP连接是怎样建立的。 假设运行在某台主机上的一个进程想与另一台主机上的一个进程建立一条连接。发起连接的这个进程被称为客户进程,而另一个进程被称为服务器进程。该客户应用进程首先要通知客户运输层, 它想与服务器上的一个进程建立一条连接。一个 Python 客户程序通过发出下面的命令来实现此目的
python
clientSocket.connect((serverName, serverPort))
其中 serverName 是服务器的名字, serverPort(端口号)标识了服务器上的进程。客户端的TCP便开始与服务器上的TCP建立一条TCP连接。 我们将在本文后面更为详细地讨论连接建立的过程。现在知道下列事实就可以了:客户首先发送一个特殊的TCP报文段,服务器用另一个特殊的TCP报文段来响应,最后,客户再用第三个特殊报文段作为响应。前两个报文段不承载 有效载荷,,也就是不包含应用层数据;而第三个报文段可以承载有效载荷。由于在这两台主机之间发送了 3 个报文段,所以这种连接建立过程常被称为三次握手。
一旦建立起一条TCP连接,两个应用进程之间就可以相互发送数据了。 我们考虑一下从客户进程向服务器进程发送数据的情况。客户进程通过套接字传递数据流。 数据通过该套接字,就由客户中运行的TCP控制了。TCP将这些数据引导到该连接的发送缓存里,接下来TCP就会不时从发送缓存里取出一块数据,并将数据传递到网络层。TCP可从缓存中取出并放入报文段中的数据数量受限于最大报文段长度(MSS)。
MSS 通常根据最初的由本地发送主机发送的最大链路层帧长度(即所谓的最大传输单元,MTU)来设置。设置该 MSS 要保证一个 TCP报文段(当封装在一个IP数据报中)加上TCP/IP首部长度(通常40 字节) 将适合单个链路层帧。以太网和PPP链路层协议都具有)500 字节的 MTU, 因此MSS 的典型值为 1460字节。注意, MSS 是指在报文段里应用层数据的最大长度,而不是指包括首部的TCP报文段的最大长度。
TCP 为每块客户数据配上一个TCP首部,从而形成TCP 报文段。这些报文段被下传给网络层, 网络层将其封装在网络层IP数据报中。然后这些IP数据报被发送到网络中。 接收端TCP收到下方网络层传输来的报文段后,该报文段的数据就被放入该TCP 连接的接收缓存中,应用程序从此缓存中读取数据流。
TCP 报文段结构
简要地了解了 TCP连接后,我们研究一下TCP报文段结构。 TCP报文段由首部字段和一个数据字段组成。 数据字段包含应用数据。如前所述, MSS 限制了报文段数据字段的最大长度。 当TCP发送一个大文件,TCP通常是将该文件划分成长度为 MSS 的若干块(最后一块除外,它通常小于MSS)。

上图显示了TCP 报文段的结构。首部包括源端口号和目的端口号,它被用于多路复用/分解来自或送到上层应用的数据。同 UDP 一样, TCP首部也包括检验和字段。
- 32 比特的序号字段和 32 比特的确认号字段。 这些字段被 TCP 发送方和接收方用来实现可靠数据传输服务(讨论见后文)。
- 16 比特的接收窗口字段 , 该字段用于流量控制。该字段用于指示接收方愿意接受的字节数矗。
- 4 比特的首部长度字段, 该字段指示了以 32 比特的字为单位的 TCP首部长度。由于TCP选项字段的原因, TCP首部的长度是可变的。 (通常选项字段为空,所以TCP首部的典型长度是20字节。)
- 选项,该字段用于发送方与接收方协商最大报文段长度 (MSS) 时,或在高速网络环境下用作窗口调节因子时使用。
- 6 比特的标志字段。ACK 比特用于确认字段中的值是有效的,即该报文段对已被成功接收报文段的确认。 AST、 SYN 和 FIN 比特用于连接建立和拆除,我们将在后面讨论该问题。 在明确拥塞通告中使用了 CWR 和 ECE 比特,后文会提到。 当 PSH 比特被置位时,就指示接收方应立即将数据交给上层。 最后, URG 比特用来指示报文段里存在着被发送端的上层实体置为"紧急"的数据。 紧急数据的最后一个字节由 16 比特的紧急数据指针字段指出 。 当紧急数据存在并给出指向紧急数据尾指针的时候, TCP 必须通知接收端的上层实体。 (在实践中, PSH、 URG 和紧急数据指针并没有使用。 为了完整性起见,我们才提到这些字段。)
序号和确认号
TCP 报文段首部中序号字段和确认号字段是TCP可靠传输服务的关键部分。TCP 把数据看成一个无结构的、有序的字节流。序号建立在传送的字节流之上,一个报文段的序号因此是该报文段首字节的字节流编号。举例来说,假设主机A上的一个进程想通过一条TCP连接向主机B上的一个进程发送一个数据流。 主机A中的TCP将隐式地对数据流中的每一个字节编号。 假定数据流由一个包含500000 字节的文件组成, 其MSS 为 1000 字节,数据流的首字节编号是0。如图所示,该TCP将为该数据流构建500个报文段。 给第一个报文段分配序号0, 第二个报文段分配序号1000, 第三个报文段分配序号2000, 以此类推。 每一个序号被填入到相应TCP报文段首部的序号字段中。

现在我们考虑一下确认号。 TCP是全双工的,因此主机A在向主机B发送数据的同时,也许也接收来自主机B 的数据。从主机B到达的每个报文段中都有一个序号用于表示从 B流向A的数据。主机A 填充进报文段的确认号是主机A期望从主机B收到的下一个字节的序号。假设主机A 已收到了来自主机B 的编号为0-535 的所有字节,同时主机A打算发送一个报文段给主机B。 主机A等待主机B的数据流中字节536及之后的所有字节。 所以主机A就会在它发往主机B的报文段的确认号字段中填上536。再举一个例子,假设主机A 已收到来自主机B 的包含字节0-535 的报文段,以及另一个包含字节900-1000的报文段。 由于某种原因,主机A 还没有收到字节536 ~899 的报文段。主机A为了重新构建主机B的数据流,仍在等待字节536 (和其后的字节)。 因此, A到B 的下一个报文段将在确认号字段中包含536。
第二个例子中会引发一个重要而微妙的问题门 主机A 在收到第二个报文段(字节536 ~ 899) 之前收到第三个报文段(字节 900 ~ 1000) 。 因此,第三个报文段失序到达。问题是: 当主机在一条TCP连接中收到失序报文段时该怎么办?TCP RFC 并没有为此明确规定任何规则,而是把这一问题留给实现TCP的编程人员去处理。 他们有两个基本的选择:1.接收方立即丢弃失序报文段 2.接收方保留失序的字节,并等待缺少的字节以填补该间隔。显然,后一种选择对网络带宽而言更为有效,是实践中采用的方法。
注意,上述图中我们假设初始序号为0。 事实上, 一条TCP连接的双方均可随机地选择初始序号。 这样做可以减少将那些仍在网络中存在的来自两台主机之间先前已终止的连接的报文段,误认为是后来这两台主机之间新建连接所产生的有效报文段的可能性。假设两台主机在相互通信并传输报文段,随即便终止了连接。但报文段仍存在网络中,随后两台主机又建立连接,旧连接的报文段到达了接收主机,接收主机可能会认为这是新连接建立后的有效报文,用随即选择初始序号可以减少这一问题。
Telnet: 序号和确认号的一个学习案例
Telnet是一个用千远程登录的流行应用层协议。 与批量数据传输应用不同,它是一个交互式应用。许多用户现在更愿意采用SSH 协议而不是Telnet , 因为在Telnet 连接中发送的数据(包括口令!)是没有加密的,使得Telnet 易于受到窃听攻击。
假设主机A发起一个与主机B 的Telnet 会话。 因为是主机 A 发起该会话,因此它被标记为客户,而主机 B 被标记为服务器。在客户端的用户键入的每个字符都会被发送至远程主机; 远程主机将回送每个字符的副本给客户,并将这些字符显示在Telnet 用户的屏幕上。这种回显用于确保由 Telnet 用户发送的字符已经被远程主机收到并在远程站点上得到处理。 因此,在从用户击键到字符被显示在用户屏幕上这段时间内,每个字符在网络中传输了两次。
现在假设用户输入了一个字符 C,我们考察一下在客户与服务器之间发送的TCP报文段。

假设客户和服务器的起始序号分别是42 和79。前面讲过,一个TCP报文段的序号就是数据首字节的序号。因此,客户发送的第一个报文段的序号为42, 服务器发送的第一个报文段的序号为79。 前面讲过,确认号就是主机正在等待的数据的下一个字节序号。 在TCP连接建立后但没有发送任何数据之前,该客户等待字节79, 而该服务器等待字节42。
如图所示,共发送3 个报文段。 第一个报文段是由客户发往服务器,在它的数据字段里包含一字节的字符C的ASCII码。 如我们刚讲到的那样,第一个报文段的序号是42。 另外,由于客户还没有接收到来自服务器的任何数据,因此该第一个报文段中的确认号字段中是79。
第二个报文段是由服务器发往客户。 它有两个目的: 首先它是为该服务器所收到数据提供一个确认。 通过在确认号字段中填人43, 服务器告诉客户它已经成功地收到字节42及以前的所有字节,现在正等待着字节43 的出现。该报文段的第二个目的是回显字符C。 因此,在第二个报文段的数据字段里填入的是字符'C'的 ASCII 码。 第二个报文段的序号为79, 它是该TCP连接上从服务器到客户的数据流的起始序号,这也正是服务器要发送的第一个字节的数据。值得注意的是,对客户到服务器的数据的确认 被捎带 在服务器到客户的数据报文段中的。
第三个报文段是从客户发往服务器的。 它的唯一目的是确认巳从服务器收到的数据。该报文段的数据字段为空,因为客户没有再输入其他字符了。该报文段的确认号字段填人的是80, 因为客户已经收到了字节流中序号为79 及以前的字节,它现在正等待着字节80 的出现。你可能认为这有点奇怪,该报文段里没有数据还仍有序号。这是因为TCP报文中的序号字段需要填入某个序号。
往返时间的估计与超时
TCP 如同前面所讲的 rdt 协议一样,它采用超时/重传机制来处理报文段的丢失问题。尽管这在概念上简单,但是当TCP这样的实际协议中实现超时/重传机制时还是会产生许多微妙的问题。 也许最明显的一个问题就是超时间隔的设置。 显然,超时间隔必须大于该连接的往返时间 (RTT), 即从一个报文段发出到它被确认的时间。 否则会造成不必要的重传。 但是这个时间间隔到底应该是多大呢?刚开始时应如何估计往返时间呢?是否应该为所有未确认的报文段各设一个定时器?问题竟然如此之多!
估计往返时问
我们开始学习 TCP定时器的管理问题,要考虑一下TCP是如何估计发送方与接收方之间往返时间的。 报文段的样本RTT(SampleRTT)就
是从某报文段被发出到对该报文段的确认被收到之间的时间量。在任意时刻,TCP为一个已发送的但目前尚未被确认的报文段估计Samp
leRTT, 从而产生一个接近每个 RTT 的新 SampleRTT 值。也就表示, TCP 决不为已被重传的报文段计算 SampleRTT; 它仅为传输一次的报文段测量SampleRTT。
显然,由于路由器的拥塞和端系统负载的变化,这些报文段的SampleRTT 值会随之波动。因此,为了估计一个典型的RTT, 自然要采取某种对 SampleRTT 取平均的办法。 TCP 维持一个 SampleRTT 均值(称为 EstimatedRTT) 。一旦获得一个新 SampleRTT 时, TCP 就会根据下列公式来更新 EstimatedRTT
E s t i m a t e d R T T = ( 1 − α ) ⋅ E s t i m a t e d R T T + α ⋅ S a m p l e R T T EstimatedRTT = (1 - α ) ·EstimatedRTT + α·SampleRTT EstimatedRTT=(1−α)⋅EstimatedRTT+α⋅SampleRTT
α 给出的推荐值是a= 0. 125
SampleRTT虽然直译过来是样本RTT,但就是报文段从发出到被接收的实际时间量,EstimatedRTT就是一个估计的平均RTT。

但估计的RTT(EstimatedRTT)与实际RTT(SampleRTT)还是会有偏差的,因此对这个偏差进行定义,并用以下公式表示:
D e v R T T = ( 1 − β ) ⋅ D e v R T T + β ⋅ ∣ S a m p l e R T T − E s t i m a t e d R T T ∣ DevRTT = (1 - β )·DevRTT + β · | SampleRTT - EstimatedRTT | DevRTT=(1−β)⋅DevRTT+β⋅∣SampleRTT−EstimatedRTT∣
β的推荐值为0.25
设置和管理重传超时间隔
假设巳经给出了EstimatedRTT值和 DevRTT 值,那么 TCP 超时间隔应该用什么值呢?很明显,超时间隔应该大于等于EstimatedRTT, 否则,将造成不必要的重传。 但是超时间隔也不应该比EstimatedRTT 大太多,否则当报文段丢失时, TCP 不能很快地重传该报文段,导致数据传输时延大。 因此要求将超时间隔设为 EstimatedRTT 加上一定余量。 当EampleRTT 值波动较大时,这个余量应该大些;当波动较小时,这个余量应该小些。 因此, DevRTT 值应该在这里发挥作用了。 在TCP 的确定重传超时间隔的方法中,所有这些因素都考虑到了:
T i m e o u t l n t e r v a l = E s t i m a t e d R T T + 4 ⋅ D e v R T T Timeoutlnterval = EstimatedRTT + 4 · DevRTT Timeoutlnterval=EstimatedRTT+4⋅DevRTT
推荐的初始Timeoutlnterval 值为 1 秒。只要收到报文段并更新 EstimatedRTT, 就使用上述公式再次计算 Timeoutlnterval。
可靠数据传输
因特网的网络层服务 (IP服务) 是不可靠的。 IP不保证数据报的交付,不保证数据报的按序交付,也不保证数据报中数据的完整性。对于IP服务,数据报能够溢出路由器缓存而永远不能到达目的地,数据报也可能是乱序到达,而且数据报中的比特可能损坏(由0变为1 或者相反)。 由于运输层报文段是被IP数据报携带着在网络中传输的,所以运输层的报文段也会遇到这些问题。
TCP 的可靠数据传输服务确保一个进程从其接收缓存中读出的数据流是无损坏、 无间隙、 非冗余和按序的数据流;我们将将以两个递增的步骤来讨论TCP是如何提供可靠数据传输的。
前面提到可靠数据传输技术时,曾假定每一个已发送但未被确认的报文段都与一个定时器相关联,这在概念上是最简单的。 虽然这在理论上很好,但定时器的管理却需要相当大的开销。 因此,推荐的定时器管理策略仅使用单一的重传定时器,即使有多个已发送但还未被确认的报文段,还是使用一个定时器进行管理。
我们先给出一个TCP发送方的高度简化的描述,该发送方只用超时来恢复报文段的丢失;然后再给出一个更全面的描述,该描述中除了使用超时机制外, 还使用冗余确认技术。在接下来的讨论中,我们假定数据仅向一个方向发送,即从主机A到主机B, 且主机A在发送一个大文件。
python
/* 假设发送方不受TCP流量和拥塞控制的限制, 来自上层数据的长度小于MSS. 且数据传送只在一个方向进行。 */
NextSeqNum=InitialSeqNumber
SendBase=InitialSeqNumber
loop (永远) {
switch (事件)
事件: 从上面应用程序接收到数据e
生成具有序号NextSeqNum的TCP报文段
if (定时器当前没有运行)
启动定时器
向IP传递报文段
NextSeqNum=NextSeqNum+length(data)
break;
事件: 定时器超时
重传具有最小序号但仍未应答的报文段
启动定时器
break;
事件: 收到ACK. 具有ACK字段值y
if (y > SendBase) {
SendBase=y
if (当前仍有未收到ACK的报文段)
启动定时器
}
break;
} /*结束永远循环 */
上方给出了一个TCP发送方高度简化的描述。我们看到在TCP发送方有3 个与发送和重传有关的主要事件:从上层应用程序接收数据;定时器超时和收到ACK。第一个事件发生, TCP从应用程序接收数据,将数据封装在一个报文段中,并把该报文段交给IP。每一个报文段都包含一个序号,这个序号就是该报文段第一个数据字节的字节流编号。如果定时器还没有为某些其他报文段而运行,则当报文段被传给IP时, TCP就启动该定时器。
第二个主要事件是超时。 定时器超时后重传引起超时的报文段,然后TCP重启定时器。
TCP 发送方必须处理的第三个主要事件是,一个来自接收方的确认报文段到达时,TCP将ACK的值y与它的变量SendBase 进行比较。TCP状态变量 SendBase 是最早未被确认的字节的序号。( 因此SendBase - 1 是指接收方已正确按序接收到的数据的最后一个字节的序号。)如前面指出的那样, TCP采用累积确认,所以y确认了字节编号在y之前的所有字节都已经收到。 如果y > SendBase , 则该 ACK 是在确认一个或多个先前未被确认的报文段。 因此发送方更新它的 SendBase 变量;如果当前有未被确认的报文段, TCP还要重新启动定时器。
-
一些有趣的情况
我们刚刚描述了一个关千TCP如何提供可靠数据传输的高度简化的版本。 为了较好地感受该协议的工作过程,我们来看几种简单情况。

上图描述了第一种情况,主机A 向主机B 发送一个报文段。假设该报文段的序号是92, 而且包含8 字节数据。 在发出该报文段之后,主机A 等待一个来自主机B 的确认号为 100 的报文段。 虽然A 发出的报文段在主机B 上被收到,但从主机B发往主机A 的确认报文丢失了。 在这种情况下,超时事件就会发生,主机A会重传相同的报文段。 当然,当主机B收到该重传的报文段时,会返回一个确认报文。另外,它将通过序号发现该报文段包含了早已收到的数据。 因此,主机B 中的TCP将丢弃该重传的报文段中的这些字节。

上图描述了第二种情况,主机A 连续发送了两个报文段。 第一个报文段序号是92, 包含8 字节数据;第二个报文段序号是100, 包含20字节数据。 假设两个报文段都完好无损地到达主机B, 并且主机B为每一个报文段分别发送一个确认。 第一个确认报文的确认号是100, 第二个确认报文的确认号是120。 现在假设在超时之前这两个确认报文段中没有一个确认报文到达主机A。 当超时事件发生时,主机A重传序号92 的第一个报文段,并重启定时器。 只要第二个报文段的ACK在新的超时发生以前到达,则第二个报文段将不会被重传。

上图描述了第三种情况,假设主机A与在第二种情况中完全一样,发送两个报文段。 第一个报文段的确认报文在网络丢失,但在超时事件发生之前主机A收到一个确认号为 120 的确认报文。 主机A因而知道主机B 已经收到了序号为 119 及之前的所有字节;所以主机A不会重传这两个报文段中的任何一个。
-
超时间隔加倍
随后对简化的TCP模型做一些修改,首先关注的是在定时器超时发生后 超时间隔的长度。每当超时事件发生时,如前所述, TCP重传
具有最小序号的还未被确认的报文段。 只是每次TCP重传时都会将下一次的超时间隔设为先前值的两倍,而不是用从EstimatedRTT和 DevRTT 推算出的值。然而,每当定时器在另两个事件(即收到上层应用的数据和收到ACK) 中的任意一个启动时,超时时间Timeoutlnterval 由最近的 EstimatedRTT 值与 DevRTT值推算得到。
这种将超时间隔加倍的方法提供了一个简单的拥塞控制。定时器超时很可能是由网络拥塞引起的,即太多的分组到达源与目的地之间路径上的一台(或多台)路由器的队列中,造成分组丢失或长时间的排队时延。 在拥塞的时候,如果发送方持续重传分组,会使拥塞更加严重。 相反, TCP使用更文雅的方式,每个发送方的重传都是经过越来越长的时间间隔后进行的。
- 快速重传
超时触发重传存在的问题之一是超时周期可能相对较长。当一个报文段丢失时,由于超时周期较长需要等待超时发生了再重传报文段,从而增加了端到端时延。 幸运的是,发送方通常可在超时发生之前通过注意所谓冗余ACK来较好地检测到丢包情况。冗余ACK就是发送方再次收到确认某个报文段的 ACK, 发送方先前已经收到对该报文段的确认。 要理解发送方对冗余 ACK 的响应,我们必须首先看一下接收方为什么会发送冗余 ACK。

上述表中总结了TCP接收方的 ACK生成策略。当 TCP 接收方收到一个序号大于所期望的、按序的报文段,它检测到了数据流中的一个间隔,这就是说有报文段丢失。 可能是由于在网络中报文段丢失或重新排序造成的。因为 TCP不使用否定确认,所以接收方不能向发送方发回一个显式的否定确认。 相反,它只是对已经接收到的最后一个按序字节数据进行重复确认 ,产生一个冗余ACK。
因为发送方经常一个接一个地发送大量的报文段,如果一个报文段丢失, 后续的报文段每次到达接收方,接收方就要产生一个冗余ACK。如果TCP 发送方接收到对相同数据的3 个冗余ACK, 它把这当作一种指示,说明在这个已被确认过3 次的报文段 之后的报文段已经丢失。一且收到 3 个冗余ACK, TCP就执行快速重传 , 即在该报文段的定时器过期之前重传丢失的报文段。
python
事件: 收到ACK,具有ACK字段值y
if (y > SendBase) {
SendBase=y
if (当前仍有未收到ACK的报文段)
启动定时器
}
else ( // 对巳经确认的报文段的一个冗余ACK
// 对y收到的冗余ACK数加1
if(对y==3收到的冗余ACK数)
// TCP快速重传
重新发送具有序号y的报文段
}
break;
- 是回退N步还是选择重传
接下来考虑TCP是一个 回退N步(GBN) 协议还是一个选择重传(SR)协议?前面讲过, TCP确认是累积式的,正确接收但失序的报文段是不会被接收方逐个确认的。TCP发送方仅需维持已发送过但未被确认的字节的最小序号 (SendBase) 和下一个要发送的字节的序号(NextSeqNum) 。在这种意义下, TCP 看起来更像一个GBN 风格的协议。 但是 TCP 和 GBN协议之间有着一些显著的区别。 许多 TCP实现会将正确接收但失序的报文段缓存起来。另外考虑一下,当发送方发送的一组报文段 1 , 2, ..., N, 并且所有的报文段都按序无差错地到达接收方。假设对分组 n<N 的确认报文丢失,但是其余 N-1 个确认报文在分别超时以前到达发送端。假如是GBN协议,不仅会重传分组 n, 还会重传所有后继的分组 n+1, n+2, ... , N。而TCP将重传至多一个报文段,即报文段n。此外,如果对报文段n+1 的确认报文在报文段 n 超时之前到达,TCP 甚至不会重传报文段n。
TCP是采用了所谓的选择确认。它允许 TCP 接收方有选择地确认失序报文段,而不是累积地确认最后一个正确接收的有序报文段。 当将该机制与选择重传机制结合起来使用时(即跳过重传那些已被接收方选择性地确认过的报文段), TCP看起来就很像我们通常的 SR协 议。 因此, TCP 的差错恢复机制也许最好被分类为GBN 协议与SR协议的混合体。
流量控制
前面讲过, 一条TCP连接的每一侧主机都为该连接设置了接收缓存。当该TCP连接收到正确、 按序的字节后,它就将数据放人接收缓存。 相关联的应用进程会从该缓存中读取数据,但不必是数据刚一到达就立即读取。接收方应用也许正忙千其他任务,甚至要过很长时间后才去读取该数据。 如果某应用程序读取数据时相对缓慢,而发送方发送得太多、太快,发送的数据就会很容易地使该连接的接收缓存溢出。
TCP 为它的应用程序提供了流量控制服务以消除发送方使接收方缓存溢出的可能性。 流量控制因此是一个速度匹配服务,即发送方的发送速率与接收方应用程序的读取速率相匹配。
TCP 通过让发送方维护一个称为接收窗口的变量来提供流量控制。接收窗口用于给发送方一个指示------该接收方还有多少可用的缓存空间。 因为TCP 是全双工通信,在连接两端的发送方都各自维护一个接收窗口。假设主机A通过一条TCP连接向主机B发送一个大文件。 主机B 为
该连接分配了一个接收缓存,并用RcvBuffer 来表示其大小。主机 B 上的应用进程不时地从该缓存中读取数据。 我们定义以下变量:
LastByteRead : 主机 B 上的应用进程从缓存读出的数据流的最后一个字节的编号。
LastByteRcvd: 从网络中到达的并且已放入主机 B 接收缓存中的数据流的最后一个字节的编号。
由于TCP 不允许已分配的缓存溢出,下式必须成立:
L a s t B y t e R c v d − L a s t B y t e R e a d ≤ R c v B u f f e r LastByteRcvd - LastByteRead ≤ RcvBuffer LastByteRcvd−LastByteRead≤RcvBuffer
接收窗口用rwnd表示,缓存剩余可用空间
r w n d = R c v B u f f e r − [ L a s t B y t e R c v d − L a s t B y t e R e a d ] rwnd = RcvBuffer - [ LastByteRcvd - LastByteRead ] rwnd=RcvBuffer−[LastByteRcvd−LastByteRead],该缓存空间是随着时间变化的, rwnd 是动态的。,如下图所示。

主机 B 通过把当前的rwnd 值放入它发给主机 A 的报文段接收窗口字段中,通知主机A它在该连接的缓存中还有多少可用空间。 开始时,主机B设定rwnd = RcvBuffer。
主机A需要轮流跟踪两个变量,LastByteSent(已经发送最后一个字节的序号) 和 LastByteAcked(已经发送并被确认的最后一个字节的序号)。这两个变量之间的差LastByteSent - LastByteAcked, 就是主机 A 发送到连接中但未被确认的数据量。 通过将未确认的数据量控制在值rwnd 以内,就可以保证主机A不会使主机B 的接收缓存溢出。
对于这个方案还存在一个小小的技术问题。 假设主机B 的接收缓存已经存满,使得rwnd=0。 在将rwnd =0 通告给主机A之后,但主机B 没有任何数据要发给主机A。此时,主机B上的应用进程将缓存清空,rwnd已经不为0了,但TCP 并不向主机 A发送带有 rwnd 新值的新报文段。这是因为TCP仅当在它有数据或有确认报文要发时才会发送报文段给主机A。这样,主机A不可能知道主机B 的接收缓存已经有新的空间了,即主机A被阻塞而不能再发送数据!为了解决这个问题, TCP规范中要求: 当主机B 的接收窗口为0时,主机A继续发送只有一个字节数据的报文段。 这些报文段将会被接收方确认。 最终缓存将开始清空,并且确认报文里将包含一个非0 的rwnd 值。
描述了TCP 的流量控制服务以后,我们在此要简要地提一下UDP并不提供流量控制,报文段由于缓存溢出可能在接收方丢失。
TCP 连接管理
在本小节中,我们更为仔细地观察如何建立和拆除一条TCP连接。
现在我们观察一下一条TCP连接是如何建立的。假设运行在一台主机 (客户)上的一个进程想与另一台主机(服务器)上的一个进程建立一条连接。 客户应用进程首先通知客户TCP, 它想建立一个与服务器上某个进程之间的连接。 客户中的TCP会用以下方式与服务器中的TCP建立一条TCP连接:
- 笫一步: 客户端的TCP首先向服务器端的TCP发送一个特殊的TCP报文段。 该报文段中不包含应用层数据。 但是在报文段的首部中的一个标志位(即 SYN 比特)被置为 1。 因此,这个特殊报文段被称为 SYN 报文段。 另外,客户会随机地选择一个初始序号 (client_isn) , 并将此编号放置于该SYN 报文段的序号字段中。 该报文段会被封装在一个IP数据报中,并发送给服务器。
- 第二步: 一旦包含SYN 报文段的IP数据报到达服务器主机,服务器会从该数据报中提取出SYN 报文段,为该TCP连接分配TCP缓存和变量,并向该客户TCP发送允许连接的报文段。这个允许连接的报文段也不包含应用层数据。但是,在报文段的首部却包含3个重要的信息。 首先, SYN 比特被置为 1。 其次,该TCP报文段首部的确认号字段被置为 client_isn + 1。 最后,服务器选择自己的初始序号(server_isn) , 并将其放置到 TCP 报文段首部的序号字段中。 这个允许连接的报文段实际上表明了: "我收到了你发起建立连接的 SYN 分组,该分组带有初始序号client_isn 。 我同意建立该连接。 我自己的初始序号是 server_isn。该允许连接的报文段被称为SYNACK报文段。
- 第三步: 在收到 SYNACK报文段后,客户也要给该连接分配缓存和变量。 客户主机则向服务器发送另外一个报文段;这最后一个报文段对服务器的允许连接的报文段进行了确认(该客户通过将值server_isn + 1 放置到 TCP 报文段首部的确认字段中来完成此项工作)。因为连接已经建立了,所以该SYN 比特被置为0。 这个阶段,可以在报文段负载中携带客户到服务器的数据。
一旦完成这3 个步骤,客户和服务器主机就可以相互发送包括数据的报文段了。 在以后每一个报文段中, SYN 比特都将被置为0。 注意到为了创建该连接,在两台主机之间发送了3 个分组,由于这个原因,这种连接创建过程通常被称为3次握手。如下图所示

天下没有不散的宴席,对于TCP连接也是如此。 参与一条TCP连接的两个进程中的任何一个都能终止该连接。 当连接结束后,主机中的"资源" (即缓存和变量)将被释放。举一个例子,假设某客户打算关闭连接,如下图所示。

客户应用进程发出一个关闭连接命令。 这会引起客户 TCP 向服务器进程发送一个特殊的 TCP 报文段。这个特殊的报文段让其首部中的一个标志位(即 FIN 比特 )被设置为1 。 当服务器接收到该报文段后,就向发送方回送一个确认报文段。 然后,服务器发送它自己的终止报文段,其 FIN 比特被置为 1。 最后,该客户对这个服务器的终止报文段进行确认。 此时,在两台主机上用于该连接的所有资源都被释放了。
在一个TCP连接的生命周期内,运行在每台主机中的 TCP 协议在各种 TCP 状态之间变迁。

上图说明了客户 TCP会经历的一系列典型 TCP状态。客户 TCP开始时处于 CLOSED (关闭)状态。 客户的应用程序发起一个新的 TCP 连接,这引起客户中的TCP向服务器中的TCP发送一个SYN报文段(并进入了 SYN_SENT 状态)。当客户 TCP处在SYN_SENT状态时,它等待来自服务器TCP 的对客户所发报文段进行确认。当收到服务器的确认报文后, 客户主机则向服务器发送另外一个报文段;这最后一个报文段对服务器的允许连接的报文段进行了确认。客户 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客户重传最后的确认报文。 经过等待后,连接就正式关闭,客户端所有资源(包括端口号)将被释放。

上图说明了服务器端的TCP会经历的一系列典型 TCP状态。在这两个状态变迁图中,我们只给出了TCP连接是如何正常地被建立和拆除的。 我们没有描述在某些不正常的情况下(例如当连接的双方同时都要发起或终止一条连接时)发生的事情。 如果你对此问题及其他与TCP有关的高级问题感兴趣,请自行查阅资料。
我们上面的讨论假定了客户和服务器都准备通信, 即服务器正在监听客户发送其SYN报文段的端口。 我们来考虑当一台主机接收到一个TCP报文段,其端口号或源 IP地址与该主机上进行中的套接字都不匹配的情况。例如, 假如一台主机接收了具有目的端口 80的一个TCP SYN分组,但该主机在端口 80不接受连接。则该主机将向源发送一个特殊重置报文段, 该TCP报文段将 RST 标志位置为 1。 因此, 当主机发送一个重置报文段时,它告诉该源"我没有那个报文段的套接字。 请不要再发送该报文段了"。
既然我们知道了上述这一点,我们介绍nmap端口扫描工具,并大致研究它的工作原理。 为了探索目标主机上的一个特定的TCP端口,如端口6789, nmap 将对那台主机的目的端口 6789 发送一个特殊的 TCP SYN 报文段。 有3 种可能的输出:
- 源主机从目标主机接收到一个TCP SYNACK 报文段。 因为这意味着在目标主机上一个应用程序使用TCP端口 6789 运行, nmap 返回"打开"。
- 源主机从目标主机接收到一个TCP RST报文段。 这意味着该SYN 报文段到达了目标主机,但目标主机没有运行一个使用TCP端口 6789 的应用程序。 但源主机至少知道发向该主机端口 6789 的报文段没有被源和目标主机之间的任何防火墙所阻挡。
- 源什么也没有收到。 这很可能表明该SYN 报文段被中间的防火墙所阻挡,无法到达目标主机。
nmap 是一个功能强大的工具,该工具不仅能"侦察"打开的 TCP端口,也能 "侦察"打开的UDP端口,还能"侦察"防火墙及其配置,甚至能"侦察"应用程序的版本和操作系统。 其中的大多数功能都能通过操作TCP连接管理报文段完成。
参考目录
书籍:《计算机网络:自顶向下方法(第七版)》