网络协议 TCP
UDP 是一种没有复杂的控制,提供无连接通信服务的一种协议,换句话说,它将部分控制部分交给应用程序去处理,自己只提供作为传输层协议最基本的功能。
而与 UDP 不同的是,同样作为传输层协议,TCP 协议要比 UDP 的功能多很多。
TCP 的全称是 Transmission Control Protocol,它被称为是一种面向连接(connection-oriented) 的协议,这是因为一个应用程序开始向另一个应用程序发送数据之前,这两个进程必须先进行握手,握手是一个逻辑连接,并不是两个主机之间进行真实的握手。
TCP协议详解 - 图1
这个连接是指各种设备、线路或者网络中进行通信的两个应用程序为了相互传递消息而专有的、虚拟的通信链路,也叫做虚拟电路。
一旦主机 A 和主机 B 建立了连接,那么进行通信的应用程序只使用这个虚拟的通信线路发送和接收数据就可以保证数据的传输,TCP 协议负责控制连接的建立、断开、保持等工作。
TCP 连接是全双工服务(full-duplex service) 的,全双工是什么意思?全双工指的是主机 A 与另外一个主机 B 存在一条 TCP 连接,那么应用程数据就可以从主机 B 流向主机 A 的同时,也从主机 A 流向主机 B。
TCP 只能进行 点对点(point-to-point) 连接,那么所谓的多播,即一个主机对多个接收方发送消息的情况是不存在的,TCP 连接只能连接两个一对主机。
TCP协议详解 - 图2
TCP 的连接建立需要经过三次握手,这个下面再说。一旦 TCP 连接建立后,主机之间就可以相互发送数据了,客户进程通过套接字传送数据流。数据一旦通过套接字后,它就由客户中运行的 TCP 协议所控制。
TCP 会将数据临时存储到连接的发送缓存(send buffer) 中,这个 send buffer 是三次握手之间设置的缓存之一,然后 TCP 在合适的时间将发送缓存中的数据发送到目标主机的接收缓存中,实际上,每一端都会有发送缓存和接收缓存,如下所示
TCP协议详解 - 图3
主机之间的发送是以 报文段(segment) 进行的,那么什么是 Segement 呢?
TCP 会将要传输的数据流分为多个块(chunk),然后向每个 chunk 中添加 TCP 标头,这样就形成了一个 TCP 段也就是报文段。每一个报文段可以传输的长度是有限的,不能超过最大数据长度(Maximum Segment Size),俗称 MSS。在报文段向下传输的过程中,会经过链路层,链路层有一个 Maximum Transmission Unit ,最大传输单元 MTU, 即数据链路层上所能通过最大数据包的大小,最大传输单元通常与通信接口有关。
那么 MSS 和 MTU 有啥关系呢?
因为计算机网络是分层考虑的,这个很重要,不同层的称呼不一样,对于传输层来说,称为报文段而对网络层来说就叫做 IP 数据包,所以,MTU 可以认为是网络层能够传输的最大 IP 数据包,而 MSS(Maximum segment size)可以认为是传输层的概念,也就是 TCP 数据包每次能够传输的最大量

TCP 报文段结构

在简单聊了聊 TCP 连接后,下面就来聊一下 TCP 的报文段结构,如下图所示
TCP协议详解 - 图4
TCP 报文段结构相比 UDP 报文结构多了很多内容。但是前两个 32 比特的字段是一样的。它们是 源端口号 和 目标端口号,这两个字段是用于多路复用和多路分解的。另外,和 UDP 一样,TCP 也包含校验和(checksum field) ,除此之外,TCP 报文段首部还有下面这些

  • 32 比特的序号字段(sequence number field) 和 32 比特的确认号字段(acknowledgment number field) 。这些字段被 TCP 发送方和接收方用来实现可靠的数据传输。
  • 4 比特的首部字段长度字段(header length field),这个字段指示了以 32 比特的字为单位的 TCP 首部长度。TCP 首部的长度是可变的,但是通常情况下,选项字段为空,所以 TCP 首部字段的长度是 20 字节。
  • 16 比特的 接受窗口字段(receive window field) ,这个字段用于流量控制。它用于指示接收方能够/愿意接受的字节数量
  • 可变的选项字段(options field),这个字段用于发送方和接收方协商最大报文长度,也就是 MSS 时使用
  • 6 比特的 标志字段(flag field), ACK 标志用于指示确认字段中的值是有效的,这个报文段包括一个对已被成功接收报文段的确认;RST、SYN、FIN 标志用于连接的建立和关闭;CWR 和 ECE 用于拥塞控制;PSH 标志用于表示立刻将数据交给上层处理;URG 标志用来表示数据中存在需要被上层处理的 紧急 数据。紧急数据最后一个字节由 16 比特的紧急数据指针字段(urgeent data pointer field) 指出。一般情况下,PSH 和 URG 并没有使用。

