计算机网络-TCP和UDP协议

警告
本文最后更新于 2020-08-20,文中内容可能已过时。

本篇介绍计算机网络运输层的的 TCP 和 UDP 协议,主要是过一遍,加深理解,以及方便以后查看,毕竟不是每次都带着书。

1. 运输层

五层体系结构中,运输层是从上往下的第二层,主要是 TCP 和 UDP 两个协议。

运输层的作用是为相互通信的应用进程提供逻辑通信,因此,运输层的端点是主机中的进程。而网络层为主机间提供通信,因此端点则是网络中的主机。数据在网络中传输时,路由器转发分组都只会用到下三层的功能,与运输层无关,因此,运输层仅在主机的协议栈中起作用。下面的图片比较清楚的说明了这一点

根据应用程序的不同需求,运输层有两种不同的协议,即用户数据报协议 UDP(User Datagram Protocol)和传输控制协议 TCP(Transmission Control Protocol)。这两种协议是我们在运输层需掌握的主要内容,它们在整体的协议栈中的位置如下所示

网络层的端点是主机,采用 IP 来标识,运输层的端点是应用进程,使用协议端口号(protocol port number)标识,通常简称为端口。不同的操作系统实现端口的方法可能是不同的,但其作用是相同的。传输层协议定义一个端口号为 16 位,仅在本地主机具有实际意义,目的是标识各应用进程与传输层交互的接口,因此不同的主机间相同的端口号没有关联。

16 位的端口号允许 65535 个不同的端口号。服务端使用的端口主要分为两类。一类叫做熟知端口或系统端口,数值为 0~1023,这些端口号被指派给了固定的某类应用,其它的应用不得使用。另一类叫做登记端口号,数值为 1024~49151,主要为没有熟知端口号的应用使用,但也有进行登记防止重复。常用的熟知端口号如下

应用程序FTPTELNETSMTPDNSTFTPHTTPSNMPSNMP(trap)
熟知端口号212325536980161162

客户端使用的端口号数值为 49152~65535,仅在客户进程运行时才动态选择,存在周期较短,又叫做短暂端口号。当服务器进程收到客户进程的报文时,就知道了客户进程所使用的端口号,因而可以将数据发送给客户进程,通信结束后,该客户端端口就被释放。

最后要注意的是,对所传输的数据单元,TCP叫做报文段,UDP叫做用户数据报。

2. UDP

UDP 仅在 IP 的基础上增加了很少的功能,关于 UDP 的特点介绍如下

  1. UDP 是无连接的。无连接的含义是发送数据前不需要建立连接,发送结束后不需要释放连接;

  2. UDP 尽最大努力交付。尽最大努力的意思是不保证可靠;

  3. UDP 是面向报文的。发送方的 UDP 对应用程序交下来的报文,添加首部后直接交付 IP 层,不做合并或拆分处理,一次发送一个报文,接收方的 UDP 对 IP 层交上来的报文,去除首部后直接交付应用进程,一次接受一个报文。简单的逻辑图如下

  4. UDP 支持一对一、一对多、多对一和多对多的交互通信;

  5. UDP 首部只有 8 个字节,比 TCP 的20 个字节小的多;

  6. UDP 没有拥塞控制,这意味着网络上的拥塞不会影响源主机的发送速率,这种特性使其适合于一些实时的应用,如 IP电话、实时视频会议等,这些应用允许一定的数据丢失,却不允许太大的时延。

UDP 的数据报非常简单,除了 8 个字节的首部,剩下的全是数据部分,首部格式如下图所示

首部共分四个字段,每个字段两个字节,具体解释如下

  1. 源端口:源端口号,需要对方回复时使用,不需要时置全 0;

  2. 目的端口:目的端口号,接收端交付报文必须的;

  3. 长度:这里的长度是 UDP 整个报文的长度,最小为 UDP 首部 8 个字节

  4. 校验和:UDP 校验和的计算包括整个 UDP 数据报,除此之外和 IP 首部校验和计算很相似。发送端首先将校验和字段置0,然后在 UDP 首部前加 12 个字节的伪首部,再按 16 位进行划分,最后一行不足16位补 0,写成一串竖式,进行求和计算,对和取反码,从而得到校验和。过程如下图所示

    将得到的结果填入校验和字段,并发送出去,接收方收到报文后,添加 12 个字节的伪首部然后继续 16 位划分求和,结果取反码后应该为 1,否则就表明有差错,接收方就会丢弃该报文。

