注:本文为 “ RFC793” 相关文章合辑。
RFC793-TCP 中文翻译
编码那些事儿已于 2022-07-14 16:02:16 修改
简介
翻译自:
- RFC 793 - Transmission Control Protocol
https://datatracker.ietf.org/doc/html/rfc793
TCP 是一个高可靠的主机到主机之间的通讯协议,这些主机通常位于分组交换网络(基于 IP 协议)之中。
本文档主要描述 TCP 的一些基本功能、实现时的相关步骤、以及在使用 TCP 时用户和程序需要关心的相关接口。
动机(Motivation)
计算机通讯系统在军事、政府、社会中越来越重要。 本文档主要关注军事方面的通讯需求,特别是如何实现可靠的通讯以及在出现网络拥塞时如何保证网络可用性,这些问题也同样出现在政府和社会通讯系统中。
随着战略战术计算机通讯网络的研发和部署,有必要提供一些连通这些网络的方法以及可以支持大部分应用进程可以互相通讯的标准协议。出于对这种标准的需要,负责研究与工程的副部长助理将本文讨论的 TCP 协议作为美国国防部系统中进程通讯协议标准的基础。
TCP 是面向连接的、端到端的可靠通讯协议,它被设计到一个支持多网络应用的层级网络协议栈中。TCP 主要为两个不同主机中的进程提供可靠的通讯服务,这两个主机处于独立并且相互连通的计算机通讯网络中。在网络协议栈(OSI 七层或者 TCP/IP 五层网络架构)中,处于 TCP 下面的网络协议不保证数据通讯的可靠性。TCP 假定会从下层协议获取简单的但是可能乱序的数据报服务。原则上 TCP 可以在很多种类的通讯系统上运行,其中包括分组交换网络、电路交换网络(虚电路)、硬线连接的网络等。
TCP 基于 Cerf 和 Kahn 在【Cerf, V., and R. Kahn, “A Protocol for Packet Network Intercommunication”, IEEE Transactions on Communications,Vol. COM-22, No. 5, pp 637-648, May 1974.】论文中提出的相关概念。在网络协议栈中,TCP 位于 IP 之上,IP 协议可以为 TCP 提供接收与发送可变长度的 TCP 数据段服务,IP 协议把这些数据段放在网际报文 ’ 信封 ’ 中。网络报文可以在不同网络中定位源 TCP(端口)和目的 TCP(端口)。当 TCP 数据段需要在多个网络和互连网关转发和传输时,IP 协议需要处理 TCP 数据段的拆包和重组问题。IP 协议可以携带关于优先级、安全等级、TCP 数据段拆包相关信息,这些信息可以穿过不同的网络保证了端(进程)到端之间的通信。
Protocol Layering
+---------------------+
| higher-level |
+---------------------+
| TCP |
+---------------------+
| internet protocol |
+---------------------+
|communication network|
+---------------------+
Figure 1
本文档约定 TCP 协议实现程序需要和主机中的上层协议实现程序共同运行。有些计算机系统需要通过一个前端机去联网,这个前端机安装有 TCP 和 IP 协议软件,同样也包括其他相关的网络软件。TCP 规范为上层网络协议提供了一套接口,这套接口同样适合前端机场景,只要主机到前端机之间实现了相关的通讯协议(host-to-front end protocol)。
范围(Scope)
TCP 为在多网络环境中的主机进程提供进程到进程的可靠通讯服务。TCP 希望成为多种网络环境中广泛使用的通讯协议。
关于本文档(About this Document)
本文档描述了实现 TCP 的具体规则,包括与上层协议交互时的规则、与其他 TCP 程序进行交互所需要的规则。本章剩余部分对协议接口和具体操作进行了详细地描述。第二章总结了 TCP 设计的哲学基础。第三章描述了不同的事件(新数据段到达、用户调用、出错等)出现的时候 TCP 采用的动作和 TCP 数据段的数据格式。
接口(Interfaces)
TCP 接口包括面向用户或者应用进程的接口和面向底层协议(比如 IP 协议)的接口。
我们会比较详细的介绍应用程序和 TCP 之间的接口,这个接口包括一组调用,这种调用类似于操作系统为应用程序提供的操作文件的系统调用。比如用来打开链接和关闭链接的调用,在建立的链接上发送数据和接收数据的调用。同时 TCP 组件和应用程序异步通讯的功能比较受期待。针对特定的操作系统平台,TCP 在实现的过程中是可以有合理的自由度去设计接口,对于 TCP/user 之间的接口,任何合理的实现都应该保持一个约定的最小范围内的功能列表。
TCP 和底层协议之间没有规定具体的实现细节,但是假设有一种机制可以在两层协议间进行异步的消息传递。通常人们希望底层协议会定义一个这样的异步接口。这里的底层协议在本文档中一般指 IP 协议。
操作(Operation)
正如上面文章描述的,TCP 主要的目的是为两个不同进程间提供可靠的、安全的逻辑电路(logical circuit)或者连接(connection)服务。在不可靠的网络通讯系统中提供这种稳定可靠的服务需要在以下几个领域具备基本的实现(facilities):
- 基本数据传输
- 可靠性
- 流量控制
- 多路复用
- 长连接
- 优先级和安全
关于 TCP 这几个实现的基本操作(basic operation)都在后续章节进行介绍。
TCP 介绍
基本数据传输
TCP 可以在两个用户进程之间进行双向的字节流传输,它是通过把一定数量的字节放在数据段中并在网络上传输来实现(字节流传输)的。通常,TCP 自主决定什么时候对数据进行打包(block)并发送出去。
有时候用户需要确定提交给 TCP 的数据已经被 TCP 发送出去了。为了实现这个功能,TCP 定义了一个推数据(PUSH)的功能。发送者需要指定要把数据推送(PUSH)到接收方,才能保证提交给 TCP 的数据被发送出去。一个推的指令会立即把在那个时刻收集到的数据(data up to that point)发送到接收方。发送 PUSH 指令的准确时间接收方可能不会知道,并且推送功能也不会提供这次推送的数据量相关信息。
可靠性(序号、超时、确认、重传)
TCP 必须能解决由通讯网络造成的数据损坏、丢失、重复、或者乱序问题。TCP 通过给每个字节添加递增序列号并且要求接收方收到数据后发送一个确认(ack)来解决上述问题。如果在一定时间内没有收到确认,发送方会再次发送数据。接收方通过序列号为乱序的字节排序,并且可以把重复的数据段丢弃。通过给每个传送的数据段添加校验和可以检测数据是否被破坏,接收方收到数据后检测校验和并丢弃受到破坏的数据段。
只要 TCP 运行良好,并且通讯网络不会出现完全独立的两个子网(网络分区),数据的传输错误一般都不会影响数据的正常分发。TCP 也通常可以从通讯网络错误中恢复。
流量控制(Flow Control)
TCP 为接收方提供一种可以控制发送方发送数据量的方法。接收方在每次发送确认信息(ACK)的时候携带一个 win 参数来通知发送方剩余的可发送字节数。
多路复用(Multiplexing)
为了可以让在同一个主机上的多个进程同时使用 TCP 提供的通讯组件,TCP 为每个主机提供了多个端口。Socket 把 TCP 和底层的网络传输层串联起来。一对 Socket 独立标识一个连接。一个 Socket 可以同时用在多个连接中(java 中的 serversocket 便是如此,主动模式下的 FTP 20 数据端口也可以)。
每个主机会把不同的进程绑定到不同的端口上。通常会把经常使用的应用程序(日志或者日期服务,http,ft 应用)绑定到固定的端口上。这些应用程序提供的服务之后可以通过这些端口地址访问。
连接(Connections)
上面文章讨论的可靠性和流量控制机制需要 TCP 为每个数据流建立和维护相关的状态信息。Socket、字节序列号、窗口大小还有其他类似的状态信息统称为连接。每个连接都被一对 Socket 唯一定义,每个 Socket 定义连接的一端。
当两个进程想要通讯,他们必须使用 TCP 建立一个连接(在每个端建立状态信息)。当他们通讯结束后,相对应的连接会被中断或者关闭以释放资源让其他进程继续使用。
因为连接可能建立在不可靠的计算机和不可靠的通讯网络上,所以基于时钟序号的握手机制可以避免错误地初始化连接。
优先级和安全(Precedence and Security)
TCP 用户可以为连接设置相关的安全和优先级。当这些功能没有被显式设置的时候,会被设置一些默认值。
设计的哲学(PHILOSOPHY)
网络系统中的元素(Elements of the Internetwork System)
互连网络包括通讯网络以及连接到这些网络的主机,通讯网络通过网关(ip 层面上,网关一般就是我们连的路由器,链路层或者物理层是指具体的网关设备)相互连接。这里假设通讯网络有可能是本地网络(以太网)也有可能是大的广域网(阿帕网),这些通讯网络(跨网通讯,不管是跨越多个局域网,还是跨越多个地区的城域网)都是基于分组交换技术。位于通讯网络不同层次的通讯协议、网关、以及主机共同组成了可以保证进程相互通讯的系统,这个系统支持在两个进程端口间的逻辑链路上进行双向的数据传输。
这里的 packet 一般指主机和网络之间一次交换的数据。我们一般不用关心数据包在网络间传输的格式。
主机就是连接到网络的电脑,从通讯网络角度来看,主机就是数据包的来源地和目的地。进程就是主机中活跃的元素(进程通常的定义就是执行中的程序)。 即使是终端、文件、io 设备也被认为是进程间的通讯方式。
因为一个进程需要区分与其他多个进程之间的通讯,所以每个进程都可以申请多个端口。
操作模型(Model of Operation)
进程把需要发送的数据以参数的形式传给 TCP 的系统调用。TCP 收到数据后,从参数中获取数据并打包成多个数据段,然后调用 IP 层提供的接口来把数据段发送到目的进程。负责接收的 TCP 把数据段传递到用户进程设置的缓存中,并通知用户进程。TCP 数据段包括一些控制信息,从而保证可靠的有序的的数据传输。
网络通讯模型就是,每个 TCP 都有关联的网络通讯模块,TCP 为上层用户提供了一个可以连接本地网络的接口。这个网络通讯模块把 TCP 数据段打包成数据报文并把这些数据报文路由到目的网络模块或者离本地网络最近的网关。为了通过本地网络,数据段是被包装在本地网络报文(local network packet)中的。
分组交换程序可能会进行打包、分段、或者其他的一些操作来保证可以通过本地分组(物理链路层的数据包)到达目的网络模块。
位于不同本地网络之间的网关,负责将 IP 报文从本地网络报文中拆解出来,然后找到下一个 IP 报文需要通过的本地网络。将 IP 报文包装到下一个本地网络协议包中并路由到目的网络的相关网关(网关和网关之间也是一个本地网络连接),或者直达目的网络模块(比如计算机)。
通过下一个本地网络时,网关有可能会把网络报文拆分成更小的网络报文(下一个本地网络 MTU 比较小)。在后面的传输过程中,有可能会继续拆分网络报文。网络报文段数据格式可以方便数据接收方(网络模块)把网络报文段重新组成一个网络报文。
网络模块(接收方)把 TCP 数据段从 IP 报文中拆解出来发送到上层的 TCP 模块。
上述操作模型看着非常简单,但是实际上整个过程是非常复杂的。一个重要的特性就是可以选择服务类型。服务类型可以指导网关或者网络模块在发送数据包到下一个网络的时候设置合适的参数。服务类型中就包括一个数据包优先级。报文也有可能会携带相关安全设置来允许主机或者网关在不同级别的安全环境中运行以便隔离数据包。
主机环境(The Host Environment)
TCP 是操作系统的一个模块。用户使用 TCP 就像他们操作本地文件一样。TCP 有可能会调用其他系统功能,比如操作数据结构。与网络相关的接口被相应的设备驱动(网卡)模块管理。TCP 不直接调用网络设备驱动而是调用网络模块,网络模块会直接调用相应的设备驱动。
前端处理机可以方便的实现 TCP 的相关功能和机制,TCP 并不会阻碍这种实现方式。但是,在这种实现方式下,主机到前端处理机之前的通讯协议必须支持本文档规定的相关功能,并且把相应的功能接口提供给上层用户。
接口(Interfaces)
TCP/User 接口可以为使用 TCP 的用户提供打开、关闭、获取连接状态的功能。这些系统调用与用户操作文件进行的系统调用是一样的,比如打开文件、读取数据、关闭文件。TCP/IP 接口为主机的 TCP 模块提供与网络系统中任何其他地方的 TCP 模块进行数据收发的功能。这些系统调用可以传递地址、服务类型、优先级、安全或者其他控制信息。
与其他协议的关系(Relation to Other Protocols)
下面展示了 TCP 在协议栈中的位置:
+------+ +-----+ +-----+ +-----+
|Telnet| | FTP | |Voice| ... | | Application Level
+------+ +-----+ +-----+ +-----+
| | | |
+-----+ +-----+ +-----+
| TCP | | RTP | ... | | Host Level
+-----+ +-----+ +-----+
| | |
+-------------------------------+
| Internet Protocol & ICMP | Gateway Level
+-------------------------------+
|
+---------------------------+
| Local Network Protocol | Network Level
+---------------------------+
Protocol Relationships
Figure 2.
TCP 应该比较高效地支持上层协议。TCP 为上层提供的接口应该与 IP 为 TCP 提供的接口一样简单易用。
可靠通讯(Reliable Communication)
在 TCP 连接上发送的数据流会可靠、按顺序地到达目的地。通过递增序列号和反馈机制保证传输的可靠性。概念上,每个字节都会赋予一个序列号。数据段传输的一段字节数据,第一个字节的编码就是这个数据段的编码。发送出去的数据段也会携带一个确认序列码,指示期望的下一个数据段的起始字节序列号。当 TCP 发送数据段的时候,会复制一份数据到重发队列中,并启动定时器。当收到对这个数据段的确认时,这个数据段会从重发队列中删除。如果数据段没有收到确认,并且定时器超时,TCP 会重新发送对应的数据段。
确认机制只能确保 TCP 模块收到了数据,并不能确保应用程序确实收到了数据。
为了控制 TCP 模块之间的数据流量,流量控制机制被引入。接收方向发送方报告一个窗口(win)。这个窗口表示从接收方收到的最后一个字节开始,还能接收的字节数(接收方剩余的 read buffer 空间)。
连接的建立与清除
每个 TCP 端口都是独立选择的,所以端口号有可能会重复。为了保证 TCP 地址的唯一性,我们把 IP 地址与端口号连接在一起,IP:PORT,这两个可以保证全网唯一的连接。
一个连接会被两端的 socket 唯一确定。一个本地的 socket 可能会参与多个连接,每个连接的另一端就是其他主机上的 socket。一个连接可以在两个方向传递数据,这个被称为全双工。
TCP 可以为进程自由选择端口号。通常会为那些常用的 socket(端口号)绑定常用的应用进程。我们可以假定某些进程拥有某些固定的端口号,这些进程只能绑定到那些它们拥有的端口上。(实现进程和端口的绑定规则由使用者自己规定,但是现在我们假定用户请求为一个进程绑定一个端口,或者可以把一组端口独立的赋予一个进程,比如把某些端口的高位为某些数值的一组端口赋予一个给定的进程。)
OPEN 方法中指定的本地端口和远端 socket 确定了一个连接。之后,用户会使用 tcp 提供的一个本地连接来进行后续的操作。我们假设有 Transmission Control Block (TCB) 数据结构来记录连接的相关信息。一个关于代码实现的建议就是把指向 TCB 的指针当作这个连接的名字。OPEN 调用也可以指定连接是主动建立还是被动建立(client connect、server listen)。
被动的 OPEN 调用表示进程想接收建立连接的请求而不是主动去建立一个连接。通常被动 OPEN 可以接收任何建立连接的请求(来自任何应用)。Foreign address 都为零表示未指定 socket,未指定 socket 只有在被动 OPEN 调用时能使用。如下图是在 centos 服务器上用 netstat -nltp 查看的。
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:5601 0.0.0.0:* LISTEN 30577/node
tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN 21514/mysqld
一个对外提供服务的进程如果要为其他未知的进程提供服务,就需要一个被动 OPEN 并且 Foreign Address 都指定为 0 的接口调用。之后外部进程就可以向本地进程请求建立连接。如果这个服务使用的端口号是比较知名的端口号,那么请求服务时会比较方便。
一些标准服务都是提前绑定到了比较常见的一些端口上。比如,Telent 服务被永久的绑定到了特定的端口上,其他的端口也为文件传输、远程 job、文本生成器、回声、或者 sink 处理等服务预留(后面三个服务只为了测试使用)。有的端口会提供服务查询功能,并且会根据的查询信息在相应的端口上启动新的服务。预留端口是 TCP 协议规范的一部分,但是端口与服务的绑定关于超出了本文档的讨论范围。
进程可以打开一个被动的连接,等待其他进程向本进程发起建立连接的请求,当新连接建立时,本进程会收到相关的通知。两个进程同时发起向对方建立的申请,连接依然会正常建立。在分布式计算情况下,每个计算机都是相互独立异步运行,这种灵活建立连接的方式很方便地为分布式计算提供支持。
在本地等待被连接的服务和外部主动请求连接的 OPEN 请求,有两个规则可以确定本地服务是否会接收外部的连接请求。第一种情况下,本地服务准确指定了可以建立连接的外部地址。另一种情况是本地服务没有指定可以建立连接的外部地址,任何外部连接请求都会被处理。其他处理规则包括部分匹配限制。
如果存在多个本地服务去请求绑定同一个端口,并且有的指定 了外部服务地址(有的没有指定),那么当一个主动建立连接的请求到来的时候,指定了外部服务地址的本地服务会优先得到匹配(如果匹配的话)。
建立连接的过程中使用到了 SYN 控制标识符并且涉及到三个消息的交换。这个交换被称为三次握手。
当用户通过 OPEN 指令发送一个连接请求的时候,请求的数据段会携带 SYN 到达服务器,这个时候新的连接会被初始化,同时一个等待处理的 TCB 数据结构也会被建立并保存。本地和外部地址的匹配程度确定何时去初始化一个连接。当交互的两端同步了各自的初始序列号,这个连接便会正确建立。
关闭连接同样需要交换数据段,在这种情况下数据段中会包含 FIN 控制位。
数据通讯(Data Communication)
可以把数据看做是连接上的字节流。发送方通过 SEND 指令发送数据,如果发送的数据中指定了 PUSH 标志,数据会立即从发送方的缓存中发送出去。
TCP 模块可以缓存用户传过来的数据,并以一定的方式发送出去,但是如果上层用户指定了 PUSH 标志,TCP 模块必须把缓存的数据发送出去。如果接收方收到带有 PUSH 标志的数据,那么它应该马上把数据传递到用户进程。
push 函数调用与发送数据段的大小无关。数据段中的数据有可能来自一个 send 调用,两个 send 调用或者多个 send 调用。
push 函数和 PUSH 标志是为了把数据尽快发送到接收方,这个操作没有相关记录的服务。
push 函数和介于 TCP/USER 之间的 buffer 有一定的关系。每次带有 PUSH 标志的数据放入用户读缓存的时候,这个 buffer 会立即提交到用户进程(无论 buffer 是不是满的),如果 buffer 满了,没有收到 PUSH 标志,那么 buffer 会直接提交到用户进程。
当前进程正在处理数据流,可以在数据流之后的某个时刻为某个数据标记为紧急,TCP 提供了这种与接收方进行交流的方式。TCP 不规定当收到紧急数据时用户进程如何处理,但是标准流程是,当收到紧急数据时,用户进程应该尽快处理相关数据。
优先级和安全(Precedence and Security)
TCP 基于 IP 模块提供的服务类型字段和安全选项为每个建立连接的用户提供优先级和安全方面的服务。并不是所有的 TCP 模块都需要在不同级别的安全环境(multilevel secure,IBM Docs )中运行,有些可能只在非安全情况下使用,或者有些只在一级隔离中运行。有些 TCP 实现和服务有可能只会在多级别安全的某些场景下使用。
运行在多环境下的 TCP 模块,必须为每个发送出去的数据段添加安全、隔离区、优先级。TCP 模块也必须为上层协议或者用户提供可以设置安全级别、隔离区、优先级的接口。
健壮性规则(Robustness Principle)
TCP 遵循一般健壮性准则:对于要做的事情持保守态度,外部的东西要保持欢迎的态度。
功能规格(FUNCTIONAL SPECIFICATION)
头部格式(Header Format)
TCP 数据段被包装成 IP 数据报文发送。IP 数据报文包括几个信息字段,包括源地址和目的地地址。TCP 头紧随 IP 头之后,表示 TCP 报文需要的几个字段。这么划分 IP 和 TCP 数据头可以为以后主机协议(host level protocols)提供更好的扩展。
TCP Header Format
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgment Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data | |U|A|P|R|S|F| |
| Offset| Reserved |R|C|S|S|Y|I| Window |
| | |G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum | Urgent Pointer |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
TCP Header Format
Note that one tick mark represents one bit position.
Figure 3.
源端口: 16 bits
源端口号
目的端口:16 bits
目的端口号
序列号:32 bits
如果没有 SYN 标志,数据段的序列号就是第一个字节的序列号。如果有 SYN 标志,序列号就是初始序列号,第一个要发送的字节序列号是初始序列号 + 1。
确认序列号:32 bits
当 ACK 标志被设置,确认序列号表示接收方期望收到的下一个数据段开头的字节序号。连接建立后,该序列号一直会被传送。
数据偏移地址:4 bits
表示数据数据体开始的位置。TCP 的头部长度是 32 位的整数倍。
预留:6 bits
为以后功能扩展预留,必须为 0
控制位:6 bits (自左到右):
-
URG: 紧急标识位
-
ACK: 确认标识位
-
PSH: 推送标志位
-
RST: 复位连接标识位
-
SYN: 同步标志位
-
FIN: 结束标志位
窗口:16 bits
接收方发送的确认字段中会携带一个数字,这个数字表示接收方最后一次成功确认之后还能接收的字节数。
校验和:16 bits
伪首部、TCP 报头、TCP 数据按照 16 位分组,对这 16 位分组分别进行反码求和运算(二进制反码求和是怎样求的?_百度知道IP 和 TCP 头校验和计算算法详解_jiangqin115 的专栏 - CSDN 博客 关于二进制反码求和运算需要说明的一点是,先取反后相加与先相加后取反,得到的结果是一样的),最后把得到的数据进行反码运算获得的 16 数据就是校验和。 反码求和运算:
-
把校验和字段置为 0;
-
对 IP 头部中的每 16bit 进行二进制求和;
-
如果和的高 16bit 不为 0,则将和的高 16bit 和低 16bit 反复相加,直到和的高 16bit 为 0,从而获得一个 16bit 的值;
-
将该 16bit 的值取反,存入校验和字段。
-
反码运算时,其符号位与数值一起参加运算。
如果最后字节数是奇数,为了获得校验码,需要在最后添加一个字节,这个字节的每位都是 0。添加的字节不会被发送(只是为了计算校验和)。计算校验和的时候会在 TCP 头部前面添加 96 位的伪头部。伪头部包括源地址(ipv4)、目的地址(ipv4)、协议、TCP 数据长度。这样 TCP 可以防止数据包错误路由。这些信息(地址信息)会通过 TCP/IP 接口从 IP 模块获取到。
+--------+--------+--------+--------+
| Source Address |
+--------+--------+--------+--------+
| Destination Address |
+--------+--------+--------+--------+
| zero | PTCL | TCP Length |
+--------+--------+--------+--------+
TCP 长度包括 TCP 头部和数据长度,不包括添加的 12 个字节伪头部。
紧急指针:16 bits
这个值与数据段的第一个字节的序列号相加就是紧急数据的起始位置。这个字段只有在紧急标志位设置了才会被处理。
选项:长度可变
选项出现在 TCP 尾部,并且是长度是 8 位的倍数。所有的选项也会被计算在校验和内。选项可以从任意自己开始。选项有两种格式:
- 单字节,表示选项类型
- 单字节选项类型、一个字节表示选项长度、实际的选项数据
选项长度包括选项类型、选项具体的数据值。
选项列表的数据长度可能要比 TCP 头部指定的数据的起始位置要少。在 End-of-Option 与实际数据之间使用值为 0 的字节填充。
目前定义的选项包括:
Kind Length Meaning
---- ------ -------
0 - 选项列表结束标志
1 - 误操作
2 4 最大段长度
具体定义:
- 选项列表结束标志位:
+--------+
|00000000|
+--------+
Kind=0
表示选项列表的结束,这个可能与 TCP 头部的数据起始位置(TCP 头部结束位置)不相符。这个表示整个选项列表的结束,不是每个选项的结束。
- 无操作:
+--------+
|00000001|
+--------+
Kind=1
这个选项一般用在其他选项之间,比如用来将某些选项对齐到某个字上。发送方可能不会使用这种选项,接收方应该时刻准备接收没有对齐到字的选项。
- 最大段长度:16 位
+--------+--------+---------+--------+
|00000010|00000100| max seg size |
+--------+--------+---------+--------+
Kind=2 Length=4
这个选项表示接收方可以接收的最大数据段,这个选项一般会在连接建立时刻指定(SYN)。如果没有指定这个字段的值,则表示数据段大小不受限制。
- 填充选项:可变长度 填充选项用来保证 TCP 头部结束和数据开始部分对齐与 32 位数据边界。
术语(Terminology)
在我们详细讨论 TCP 操作的具体细节前,需要对一些重要的关键词做一些解释。需要几个变量来维护 TCP 连接的状态,把保存这种变量的数据结构叫做 TCP 控制块或者 TCB。这些变量包括:本地或者对端端口号,连接的安全和优先级,指向用户发送或者接收数据 buffer 缓存的指针,指向重发队列和当前发送数据段的指针。关于发送和接收的序列号的信息也被存储在 TCB 中。
发送序列变量
SND.UNA - 发送但未确认的
SND.NXT - 下次发送的字节序列号
SND.WND - 发送窗口
SND.UP - 紧急数据指针
SND.WL1 - 上次更新窗口使用的字节序列号
SND.WL2 - 上次更新窗口使用的确认序列号
ISS - 初始序列号
接收序列变量
RCV.NXT - 下次希望接收的字节序列号
RCV.WND - 接收窗口大小
RCV.UP - 紧急数据指针
IRS - 连接建立时初始序列号
下图有助于理解这几个变量:
发送序列号空间
1 2 3 4
----------|----------|----------|----------
SND.UNA SND.NXT SND.UNA
+SND.WND
1 - 已经被确认的字节序列
2 - 未确认的字节序列
3 - 新数据发送可以使用的字节序列
4 - 还没有分配的,未来会用的字节序列号
Send Sequence Space
Figure 4.
图 4 中的 3 表示发送窗口(发送窗口可以控制应用向 TCP 写数据的速度)大小。
接收序列号空间
1 2 3
----------|----------|----------
RCV.NXT RCV.NXT
+RCV.WND
1 - 被确认的序列号
2 - 打算接收的序列号
3 - 还未被分配的序列号
Receive Sequence Space
Figure 5.
图 5 中的 2 表示接收窗口。
本节讨论的常用变量的数据都是从当前数据段中的相关字段中获取的。
Current Segment Variables
SEG.SEQ - 数据段序列号
SEG.ACK - 数据段确认序列号
SEG.LEN - 数据段长度
SEG.WND - 发送方窗口大小
SEG.UP - 数据段紧急数据指针
SEG.PRC - 数据段优先级值
一个连接在一个生命周期会经历很多状态。这些状态包括: LISTEN, SYN-SENT, SYN-RECEIVED,ESTABLISHED, FIN-WAIT-1, FIN-WAIT-2, CLOSE-WAIT, CLOSING, LAST-ACK,TIME-WAIT 和不存在的状态:CLOSED。CLOSED 是不存在的是因为没有相关的 TCB 数据块,因此也没有连接。这些状态更详细的解释:
- LISTEN - 代表接收来自任何其他 TCP 组件的连接请求。
- SYN-SENT - 发送完 SYN 标志位之后,等待对方发送 ACK 标志。
- SYN-RECEIVED 等待接收之前发送的 syn 的 ack。
- ESTABLISHED 连接已经建立,用户进程可以收发数据。
- FIN-WAIT-1 等待之前发送 fin 的 ack 或者连接对端的 fin 标志。
- FIN-WAIT-2 等待对端的 fin 标识数据。
- CLOSE-WAIT 等待本地用户进程发送关闭指令(一般是半关闭,比如 java socket 的 shutdownOutput)。
- LAST-ACK 等待之前接收发送的 fin 的 ack。
- TIME-WAIT 对端收到本地发送回去 fin ack 的最长时间。
- CLOSED 标识连接已经拆除。
事件驱动 TCP 连接状态的变化。事件包括:用户进程调用的方法,OPEN, SEND, RECEIVE, CLOSE,ABORT, and STATUS,收到的数据段、特别是包含了 SYN, ACK, RST and FIN 这些标志的数据段,还有超时等。
下图 6 描述了 TCP 的状态变化,同时也标注了导致状态变化的事件、TCP 的相关行为,但是没有涉及一些异常条件和一些没有导致 TCP 状态变化的动作。在后续章节会详细描述 TCP 对于事件处理细节。
注意:这张图只是一个大概总结,并不是 TCP 规范的全部。
+---------+ ---------\ active OPEN
| CLOSED | \ -----------
+---------+<---------\ \ create TCB
| ^ \ \ snd SYN
passive OPEN | | CLOSE \ \
------------ | | ---------- \ \
create TCB | | delete TCB \ \
V | \ \
+---------+ CLOSE | \
| LISTEN | ---------- | |
+---------+ delete TCB | |
rcv SYN | | SEND | |
----------- | | ------- | V
+---------+ snd SYN,ACK / \ snd SYN +---------+
| |<----------------- ------------------>| |
| SYN | rcv SYN | SYN |
| RCVD |<-----------------------------------------------| SENT |
| | snd ACK | |
| |------------------ -------------------| |
+---------+ rcv ACK of SYN \ / rcv SYN,ACK +---------+
| -------------- | | -----------
| x | | snd ACK
| V V
| CLOSE +---------+
| ------- | ESTAB |
| snd FIN +---------+
| CLOSE | | rcv FIN
V ------- | | -------
+---------+ snd FIN / \ snd ACK +---------+
| FIN |<----------------- ------------------>| CLOSE |
| WAIT-1 |------------------ | WAIT |
+---------+ rcv FIN \ +---------+
| rcv ACK of FIN ------- | CLOSE |
| -------------- snd ACK | ------- |
V x V snd FIN V
+---------+ +---------+ +---------+
|FINWAIT-2| | CLOSING | | LAST-ACK|
+---------+ +---------+ +---------+
| rcv ACK of FIN | rcv ACK of FIN |
| rcv FIN -------------- | Timeout=2MSL -------------- |
| ------- x V ------------ x V
\ snd ACK +---------+delete TCB +---------+
------------------------>|TIME WAIT|------------------>| CLOSED |
+---------+ +---------+
TCP Connection State Diagram
Figure 6.
序列号(Sequence Numbers)
TCP 连接发送的每个字节都有一个序列号,这是设计中比较基础的概念。因为每个字节都编号了,所以可以对每个字节进行确认。确认机制具有累计的效果,比如 X 编号表示 X 之前的字节都收到了(不包括 X)。这种机制可以实现重发情况下的数据去重。数据段中紧随 header 之后的字节编号最小,之后的字节序号累加。
必须要记住,序列空间是有限的,但是非常大。序列空间从 0 to 2^32 - 1。因为序列号空间是有限的,所以所有处理序列号的算法都应该进行对 232 的取模运算。无符号的算法保留了序列号的关系,因为序列号总是从 232 到 0 来回循环往复。计算模运算的时候会有一些细微差距,所以在比较这些序列号时必须格外小心。“=<” 表示小于等于 (与 2^32 进行模运算)。
TCP 必须包括的序列号比较有:
- 确定那些对发送出去的数据进行确认的序列号。
- 确定一个数据段包含的所有序列号都已经被确认(从重发队列里删除数据段)。
- 确定接收的数据段包含接收方期望的序列号。(比如数据段与接收窗口重叠)
TCP 发出去数据后会收到对应的确认。以下几个比较运算被用来处理确认。
SND.UNA = 最久没有确认的数据序列号 SND.NXT = 下一个发送数据的序列号 SEG.ACK = 连接对端期望接收到的下一个序列号 SEG.SEQ = 接收数据段的第一个字节的序列号 SEG.LEN = 数据段中的数据占用的字节数,包括 SYN 和 FIN 两个标志位占用的。 SEG.SEQ+SEG.LEN-1 = 接收的某个数据段的最后一个字节。
一个新的确认满足以下不等式:
SND.UNA < SEG.ACK =< SND.NXT
处于重发队列中的数据段,如果它的序列号和长度小于或者等于接收的数据段的确认序列号,那么这个数据段整个的被确认了。
当数据收到时,以下几个比较运算是必要的。
RCV.NXT = 期望接收的下一个数据段的序列号,是接收窗口的左边沿或者窗口最左边。 RCV.NXT+RCV.WND-1 = 接收方期望接收的数据段的最后一个序列号,或者是接收窗口的最大序列号或者右边延。 SEG.SEQ = 数据段的第一个字节的序列号 SEG.SEQ+SEG.LEN-1 = 数据段最后一个字节的序列号
一个数据段占用了正常的接收序列号空间,如果满足:
RCV.NXT =< SEG.SEQ < RCV.NXT+RCV.WND
或者满足
RCV.NXT =< SEG.SEQ+SEG.LEN-1 < RCV.NXT+RCV.WND
第一个检查是判断数据段开始字节的序列号是否落入接收窗口中,第二个检查是判断数据段最后一个字节是否也落入接收窗口中。只要通过一个检查,就被认为数据段有数据可以放入接收窗口中。
实际要比这里描述的复杂得多。由于 0 大小的窗口和 0 长度的数据段,对于一个数据段,我们有四种接收的可能。
Segment Receive Test
Length Window
------- ------- -------------------------------------------
0 0 SEG.SEQ = RCV.NXT
0 >0 RCV.NXT =< SEG.SEQ < RCV.NXT+RCV.WND
>0 0 not acceptable
>0 >0 RCV.NXT =< SEG.SEQ < RCV.NXT+RCV.WND
or RCV.NXT =< SEG.SEQ+SEG.LEN-1 < RCV.NXT+RCV.WND
需要注意的是,当接收窗口大小为 0 时,除了 ack 数据段以外其他数据段都不会被接收。所以,TCP 可以在窗口为 0 的情况下继续发送数据和接收确认细腻。然而,即使接收窗口为 0,TCP 也必须接收带有 RST 和 URG 标识的数据段。
我们使用编号方法同样也保护了某些控制信息。隐式地为某些控制信息编号,可以对这些控制信息进行重发和确认防止出现混乱(可以防止对一个控制信息进行多次处理)。控制信息不会占用数据段的数据部分(空间)。所以,我们必须有规则地为控制信息进行隐式编码。SYN 和 FIN 是需要隐式编号的两个控制信息,并且这两个控制信息只用在连接建立和拆除过程。为了编码目的,SYN 的编号是在它所处的数据段中第一个字节数据的编号之前,FIN 的编号是在它所处的数据段中最后一个字节数据的编号之后。SEG.LEN 包括数据字节编号和控制信息编号。当一个数据段有 SYN 标志时,SEG.SEQ 就是 SYN 的编号。
初始编号的选择一个 TCP 连接可以重复使用,规范没有任何限制。一个连接被一对 socket 确定。连接的新实例被称为连接实体。那么有问题就是,连接如何确定从上一个连接实体发送过来的重复数据段。当连接关闭并且被快速的重新打开时或者由于系统内存不足造成的连接断开然后重新建立时这种问题更为明显。
为了避免冲突,当之前连接实体发送的数据段还在网络上传输时,新的连接实体应该避免使用相同的序列号。即使 TCP 宕机或者丢失了所有关于之前使用的序列号的信息,我们也要保证不出现冲突。当一个新的连接实体建立时,一个初始序列号(ISN)选择器会用来选择一个新的 32 的初始编号。选择器使用一个 32 位的时钟(有可能是虚拟的),这个时钟的低位会以 4 微妙涨一个的速度增长。所以 ISN 会大概 4.5 个小时循环一次。因为我们假定一个数据段在网络中最多存留一个 MSL(最大段生命周期)时间段,并且 MSG 比 4.5 个小时短很多,所以我们判断,ISN 是唯一的。
每一个连接都有一个接收序列号和发送序列号。发送初始序列号由发送数据的 TCP 模块选择,接收序列号是在连接建立时从对面传过来的。
连接建立时,两端必须要同步对方的初始序列号。双方通过同步带有 SYN 标志和初始序列号的数据段来实现。所以为了实现上述目的,一般需要一个初始化序列号选择器并且需要一些握手机制来同步初始化序列号。
同步机制需要发送自己的初始化序列号给对方,并且接收对方发送过来的确认信息。连接的两端必须接收对方发送过来的初始序列号并且发回确认信息。
1) A --> B SYN my sequence number is X
2) A <-- B ACK your sequence number is X
3) A <-- B SYN my sequence number is Y
4) A --> B ACK your sequence number is Y
因为第二步和第三步可以合并成一步,所以一般称为三次握手。
三次握手步骤是必须的,因为序列号不是在网络空间中全局同步的,并且每个 TCP 模块都有自己不同的初始化序列号选择机制。接收方收到第一个 SYN 的时候没有办法确定这个 SYN 是否是之前连接实例发送的老旧的 SYN 数据包除非接收方一直保存之前连接使用的最后一个序列号(不是一直都可以记录的。),所以一般接收方需要发送方来检验这个 SYN(通过向发送方返回 ACK 信息)。三次握手和时钟驱动的序列号选择器在【Dalal, Y. and C. Sunshine, “Connection Management in Transport Protocols”, Computer Networks, Vol. 2, No. 6, pp. 454-473,December 1978.】中有详细讨论。
知道什么时候保持静默
TCP 在丢失以前连接序列号的情况下(宕机后重启)必须保持一个 MSL 时间段,并且在这个时间段内不创建新的包含序列号的数据段,因为这个序列号可能会与网络上传输的老旧数据段的序列号重复。本规范中 MSL 采用 2 分钟。这是一个工程上的选择,根据各方面的经验,可以修改该值。如果 TCP 重新初始化,并且保留了之前的字节序号状态,这时不需要保持静默,但是必须保证后续的字节序号要比之前的大。(注:如果不存在宕机或者重启等情况,TCP 建立新连接是不需要保持 2 分钟的静默时间的,因为 TCP 模块会记录之前连接使用的序列号。)
TCP 静默时间概念
如果在一个主机组成的计算机网络系统中主机突然宕机了并且没有保存所有处于活跃状态(Closed 以外的其他状态)的连接的相关状态信息的情况下,主机重启后需要等待约定的一段时间(MSL)才能开始重新建立连接。以下段落会继续介绍该概念。在网络系统中,可以冒着上个连接的数据被新连接接收或者新数据被当做重复数据丢掉的风险不去等待一段时间。
数据包每次从产生到发送到网络中时都会消耗一定的序号空间。TCP 中的重复判断和序号计算算法都依赖于数据段唯一绑定到序列号空间中,并且在网络传输的数据段被确认之前以及这些数据段的副本在从网络中清除掉之前,后续发送的数据序列号不能循环超过 2^32。如果没有这个规定,可以想象两个不同的数据段有可能会被赋予相同的序列号或者交叉的序列号,导致接收方没办法区分哪个是新的数据包,哪个是老的数据包。数据段包含的字节越多,该数据段占用的序列号空间就越多。
正常情况下,TCP 跟踪发送下一个字节的序号,并且保存需要确认的最早的那个序号,这样可以防止 TCP 错误使用之前被使用过并且得到确认的序列号再次被使用。这种方式不能保证旧的重复数据从网络上剔除,所以序列空间被设置非常大,来减少因为网络中传输的老重复数据段导致数据传输的问题。在每秒 2 兆位的网速中,大概需要 4.5 个小时消耗掉 2^32 的序号空间。数据段在网络中的最大存在周期一般不会超过几十秒,这被认为对现有的网络有足够的保护空间,即使网速到了 10 兆位 /s。在 100 兆位 /s 的网速中用掉 2^32 的序号空间需要 5.4 分钟,虽然比较少,但是依然够用。
在 TCP 连接的一端,如果 TCP 模块没有记录上次使用的序列号,那么基本的重复判断和序号算法将会失效。比如当一个 TCP 模块建立了几个连接,每个连接的初始序号为 0,经过宕机然后重启之后,连接有可能会重新创建之前的连接实例(这个时候的连接有可能处于半打开状态),这个链接实例发送的数据序列号有可能与之前连接实例发送的并且在网络中传输中的数据序列号相等或者有重叠。在不了解一个指定的连接之前的序号的情况下,TCP 规范规定 TCP 发送模块应该等待一个 MSL 时间,来让之前连接实例发送的数据段从网络上排空。
即使主机能记住时间并且可以根据时间选择初始序列号,依然不能免于上述问题。
(注:前面几句话和后面没联系,故删除)现在假设突然宕机了,然后恢复,然后建立了该连接的另个一新的实例,实例的初始化序号是 S1=ISN (t)(上一个连接实例使用的最后一个字节序列)。如果恢复得非常快,上一个连接实例发送的在 S1 周围的数据段(序号大于 S1),会被接收方认为是新连接实例的数据。
现在问题是重启后的主机不知道宕机了多久,也不知道网络上是否还有上一个连接实例发送的老数据包。
解决这种问题的办法,就是在从宕机中恢复过来后,在发送数据前静默一段时间。如果主机不担心两个连接实例发送的新老数据包可能造成数据问题的话,可以在发送数据前不用等待一段时间。TCP 实现应该为用户提供在宕机重启后是否选择等待这一选项,或者为用户提示性的为所有连接实现静默等待。然而在启动后至少保持 MSL 这些时间后,再提示用户选择是否等待显得不是那没有必要。
最后总结:每一个发送出去的数据段占用一个或者多个序列号,一个数据段发送之后,数据段的序列号处于使用或者繁忙状态中直到 MSL 时间过去,当遇到宕机时,连接发送的最后一段数据段会占用一段序列号空间,如果很快地建立了一个新的连接实例并且使用了上一个连接实例使用的数据序列的话,那么将会在接收端造成数据错乱的问题。
建立一个连接(Establishing a connection)
三次握手是建立连接的一个过程。这个过程一般是由一端发起,然后由另一端响应。当两端同时发起连接请求时,三次握手也同样适用。当同时发起连接建立请求时,发送方发出去 SYN 数据段之后,并没有收到对方对 SYN 的确认,而是收到对端同时发送过来的 SYN 数据段。对于接收方来说,有可能会在处于同时建立连接状态时又收到一个老旧的 SYN 标志数据段。使用 reset 标志段可以解决这个问题。
以下有几个建立连接的实例。尽管这几个实例没有使用带有用户数据的数据段来做连接同步操作,TCP 也需要确保在能保证接收缓冲的数据都是有效的时候才会把数据传递到上层应用(只有把数据缓冲到连接建立阶段,数据才是有效的)。三次握手可以减少假连接的发生概率。这是协议实现在内存和消息检验之间做的权衡。
最简单的三次握手过程如下图。图的具体解释如下。每行都有序号为了方便引用描述。右箭头表示 TCP 数据段从 A 发送到 B,左箭头则相反。省略号 (…) 表示数据段依然在网络中传输。“XXX” 表示数据段丢失或者被拒绝接收。TCP 状态表示离开或者接收到某些数据段(中间横线表示数据段的内容)之后触发的。数据段同时带有序列号、控制字段、ACK 字段。为了明确起见,其余的字段比如窗口大小、地址、长度、文本什么的没有描述出来。
TCP A TCP B
1. CLOSED LISTEN
2. SYN-SENT --> <SEQ=100><CTL=SYN> --> SYN-RECEIVED
3. ESTABLISHED <-- <SEQ=300><ACK=101><CTL=SYN,ACK> <-- SYN-RECEIVED
4. ESTABLISHED --> <SEQ=101><ACK=301><CTL=ACK> --> ESTABLISHED
5. ESTABLISHED --> <SEQ=101><ACK=301><CTL=ACK><DATA> --> ESTABLISHED
Basic 3-Way Handshake for Connection Synchronization
Figure 7.
第二行表示 A 使用序号 100 发送数据。第三行 B 发送了一个 SYN 标志,同时对 A 发送的 SYN 做了一个反馈。B 对 A 的反馈中期望下一个接收的序列号是 101,这个 SYN 占用了 100 这个序列号。第四行 A 使用空的数据段对 B 的 SYN 做了一个反馈。第五行,A 发送了一些数据。注意第五行序列号与第四行序列号是一样的,因为 ACK 数据段不占用序号空间(如果 ACK 占用序号空间,那我们也要对 ACK 标志位做 ACK,这样会一直处于往返确认的循环中)。
同时建立稍微有些复杂,如下图。每个 TCP 的状态从 CLOSED 变化到 SYN-SENT 再到 SYN-RECEIVED 再到 ESTABLISHED.
TCP A TCP B
1. CLOSED CLOSED
2. SYN-SENT --> <SEQ=100><CTL=SYN> ...
3. SYN-RECEIVED <-- <SEQ=300><CTL=SYN> <-- SYN-SENT
4. ... <SEQ=100><CTL=SYN> --> SYN-RECEIVED
5. SYN-RECEIVED --> <SEQ=100><ACK=301><CTL=SYN,ACK> ...
6. ESTABLISHED <-- <SEQ=300><ACK=101><CTL=SYN,ACK> <-- SYN-RECEIVED
7. ... <SEQ=101><ACK=301><CTL=ACK> --> ESTABLISHED
Simultaneous Connection Synchronization
Figure 8.
使用三次握手的主要原因是可以防止上一个连接实例发送的老旧的连接建立标志会对现在进行的连接初始化造成影响。为了防止这种老旧包对当前连接建立造成影响,引入了 reset 控制包。如果接收 TCP(发送方处理逻辑请看图 9)处于一个非同步的状态(比如 SYN-SENT, SYN-RECEIVED)时接收到了一个 reset 控制包,他会返回到 LISTEN 状态。如果 TCP 处于已经同步状态(ESTABLISHED, FIN-WAIT-1, FIN-WAIT-2, CLOSE-WAIT, CLOSING, LAST-ACK, TIME-WAIT)他会拆除当前连接,并且通知上层连接用户(java 中的 connection reset by peer 异常)。我们接下来讨论当连接处于半打开情况下,出现这种异常的场景。
TCP A TCP B
1. CLOSED LISTEN
2. SYN-SENT --> <SEQ=100><CTL=SYN> ...
3. (duplicate) ... <SEQ=90><CTL=SYN> --> SYN-RECEIVED
4. SYN-SENT <-- <SEQ=300><ACK=91><CTL=SYN,ACK> <-- SYN-RECEIVED
5. SYN-SENT --> <SEQ=91><CTL=RST> --> LISTEN
6. ... <SEQ=100><CTL=SYN> --> SYN-RECEIVED
7. SYN-SENT <-- <SEQ=400><ACK=101><CTL=SYN,ACK> <-- SYN-RECEIVED
8. ESTABLISHED --> <SEQ=101><ACK=401><CTL=ACK> --> ESTABLISHED
Recovery from Old Duplicate SYN
Figure 9.
图 9 简单描述了从重复老旧包中恢复的过程。第三行,之前连接实例的老包到达了 B。B 没法确认这是新包还是老包,所以它正常的对这个包进行了回馈。A 收到回馈包后发现进行确认的序列号不对,所以他向 B 发送了一个 reset 控制包,这个控制包还是使用了 91 这个老包的序列号。B 收到 reset 控制包之后重新进入 LISTEN 状态。当新的 SYN(100)包到达 B 时,连接正常建立。如果第六行的 SYN 包早于 RST 到达 B 的话,情况会有些复杂。
半打开连接和其他异常状态状态的连接连接的一端已经关闭连接并且没有通知对方或者由于突然宕机导致连接的一端关于 TCP 状态的记录都丢失,这种连接被称为半打开连接。如果它试图向对方发送数据,这种连接会被重置。
如果连接在 A 端的状态已经不存在,那么在 B 端的应用试图给 A 端发送数据的时候会收到 A 端发送过来的 RESET 控制消息。这个消息通知 B,该连接出了某些问题,可以拆除该链接。
假设 A 和 B 正在通过 TCP 通讯,之后由于 A 宕机,导致 A 关于连接的状态都丢失了。有些系统有故障恢复机制,这个取决于 A 使用的 TCP 模块所在的操作系统实现。当 A 主机重启后,A 仿佛重新开始,或者从某个恢复点恢复过来。所以 A 试图重新建立连接,或者在他认为已经建立的连接上发送数据。在第二种情况下,他会收到 TCP 模块的连接已经关闭的通知。第一种情况下,A 试图发送 SYN 数据包。这种情况会在图十中描述。
TCP A TCP B
1. (CRASH) (send 300,receive 100)
2. CLOSED ESTABLISHED
3. SYN-SENT --> <SEQ=400><CTL=SYN> --> (??)
4. (!!) <-- <SEQ=300><ACK=100><CTL=ACK> <-- ESTABLISHED
5. SYN-SENT --> <SEQ=100><CTL=RST> --> (Abort!!)
6. SYN-SENT CLOSED
7. SYN-SENT --> <SEQ=400><CTL=SYN> -->
Half-Open Connection Discovery
Figure 10.
当第三行的 SYN 到达 B 时,B 处于同步状态,并且 SYN 携带的序列号超出了 B 的窗口范围,所以 B 返回了 100,通知 A 希望下次发送 100 的序号。A 收到 100 的反馈后发现,并不是自己曾经发送过的序列号,数据不同步,所以发送 reset 控制到到 B。B 在第五行关掉连接。A 还是尝试与 B 建立连接。这又回到了图 7 描述的三次握手过程上。
当 A 宕机并且 B 还认为连接是同步的并且打算发送数据时,情况会比较有意思。这在图 11 有描述。在这种情况下 A 不会接收 B 发送过来的数据,因为 A 已经不存在之前的连接了,所以 A 发送了一个 RST 控制包。B 收到 RST 控制包后重置了连接。
TCP A TCP B
1. (CRASH) (send 300,receive 100)
2. (??) <-- <SEQ=300><ACK=100><DATA=10><CTL=ACK> <-- ESTABLISHED
3. --> <SEQ=100><CTL=RST> --> (ABORT!!)
Active Side Causes Half-Open Connection Discovery
Figure 11.
图 12 描述了 A 和 B 都处于被动打开状态(LISTEN)。上一个连接实例的 SYN 数据包到达了 B,B 进行了状态转换。B 对 A 发送了一个对老包的确认。A 收到确认后,向 B 发送了一个 RST 控制包,B 重新进入监听状态。
TCP A TCP B
1. LISTEN LISTEN
2. ... <SEQ=Z><CTL=SYN> --> SYN-RECEIVED
3. (??) <-- <SEQ=X><ACK=Z+1><CTL=SYN,ACK> <-- SYN-RECEIVED
4. --> <SEQ=Z+1><CTL=RST> --> (return to LISTEN!)
5. LISTEN LISTEN
Old Duplicate SYN Initiates a Reset on two Passive Sockets
Figure 12.
还有很多其他情况也可能发生,RST 的产生和处理情况遵守以下规则:
重置的产生(Reset Generation)
通常情况下如果收到一个不属于本连接实例的数据段的话都要产生并发送重置控制包。如果不确定这个包是不是这个连接的,不能发送重置控制包。
有三种状态:
- 当连接不存在时除了 reset 控制包之外的其他数据包发送过来时,都应该向对端发送一个重置控制包。特别的,如果 SYN 试图与一个不存在的 TCP 模块建立连接,也应该通过发送重置控制包通知对方。如果接收到的数据段有 ACK 标志,重置数据包使用 ACK 的序列号,否则重置数据包使用序列号 0,并且 ACK 字段设置为 0 与数据段长度的和。最后连接还是保持关闭状态。
- 如果连接处于非同步状态(LISTEN,SYN-SENT,SYN-RECEIVED)收到了未曾发送过的数据段的确认信息或者收到的数据段的安全等级、隔离级别与当前连接要求的不符合,接收端将会向发送端发送一个重置消息。如果我们发送的 SYN 还没有得到确认(同时打开的情况下)并且收到的数据段的优先级比本地的优先级高,那我们要么升级(如果用户或者系统允许的话,这里 TCP 应该会提供相应的控制接口)本地的优先级要么发送 reset 控制包到对端;或者如果接收的数据包的优先级比本地端要求的低,我们会继续处理(如果发送方不能把自己的优先级提升到与我们本地一样的优先级的话,连接会被关闭,这个是通过后续的数据段来检测到的)。如果我们的 SYN 已经被确认了,那么接收到的数据包的优先级必须与我们本地的优先级一样,否则会向对端发送 reset 控制包。如果接收到的数据段有 ACK 标志,重置数据包使用 ACK 的序列号,否则重置数据包使用序列号 0,并且 ACK 字段设置为 0 与数据段长度的和。最后连接还是保持关闭状态。
- 如果连接已经处于同步状态(ESTABLISHED, FIN-WAIT-1, FIN-WAIT-2, CLOSE-WAIT, CLOSING, LAST-ACK, TIME-WAIT)并且收到不可接收的数据段(超过接收窗口大小或者不可接收的确认 — 没有发送过的数据包的确认,通过 SND.UNA 与 SND.NXT 确定)的时候,需要发送一个带有 SND.NXT 序列号的空数据包与一个期望下次收到的序列号的确认包,TCP 保持现有状态。如果数据包包括安全级别、隔离、优先级,但是不满足本地连接对安全级别、隔离、优先级的要求,本地会关闭连接并进入到关闭状态,并发送 RESET 控制包,RESET 控制包使用发送过来的确认字段中的序列号。
RESET 处理(Reset Processing) 除 SYN-SENT 以外的其他状态,reset 包都是通过 SEQ 字段来检查是否是正常的。如果 reset 包的序列号在窗口内,那么它就是合法的。在 SYN-SENT 状态中,如果接收到了对本地之前发送的 SYN 的确认的话,收到的 reset 包就是正常的。
接收方收到 reset 包之后先检查是不是正常的(在接收窗口内),如果是正常的然后改变状态。如果连接处于 LISTEN 状态,忽略这个 reset 包。如果连接处于 SYN-RECEIVED 状态并且之前是处于 LISTEN 状态的话,连接重新进入 LISTEN 状态,否则的话连接关闭连接并且进入 CLOSED 状态。如果连接处于其他状态,直接关闭连接并且通知上层用户进程。
关闭连接
CLOSE 操作意味着我没有数据需要发送了。对于全双工的连接来说,CLOSE 操作可能显得比较模糊,并且没有对对端做任何的说明。执行 CLOSE 操作的一端仍然可以继续读取数据,除非对端也停止发送数据。所以一个程序可以发送多个 send 指令最后跟着一个 close,然后等待对面发送数据过来直到对面停止发送数据。当对面没有数据发送的时候,我们可以通知用户,让用户可以优雅的关闭连接。TCP 在连接关闭前会可靠地把数据分发出去,如果一个用户不需要新数据了,那么他可以监听连接直到收到对面关闭连接的通知(-1),这样可以确保自己发送的数据对面都已经接收到了。用户发送完数据并且关闭连接后,需要一直读取连接传递上来的数据直到收到连接关闭的通知(-1)。
有三个关闭场景:
-
本地用户进程主动关闭连接
把位于等待发送队列中最后一个的数据段打上 FIN 标志。本地 TCP 不再接收用户发送的数据,并且本地连接进入 FIN-WAIT-1 状态。这个状态中的连接还可以继续接收数据。发送出去的数据包括 FIN 会一直重试发送直到收到确认。当连接对端收到所有数据并且为刚才发送的 FIN 返回了一个确认,并且也发送一个 FIN,本地连接会确认对端发送的 FIN。注意:收到对面的 FIN 指令后会对这个 FIN 做 ACK,但是本地的 FIN 需要等待本地用户调用 CLOSE 方法来触发发送。
-
远端用户进程发送 FIN 指令关闭连接
收到 FIN 数据后,TCP 会通知本地用户连接对端停止发送数据。用户会调用 CLOSE 方法来关闭本地连接,TCP 把本地数据都发送到对端之后,发送 FIN 数据到对端。等收到 FIN 的确认信息后,本地连接被关闭并删除。如果发送出去的 FIN 很久没有得到确认,超过用户设置的一段时间后,连接被关闭。
-
两端用户进程同时关闭连接
连接两段同时关闭连接,两段会同时发送 FIN 控制包。当 FIN 控制包之前的数据都被处理和确认后,TCP 可以对 FIN 控制包进行确认。两段收到 ACK 后,关闭连接。
TCP A TCP B
1. ESTABLISHED ESTABLISHED
2. (Close)
FIN-WAIT-1 --> <SEQ=100><ACK=300><CTL=FIN,ACK> --> CLOSE-WAIT
3. FIN-WAIT-2 <-- <SEQ=300><ACK=101><CTL=ACK> <-- CLOSE-WAIT
4. (Close)
TIME-WAIT <-- <SEQ=300><ACK=101><CTL=FIN,ACK> <-- LAST-ACK
5. TIME-WAIT --> <SEQ=101><ACK=301><CTL=ACK> --> CLOSED
6. (2 MSL)
CLOSED
Normal Close Sequence
Figure 13.
TCP A TCP B
1. ESTABLISHED ESTABLISHED
2. (Close) (Close)
FIN-WAIT-1 --> <SEQ=100><ACK=300><CTL=FIN,ACK> ... FIN-WAIT-1
<-- <SEQ=300><ACK=100><CTL=FIN,ACK> <--
... <SEQ=100><ACK=300><CTL=FIN,ACK> -->
3. CLOSING --> <SEQ=101><ACK=301><CTL=ACK> ... CLOSING
<-- <SEQ=301><ACK=101><CTL=ACK> <--
... <SEQ=101><ACK=301><CTL=ACK> -->
4. TIME-WAIT TIME-WAIT
(2 MSL) (2 MSL)
CLOSED CLOSED
Simultaneous Close Sequence
Figure 14.
优先级和安全
两个端口必须有相同的隔离级别和安全才能建立连接,并且连接的优先级要高于或者等于两个端口的优先级。
TCP 使用的优先级和安全参数就是 IP 中定义的相关参数。本协议规范中的安全 / 隔离都是指 IP 中的安全相关的参数,包括安全、隔离、用户组和相关限制。
如果一个带有不相符的安全 / 隔离连接请求发送过来,或者优先级比较低,应该拒绝这个连接请求。应该在发送 SYN 之后收到对面的 ACK 时(给发送方一个提升自己优先级的机会),发现连接请求的优先级还是低的时候,需要拒绝这个链接。
需要注意的是,即使是 TCP 使用默认的安全配置,在接收到数据包时仍然需要检查数据包的优先级,并且在必要的时候需要试着提升自己的优先级。
即使是在非安全环境中,安全参数也有可能会使用(值可能表示数据是不需要加密的),所以当非安全环境中的主机 TCP 模块收到用户的安全参数时仍然需要接收这些参数,但是不需要把这些参数发送出去。
数据交流
连接一旦建立,数据就可以通过数据段的方式在连接上传递。因为数据段有可能会丢失或者有可能会校验出错,或者网络拥塞,TCP 通过重发机制(超时)保证每个数据段都可以可靠传递。因为网络问题或者重传机制,重复的数据段有可能会被接收到。在序列号章节已经讨论过,TCP 通过对接收数据段的序列号和确认序列号进行测试,来确定接收的数据段是不是可以接收的。
发送端通过 SND.NXT 变量来跟踪下一个发送的数据使用的序列号。接收端通过 RCV.NXT 变量跟踪下一个期望收到的数据使用的序列号。发送端使用 SND.UNA 变量跟踪最久发送的但是没有收到确认的序列号。如果数据通道短暂的空闲,并且发送出去的数据都得到确认了,上述三个变量应该是相等的。
当发送端将数据发送出去,发送端会增加 SND.NXT 变量的值。当接收端收到数据的时候它会增加 RCV.NXT 变量,并且会发送确认包。当发送方收到数据的确认信息后,他会增加 SND.UNA 变量的值。这三个值的差值可以看作是对通讯延迟的一个基本衡量。这三个变量的变化量是发送数据段的长度。注意,连接建立状态下,所有的数据段都应该包含当前的确认信息。
用户的 CLOSE 函数调用类似一个推的过程,FIN 控制标志包含有 PUSH 标志的功能。
重发超时
因为组成互连网络的各种网络与使用 TCP 的用户之多,导致超时时间需要动态的确认。下面介绍一种计算超时时间的方法。
从发送一个带有序列号的字节出去到收到对这个字节序列的确认信息(确认包确认的序列号不必与发送数据的序列号相等)的时间叫做 RTT。接下来计算平滑的 SRTT:
SRTT = ( ALPHA * SRTT ) + ((1-ALPHA) * RTT)
基于上述公式,计算超时时间:
RTO = min [UBOUND,max [LBOUND,(BETA*SRTT)]]
UBOUND 是超时时间的上限,LBOUND 是下限,ALPHA 是平滑因子 (e.g., .8 to .9),BETA 是延迟变化因子 (e.g., 1.3 to 2.0)。
紧急信息的传递
TCP 紧急机制可以允许发送方让接收方接收某些紧急数据,并且允许接收方确定什么时候把紧急数据都接收完成(紧急数据边界)。这个机制允许在数据流中放一个指针来表示紧急数据的结尾。当前数据段如果有紧急指针,并且有紧急标志,那么 TCP 会通知用户进入紧急模式;当处理到紧急指针时,TCP 通知用户进入紧急模式。当用户处于紧急模式时,紧急指针被更新了,用户是不会被通知的。
每个数据段都有紧急指针字段,紧急标志表明 TCP 必须把紧急指针字段的值与当前段序列号相加获得紧急指针的具体值。如果没有紧急标志的话,表明当前段没有紧急数据。
发送紧急数据最少也要一个字节。如果紧急数据段同时也标志了 PUSH 标志,那么数据会被尽快发送到对端用户进程。
管理窗口
窗口是发送方用来表明它还可以接收的数据序列号范围。这个窗口基本就可以认为是接收方的接收缓存大小。
大的窗口可以加快数据传输。如果传送过来的数据多于窗口大小,多余的数据会被丢弃。这会导致不必要的数据重传,增大网络负荷。过小的窗口会产生更多的传输时延。
这个机制允许一开始使用大的窗口,随后如果没有更多缓存空间的话,可以慢慢减小窗口尺寸。这被称作窗口缩小。可靠性规则制定,TCP 不会主动减小窗口的大小,而是要根据对面 TCP 的情况来改变窗口大小。
即使发送窗口为 0,TCP 发送模块在收到用户传过来的数据后,也要发送最少一个字节。即使接收窗口为 0,发送方也要定时发送数据到接收方。当接收窗口为 0 时,发送方的数据重试间隔为 2 分钟。当任一个 TCP 接收窗口为 0 时,定时发送数据会把接收方重新打开的窗口通知到发送方,这样是很有必要的。
当接收窗口为 0 时,如果收到了数据,也应该返回一个回馈来表示下一个期望收到的字节序列号。
为了适应接收窗口的大小,TCP 把需要发送的数据打包成窗口大小,在重新发送队列中有可能会重新打包。这种重新打包不是必须的,但是也许很有帮助。
在一个单向的数据流连接中,确认信息中有相关窗口大小,这个确认信息使用相同的序列号,这样在多个确认包乱序到达时 TCP 没办法对这些确认包进行重新排序。这不是很严重的问题,但是这样会导致窗口会根据老确认包中的窗口大小来发送数据。这个问题的改善方法就是,对于收到接收方的确认包中,他确认的序列号必须比上一次处理的确认序列号大,发送方才会调整发送窗口。
窗口的管理对通讯性能有很大的影响。下面是对 TCP 实现的几个建议。
- 过小的窗口会让数据通过很小的数据段发送,如果网络性能提高,可以稍微增大窗口的大小。
- 接收方可以延迟更新窗口大小,当窗口大小可以达到最大窗口大小的一定比例后再更新(比例有可能是 20% 或者是 40%)。
- 对于发送方的一个建议就是为了防止发送小数据包,可以等窗口到达一定大小后再发送数据。当用户调用 PUSH 函数时,TCP 应该立即发送数据,不管是多小的包。
- 确认包不应该被延迟发送,否则对端会因超时重新发送数据。当收到小数据包,发送确认包时不更新窗口大小,等到窗口变大时,再通过确认信息更新窗口大小。
- 探测接收方窗口大小的数据包有可能会被截断成更小的数据包。如果一个字节的数据包被接收方接收了,那么这个字节会占用刚开始释放的窗口的一个字节的空间。当窗口不在是 0 时,发送方尽可能发送多的数据出去,那么这个数据有可能会被打包成很多大的或者小的数据包。随着时间推移,接收方会短暂停止分配更多的窗口空间,导致大的数据包会被切分成更小的数据包。时间久了数据会通过很多小数据包来传输。
这里给 TCP 实现的建议就是尽可能的将小窗口合并成大窗口。因为一些简单的 TCP 实现总是会分配一些小窗口。
接口(Interfaces)
两种接口
用户进程到 TCP 的接口,TCP 到更底层服务的接口。
我们对用户进程到 TCP 之间的接口有详细的定义,但是 TCP 到底层协议之间的接口这里没有表述,因为这个是需要底层协议进行定义。这里底层协议就是 IP,这里我们只涉及 TCP 使用的几个 IP 参数。
用户与 TCP 之间的接口
以下用户使用的 TCP 函数都是虚构的,因为每个操作系统的实现都不尽相同。所以,我们必须提示读者,每个 TCP 实现有可能有不同的 TCP 接口。但是所有的 TCP 实现都应该提供最小的一个服务子集,这个子集可以维持 TCP 的运行。这部分描述了所有 TCP 实现应该实现的功能接口。
用户命令
接下来的章节会功能性的描述每个用户 / TCP 接口。每个接口的名字和大多数高级语言中的函数方法名字一样,这种命名方法并不是排除一些自陷类型的服务调用(比如 SVCs,UUOs,EMTs)。
下面的用户命令指定了 TCP 用于支持通讯所需的最基本的操作。不同的实现可能定义不同的方法格式,并且它们有可能会合并一些基础方法为一个方法,或者把一些基础方法分解成多个方法。特别的,用户可能希望当他们在对一个连接进行第一次的 SEND 或者 RECEIVE 时,TCP 会自动打开这个连接。在提供通讯功能时候,TCP 实现不能只接收用户请求,也同时给它服务的进程返回相关信息。相关信息包括:
-
连接的基本信息。(中断,远程关闭,绑定一个未指定的远程主机)
-
对于用户的方法调用给予反馈,比如成功或者各种类型的失败。
Open
格式:OPEN (local port, foreign socket, active/passive [, timeout] [, precedence] [, security/compartment] [, options]) -> local connection name
我们假设 TCP 知道他服务的进程身份,并且进程在使用连接的时候 TCP 会检查进程有没有相应的权限。根据 TCP 实现的不同,连接的源地址的与端口是由 TCP 与 IP 来提供的。这些都是基于安全考虑,这样的话,就不会出现一个 TCP 主机装扮成另一个 TCP 主机。所以一个进程就不能通过与 TCP 合作来装扮成另一个进程。
当主动 / 被动标志被设置为被动时,相当于调用 LISTEN 方法,来等待连接。被动连接有可能会指定特定的外部主机地址来等待特定主机的连接,或者不指定主机地址,可以接收所有的主机发起连接的请求。指定了特定远端主机的连接通过 SEND 指令可以转变成主动连接。一个 TCB(传输控制块)数据结构被创建并且使用 OPEN 命令传过来的参数来填充部分字段的值。
主动的 OPEN 调用会立即启动连接建立(同步)步骤。用户提交到 TCP 的数据如果在指定的超时时间段内没有发送到目的主机的话,TCP 会关闭这个链接。这个全局的超时时间配置是 5 分钟。
TCP 或者操作系统的某些组件会检查用户是否有权限设置打开一个指定了优先级或者安全 / 隔离级别参数的连接。如果这些参数没有设置,参数默认值变会指定。
TCP 只会接收安全 / 隔离级别与 OPEN 设置相等的并且安全等级比 OPEN 设置的安全等级相等或者高的数据段。
连接的优先级取 OPEN 参数中指定的值与接收到的数据(SYN 或者 ACK 中的)中优先级之间最大的那个值,并且这个优先级在之后的连接生命周期中一直固定不变。TCP 实现也许允许用户控制优先级协商。比如,用户可以指定只能优先级相等,或者在提升优先级等级时必须得到用户的确认。
TCP 会把本地连接的名字返回给用户。这个连接名字可以被认为是由 < 本地 socket, 外部 socket > 指定的连接的别名。Send
格式:SEND (local connection name, buffer address, byte count, PUSH flag, URGENT flag [,timeout])这个调用会把用户指定缓存中的数据通过指定的连接发送出去(未必真的发送)。如果连接没有打开,SEND 方法可能会触发报错异常。一些 TCP 实现也许允许先执行 SEND 命令然后自动打开连接。如果用户进程没有使用这个连接的权限则会报错。
如果 PUSH 标志被设置了,数据会马上传送到接收方,并且在创建的最后的数据段中会设置 PUSH 标志。如果 PUSH 没有设置,那么数据有可能会等待下次的 SEND 调用然后合并在一起再发送。如果 URGENT 紧急标志设置了,发送到目的地的数据段都会设置紧急指针字段值。如果紧急指针之前的紧急数据都没有被用户进程处理的话,TCP 会把紧急条件报告给用户进程。紧急标志就是为了让接收方尽快处理相关数据,同时也可以标记什么时候紧急数据都被接收方收到了。发送方指定的紧急数据的次数与接收方收到的紧急数据通知的次数可能不一样。
如果在 OPEN 函数调用中没有指定外部地址,但是连接已经建立了(LISTEN 建立的连接),一个指定的 buffer 窗口大小会被发送到连接的另一端。用户使用 OPEN 时没指定外部地址的话,在使用 SEND 时可以不用了解连接另一端的地址。
然而,在连接另一端没有确定前,用户调用 SEND 会报错。用户可以通过 STATUS 函数查看链接状态。在某些 TCP 实现中绑定一个未指定的外部地址会通知用户。当 SEND 提供超时时间时,连接的超时时间会被这个时间替换。
在最简单的 TCP 实现中,调用 SEND 会一直阻塞,直到数据发送完成或者超时。但是这种简单实现会导致死锁(比如连接两端都发送数据而不接收数据)并且性能也不高,所以不推荐。稍微复杂的实现会让 SEND 方法立即返回,这样可以让用户进程与网络 io 同时进行,况且也可以调用多次 SEND 函数。SEND 函数调用遵守先来后到准则,所以如果 TCP 来不及处理这些数据,会把这些请求数据放入队列中。
我们隐式的引出了异步的用户接口,SEND 调用后,底层 TCP 模块会通过信号或者模拟中断来通知用户进程。一个可选的实现就是立即返回一个响应给调用进程。比如,SEND 会立即返回一个本地确认,即使发送出去的数据还没有被远端 TCP 接收到。我们可以乐观的认为最终会成功。如果我们错了,连接会因为超时断开。在这种同步的调用实现中,也会有一些信号通知,只不过这些通知,但是这些通知是与连接相关的并不是关于数据收发的。
为了分别处理多个 SEND 发送之后的结果(成功或者失败),每个 SEND 调用都应该返回对应的响应状态码与存放接收数据缓存的内存地址。TCP 到用户的接口会在下文讨论,主要讨论函数调动应该返回什么的信息给用户进程。
Receive
格式: RECEIVE (local connection name, buffer address, byte count) -> byte count, urgent flag, push flag这个命令会为相关连接分配一个接收数据的缓存。如果在这个命令前没有执行 OPEN 命令或者进程没有相关权限,那么会抛出异常。
在最简单的实现里,调用这个函数的进程会一直阻塞,直到接收缓存(进程传递的缓存不是 TCP 的接收缓存)填满或者某些异常出现,但是这种实现方式会导致死锁。复杂的实现版本允许多个 RECEIVE 调用。这些 RECEIVE 的接收缓存会在数据段到达时被填充。精心设计的系统(有可能是异步系统)可以在收到 PUSH 标志或者 RECEIVE 接收缓冲被填满时通知调用进程,这样大大提升了系统吞吐量。
如果在收到 PUSH 标志前,接收到的数据已经可以填满 RECEIVE 接收缓冲,那么 RECEIVE 方法调用会返回,但是 PUSH 标志不会被标记。RECEIVE 接收缓存会尽可能的被填充,但是如果在没被填满前收到了 PUSH 标志,那么数据会被立即返回,并且会设置 PUSH 标记。
如果有紧急数据到达,那么 TCP 会通过 TCP-to-user 信号通知用户进程。用户进程之后进入紧急模式。如果 URGENT 标志开启,需要等待更多的紧急数据。如果 URGENT 标志关闭,RECEIVE 会返回接收到的所有紧急数据,并且用户进程进入正常模式。需要注意的是,紧急数据不能与非紧急数据处于同一个 RECEIVE 接收缓存中,除非有标志来明确这两种数据的边界。
为了区分多个正在等待接收数据的 RECEIVE 调用,同时也考虑有些接收缓存不会被填满,在调用返回时会跟着一个接收缓存的地址,以及读取的字节数量。其他 TCP 实现,可能允许进程调用 RECEIVE 时使用 TCP 的内部接收缓存,或者 TCP 与用户进程共享一个环形缓存。
Close
格式: CLOSE (local connection name)这个命令会将指定的连接关闭。如果连接没有打开,或者进程没有相关权限,异常会被返回。关闭命令是一个优雅的操作,当多个 SEND 命令发送的数据在传输中(或者重传)并且流量控制允许,这些数据会在成功发送到对面后,连接才会关闭。所以,可以在多个 SEND 命令后跟一个 CLOSE 命令,这样也会保证发送的数据会到达目的地。需要注意的是,即使连接处于半关闭状态,但是进程仍然可以通过 RECEIVE 命令收数据,因为对面进程仍然会发送数据。所以 Close 命令是我不想发送数据了,但是不代表我不想接收数据了。在超时前,关闭连接的一方有可能不能抛弃剩余的数据。在这种场景下 Close 命令可以转成 ABORT 命令,关闭连接。
用户可以在任何时间主动关闭连接,有可能会因为 TCP 的某些提示(远程关闭,重发超时,远端不可达)关闭连接。
因为关闭连接需要与对端进行通讯,所以关闭连接的一方会短暂地以处于 closing 状态。在 TCP 还没有对 close 做出响应前重新打开连接会报错。Close 有着与 push 一样的语义。
Status
格式:STATUS (local connection name) -> status data不同的 TCP 实现,这个命令可能不一样,没有这个指令对整体 TCP 接口并没有影响。状态信息一般是从连接关联的 TCB 查询获得的。
该命令返回包含如下信息的数据块:
- 本地 socket
- 外部 socket
- 本地连接名称
- 接收窗口
- 发送窗口
- 连接状态
- 等待确认的缓存数量
- 等待接收的缓存数量
- 紧急状态
- 优先级
- 安全 / 隔离
- 超时时间
根据连接状态以及实现,某些字段可能没有或者没有任何意义。如果进程没有权限调用这个命令,则会返回一个异常,这样会防止没有授权的进程获取相关连接的状态。
Abort
格式:ABORT (local connection name)这个命令会丢弃所有等待发送的数据和等待接收的数据,同时会移除 TCB 控制块,并且向对端发送一个 RESET 消息。根据实现的不同,用户进程可能会收到每个发送或者接收数据块的删除通知,或者只会收到一个 ABORT 确认。
TCP-to-User Messages
假定操作系统提供了 TCP 异步通知用户进程的方式。当 TCP 通知用户进程时,相关数据会传递给用户进程。本协议一般指异常消息。在其他情况下,有可能会通知关于 SEND,RECEIVE 或者其他命令的处理返回结果。一般包含一下信息:
- 本地连接名称 经常
- 相应字符串 经常
- 缓存地址 发送或者接收
- 接收的字节数 接收
- PUSH 标志 接收
- 紧急标志 接收
TCP/Lower-Level Interface
TCP 实际上通过调用底层的协议模块来在网络上实现发送和接收数据。一个例子就是 ARPA 网中,底层模块是 IP 协议。 当底层协议是 IP 时,它提供服务类型参数和数据包存活时间选项。TCP 使用如下参数来对接 ip 的这些参数:
Type of Service = Precedence: routine, Delay: normal, Throughput: normal, Reliability: normal; or 00000000. Time to Live = one minute, or 00111100.
最大的数据段生命周期是 2 分钟。我们明确如果数据在一分钟内不能分发出去,便把这个数据段丢弃。
如果底层协议是 IP(其他也能提供类似特性的协议)并且源地址路由(Source Routing .)被使用,TCP 提供的接口必须提供可以操作此类信息的接口。这个功能非常重要,这样 TCP 就可以获取源地址与目的地地址来计算校验和(头部的伪地址)。这个也可以保留请求的返回路由信息。
任何底层的协议必须提供源地址、目的地址、协议字段、检测 TCP 数据长度的方式,这些可以提供与 ip 一样的服务并且可以用来计算 TCP 的校验和。
事件处理(Event Processing)
这个章节描述的处理过程只是一个可能实现的示例。其他的实现可能有不同的处理序列,但是他们同本章描述的只有细微的差别,而不应该有很大的实质性的差别。
TCP 的活动可以简单描述成对事件的响应。事件可以被分成三类:用户调用、数据段到达和超时。本章描述 TCP 处理这些事件的过程。在很多场景中,TCP 的处理流程依赖当前连接所处的状态。
Events that occur:
User Calls
OPEN
SEND
RECEIVE
CLOSE
ABORT
STATUS
Arriving Segments
SEGMENT ARRIVES
Timeouts
USER TIMEOUT
RETRANSMISSION TIMEOUT
TIME-WAIT TIMEOUT
TCP/user 接口模型是指用户命令会立即返回,并且延迟一段时间后会得到相应结果。在下文,‘signal’是指延迟获得结果。异常响应被描述为一段字符串。比如用户引用了不存在的连接会得到:error:connection not open 这个响应。
在下文描述的针对序列号、确认序列号、窗口等等的运算都是基于 232 这个序列号空间做的取模。同样 =< 意思是等于或者小于某个数值(依旧是对 232 做取模运算后的)。
处理到达数据包的第一步就是检验序列号(比如数据段中的序列号位于接收窗口中),然后被放入队列中,然后通过序列号排序。
如果我们收到的数据段与之前已经收到的数据段有一部分重叠,那么我们重新构造这个数据段,只包含新的数据,然后重新组织头部信息来保持一致。
注意,如果没有提及连接状态变更,那么连接一直保持这个状态不变。
OPEN Call
CLOSED STATE (i.e., TCB does not exist)
创建一个新的 TCB 来保存连接的状态,保存本地 socket 标识符(变量引用)、外部 socket 地址、优先级、安全 / 隔离、以及超时相关的信息。
注意,被动打开的连接在没有指定外部 socket 地址,TCB 中的相关信息需要等待外部进程进行 SYN 同步的时候进行填充。检查这个用户被允许的安全与优先级,
如果不符合则返回 “error: precedence not allowed” 或者返回 “error: security/compartment not allowed.” 如果是被动打开的话,连接进入 LISTEN 状态并且返回。
如果是主动打开,但是没有指定外部 socket 地址,返回 “error:foreign socket unspecified”,如果主动打开,并且指定了外部 socket 地址,发送一个 SYN 数据包。
选择一个初始化序列号,一个 < SEQ=ISS><CTL=SYN > 这样的格式的数据包被发送。把 SND.UNA 值设置为初始化序列号,SND.NXT 值设置为初始化序列号 + 1,进入 SYN-SENT 状态并返回。
如果用户进程没有创建连接的权限,返回 “error: connection illegal for this process”。如果没有足够的内存空间来创建连接,返回 “error: insufficient resources”。
LISTEN 状态
如果打开时模式是主动模式,并且外地 socket 地址指定了,那么连接状态从被动转成主动,并选择一个初始化序列号。
发送一个 SYN 数据包,把 SND.UNA 值设置为初始化序列号,SND.NXT 值设置为初始化序列号 + 1,进入 SYN-SENT 状态。与 SEND 命令发送的数据可能与 SYN 数据包一起发送,也有可能缓存起来等到连接进入 ESTABLISHED 时再发送。
如果 SEND 命令设置了紧急标志,那么这个标志必须与数据包一起发送出去。如果没有内存空间来缓存发送的数据,返回 “error: insufficient resources”。
如果外部地址没有指定那么返回 “error: foreign socket unspecified”。
SYN-SENT STATE
SYN-RECEIVED STATE
ESTABLISHED STATE
FIN-WAIT-1 STATE
FIN-WAIT-2 STATE
CLOSE-WAIT STATE
CLOSING STATE
LAST-ACK STATE
TIME-WAIT STATE
返回:error: connection already exists
SEND Call
CLOSED STATE (i.e., TCB does not exist)
如果用户没有权限访问此链接,返回 “error: connection illegal for this process”。
否则返回:“error: connection does not exist”。
LISTEN STATE
如果指定了外部 socket 地址,那么连接状态从被动转成主动,并选择一个初始化序列号。
发送一个 SYN 数据包,把 SND.UNA 值设置为初始化序列号,SND.NXT 值设置为初始化序列号 + 1,进入 SYN-SENT 状态。与 SEND 命令发送的数据可能与 SYN 数据包一起发送,也有可能缓存起来等到连接进入 ESTABLISHED 时再发送。
如果 SEND 命令设置了紧急标志,那么这个标志必须与数据包一起发送出去。如果没有内存空间来缓存发送的数据,返回 “error: insufficient resources”。
如果外部地址没有指定那么返回 “error: foreign socket unspecified”。
SYN-SENT STATE
SYN-RECEIVED STATE
保存数据,等到状态变成 ESTABLISHED 时,把数据发送出去。如果没有足够内存来保存数据,返回 “error: insufficientresources”。
ESTABLISHED STATE
CLOSE-WAIT STATE
把数据打包成数据段,并且把确认信息、希望收到的下一个数据段序列号一并发送出去。如果没有足够的内存空间,返回 “error:insufficient resources”。如果设置了紧急状态字段,把 TCP 维护的 SND.UP(紧急指针) 值更新为 SND.NXT-1 并设置数据段的紧急状态指针。
FIN-WAIT-1 STATE
FIN-WAIT-2 STATE
CLOSING STATE
LAST-ACK STATE
TIME-WAIT STATE
返回 “error: connection closing”,并且不会为当前请求服务。
RECEIVE Call
CLOSED STATE (i.e., TCB does not exist)
如果用户没有权限访问此链接,返回 “error: connection illegal for this process”。
否则返回:“error: connection does not exist”。
LISTEN STATE
SYN-SENT STATE
SYN-RECEIVED STATE
保存数据,等到状态变成 ESTABLISHED 时,把数据发送出去。如果没有足够内存来保存数据,返回 “error: insufficientresources”
ESTABLISHED STATE
FIN-WAIT-1 STATE
FIN-WAIT-2 STATE
如果收到的数据不能满足 receive 请求,缓存 receive 请求。如果没有足够的内存空间来记录 receive 状态,返回 “error: insufficientresources”。
把收到的数据段组合起来放进 receive 的接收缓存,返回给用户。如果在传递给用户数据的前面有紧急数据,那么通知用户进程。如果 TCP 负责把数据分发到用户进程,那么 TCP 必须给数据发送者一个确认信息。在下面的处理数据段来介绍具体的确认格式。
CLOSE-WAIT STATE
因为对端已经停止发送数据,所以 receive 只能从 TCP 接收缓存中获取数据,如果接收缓存中没有数据,返回 “error: connection closing”。
CLOSING STATE
LAST-ACK STATE
TIME-WAIT STATE
返回 “error: connection closing”。
CLOSE Call
CLOSED STATE (i.e., TCB does not exist)
如果用户没有权限访问此链接,返回 “error: connection illegal for this process”。
否则返回:“error: connection does not exist”。
LISTEN STATE
所有等待接收的数据都被返回 “error: closing”。删除 TCB,进入 CLOSED 状态,返回。
SYN-SENT STATE
删除 TCB,任何等待处理的 receive 和 send 数据都被返回 “error: closing”。
SYN-RECEIVED STATE
如果没有 SENDs 命令,或者没有要发送的数据,构造一个 FIN 数据段并发送。
ESTABLISHED STATE
缓存这个请求,直到所有以前的发送请求都完成,然后生成一个 FIN 数据段发送出去。然后进入 FIN-WAIT-1 状态。
FIN-WAIT-1 STATE
FIN-WAIT-2 STATE
严格讲,这是一个错误操作,应该返回 “error:
connection closing”。返回 ok 也是可以接收的只要第二个 FIN 没有发出去,因为第一个 FIN 有可能在进行重发。
CLOSE-WAIT STATE
缓存这个请求,直到所有的 send 指令都完成,然后发送一个 FIN 标志出去,之后进入 CLOSING 状态。
CLOSING STATE
LAST-ACK STATE
TIME-WAIT STATE
返回 “error: connection closing”。
ABORT Call
CLOSED STATE (i.e., TCB does not exist)
如果用户没有权限访问此链接,返回 “error: connection illegal for this process”。
否则返回:“error: connection does not exist”。
LISTEN STATE
所有缓存的 receive 都会收到 “error:connection reset”。删除 TCB,进入 CLOSED 状态然后返回。
SYN-SENT STATE
所有的 SENDs 和 RECEIVEs 操作都应该收到 “connection reset” 通知,删除 TCB,进入 CLOSED 状态然后返回。
SYN-RECEIVED STATE
ESTABLISHED STATE
FIN-WAIT-1 STATE
FIN-WAIT-2 STATE
CLOSE-WAIT STATE
发送一个重置数据包:
<SEQ=SND.NXT><CTL=RST>
所有的 SENDs 和 RECEIVEs 操作都应该收到 “connection reset” 通知,所有的数据段都应该清空(rst 数据段除外),所有的重发队列的数据都应该清空,删除 TCB,进入 CLOSED 状态然后返回。
CLOSING STATE
LAST-ACK STATE
TIME-WAIT STATE
返回 ok,删除 TCB,进入 CLOSED 状态然后返回。
STATUS Call
CLOSED STATE (i.e., TCB does not exist)
If the user should not have access to such a connection, return
"error: connection illegal for this process".
Otherwise return "error: connection does not exist".
LISTEN STATE
Return "state = LISTEN", and the TCB pointer.
SYN-SENT STATE
Return "state = SYN-SENT", and the TCB pointer.
SYN-RECEIVED STATE
Return "state = SYN-RECEIVED", and the TCB pointer.
ESTABLISHED STATE
Return "state = ESTABLISHED", and the TCB pointer.
FIN-WAIT-1 STATE
Return "state = FIN-WAIT-1", and the TCB pointer.
FIN-WAIT-2 STATE
Return "state = FIN-WAIT-2", and the TCB pointer.
CLOSE-WAIT STATE
Return "state = CLOSE-WAIT", and the TCB pointer.
CLOSING STATE
Return "state = CLOSING", and the TCB pointer.
LAST-ACK STATE
Return "state = LAST-ACK", and the TCB pointer.
TIME-WAIT STATE
Return "state = TIME-WAIT", and the TCB pointer.
SEGMENT ARRIVES
如果状态是 CLOSED
所有接收的数据都会被丢弃,包含 RST 标志的数据段也会被丢弃。不包含 RST 标志的数据段,会返回一个包含 RST 的数据段返回给发送者。
获取数据包中的确认序列号,然后把 RST 数据包的序列号设置为这个序列号,返回给发送者。如果没有设置 ACK 标志,那么 RST 数据包使用序列号 0:
<SEQ=0><ACK=SEG.SEQ+SEG.LEN><CTL=RST,ACK>
如果 ACK 标志被设置了:
<SEQ=SEG.ACK><CTL=RST>
如果状态是 LISTEN
1. 首先检查是不是 RST 数据包
如果是 RST 数据包,直接忽略。
2. 然后检查是不是 ACK 数据包
如果连接处于 LISTEN 状态,收到确认信息是不正常的。对于收到的任何确认信息都应该返回 RST 数据包:
<SEQ=SEG.ACK><CTL=RST>
3. 再然后检查是不是 SYN 数据包
如果收到 SYN 数据包,检查安全性。如果数据包的安全 / 隔离不满足 TCB 里记录的值,直接返回一个 RST 数据包。
<SEQ=SEG.ACK><CTL=RST>
如果数据包的 SEG.PRC(优先级)大于 TCB 里记录的 TCB.PRC,如果用户允许,那么设置 TCB.PRC<-SEG.PRC,如果不允许,直接返回一个 RST 数据包。
<SEQ=SEG.ACK><CTL=RST>
如果数据包的 SEG.PRC(优先级)小于 TCB 里记录的 TCB.PRC,继续。
设置 RCV.NXT 的值为 SEG.SEQ+1,IRS 被设置为 SEG.SEQ,其他控制信息和数据应该被缓存起来等待后续处理。选择一个 ISS,然后发送一个格式为如下的 SYN 包:<SEQ=ISS><ACK=RCV.NXT><CTL=SYN,ACK>
SND.NXT 被设置为 ISS+1,SND.UNA 设置为 ISS。连接状态变更为 SYN-RECEIVED。注意,收到的控制信息和数据可以在 SYN-RECEIVED 状态中处理,但是 SYN 和 ACK 不能重复处理。如果在 listen 时没有指定外部 socket 信息,那么现在可以通过数据包中的信息来填充相关信息了。
4. 最后关于其他控制信息和数据
其他控制信息和数据段(没有 SYN 标志),都应该有反馈信息,收到反馈信息的数据会被丢弃(发送方丢弃)。一个 RST 包可能是非法的,因为这个数据包并不是之前发送过的任何数据的响应信息。程序流程一般到不了这里,如果走到这里,直接把数据删除。
如果状态是 SYN-SENT
1. 检查 ACK 标志
如果设置
如果 SEG.ACK =< ISS 或者 SEG.ACK > SND.NXT,发送一个 RST 控制包(除非接收的数据包 RST 标志被设置了),删除数据段,返回。
<SEQ=SEG.ACK><CTL=RST>
如果 SND.UNA =< SEG.ACK =< SND.NXT
2. 然后检查 RST 标志
如果设置
如果 ACK 被标志了并且是可以接收的,那么通知用户 “error:
connection reset”,删除数据段,进入 CLOSED 状态,删除 TCB,返回。如果没有 ACK 标志,丢弃 RST 控制包,直接返回。
3. 检查安全性和优先级
如果数据包的安全 / 隔离与 TCB 记录的不符合,生成一个 RST 控制包,发送。
如果有 ACK 标志
<SEQ=SEG.ACK><CTL=RST>
如果没有 ACK 标志
<SEQ=0><ACK=SEG.SEQ+SEG.LEN><CTL=RST,ACK>
如果有 ACK 标志
数据段优先级必须与 TCB 优先级相符合否则:
<SEQ=SEG.ACK><CTL=RST>
如果没有 ACK 标志
如果数据包优先级比 TCB 优先级高,并且用户进程允许的话,提升 TCB 优先级,如果用户进程不允许提升 TCB 优先级,那么返回一个 RST 控制包
<SEQ=0><ACK=SEG.SEQ+SEG.LEN><CTL=RST,ACK>
如果数据段优先级比 TCB 优先级低,直接忽略。
如果 RST 控制包发送了,丢弃收到的数据包,然后返回。
4. 检查 SYN 标志
这步骤只应该在 ACK 已经标记或者 ACK 没有标记 RST 也没有标记的情况下。
如果有 SYN 标记,并且优先级和安全 / 隔离都符合,那么 RCV.NXT 设置为 SEG.SEQ+1,IRS 设置为 SEG.SEQ。如果有 ACK 标记,那么 SND.UNA 应该和 SEG.ACK 相等。在重发队列的被确认的数据段应该被删除。
如果 SND.UNA > ISS(我们的 SYN 已经被确认了),连接状态变更为 ESTABLISHED,生成一个 ACK 控制包:
<SEQ=SND.NXT><ACK=RCV.NXT><CTL=ACK>
发送队列中的数据段或者控制包可能会与 ACK 一起发送。如果数据段中还有其他控制信息,那么将在下面检查 URG 标志的第 6 步中处理,否则将会返回。
其他情况下,进入 SYN-RECEIVED 状态,生成 SYN,ACK 数据段并发送:
<SEQ=ISS><ACK=RCV.NXT><CTL=SYN,ACK>
如果有其他控制信息或者数据,缓存这些数据,等到连接进入 ESTABLISHED 状态再去处理。
5. 如果 SYN 和 RST 都没有设置,直接丢弃数据包,返回。
其他状态
1. 检查序列号
SYN-RECEIVED STATE
ESTABLISHED STATE
FIN-WAIT-1 STATE
FIN-WAIT-2 STATE
CLOSE-WAIT STATE
CLOSING STATE
LAST-ACK STATE
TIME-WAIT STATE
数据段按序列号顺序处理。数据段的初步判断主要用来去除重复的数据,然后按照 SEG.SEQ 顺序处理。如果一个数据段既有老数据又有新数据,那么新数据会得到处理。
下面有四种情况用来检验数据段的序列号:
Segment Receive Test
Length Window
------- ------- -------------------------------------------
0 0 SEG.SEQ = RCV.NXT
0 >0 RCV.NXT =< SEG.SEQ < RCV.NXT+RCV.WND
>0 0 not acceptable
>0 >0 RCV.NXT =< SEG.SEQ < RCV.NXT+RCV.WND
or RCV.NXT =< SEG.SEQ+SEG.LEN-1 < RCV.NXT+RCV.WND
如果 RCV.WND 是 0,那么不会接收新数据,但是 ACK、URG、RST 仍然会被处理。
如果数据段不能被接收,那么应该返回一个响应。(如果数据段的 RST 被设置了,接收方就不用返回确认信息了,之后丢弃收到的数据段并返回。):
<SEQ=SND.NXT><ACK=RCV.NXT><CTL=ACK>
发送确认信息后,丢弃数据包。
接下来,讨论的数据段都是理想的数据段,这些数据段的序列号起始于 RCV.NXT,没有超过接收方的窗口大小。有可能会把多余窗口的数据段剪裁成一个新的数据段(包括 SYN 和 FIN),等待后续处理。
比当前期望接收的序列号大的数据段有可能会被缓存以备后续处理。
2. 检查 RST 标志
SYN-RECEIVED STATE
如果设置了 RST 标志
如果接收端连接状态之前是 LISTEN 状态,连接重置为 LISTEN 状态,然后返回,不用通知用户。如果用户是主动打开的(当前状态是从 SYN-SENT 状态转变过来的),通知用户 “connection refused”,然后拒绝建立连接。上述两种情况都会把重发队列中的数据清空,在主动打开情况下,进入 CLOSED 状态,然后删除 TCB,返回
ESTABLISHED STATE
FIN-WAIT-1 STATE
FIN-WAIT-2 STATE
CLOSE-WAIT STATE
如果设置了 RST 标志
所有正在处理或者缓存的 RECEIVEs 与 SEND 方法都会收到 “reset”。所有队列会被清空。用户进程会收到 “connection reset” 通知。连接进入 CLOSED 状态,删除 TCB,返回。
CLOSING STATE
LAST-ACK STATE
TIME-WAIT
如果设置了 RST 标志,直接进入 CLOSED 状态,删除 TCB,返回。
3. 检查优先级、安全 / 隔离
SYN-RECEIVED
如果接收数据段的优先级、安全 / 隔离与 TCB 的不符合,直接返回 reset 控制包,然后返回。
ESTABLISHED STATE
如果接收数据段的优先级、安全 / 隔离与 TCB 的不符合,直接返回 reset 控制包,未处理的 RECEIVEs 和 SEND 都应该收到 “reset”。在队列中的数据段都应该清空。用户进程收到 “connection reset” 通知,进入 CLOSED 状态,删除 TCB,返回。
这个检查放在序列号检查之后,为了防止上一个连接实例的数据段导致现在连接被关闭。
4. 检查 SYN 标记
SYN-RECEIVED
ESTABLISHED STATE
FIN-WAIT STATE-1
FIN-WAIT STATE-2
CLOSE-WAIT STATE
CLOSING STATE
LAST-ACK STATE
TIME-WAIT STATE
如果 SYN 的序列号在接收窗口中,那么这是一个错误包,直接发送 reset 控制包,任何 RECEIVEs 和 SEND 都应该收到 “reset”,所有队列中的数据都应该清空,用户进程也应该收到 “connection reset” 通知,然后进入 CLOSED 状态,删除 TCB,返回。
SYN 控制包不在窗口中,流程不应该能走到这里。序列号确认那个流程中应该会返回确认信息。
5. 检查 ACK 标记
如果没有 ACK 标记直接丢弃并返回。
如果有 ACK
SYN-RECEIVED STATE
如果 SND.UNA =< SEG.ACK =< SND.NXT 直接进入 ESTABLISHED 状态,继续处理。
如果 ACK 不可接收,直接返回 reset 控制包
<SEQ=SEG.ACK><CTL=RST>
ESTABLISHED STATE
如果 SND.UNA < SEG.ACK =< SND.NXT,那么 SND.UNA 设置为 SEG.ACK。重发队列中的被确认的数据都可以删除。用户应该收到正数的确认序号,所有发送的数据都被完全确认(同步情况下,SEND 函数返回 ok)。如果 ACK 是重复确认(SEG.ACK < SND.UNA)直接忽略。如果确认了没有发送过的数据,直接把数据包丢弃,返回(因为是同步状态,这种情况下直接丢弃就行,但是非同步状态下,就需要特殊处理了)。
如果 SND.UNA < SEG.ACK =< SND.NXT,应该更新发送窗口大小。如果 SND.WL1 < SEG.SEQ or (SND.WL1 = SEG.SEQ SND.WL2 =< SEG.ACK) 设置 SND.WND <- SEG.WND,SND.WL1 <- SEG.SEQ, SND.WL2 <- SEG.ACK
SND.WND 是相对于 SND.UNA 的偏移量,SND.WL1 记录上次更新 SND.WND 时数据段的序列号,SND.WL2 记录上次更新 SND.WND 时 ACK 中的序列号。这里检查为了防止老数据段更新窗口大小。
FIN-WAIT-1 STATE
如果发出去的 FIN 得到确认,直接进入 FIN-WAIT-2 状态。
FIN-WAIT-2 STATE
如果重发队列是空的,那么可以确认连接对端发送的 FIN,但是不删除 TCB。
CLOSE-WAIT STATE
逻辑与 ESTABLISHED 状态时一样。
CLOSING STATE
如果发送的 FIN 得到确认直接进入 TIME-WAIT 状态,其他情况直接删除数据包。
LAST-ACK STATE
这个状态唯一可以接收的数据,就是对发送的 FIN 进行的确认信息。如果 FIN 得到确认,进入 CLOSED 状态,删除 TCB,返回。
TIME-WAIT
唯一可以接收的数据就是对面重发过来的 FIN 消息,对这个 IFN 进行确认,然后重新等待 2 MSL 的时间。
6. 检查 URG 标志
ESTABLISHED STATE
FIN-WAIT-1 STATE
FIN-WAIT-2 STATE
如果 URG 标记被设置,RCV.UP <- max (RCV.UP,SEG.UP),并且通知用户对端有紧急数据需要处理。如果用户已经在紧急模式中,不用重复通知用户。
CLOSE-WAIT STATE
CLOSING STATE
LAST-ACK STATE
TIME-WAIT
不会到达这个流程,因为之前收到了 FIN,直接忽略数据段。
7. 处理用户数据
ESTABLISHED STATE
FIN-WAIT-1 STATE
FIN-WAIT-2 STATE
一旦进入 ESTABLISHED 状态,收到的数据段应该分发到用户的 RECEIVE 缓冲中。数据段可以分配到用户缓冲中,直到用户缓冲满了或者数据段没数据了。如果数据段是空的,但是有 PUSH 标志,这是会把用户缓冲返回给用户进程,并且会通知用户进程,接收到了 PUSH 标志。
如果 TCP 负责把数据传递给用户进程,那么 TCP 负责确认收到的数据。
一旦 TCP 接收到数据,他就把 RCV.NXT 前进到接收的数据字节数,并把 RCV.WND 调整到目前 TCP 接收缓存可用的内存大小。RCV.NXT 与 RCV.WND 的总和不能减少,只能前进。
请在数据交流章节了解窗口管理。
确认数据包格式为:
<SEQ=SND.NXT><ACK=RCV.NXT><CTL=ACK>
这个确认标识应该和数据包一起发送,但是延迟时间应该控制在合理的范围内。
CLOSE-WAIT STATE
CLOSING STATE
LAST-ACK STATE
TIME-WAIT STATE
不应该走到这个流程,因为收到了 FIN 标志,直接删除数据包。
8. 检查 FIN 标志
如果连接处于 CLOSED, LISTEN 或者 SYN-SENT 状态,不用处理 FIN 标志,因为无法检验 SEG.SEQ 是否合法,删除数据包,返回。
如果数据段标记了 FIN,提示用户 “connection closing”, 对所有的 RECEIVEs 返回同样的信息。设置 RCV.NXT 为 FIN 的数据包的序列号 + 1,对 FIN 返回一个确认信息。FIN 和 PUSH 有相同的作用,把所有已经接收但是还没分发到用户进程的数据,分发到用户进程。
SYN-RECEIVED STATE
ESTABLISHED STATE
进入 CLOSE-WAIT 状态。
FIN-WAIT-1
如果之前发送的 FIN 得到确认(接收的 FIN 包中有 ACK 信息),进入 TIME-WAIT 状态,然后启动 time-wait 定时器,关掉其他定时器。否则进入 CLOSING 状态。
FIN-WAIT-2 STATE
进入 TIME-WAIT 状态,然后启动 time-wait 定时器,关掉其他定时器。
CLOSE-WAIT STATE
继续保持 CLOSE-WAIT 状态
CLOSING STATE
继续保持 CLOSING 状态
LAST-ACK STATE
继续保持 LAST-ACK 状态
TIME-WAIT STATE
继续保持 TIME-WAIT 状态,重启 2 MSL 定时器。
USER TIMEOUT
USER TIMEOUT(用户指定的时间)
不管连接处于什么状态,用户指定的超时时间超时,清空所有队列中的数据,通知用户进程 “error: connection aborted due to user timeout”,删除 TCB,进入 CLOSED 状态,返回。
RETRANSMISSION TIMEOUT(重发超时)
不管连接处于什么状态,重发队列中的某个数据段超时,直接把数据段前移到队列前端,重启定时器。
TIME-WAIT TIMEOUT
如果超时,删除 TCB,纳入 CLOSED 状态,返回。
【RFC793】TCP 协议分析总结
ZHONGCAI0901 于 2020-11-02 22:05:25 发布
1.TCP 报头格式
TCP 段作为IP 段数据发送。IP 报头携带几个信息字段,包括:源主机地址和目标主机地址。TCP 头紧跟在IP 头后面,提供特定的 TCP 协议信息。如下图所示:
- TCP 报文头格式:
- TCP 报文头说明:
Name | Lenght | Func Description |
---|---|---|
Source Port | 16 bits | 源端口号 |
Destination Port | 16 bits | 目标端口号 |
Sequence Number | 32 bits | 会话的序列号; 该字段包含由这个主机随机选择的初始序号 ISN (Initial Sequence Number ),发送数据的第一个字节序号为 ISN+1 ,因为 SYN 标志会占用一个序号。 |
Acknowledgment Number | 32 bits | 确认序号; 确认序号就包含接收端所期望收到的下一个报文的 Sequence Number 为该确认序号。 |
Data Offset | 4 bits | 数据区域偏移量(又名:首部长度); 这指明数据从哪里开始,同时说明了 TCP Header 的大小。计算公式:Data Index = Data Offset * 4 (Data Index 单位:Bytes) |
Reserved | 6 bits | 预留,必须是零。 |
Control Bits | 6 bits | Flags 标记字段:URG:首部中的紧急指针字段标志,如果是 1 表示紧急指针字段有效。ACK:首部中的确认序号字段标志,如果是 1 表示确认序号字段有效。PSH:该字段置一表示接收方应该尽快将这个报文段交给应用层。RST:重新建立 TCP 连接。SYN:用同步序号发起连接。FIN: 中止连接。 |
Window | 16 bits | 窗口大小;每一端通过提供的窗口大小来实现TCP 流量控制,窗口大小为字节数。 |
Checksum | 16 bits | 检验和字段; 检验和覆盖了整个的 TCP 报文段:TCP 首部和 TCP 数据区域,由发送端计算和填写,并由接收端进行验证。 |
Urgent Pointer | 16 bits | 紧急指针字段; 只有当 URG 标志置 1 时紧急指针才有效,紧急指针是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。 |
Options | variable | 选项字段; 在下面专门解释… |
Padding | variable | 填充字段; TCP 报头填充用于确保 TCP 报头以 32 位对齐结束,数据以 32 位对齐开始。填充由零组成。 |
- Options 选项字段说明
当前定义的选项列表如下:
Kind (Option Code) | Length | Meaning |
---|---|---|
0x00 | - | End of option list. |
0x01 | - | No-Operation. |
0x02 | 4 | Maximum Segment Size. |
具体选项定义:
-
End of Option List(0x00)
这个Option Code(0x00)
表示选项列表的末尾。 -
No-Operation(0x01)
这个Option Code(0x01)
可能被用在 2 个 Options 之间,为了后面的 Option 能够字对齐开始。 -
Maximum Segment Size(0x02)
如上图所示,Maximum Segment Size Option 长度为4 个 byte(包括:Kind、Length 和 Max Seg Size)。如果存在这个选项,它将指示发送这个报文的 TCP 端最大可接收报文的大小。这个字段只能在初始连接请求时发送(即,在发送 SYN
报文中)。如果这个选项没有被使用,那么将允许任何大小的报文通信。
2.TCP 连接状态总图
一个 TCP 连接进程在生命周期会经历一系列的状态变化。这些状态是:LISTEN, SYN-SENT, SYN-RECEIVED,ESTABLISHED, FIN-WAIT-1, FIN-WAIT-2, CLOSE-WAIT, CLOSING, LAST-ACK, TIME-WAIT,CLOSED
。
TCP 连接状态总图如下:
Name | Func Description |
---|---|
LISTEN | 表示等待任何一个远程的 TCP 连接请求。 |
SYN-SENT | 表示已经发送了一个连接请求后,等待一个匹配连接请求。 |
SYN-RECEIVED | 表示在接收和发送连接请求后,等待一个确认连接请求 ACK。 |
ESTABLISHED | 表示已经建立连接,接收到的数据可以传递给用户。这个是数据传输阶段的正常状态。 |
FIN-WAIT-1 | 表示等待来自远程 TCP 的连接终止请求,或对先前发送的连接终止请求的确认。 |
FIN-WAIT-2 | 表示等待来自远程 TCP 的连接终止请求。 |
CLOSE-WAIT | 表示等待本地用户的连接终止请求。 |
CLOSING | 表示等待来自远程 TCP 的连接终止请求 ACK。 |
LAST-ACK | 表示等待之前发送到远程 TCP 的连接终止请求的 ACK (包括对其连接终止请求的 ACK)。 |
TIME-WAIT | 表示等待足够的时间以确保远程 TCP 接收到连接终止请求的 ACK。 |
CLOSED | 表示没有在连接状态。 |
3. TCP Client 连接状态图
TCP Client 连接状态图是根据 TCP Client 发起连接、传输数据和断开连接这个三个过程所经历的状态。如下图(此图相当于上面的总图分解):
4.TCP Server 连接状态图
5.TCP 三次握手和四次握手
- TCP 三次握手(左)和TCP 四次握手(右)
- TCP 连接、发送数据、断开
6.Wireshark 如何显示通信时的时序图
via:
-
RFC793-TCP 中文翻译_rfc793 中文 - CSDN 博客
https://blog.csdn.net/aigoogle/article/details/121036557 -
【RFC793】TCP 协议分析总结_rfc tcp options-CSDN 博客
https://blog.csdn.net/ZHONGCAI0901/article/details/109436851