TCP 的各种功能和特点都是通过 TCP 报文结构来体现的,在聊完 TCP 报文结构之后,下面就来聊一下 TCP 有哪些功能及其特点了。

序号、确认号实现传输可靠性

TCP 报文段首部中两个最重要的字段就是 序号 和 确认号,这两个字段是 TCP 实现可靠性的基础,那么你肯定好奇如何实现可靠性呢?要了解这一点,首先得先知道这两个字段里面存了哪些内容吧?
一个报文段的序号就是数据流的字节编号 。因为 TCP 会把数据流分割成为一段一段的字节流,因为字节流本身是有序的,所以每一段的字节编号就是标示是哪一段的字节流。比如,主机 A 要给主机 B 发送一条数据。数据经过应用层产生后会有一串数据流,数据流会经过 TCP 分割,分割的依据就是 MSS,假设数据是 10000 字节,MSS 是 2000 字节,那么 TCP 就会把数据拆分成 0 - 1999 , 2000 - 3999 的段,依次类推。
所以,第一个数据 0 - 1999 的首字节编号就是 0 ,2000 - 3999 的首字节编号就是 2000 。
然后,每个序号都会被填入 TCP 报文段首部的序号字段中。
TCP协议详解 - 图5
至于确认号的话,会比序号要稍微麻烦一些。这里先拓展下几种通信模型。

  • 单工通信:单工数据传输只支持数据在一个方向上传输;在同一时间只有一方能接受或发送信息,不能实现双向通信,比如广播、电视等。
  • 双工通信是一种点对点系统,由两个或者多个在两个方向上相互通信的连接方或者设备组成。双工通信模型有两种:全双工(FDX)和半双工(HDX)
  • 全双工:在全双工系统中,连接双方可以相互通信,一个最常见的例子就是电话通信。全双工通信是两个单工通信方式的结合,它要求发送设备和接收设备都有独立的接收和发送能力。
  • 半双工:在半双工系统中,连接双方可以彼此通信,但不能同时通信,比如对讲机,只有把按钮按住的人才能够讲话,只有一个人讲完话后另外一个人才能讲话。

单工、半双工、全双工通信如下图所示
TCP协议详解 - 图6
TCP 是一种全双工的通信协议,因此主机 A 在向主机 B 发送消息的过程中,也在接受来自主机 B 的数据。主机 A 填充进报文段的确认号是期望从主机 B 收到的下一字节的序号。稍微有点绕,来举个例子看一下。比如主机 A 收到了来自主机 B 发送的编号为 0 - 999 字节的报文段,这个报文段会写入序号中,随后主机 A 期望能够从主机 B 收到 1000 - 剩下的报文段,因此,主机 A 发送到主机 B 的报文段中,它的确认号就是 1000 。

累积确认

这里再举出一个例子,比如主机 A 在发送 0 - 999 报文段后,期望能够接受到 1000 之后的报文段,但是主机 B 却给主机 A 发送了一个 1500 之后的报文段,那么主机 A 是否还会继续进行等待呢?
答案显然是会的,因为 TCP 只会确认流中至第一个丢失字节为止的字节,因为 1500 虽然属于 1000 之后的字节,但是主机 B 没有给主机 A 发送 1000 - 1499 之间的字节,所以主机 A 会继续等待。
在了解完序号和确认号之后,下面来聊一下 TCP 的发送过程。下面是一个正常的发送过程
TCP协议详解 - 图7
TCP 通过肯定的确认应答(ACK) 来实现可靠的数据传输,当主机 A将数据发出之后会等待主机 B 的响应。如果有确认应答(ACK),说明数据已经成功到达对端。反之,则数据很可能会丢失。
如下图所示,如果在一定时间内主机 A 没有等到确认应答,则认为主机 B 发送的报文段已经丢失,并进行重发。
TCP协议详解 - 图8
主机 A 给主机 B 的响应可能由于网络抖动等原因无法到达,那么在经过特定的时间间隔后,主机 A 将重新发送报文段。
主机 A 没有收到主机 B 的响应还可能是因为主机 B 在发送给主机 A 的过程中丢失。
TCP协议详解 - 图9
如上图所示,由主机 B 返回的确认应答,由于网络拥堵等原因在传送的过程中丢失,并没有到达主机 A。主机 A 会等待一段时间,如果在这段时间内主机 A 仍没有等到主机 B 的响应,那么主机 A 会重新发送报文段。
那么现在就存在一个问题,如果主机 A 给主机 B 发送了一个报文段后,主机 B 接受到报文段发送响应,此刻由于网络原因,这个报文段并未到达,等到一段时间后主机 A 重新发送报文段,然后此时主机 B 发送的响应在主机 A 第二次发送后失序到达主机 A,那么主机 A 应该如何处理呢?
TCP协议详解 - 图10
TCP RFC 并未为此做任何规定,也就是说,可以自己决定如何处理失序到达的报文段。一般处理方式有两种

  • 接收方立刻丢弃失序的报文段
  • 接收方接受失序到达的报文段,并等待后续的报文段