最后要注意的是,UDP 在 IP 报文中的协议字段为 17.

3. TCP

TCP 协议要复杂很多,我们仍然先介绍其特点

  1. TCP 是面向连接的,使用前必须先建立连接,使用后释放连接;

  2. TCP 保证可靠,可靠的含义是通过 TCP 传输的数据无差错、不丢失、不重复、按序到达;

  3. TCP 是面向字节流的,其含义是 TCP 将应用层交下来的数据看作一连串无结构的字节流,而不是看作一个整体,TCP 不知道所传输的字节流的含义,也不保证接收与交付的数据具有对应大小的关系,仅保证接收方收到的字节流和发送方发出的字节流完全一致,这里的完全一致指的是字节的顺序一致和每个字节的内容没有差错。当然,接收方的应用是一定有能力识别这一连串字节流的含义,这就是应用层的功能了。整个过程可以用下图简单的说明,虽然图中仅仅涉及几个字节,但实际上每个 TCP 报文涉及成百上千字节是很常见的。

  4. TCP 仅支持一对一的通信,不同于 UDP,TCP 连接是点对点的;

  5. TCP 提供全双工通信,通信双方的应用进程在任何时候都能发送数据。事实上,TCP 连接的两端都设有发送缓存和接收缓存,发送时应用将数据交给发送缓存后就不管了,由 TCP 在合适的时候将数据发出去,接收时把收到的数据放入缓存,上层的应用在合适的时候读取缓存中的数据。

TCP 连接的端点叫做套接字(socket),其实就是 IP 地址和端口号的拼接,尽管 socket 在其它地方还有很多含义,但在这里就这么简单。

1
套接字 socket = (IP地址:端口号)

3.1 超时重传

TCP 可靠传输主要靠下面几个机制保证

  1. 确认应答和序列号
  2. 超时重传
  3. 校验和
  4. 流量控制
  5. 拥塞控制

流量控制和拥塞控制需要理解 TCP 报文,放在后面介绍,校验和在 TCP报文中介绍,现在先介绍前两个机制。

首先,理想的情况下,也就是信道不出差错,接收的速度永远跟得上发送的速度,此时数据的发送和接收遵循简单的停等协议。由于全双工通信的双方既是发送方也是接收方,下面为了讨论方便,仅考虑 A 发送数据而 B 接收数据,而且将一个 TCP 报文包含的数据称为一个分组。

停等协议的过程如下所示,简单地讲,就是发送方发送分组,接收方接收后返回确认,发送方收到确认后再发送下一个分组,如此循环。

但我们知道现实是不可能出现这种理想情况的,我们必须考虑数据可能出差错,可能丢失,可能不按序到达等许多情况。因此必须采取一定的措施避免这些情况的出现或出现时进行纠正。

首先是发送方发送的数据出现差错。差错可以通过校验和的计算检测出来,当检测的差错后,接收方只做一件事,就是丢弃该分组(不通知发送方分组出错),可靠传输的保证是,A 在发送数据后设置了一个超时计时器,如果计时器到期前收到了确认,就撤销该计时器,而此时由于 B 丢弃了分组且没有返回确认,计时器到期后 A 仍然无法收到确认,就会重传前面发送过的分组。这一过程在上图中同样有所说明。

超时重传机制的设定还会同时引入另外两个机制:

  1. A 发送完分组后,必须暂时保留已发送的分组的副本(为发生超市重传时使用),只有在收到确认后才能清除保留的副本;
  2. 发送的分组和返回的确认分组都必须进行编号,这样才能明确是哪个发送出去的分组收到了确认,哪个没有收到确认。而由于 TCP 是面向字节流的,编号实际上也是针对字节的,也就是说每个字节一个编号。

因此我们可以看到,确认应答、序列号、超市重传实际上是协同工作的,一起来保证 TCP 连接的可靠。最后还要注意的一点是,超时计时器的重传时间应当稍微比分组传输的平均往返时间多一点点,从而确保正常网络情况下足已收到确认。

其次是发送方发送的分组丢失,丢失的后果是 B 收不到任何数据,同样也不会发送确认,之后触发的超时重传就和出现差错完全相同了。

再然后是确认丢失,确认丢失指的是 B 所发送的对分组 $M_1$ 的确认丢失了,此时 A 无法收到确认,依然会触发超时重传,但接下来我们就会注意到 B 再次收到了同样的分组,面对同样的分组 B 所作的事情是:丢弃整个重复的分组,然后向 A 发送确认,丢弃是因为分组已经在缓冲区存在,重新发送确认是因为先前的确认 A 没有收到。整个过程如下图所示

最后一种情况是确认重复。我们考虑一种可能,那就是 B 对分组 $M_1$ 的确认并不是丢失了,而是由于种种原因迟到了,当 A 收到该确认的时候,已经触发了超时重传重新发送了分组,此时 B 所作的事没有区别,依然是丢弃分组然后回复确认,A 所做的事情则是对收到的第二个确认分组进行丢弃。如上图 (b) 所示。

这些机制保证了 A 总能收到对所有发出的分组的确认,如果 A 不断重传而一直收不到确认,那么只能说明一件事,那就是信道质量太差。添加了这一系列机制的协议就叫做 自动重传请求 ARQ(Automatic Repeat reQuest)。

3.2 滑动窗口

无论是停等还是 ARQ,信道的利用率都比较低,主要是因为只有在收到上一个分组的确认后才会发送下一个分组,优化的办法就是使用流水线传输,即发送方连续发送多个分组,不必等待上一个分组的确认,从而使信道上一直有数据在传送。

当使用流水线传输时,就需要连续 ARQ 协议和滑动窗口协议。这里先介绍连续 ARQ 协议,滑动窗口之后介绍。

如下图所示,发送方维持一个发送窗口,其含义是位于发送窗口内的5个字节可以连续的发出去,不必等待对方的确认。一般我们在讨论时,默认理解为下面图中箭头所示的时间流动方向,向前就指向着时间增大的方向,向后就是向着时间减少的方向。字节的发送是按照序号从小到大的方向。

连续 ARQ 协议规定,发送方每收到一个确认,发送窗口就向前移动一格,上图中发送方收到了对第1个字节的确认,因此发送窗口向前移动了1个字节,现在可以发送第6个字节了。另外,由于发送和接收都是面向字节流的,一次发送接收很可能涉及很多字节,所以接收方一般采用累积确认的方式,也就是说,接收方不必对收到的每个字节都发送确认,而是只对按序到达的最后一个字节发送确认,这一动作就表示到该字节为止前面所有的字节都正确收到了。

累积确认尽管提高了传输效率,减少了网络中的数据,但可能会造成回退 N 问题(Go-back-N)。该问题描述如下,加入发送方发送了 5 个字节,第 3 个字节丢失了,此时接收方只能对前两个字节发送确认,发送方无法知道后面 3 个字节的下落,此时尽管第 4 和第 5 个字节接收方都已经收到了,发送方还是需要对第 3 个字节开始的数据都重传一次,回退 N 的含义就是退回来重传已发送过的 N 个数据。所以在信道质量不好时,连续 ARQ 具有较大的副作用。

3.3 TCP 报文