一般来说通常采取的做法是第二种。

传输控制

利用窗口控制提高速度

前面介绍了 TCP 是以数据段的形式进行发送,如果经过一段时间内主机 A 等不到主机 B 的响应,主机 A 就会重新发送报文段,接受到主机 B 的响应,再会继续发送后面的报文段,现在看到,这一问一答的形式还存在许多条件,比如响应未收到、等待响应等,那么对崇尚性能的互联网来说,这种形式的性能应该不会很高。
TCP协议详解 - 图11
那么如何提升性能呢?
为了解决这个问题,TCP 引入了 窗口 这个概念,即使在往返时间较长、频次很多的情况下,它也能控制网络性能的下降,听起来很牛批,那它是如何实现的呢?
如下图所示
TCP协议详解 - 图12
之前每次请求发送都是以报文段的形式进行的,引入窗口后,每次请求都可以发送多个报文段,也就是说一个窗口可以发送多个报文段。窗口大小就是指无需等待确认应答就可以继续发送报文段的最大值。
在这个窗口机制中,大量使用了 缓冲区 ,通过对多个段同时进行确认应答的功能。
如下图所示,发送报文段中高亮部分即是提到的窗口,在窗口内,即使没有收到确认应答也可以把请求发送出去。不过,在整个窗口的确认应答没有到达之前,如果部分报文段丢失,那么主机 A 将仍会重传。为此,主机 A 需要设置缓存来保留这些需要重传的报文段,直到收到他们的确认应答。
TCP协议详解 - 图13
在滑动窗口以外的部分是尚未发送的报文段和已经接受到的报文段,如果报文段已经收到确认则不可进行重发,此时报文段就可以从缓冲区中清除。
在收到确认的情况下,会将窗口滑动到确认应答中确认号的位置,如上图所示,这样可以顺序的将多个段同时发送,用以提高通信性能,这种窗口也叫做 滑动窗口(Sliding window)。

窗口控制和重发

报文段的发送和接收,必然伴随着报文段的丢失和重发,窗口也是同样如此,如果在窗口中报文段发送过程中出现丢失怎么办?
首先先考虑确认应答没有返回的情况。在这种情况下,主机 A 发送的报文段到达主机 B,是不需要再进行重发的。这和单个报文段的发送不一样,如果发送单个报文段,即使确认应答没有返回,也要进行重发
TCP协议详解 - 图14
窗口在一定程度上比较大时,即使有少部分确认应答的丢失,也不会重新发送报文段。
如果在某个情况下由于发送的报文段丢失,导致接受主机未收到请求,或者主机返回的响应未到达客户端的话,会经过一段时间重传报文。那么在使用窗口的情况下,报文段丢失会怎么样呢?
如下图所示,报文段 0 - 999 丢失后,但是主机 A 并不会等待,主机 A 会继续发送余下的报文段,主机 B 发送的确认应答却一直是 1000,同一个确认号的应答报文会被持续不断的返回,如果发送端主机在连续 3 次收到同一个确认应答后,就会将其所对应的数据重发,这种机制要比之前提到的超时重发更加高效,这种机制也被称为 高速重发控制。这种重发的确认应答也被称为 冗余 ACK(响应)。
TCP协议详解 - 图15
主机 B 在没有接收到自己期望序列号的报文段时,会对之前收到的数据进行确认应答。发送端则一旦收到某个确认应答后,又连续三次收到同样的确认应答,那么就会认为报文段已经丢失。需要进行重发。使用这种机制可以提供更为快速的重发服务。

流量控制