TCP 是面向字节流的,但意思仅限于逐字节编号,交付网络层的实际上是报文段。TCP 报文段首部有 20 个字节是固定的,后面还可以添加 40 个字节,首部最大 60 字节,再往后就是 TCP 的数据部分。TCP 报文格式如下

  • 源端口目的端口:含义同 UDP

  • 序号:所传输的数据的第一个字节的编号,也就是本报文段数据部分的第一个字节,序号一共四个字节,意味着可编号范围为 $[0, 2^{32}-1]$

  • 确认号:期望收到的对方下一个报文段的第一个数据字节的序号。例如,B 正确收到了 A 发过来的报文段,序号字段值为 501,数据部分共 200字节,所以 B 收到的最后一个字节序号为 700,那么 B 期望收到的 A 的下一个报文段的第一个字节序号就是 701,所以在确认报文中将 确认号字段置为 701。

    总之,要明白:若 确认号 = N,就说明到序号 N-1 为止的所有数据都正确收到了。

  • 数据偏移:占4位,指 TCP 报文段数据部分距离报文段起始处有多远,简单来说就是 TCP 首部长度。单位是 4 字节,因此最大值为 15 * 4 = 60 字节,这就是 TCP 首部最大长度。

  • 保留:共6位,当前没有使用,全部置0。

  • 控制位:共6位,每位的作用不一样,如下

    • 紧急URG (URGent) :当URG = 1时,表明紧急指针字段有效。它告诉系统此报文段中有紧急数据,应尽快传送(相当于高优先级的数据),而不要按原来的排队顺序来传送。例如,已经发送了很长的一个程序要在远地的主机上运行。但后来发现了一些问题,需要取消该程序的运行。因此用户从键盘发出中断命令(Control + C)。如果不使用紧急数据,那么这两个字符将存储在接收TCP的缓存末尾。只有在所有的数据被处理完毕后这两个字符才被交付接收方的应用进程。这样做就浪费了许多时间。当URG置1时,发送应用进程就告诉发送方的TCP有紧急数据要传送。于是发送方TCP就把紧急数据插入到本报文段数据的最前面,而在紧急数据后面的数据仍是普通数据。这时要与首部中紧急指针(UrgentPointer)字段配合使用。
    • 确认ACK (ACKnowlegment) :仅当ACK = 1时确认号字段才有效。当ACK = 0时,确认号无效。TCP规定,在连接建立后所有传送的报文段都必须把ACK置1。
    • 推送 PSH (PuSH):当两个应用进程进行交互式的通信时,有时在一端的应用进程希望在键入一个命令后立即就能够收到对方的响应。在这种情况下,TCP就可以使用推送(push)操作。这时,发送方TCP把PSH置1,并立即创建一个报文段发送出去。接收方TCP收到PSH = 1的报文段,就尽快地(即“推送”向前)交付接收应用进程,而不再等到整个缓存都填满了后再向上交付
    • 复位RST (ReSeT):当RST = 1时,表明TCP连接中出现严重差错(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立运输连接。RST置1还用来拒绝一个非法的报文段或拒绝打开一个连接。RST也可称为重建位或重置位。
    • 同步SYN (SYNchronization): 在连接建立时用来同步序号。当SYN = 1而ACK = 0时,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应的报文段中使SYN = 1和ACK = 1。因此,SYN置为1就表示这是一个连接请求或连接接受报文。关于连接的建立和释放,在本章的5.9节还要进行详细讨论。
    • 终止FIN (FINis,意思是“完”、“终”) :用来释放一个连接。当FIN = 1时,表明此报文段的发送方的数据已发送完毕,并要求释放运输连接。
  • 窗口:16位,指的是发送本报文段的一方的接收窗口,目的是告诉对方自己还能收多少数据。例如,设确认号是701,窗口字段是1000。这就表明,从701号算起,发送此报文段的一方还有接收1000个字节数据(字节序号是701~1700)的接收缓存空间。

  • 校验和:TCP 校验和的计算方式和 UDP 相同。

  • 紧急指针:占2字节。紧急指针仅在URG = 1时才有意义,它指出本报文段中的紧急数据的字节数(紧急数据结束后就是普通数据)。因此,紧急指针指出了紧急数据的末尾在报文段中的位置。当所有紧急数据都处理完时,TCP就告诉应用程序恢复到正常操作。值得注意的是,即使窗口为零时也可发送紧急数据。

  • 选项:长度可变,最长可达40字节。当没有使用“选项”时,TCP的首部长度是20字节。

3.4 流量控制

流量控制主要依靠滑动窗口,为了方便说明,假定数据传输只在一个方向进行,即 A 发送数据,B 给出确认。

首先,假定 A 收到了 B 发来的确认报文段,其中窗口是20(字节),确认号是31(即期望收到的B发送的下一个序号是31),根据这两个值,A 构造自己的发送窗口如下

此时 A 可以将序号 31 起连续 20 个字节都发送出去,但在未收到确认前都必须保留,以便超时重传使用。因此,发送窗口中的数据表示允许发送的数据,后沿后面的数据表示已收到确认的数据,前沿前面的数据表示不允许发送的数据。如果没有收到新的确认,后沿不可移动,收到新的确认后,后沿向前移动,而如果收到的确认中对方通知的窗口缩小了,使得发送窗口前沿正好不动,那么前沿也不动,否则前沿根据通知的窗口大小向前移动。当然,前沿也可以向后收缩,但 TCP 强烈不赞成这么做。

TCP 的流量控制指的是通过窗口和确认号来调整发送速率,让发送方的发送不要太快,从而使接收方来得及接收。

除此之外,流量控制还包括两个问题。

第一个问题是由于发送方应用层交付过慢造成的,假如发送方应用层每次交付1个字节,那么就需要加上 20 个字节的 TCP 首部,再加上 20 字节的 IP 首部,然后发送出去,每次为了1字节的数据,要多发送远大于数据部分的 40 个字节,效率非常低。应对这种情况主要使用 Nagle 算法,算法如下:若发送方应用进程把要发送的数据逐个字节地送到 TCP 的发送缓存,则发送方就把就把第一个数据字节先发送出去,把后面到达的数据字节都缓存起来,当发送方收到对第一个数据字符的确认后,再把发送缓存中的所有数据组成一个报文发送出去,同时继续对随后到达的数据进行缓存,只有在收到对前一个报文段的确认后才继续发送下一个报文段。当数据到达较快而网络速率较慢时,用这样的方法可明显地减少所用地网络带宽。Nagle 算法还规定,当到达的数据已达到发送窗口大小的一半或已达到报文段的最大长度时,就立即发送一个报文段。这样做,就可以有效地提高网络的吞吐量。

另一个问题叫做糊涂窗口综合症(silly window syndrome),是由于接收方接收过慢造成的。设想一种情况:TCP接收方的缓存已满,而交互式的应用进程一次只从接收缓存中读取1个字节(这样就使接收缓存空间仅腾出1个字节),然后向发送方发送确认,并把窗口设置为1个字节(但发送的数据报是40字节长)。接着,发送方又发来1个字节的数据(请注意,发送方发送的IP数据报是41字节长)。接收方发回确认,仍然将窗口设置为1个字节。这样进行下去,使网络的效率很低。要解决这个问题,可以让接收方等待一段时间,使得或者接收缓存已有足够空间容纳一个最长的报文段,或者等到接收缓存已有一半空闲的空间。只要出现这两种情况之一,接收方就发出确认报文,并向发送方通知当前的窗口大小。此外,发送方也不要发送太小的报文段,而是把数据积累成足够大的报文段,或达到接收方缓存的空间的一半大小。

TCP 的流量控制主要指的就是滑动窗口的控制和这两个问题。

3.5 拥塞控制

流量控制往往指点对点通信量的控制,要做的是抑制发送端发送数据的速率,以便接收端来得及接收。拥塞控制要做的则是防止过量的数据注入网络,它与流量控制的相似之处在于,某些拥塞控制算法是通过向发送端发送控制报文,令其放慢发送速率完成的,手段与流量控制相似。TCP 的拥塞控制主要是指通过某种手段判断网络是否出现拥塞,然后对发送速率进行调整。

当前进行拥塞控制主要是四种算法:慢开始(slow-start)、拥塞避免(congestion avoidance)、快重传(fast retransmit)和快恢复(fast recovery)。

这四种算法都与一个拥塞窗口 cwnd(congestion window)的状态变量有关,拥塞窗口由发送方维持,其大小取决于网络的拥塞程度,并且在动态地变化,发送方会让自己的发送窗口等于拥塞窗口大小。只要发送方没有按时收到应当到达的确认报文,就猜测网络可能出现了拥塞,此时就将拥塞窗口减小,从而减少发送到网络中的数据,如果按时收到确认,就证明网络没有出现拥塞,拥塞窗口就再增大一些。四种拥塞控制算法实际上就是在控制拥塞窗口的变化。

慢开始

慢开始的思路是这样的,当主机开始发送数据时,如果立即把大量数据字节注入到网络,那么就有可能引起网络拥塞,因此先将拥塞窗口设置为一个最大报文段的数值,每收到一个对新的报文段的确认后,把拥塞窗口增加至多一个MSS的数值,这样从小到大逐步增加拥塞窗口大小,从而使数据注入到网络的速率更加合理。

举个例子,假设在一开始发送方先设置cwnd = 1,发送第一个报文段M1,接收方收到后确认M1。发送方收到对M1的确认后,把cwnd从1增大到2,于是发送方接着发送M2和M3两个报文段。接收方收到后发回对M2和M3的确认。发送方每收到一个对新报文段的确认(重传的不算在内)就使发送方的拥塞窗口加1,因此发送方在收到两个确认后,cwnd就从2增大到4,并可发送M4~M7共4个报文段(见图5-24)。因此使用慢开始算法后,每经过一个传输轮次(transmission round),拥塞窗口cwnd就加倍。这里的传输轮次指的是一次往返时间。简单的图示如下

慢开始的“慢”并不是指cwnd的增长速率慢,而是指在TCP开始发送报文段时先设置cwnd = 1,使得发送方在开始时只发送一个报文段(目的是试探一下网络的拥塞情况),然后再逐渐增大cwnd。这当然比按照大的cwnd一下子把许多报文段突然注入到网络中要“慢得多”。这对防止网络出现拥塞是一个非常有力的措施。

拥塞避免

为了防止拥塞窗口cwnd增长过大引起网络拥塞,还需要设置一个慢开始门限 ssthresh状态变量。慢开始门限ssthresh的用法如下:

  • 当cwnd < ssthresh时,使用上述的慢开始算法。
  • 当cwnd > ssthresh时,停止使用慢开始算法而改用拥塞避免算法。
  • 当cwnd = ssthresh时,既可使用慢开始算法,也可使用拥塞避免算法。

拥塞避免算法的思路是让拥塞窗口cwnd缓慢地增大,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍。这样,拥塞窗口cwnd 按线性规律缓慢增长,比慢开始算法的拥塞窗口增长速率缓慢得多。

无论在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有按时收到确认),就要把慢开始门限ssthresh设置为出现拥塞时的发送方窗口值的一半(但不能小于2)。然后把拥塞窗口cwnd重新设置为1,执行慢开始算法。这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生拥塞的路由器有足够时间把队列中积压的分组处理完毕。

快重传

如果发送方设置的超时计时器时限已到但还没有收到确认,那么很可能是网络出现了拥塞,致使报文段在网络中的某处被丢弃。

快重传算法首先要求接收方每收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等待自己发送数据时才进行捎带确认。在下图所示的例子中,接收方收到了M1和M2后都分别发出了确认。现假定接收方没有收到M3但接着收到了M4。显然,接收方不能确认M4,因为M4是收到的失序报文段(按照顺序的M3还没有收到)。根据可靠传输原理,接收方可以什么都不做,也可以在适当时机发送一次对M2的确认。但按照快重传算法的规定,接收方应及时发送对 M2的重复确认,这样做可以让发送方及早知道报文段M3没有到达接收方。发送方接着发送M5和M6。接收方收到后,也还要再次发出对M2的重复确认。这样,发送方共收到了接收方的四个对M2的确认,其中后三个都是重复确认。快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段M3,而不必继续等待为M3设置的重传计时器到期。由于发送方能尽早重传未被确认的报文段。

快恢复

与快重传配合使用的还有快恢复算法,其过程有以下两个要点:

  1. 当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把慢开始门限ssthresh减半。这是为了预防网络发生拥塞。请注意,接下去不执行慢开始算法。
  2. 由于发送方现在认为网络很可能没有发生拥塞(如果网络发生了严重的拥塞,就不会一连有好几个报文段连续到达接收方,也就不会导致接收方连续发送重复确认),因此与慢开始不同之处是现在不执行慢开始算法(即拥塞窗口cwnd现在不设置为1),而是把cwnd值设置为慢开始门限ssthresh减半后的数值,然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大。