前面聊的是传输控制,下面再说一下 流量控制。在每个 TCP 连接的一侧主机都会有一个 socket 缓冲区,缓冲区会为每个连接设置接收缓存和发送缓存,当 TCP 建立连接后,从应用程序产生的数据就会到达接收方的接收缓冲区中,接收方的应用程序并不一定会马上读取缓冲区的数据,它需要等待操作系统分配时间片。如果此时发送方的应用程序产生数据过快,而接收方读取接受缓冲区的数据相对较慢的话,那么接收方中缓冲区的数据将会溢出。
但是还好,TCP 有 流量控制服务(flow-control service) 用于消除缓冲区溢出的情况。流量控制是一个速度匹配服务,即发送方的发送速率与接受方应用程序的读取速率相匹配。
TCP 通过使用一个 接收窗口(receive window) 的变量来提供流量控制。接受窗口会给发送方一个指示到底还有多少可用的缓存空间。发送端会根据接收端的实际接受能力来控制发送的数据量。
接收端主机向发送端主机通知自己可以接收数据的大小,发送端会发送不超过这个限度的数据,这个大小限度就是窗口大小,还记得 TCP 的首部么,有一个接收窗口,上面聊的时候说这个字段用于流量控制。它用于指示接收方能够/愿意接受的字节数量。
那么只知道这个字段用于流量控制,那么如何控制呢?
发送端主机会定期发送一个窗口探测包,这个包用于探测接收端主机是否还能够接受数据,当接收端的缓冲区一旦面临数据溢出的风险时,窗口大小的值也随之被设置为一个更小的值通知发送端,从而控制数据发送量。
下面是一个流量控制示意图
TCP协议详解 - 图16
发送端主机根据接收端主机的窗口大小进行流量控制。由此也可以防止发送端主机一次发送过大数据导致接收端主机无法处理。
如上图所示,当主机 B 收到报文段 2000 - 2999 之后缓冲区已满,不得不暂时停止接收数据。然后主机 A 发送窗口探测包,窗口探测包非常小仅仅一个字节。然后主机 B 更新缓冲区接收窗口大小并发送窗口更新通知给主机 A,然后主机 A 再继续发送报文段。
在上面的发送过程中,窗口更新通知可能会丢失,一旦丢失发送端就不会发送数据,所以窗口探测包会随机发送,以避免这种情况发生。

连接管理

在继续介绍下面有意思的特性之前,先来把关注点放在 TCP 的连接管理上,因为没有 TCP 连接,也就没有后续的一系列 TCP 特性什么事儿了。假设运行在一台主机上的进程想要和另一台主机上的进程建立一条 TCP 连接,那么客户中的 TCP 会使用下面这些步骤与服务器中的 TCP 建立连接。

  • 首先,客户端首先向服务器发送一个特殊的 TCP 报文段。这个报文段首部不包含应用层数据,但是在报文段的首部中有一个 SYN 标志位 被置为 1。因此,这个特殊的报文段也可以叫做 SYN 报文段。然后,客户端随机选择一个初始序列号(client_isn) ,并将此数字放入初始 TCP SYN 段的序列号字段中,SYN 段又被封装在 IP 数据段中发送给服务器。
  • 一旦包含 IP 数据段到达服务器后,服务端会从 IP 数据段中提取 TCP SYN 段,将 TCP 缓冲区和变量分配给连接,然后给客户端发送一个连接所允许的报文段。这个连接所允许的报文段也不包括任何应用层数据。然而,它却包含了三个非常重要的信息。

这些缓冲区和变量的分配使 TCP 容易受到称为 SYN 泛洪的拒绝服务攻击。

  • 首先,SYN 比特被置为 1 。
  • 然后,TCP 报文段的首部确认号被设置为 client_isn + 1。
  • 最后,服务器选择自己的初始序号(server_isn),并将其放置到 TCP 报文段首部的序号字段中。如果用大白话解释下就是,我收到了你发起建立连接的 SYN 报文段,这个报文段具有首部字段 client_isn。我同意建立该连接,我自己的初始序号是 server_isn。这个允许连接的报文段被称为 SYNACK 报文段
  • 第三步,在收到 SYNACK 报文段后,客户端也要为该连接分配缓冲区和变量。客户端主机向服务器发送另外一个报文段,最后一个报文段对服务器发送的响应报文做了确认,确认的标准是客户端发送的数据段中确认号为 server_isn + 1,因为连接已经建立,所以 SYN 比特被置为 0 。以上就是 TCP 建立连接的三次数据段发送过程,也被称为 三次握手。

一旦完成这三个步骤,客户和服务器主机就可以相互发送报文段了,在以后的每一个报文段中,SYN 比特都被置为 0 ,整个过程描述如下图所示
TCP协议详解 - 图17
在客户端主机和服务端主机建立连接后,参与一条 TCP 连接的两个进程中的任何一个都能终止 TCP 连接。连接结束后,主机中的缓存和变量将会被释放。假设客户端主机想要终止 TCP 连接,它会经历如下过程
客户应用进程发出一个关闭命令,客户 TCP 向服务器进程发送一个特殊的 TCP 报文段,这个特殊的报文段的首部标志 FIN 被设置为 1 。当服务器收到这个报文段后,就会向发送方发送一个确认报文段。然后,服务器发送它自己的终止报文段,FIN 位被设置为 1 。客户端对这个终止报文段进行确认。此时,在两台主机上用于该连接的所有资源都被释放了,如下图所示
TCP协议详解 - 图18
在一个 TCP 连接的生命周期内,运行在每台主机中的 TCP 协议都会在各种 TCP 状态(TCP State) 之间进行变化,TCP 的状态主要有 LISTEN、SYN-SEND、SYN-RECEIVED、ESTABLISHED、FIN-WAIT-1、FIN-WAIT-2、CLOSE-WAIT、CLOSING、LAST-ACK、TIME-WAIT 和 CLOSED 。这些状态的解释如下

  • LISTEN: 表示等待任何来自远程 TCP 和端口的连接请求。
  • SYN-SEND: 表示发送连接请求后等待匹配的连接请求。
  • SYN-RECEIVED: 表示已接收并发送连接请求后等待连接确认,也就是 TCP 三次握手中第二步后服务端的状态
  • ESTABLISHED: 表示已经连接已经建立,可以将应用数据发送给其他主机

上面这四种状态是 TCP 三次握手所涉及的。

  • FIN-WAIT-1: 表示等待来自远程 TCP 的连接终止请求,或者等待先前发送的连接终止请求的确认。
  • FIN-WAIT-2: 表示等待来自远程 TCP 的连接终止请求。
  • CLOSE-WAIT: 表示等待本地用户的连接终止请求。
  • CLOSING: 表示等待来自远程 TCP 的连接终止请求确认。
  • LAST-ACK: 表示等待先前发送给远程 TCP 的连接终止请求的确认(包括对它的连接终止请求的确认)。
  • TIME-WAIT: 表示等待足够的时间以确保远程 TCP 收到其连接终止请求的确认。
  • CLOSED: 表示连接已经关闭,无连接状态。

上面 7 种状态是 TCP 四次挥手,也就是断开链接所设计的。
TCP 的连接状态会进行各种切换,这些 TCP 连接的切换是根据事件进行的,这些事件由用户调用:OPEN、SEND、RECEIVE、CLOSE、ABORT 和 STATUS。涉及到 TCP 报文段的标志有 SYN、ACK、RST 和 FIN ,当然,还有超时。
下面加上 TCP 连接状态后,再来看一下三次握手和四次挥手的过程。

三次握手建立连接

下图画出了 TCP 连接建立的过程。假设图中左端是客户端主机,右端是服务端主机,一开始,两端都处于CLOSED(关闭)状态。
TCP协议详解 - 图19

  1. 服务端进程准备好接收来自外部的 TCP 连接,一般情况下是调用 bind、listen、socket 三个函数完成。这种打开方式被认为是 被动打开(passive open)。然后服务端进程处于 LISTEN 状态,等待客户端连接请求。
  2. 客户端通过 connect 发起主动打开(active open),向服务器发出连接请求,请求中首部同步位 SYN = 1,同时选择一个初始序号 sequence ,简写 seq = x。SYN 报文段不允许携带数据,只消耗一个序号。此时,客户端进入 SYN-SEND 状态。
  3. 服务器收到客户端连接后,,需要确认客户端的报文段。在确认报文段中,把 SYN 和 ACK 位都置为 1 。确认号是 ack = x + 1,同时也为自己选择一个初始序号 seq = y。请注意,这个报文段也不能携带数据,但同样要消耗掉一个序号。此时,TCP 服务器进入 SYN-RECEIVED(同步收到) 状态。
  4. 客户端在收到服务器发出的响应后,还需要给出确认连接。确认连接中的 ACK 置为 1 ,序号为 seq = x + 1,确认号为 ack = y + 1。TCP 规定,这个报文段可以携带数据也可以不携带数据,如果不携带数据,那么下一个数据报文段的序号仍是 seq = x + 1。这时,客户端进入 ESTABLISHED (已连接) 状态
  5. 服务器收到客户的确认后,也进入 ESTABLISHED 状态。

TCP 建立一个连接需要三个报文段,释放一个连接却需要四个报文段。

四次挥手