3.6 TCP连接

TCP 连接的建立采用客户服务器方式。主动发起连接建立的应用进程叫做客户(client),而被动等待连接建立的应用进程叫做服务器(server)。

连接建立

TCP 连接建立的过程如下图所示,假定主机 A 运行的是 TCP 客户程序,而 B 运行 TCP 服务器程序。最初两端的TCP 进程都处于 CLOSED(关闭)状态。图中在主机下面的方框分别是 TCP 进程所处的状态。请注意,A 主动打开连接,而 B 被动打开连接。

B 被动打开连接的意思不是在连接建立前 B 不做任何事,相反,B 的 TCP 服务器进程需要先创建传输控制块 TCB,准备接受客户进程的连接请求。然后使服务器进程处于 LISTEN(收听)状态,等待客户的连接请求。

A 的 TCP 客户进程也是首先创建传输控制模块 TCB,然后在需要建立连接时向 B 发出连接请求报文段,这是建立连接的第一条报文,这时首部中的同步位 SYN = 1,同时选择一个初始序号 seq= x。TCP规定,SYN 报文段(即SYN = 1的报文段)不能携带数据,但要消耗掉一个序号。这时,TCP 客户进程进入 SYN-SENT(同步已发送)状态。

B 收到连接请求报文段后,如同意建立连接,则向 A 发送确认。在确认报文段中应把 SYN 位和 ACK 位都置1,确认号是 ack = x + 1,同时也为自己选择一个初始序号 seq = y。请注意,这个报文段也不能携带数据,但同样要消耗掉一个序号。这时TCP服务器进程进入 SYN-RCVD(同步收到)状态。

TCP 客户进程收到 B 的确认后,还要向 B 给出确认。确认报文段的 ACK 置1,确认号 ack = y + 1,而自己的序号seq = x + 1。TCP 的标准规定,这一个报文段可以携带数据。但如果不携带数据则不消耗序号,在这种情况下,下一个数据报文段的序号仍是 seq = x + 1。这时,TCP 连接已经建立,A 进入 ESTABLISHED(已建立连接)状态。

当 B 收到 A 的确认后,也进入 ESTABLISHED 状态。

整个连接建立的过程涉及三个报文的发送接收,因此叫做三次握手(three-way handshake)。

面试时经常问的,也是比较关键的一个问题是第三次确认报文的作用,这里明确:A 发送第三次确认的原因是防止已失效的连接请求报文段突然又传送到了 B,从而产生错误。

所谓“已失效的连接请求报文段”是这样产生的。假设 A 发出一个连接请求,但因连接请求报文丢失而未收到确认。于是 A 再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接。A 共发送了两个连接请求报文段,其中第一个丢失,第二个到达了 B。如果 A 发出的第一个连接请求报文段并没有丢失,而是在某些网络结点长时间滞留了,以致延误到连接释放以后的某个时间才到达 B。本来这是一个早已失效的报文段。但B 收到此失效的连接请求报文段后,就误认为是 A 又发出一次新的连接请求。于是就向 A 发出确认报文段,同意建立连接。假定不采用三次握手,那么只要 B 发出确认,新的连接就建立了。

此时由于 A 并没有发出建立连接的请求,因此不会理睬 B 的确认,也不会向 B 发送数据。但 B 却以为新的运输连接已经建立了,并一直等待 A 发来数据。B 的许多资源就这样白白浪费了。第三次的确认报文可以防止上述现象的发生。例如在刚才的情况下,A 不会向 B 的确认发出确认。B 由于收不到确认,就知道 A 并没有要求建立连接。

连接释放

数据传输结束后,通信的双方都可释放连接。现在 A 和 B 都处于 ESTABLISHED 状态。A 的应用进程先向其 TCP 发出连接释放报文段,并停止再发送数据,主动关闭 TCP 连接。A 把连接释放报文段首部的终止控制位 FIN 置1,其序号 seq = u,它等于前面已传送过的数据的最后一个字节的序号加1。这时 A 进入 FIN-WAIT-1(终止等待1)状态,等待 B 的确认。请注意,TCP规定,FIN 报文段即使不携带数据,它也消耗掉一个序号。