数据传输结束后,通信的双方可以释放连接。数据传输结束后的客户端主机和服务端主机都处于 ESTABLISHED 状态,然后进入释放连接的过程。
TCP协议详解 - 图20
TCP 断开连接需要历经的过程如下

  1. 客户端应用程序发出释放连接的报文段,并停止发送数据,主动关闭 TCP 连接。客户端主机发送释放连接的报文段,报文段中首部 FIN 位置为 1 ,不包含数据,序列号位 seq = u,此时客户端主机进入 FIN-WAIT-1(终止等待 1) 阶段。
  2. 服务器主机接受到客户端发出的报文段后,即发出确认应答报文,确认应答报文中 ACK = 1,生成自己的序号位 seq = v,ack = u + 1,然后服务器主机就进入 CLOSE-WAIT(关闭等待) 状态,这个时候客户端主机 -> 服务器主机这条方向的连接就释放了,客户端主机没有数据需要发送,此时服务器主机是一种半连接的状态,但是服务器主机仍然可以发送数据。
  3. 客户端主机收到服务端主机的确认应答后,即进入 FIN-WAIT-2(终止等待2) 的状态。等待客户端发出连接释放的报文段。
  4. 当服务器主机没有数据发送后,应用进程就会通知 TCP 释放连接。这时服务端主机会发出断开连接的报文段,报文段中 ACK = 1,序列号 seq = w,因为在这之间可能已经发送了一些数据,所以 seq 不一定等于 v + 1。ack = u + 1,在发送完断开请求的报文后,服务端主机就进入了 LAST-ACK(最后确认)的阶段。
  5. 客户端收到服务端的断开连接请求后,客户端需要作出响应,客户端发出断开连接的报文段,在报文段中,ACK = 1, 序列号 seq = u + 1,因为客户端从连接开始断开后就没有再发送数据,ack = w + 1,然后进入到 TIME-WAIT(时间等待) 状态,请注意,这个时候 TCP 连接还没有释放。必须经过时间等待的设置,也就是 2MSL 后,客户端才会进入 CLOSED 状态,时间 MSL 叫做最长报文段寿命(Maximum Segment Lifetime)。
  6. 服务端主要收到了客户端的断开连接确认后,就会进入 CLOSED 状态。因为服务端结束 TCP 连接时间要比客户端早,而整个连接断开过程需要发送四个报文段,因此释放连接的过程也被称为四次挥手。

    什么是 TIME-WAIT

    上面只是简单提到了一下 TIME-WAIT 状态和 2MSL 是啥,下面来聊一下这两个概念。
    MSL 是 TCP 报文段可以存活或者驻留在网络中的最长时间。RFC 793 定义了 MSL 的时间是两分钟,但是具体的实现还要根据程序员来指定,一些实现采用了 30 秒的这个最大存活时间。
    那么为什么要等待 2MSL 呢?
    主要是因为两个理由
  • 为了保证最后一个响应能够到达服务器,因为在计算机网络中,最后一个 ACK 报文段可能会丢失,从而致使客户端一直处于 LAST-ACK 状态等待客户端响应。这时候服务器会重传一次 FINACK 断开连接报文,客户端接收后再重新确认,重启定时器。如果客户端不是 2MSL ,在客户端发送 ACK 后直接关闭的话,如果报文丢失,那么双方主机会无法进入 CLOSED 状态。
  • 还可以防止已失效的报文段。客户端在发送最后一个 ACK 之后,再经过经过 2MSL,就可以使本链接持续时间内所产生的所有报文段都从网络中消失。从而保证在关闭连接后不会有还在网络中滞留的报文段去骚扰服务器。

这里注意一点:在服务器发送了 FIN-ACK 之后,会立即启动超时重传计时器。客户端在发送最后一个 ACK 之后会立即启动时间等待计时器。

说好的 RST 呢

说好的 RST、SYN、FIN 标志用于连接的建立和关闭,那么 SYN 和 FIN 都现身了,那 RST 呢?上面探讨的都是一种理想的情况,就是客户端服务器双方都会接受传输报文段的情况,还有一种情况是当主机收到 TCP 报文段后,其 IP 和端口号不匹配的情况。假设客户端主机发送一个请求,而服务器主机经过 IP 和端口号的判断后发现不是给这个服务器的,那么服务器就会发出一个 RST 特殊报文段给客户端。
TCP协议详解 - 图21
因此,当服务端发送一个 RST 特殊报文段给客户端的时候,它就会告诉客户端没有匹配的套接字连接,请不要再继续发送了。
上面探讨的是 TCP 的情况,那么 UDP 呢?
使用 UDP 作为传输协议后,如果套接字不匹配的话,UDP 主机就会发送一个特殊的 ICMP 数据报。

SYN 洪泛攻击

下面来讨论一下什么是 SYN 洪泛攻击。
在 TCP 的三次握手中已经看到,服务器为了响应一个收到的 SYN,分配并初始化变量连接和缓存,然后服务器发送一个 SYNACK 作为响应,然后等待来自于客户端的 ACK 报文。如果客户端不发送 ACK 来完成最后一步的话,那么这个连接就处在一个挂起的状态,也就是半连接状态。
攻击者通常在这种情况下发送大量的 TCP SYN 报文段,服务端继续响应,但是每个连接都完不成三次握手的步骤。随着 SYN 的不断增加,服务器会不断的为这些半开连接分配资源,导致服务器的连接最终被消耗殆尽。这种攻击也是属于 Dos 攻击的一种。
抵御这种攻击的方式是使用 SYN cookie ,下面是它的工作流程介绍

  • 当服务器收到一个 SYN 报文段时,它并不知道这个报文段是来自哪里,是来自攻击者主机还是客户端主机(虽然攻击者也是客户端,不过这么说更便于区分) 。因此服务器不会为报文段生成一个半开连接。与此相反,服务器生成一个初始的 TCP 序列号,这个序列号是 SYN 报文段的源和目的 IP 地址与端口号这个四元组构造的一个复杂的散列函数,这个散列函数生成的 TCP 序列号就是 SYN Cookie,用于缓存 SYN 请求。然后,服务器会发送带着 SYN Cookie 的 SYNACK 分组。有一点需要注意的是,服务器不会记忆这个 Cookie 或 SYN 的其他状态信息
  • 如果客户端不是攻击者的话,它就会返回一个 ACK 报文段。当服务器收到这个 ACK 后,需要验证这个 ACK 与 SYN 发送的是否相同,验证的标准就是确认字段中的确认号和序列号,源和目的 IP 地址与端口号以及和散列函数的是否一致,散列函数的结果 + 1 是否和 SYNACK 中的确认值相同。(大致是这样,说的不对还请读者纠正) 。如果有兴趣读者可以自行深入了解。如果是合法的,服务器就会生成一个具有套接字的全开连接。
  • 如果客户端没有返回 ACK,即认为是攻击者,那么这样也没关系,服务器没有收到 ACK,不会分配变量和缓存资源,不会对服务器产生危害。

    拥塞控制

    有了 TCP 的窗口控制后,使计算机网络中两个主机之间不再是以单个数据段的形式发送了,而是能够连续发送大量的数据包。然而,大量数据包同时也伴随着其他问题,比如网络负载、网络拥堵等问题。TCP 为了防止这类问题的出现,使用了 拥塞控制 机制,拥塞控制机制会在面临网络拥塞时遏制发送方的数据发送。
    拥塞控制主要有两种方法

  • 端到端的拥塞控制: 因为网络层没有为运输层拥塞控制提供显示支持。所以即使网络中存在拥塞情况,端系统也要通过对网络行为的观察来推断。TCP 就是使用了端到端的拥塞控制方式。IP 层不会向端系统提供有关网络拥塞的反馈信息。那么 TCP 如何推断网络拥塞呢?如果超时或者三次冗余确认就被认为是网络拥塞,TCP 会减小窗口的大小,或者增加往返时延来避免

  • 网络辅助的拥塞控制: 在网络辅助的拥塞控制中,路由器会向发送方提供关于网络中拥塞状态的反馈。这种反馈信息就是一个比特信息,它指示链路中的拥塞情况。

下图描述了这两种拥塞控制方式
TCP协议详解 - 图22

TCP 拥塞控制

TCP 实现可靠性的基础就是使用序号和确认号。除此之外,另外一个实现 TCP 可靠性基础的就是 TCP 的拥塞控制。如果说
TCP 所采用的方法是让每一个发送方根据所感知到的网络的拥塞程度来限制发出报文段的速率,如果 TCP 发送方感知到没有什么拥塞,则 TCP 发送方会增加发送速率;如果发送方感知沿着路径有阻塞,那么发送方就会降低发送速率。
但是这种方法有三个问题

  1. TCP 发送方如何限制它向其他连接发送报文段的速率呢?
  2. 一个 TCP 发送方是如何感知到网络拥塞的呢?
  3. 当发送方感知到端到端的拥塞时,采用何种算法来改变其发送速率呢?