B 收到连接释放报文段后即发出确认,确认号是 ack = u + 1,而这个报文段自己的序号是 v,等于 B 前面已传送过的数据的最后一个字节的序号加1。然后 B 就进入 CLOSE-WAIT(关闭等待)状态。TCP 服务器进程这时应通知高层应用进程,因而从 A 到 B 这个方向的连接就释放了,这时的 TCP 连接处于半关闭(half-close)状态,即 A 已经没有数据要发送了,但 B 若发送数据,A 仍要接收。也就是说,从 B 到 A 这个方向的连接并未关闭,这个状态可能会持续一些时间。

A 收到来自 B 的确认后,就进入FIN-WAIT-2(终止等待2)状态,等待 B 发出的连接释放报文段。

若 B 已经没有要向 A 发送的数据,其应用进程就通知 TCP 释放连接。这时 B 发出的连接释放报文段必须使 FIN = 1。现假定 B 的序号为 w(在半关闭状态B可能又发送了一些数据)。B 还必须重复上次已发送过的确认号 ack = u + 1。这时 B 就进入 LAST-ACK(最后确认)状态,等待 A 的确认。

A 在收到 B 的连接释放报文段后,必须对此发出确认。在确认报文段中把 ACK 置1,确认号 ack = w + 1,而自己的序号是 seq = u + 1(根据TCP标准,前面发送过的FIN报文段要消耗一个序号)。然后进入到 TIME-WAIT(时间等待)状态。请注意,现在 TCP 连接还没有释放掉。必须经过时间等待计时器(TIME-WAIT timer)设置的时间2MSL 后,A 才进入到 CLOSED 状态。时间 MSL 叫做最长报文段寿命(Maximum Segment Lifetime),RFC 793建议设为2分钟。但这完全是从工程上来考虑,对于现在的网络,MSL = 2分钟可能太长了一些。因此 TCP 允许不同的实现可根据具体情况使用更小的 MSL 值。因此,从 A 进入到 TIME-WAIT 状态后,要经过4分钟才能进入到 CLOSED 状态,才能开始建立下一个新的连接。当A撤销相应的传输控制块 TCB 后,就结束了这次的 TCP 连接

为什么 A 在 TIME-WAIT 状态必须等待 2MSL 的时间呢?这有两个理由。

第一,为了保证 A 发送的最后一个 ACK 报文段能够到达 B。这个 ACK 报文段有可能丢失,因而使处在 LAST-ACK状态的 B 收不到对已发送的 FIN+ ACK 报文段的确认。B 会超时重传这个 FIN + ACK 报文段,而 A 就能在 2MSL 时间内收到这个重传的 FIN + ACK 报文段。接着 A 重传一次确认,重新启动 2MSL 计时器。最后,A 和 B 都正常进入到 CLOSED 状态。如果 A 在 TIME-WAIT 状态不等待一段时间,而是在发送完 ACK 报文段后立即释放连接,那么就无法收到 B 重传的 FIN + ACK 报文段,因而也不会再发送一次确认报文段。这样,B 就无法按照正常步骤进入 CLOSED 状态。

第二,防止上一节提到的“已失效的连接请求报文段”出现在本连接中。A在发送完最后一个ACK报文段后,再经过时间2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样就可以使下一个新的连接中不会出现这种旧的连接请求报文段。

B 只要收到了 A 发出的确认,就进入 CLOSED 状态。同样,B 在撤销相应的传输控制块 TCB 后,就结束了这次的TCP 连接。我们注意到,B 结束 TCP 连接的时间要比 A 早一些。

上述的 TCP 连接释放过程叫做四次握手。

除时间等待计时器外,TCP还设有一个保活计时器(keepalive timer)。设想有这样的情况:客户已主动与服务器建立了TCP连接。但后来客户端的主机突然出故障。显然,服务器以后就不能再收到客户发来的数据。因此,应当有措施使服务器不要再白白等待下去。这就是使用保活计时器。服务器每收到一次客户的数据,就重新设置保活计时器,时间的设置通常是两小时。若两小时没有收到客户的数据,服务器就发送一个探测报文段,以后则每隔75分钟发送一次。若一连发送10个探测报文段后仍无客户的响应,服务器就认为客户端出了故障,接着就关闭这个连接。

支付宝
微信
0%