先来探讨一下第一个问题,TCP 发送方如何限制它向其他连接发送报文段的速率呢
TCP 是由接收缓存、发送缓存和变量(LastByteRead, rwnd,等)组成。发送方的 TCP 拥塞控制机制会跟踪一个变量,即 拥塞窗口(congestion window) 的变量,拥塞窗口表示为 cwnd,用于限制 TCP 在接收到 ACK 之前可以发送到网络的数据量。而接收窗口(rwnd) 是一个用于告诉接收方能够接受的数据量。
一般来说,发送方未确认的数据量不得超过 cwnd 和 rwnd 的最小值,也就是
LastByteSent - LastByteAcked <= min(cwnd,rwnd)
由于每个数据包的往返时间是 RTT,假设接收端有足够的缓存空间用于接收数据,就不用考虑 rwnd 了,只专注于 cwnd,那么,该发送方的发送速率大概是 cwnd/RTT 字节/秒 。通过调节 cwnd,发送方因此能调整它向连接发送数据的速率。
一个 TCP 发送方是如何感知到网络拥塞的呢
这个上面讨论过,是 TCP 根据超时或者 3 个冗余 ACK 来感知的。
当发送方感知到端到端的拥塞时,采用何种算法来改变其发送速率呢 ?
这个问题比较复杂,一般来说,TCP 会遵循下面这几种指导性原则

  • 如果在报文段发送过程中丢失,那就意味着网络拥堵,此时需要适当降低 TCP 发送方的速率。
  • 一个确认报文段指示发送方正在向接收方传递报文段,因此,当对先前未确认报文段的确认到达时,能够增加发送方的速率。为啥呢?因为未确认的报文段到达接收方也就表示着网络不拥堵,能够顺利到达,因此发送方拥塞窗口长度会变大,所以发送速率会变快
  • 带宽探测,带宽探测说的是 TCP 可以通过调节传输速率来增加/减小 ACK 到达的次数,如果出现丢包事件,就会减小传输速率。因此,为了探测拥塞开始出现的频率, TCP 发送方应该增加它的传输速率。然后慢慢使传输速率降低,进而再次开始探测,看看拥塞开始速率是否发生了变化。

在了解完 TCP 拥塞控制后,下面就该聊一下 TCP 的 拥塞控制算法(TCP congestion control algorithm) 了。TCP 拥塞控制算法主要包含三个部分:慢启动、拥塞避免、快速恢复,下面依次来看一下

慢启动

当一条 TCP 开始建立连接时,cwnd 的值就会初始化为一个 MSS 的较小值。这就使得初始发送速率大概是 MSS/RTT 字节/秒 ,比如要传输 1000 字节的数据,RTT 为 200 ms ,那么得到的初始发送速率大概是 40 kb/s 。实际情况下可用带宽要比这个 MSS/RTT 大得多,因此 TCP 想要找到最佳的发送速率,可以通过 慢启动(slow-start) 的方式,在慢启动的方式中,cwnd 的值会初始化为 1 个 MSS,并且每次传输报文确认后就会增加一个 MSS,cwnd 的值会变为 2 个 MSS,这两个报文段都传输成功后每个报文段 + 1,会变为 4 个 MSS,依此类推,每成功一次 cwnd 的值就会翻倍。如下图所示
TCP协议详解 - 图23
发送速率不可能会一直增长,增长总有结束的时候,那么何时结束呢?慢启动通常会使用下面这几种方式结束发送速率的增长。

  • 如果在慢启动的发送过程出现丢包的情况,那么 TCP 会将发送方的 cwnd 设置为 1 并重新开始慢启动的过程,此时会引入一个 ssthresh(慢启动阈值) 的概念,它的初始值就是产生丢包的 cwnd 的值 / 2,即当检测到拥塞时,ssthresh 的值就是窗口值的一半。
  • 第二种方式是直接和 ssthresh 的值相关联,因为当检测到拥塞时,ssthresh 的值就是窗口值的一半,那么当 cwnd > ssthresh 时,每次翻番都可能会出现丢包,所以最好的方式就是 cwnd 的值 = ssthresh ,这样 TCP 就会转为拥塞控制模式,结束慢启动。
  • 慢启动结束的最后一种方式就是如果检测到 3 个冗余 ACK,TCP 就会执行一种快速重传并进入恢复状态。

    拥塞避免

    当 TCP 进入拥塞控制状态后,cwnd 的值就等于拥塞时值的一半,也就是 ssthresh 的值。所以,无法每次报文段到达后都将 cwnd 的值再翻倍。而是采用了一种相对保守的方式,每次传输完成后只将 cwnd 的值增加一个 MSS,比如收到了 10 个报文段的确认,但是 cwnd 的值只增加一个 MSS。这是一种线性增长模式,它也会有增长逾值,它的增长逾值和慢启动一样,如果出现丢包,那么 cwnd 的值就是一个 MSS,ssthresh 的值就等于 cwnd 的一半;或者是收到 3 个冗余的 ACK 响应也能停止 MSS 增长。如果 TCP 将 cwnd 的值减半后,仍然会收到 3 个冗余 ACK,那么就会将 ssthresh 的值记录为 cwnd 值的一半,进入 快速恢复 状态。

    快速恢复

    在快速恢复中,对于使 TCP 进入快速恢复状态缺失的报文段,对于每个收到的冗余 ACK,cwnd 的值都会增加一个 MSS 。当对丢失报文段的一个 ACK 到达时,TCP 在降低 cwnd 后进入拥塞避免状态。如果在拥塞控制状态后出现超时,那么就会迁移到慢启动状态,cwnd 的值被设置为 1 个 MSS,ssthresh 的值设置为 cwnd 的一半。