网络传输层

本指南适用于想为卡尔达诺结算层构建自己的客户端的开发人员。请阅读卡尔达诺结算层实现概述了解更多信息。本指南涵盖了卡尔达诺结算层节点中使用的网络传输层。

传输层是一个位于 TCP 和应用程序级协议的层。原则上独立于应用程序协议(事实上,参考实现被具有不同应用程序级协议的多个不同应用程序使用)。

传输层的重点在于它提供了在单个 TCP 连接上复用的多个轻量级逻辑连接。每个轻量级连接都是单向的,并提供可靠的有序消息传输(即,它在 TCP 之上提供数据帧)

传输协议的属性:

  • 单个 TCP 连接。任何一对对等点之间一次只能使用一个 TCP 连接。这些连接可能是长期存在的。一旦建立了与对等节点的连接,它将用于发送/接收信息,直到 TCP 连接被明确关闭或发生一些不可恢复的错误。

实现的属性:

  • 报告网络故障。网络故障不会从应用程序层隐藏。如果 TCP 连接意外断开,传输层应通知应用层。在卡尔达诺结算层中,策略是尝试重新连接,如果重新连接也失败,则只声明对等方无法访问。

概要

传输层的典型用途包括:

  1. 监听来自对等点的 TCP 连接。
  2. 建立到其他对等点的 TCP 连接。
  3. 在建立的 TCP 连接上创建轻型连接。
  4. 发送消息到对等节点(在一个或多个轻量级连接上)。
  5. 接收来自对等节点(在一个或多个轻量级连接上)的消息。
  6. 关闭轻量级连接
  7. 关闭 TCP 连接。

在卡尔达诺结算层中,使用多个轻量级连接来支持应用程序级的消息传递协议。可以同时发送多个应用程序级别的消息,多个对话可以一次进行。大多数应用程序消息是在新创建的轻量级连接上发送的,如果需要的话,较大的应用程序级别消息被分解为多个传输级别消息以便于传输,其他应用程序级别的消息是作为一对单向轻量级连接组成的对话的一部分发送的。

概观

传输层的基本概念有:

  • 传输
  • 接入点
  • 连接
  • 事件
  • 错误

传输指本文档描述的整个层和协议。

传输是指本文档描述的整个层和协议。一个传输实例指的是传输实现的配置和状态,特别是包括绑定到本地网络特定接口的TCP监听套接字,如 192.168.0.1:3010

接入点是传输实例的逻辑端点。这意味着它又有一个地址,连接在端点之间。在实践中,它只是一个 TCP/IP 的简单抽象,通过主机名和端口进行寻址。

端口地址是具有结构如 HOST:PORT:LOCAL_ID 的二进制字符串,例如 192.168.0.1:3010:0

注意,传输实例监听单个端口时,原则上在单个传输实例中可能有多个可寻址的接入点,这就是 LOCAL_ID 的作用,然而,卡尔达诺结算层目前还没有这个功能,所以它总是使用 LOCAL_ID 0。

重量级连接是指两个端点之前的 TCP 连接。两个接入点只使用一次 TCP 连接。

连接(或者更明确地说是轻量级连接)是端点之前的单向连接。端点之前的所有轻量级连接都在单个重量级连接(即单个TCP连接上)进行复用。

轻量级连接是在 TCP 之上分层的逻辑概念。每个连接都有一个整数 ID。原则上可以在单个重量级 TCP 连接上复用数千个轻量级连接。

典型的操作方式是应用层希望建立到端点的轻量级连接,如果还没有重量级连接,则创建一个。同样,当最后一个轻量级连接关闭时,真正的 TCP 连接将被彻底关闭。

轻量级连接时单向的:轻量级连接上的消息仅在一个方向流动。但是,轻量级连接可以在任何方向上建立。同样的重量级连接用于双向的轻量级连接;哪个端点先建立重量级连接并不重要。

双向会话可以通过使用一对单向轻量级连接来建立。卡尔达诺结算层遵循这种模式。请查阅 time-warp-nt 获取详细信息。但请注意,这个传输层没有双向对话的特殊概念,只有单向连接的集合。

网络字节顺序

在一下对控制消息的描述中,所有整数都是按网络字节顺序编码的。

下面的消息定义使用的 Int32 指的是32位的以网络字节顺序的整数值。

设置传输实例

每个传输实例都必须建立一个 TCP 监听套接字。使用的本地端口和端口号由使用传输的应用程序确定。

实现可以随时接收新的 TCP 连接(可能受限于资源策略),然后执行下面描述的新重量级连接的初始步骤。

建立重量级连接(初始化)

假设在接入点 A,B 之前建立重量级连接,端点 A 发起连接。两个端点都有端点地址,如前所述,端点地址是这种形式:HOST:PORT:LOCAL_ID

从 A 到 B 建立的一个重量级连接的过程如下。首先 A 必须在本地记录它正在初始化一个到 B 的重量级连接。在交叉连接请求的情况下(见下文)这是必须的,由端点 A 向端点 B 打开 HOSTPORT 连接。

端点 A 发送具有如下结构的连接请求消息:

+-----------+-------------+--------------------+
|   B-LID   |   A-EIDlen  |       A-EID        |
+-----------+-------------+--------------------+
|   Int32   |   Int32     |       bytes        |

其中:

  • B-LID - B 端点的本地 ID;
  • A-EIDlen - A 端点的地址;
  • A-EID - A 端口的地址。

因此 A 发送它希望连接的本地接入点 ID,它自身的地址来初始化节点。A 发送的地址应该是规范化的公共地址。主机部分可以是 IP 地址或 DNS 名称。它用于避免在端点之间建立多个 TCP 连接。在卡尔达诺结算层协议中,本地端点 ID 始终为0。

然后接入点 A 期望一个连接请求响应信息,它是下面的响应之一,一个简单的 Int32 编码。

当本地接入点 ID 所标识的端点不存在时,会返回 ConnectionRequestInvalid 响应。例如,如果 A 发送给 B,它希望连接到本地接入点 ID 1,那么只有 ID 0 存在时才会发生。在这种情况下,两个端点必须关闭 TCP 连接。

当端点 B 确定 A 与 B 之间或 B 与 A 之间,或两者同时有了一个 TCP 连接,会返回 ConnectionRequestCrossed 响应。在这种情况下,两个端点都必须关闭 TCP 连接。

建立重量级连接 (接收)

假设如前所述,在标记为 A 和 B 的端点之前建立重量级连接,并且端点 A 发起连接。我们现在从端点 B 的角度来考虑这个问题。

两个端点都有 HOST:PORT:LOCAL_ID 形式的接入点地址。具体来说,假设 B 只有一个接入点,其中 LOCAL_ID 为 0。

B 的传输实例在对应的接入点 IDs 上相应的 host 和 port 有监听套接字。它接受来自某个对等点的新的 TCP 连接。期望在该 TCP 连接上接收连接请求信息(以上述格式)。

传输实例 B 必须根据以下规则以连接请求响应消息(采用上述格式)进行响应。

如果连接请求要求本地接入点 ID 不存在(在本例中即不是0),则它必须以 ConnectionRequestInvalid 响应并关闭 TCP 连接。

ConnectionRequestCrossed 的规则将在下面更详细地描述。

否则,当接入点 ID 有效并且没有现有的 TCP 连接时,它应该以 ConnectionRequestAccepted 回复,并记录它已经与 A 建立了重量级连接的本地状态。然后它就可以继续协议的主要部分。

交叉连接请求

如前所述,该协议试图确保在两个接入点之间只使用一个 TCP 连接。典型的情况是,端点可以简单地确定它是否具有与对等体的重量级连接。因为它启动它或接收它,并且知道现有的 TCP 连接是否仍然打开。难处理的情况是两个端点同时建立重量级连接(分布式系统意义上的『同一时间』)。

每个端点初始化重量级连接的过程都记录在本地状态中。每个端点都将照常发送连接请求消息。当每个端点接受传入的 TCP 连接时,它会从连接请求消息获取端点 ID。

额外的规则是,它必须在其本地状态查到,对等点的连接1. 已经建立(TODO)2. 已经完全确立。在第一种情况下,我们处于交叉连接的情况。第二种情况是当一个对等房发现现有的 TCP 连接失败(即它的端点被关闭),并且正尝试建立一个新的 TCP 连接,而其他的对等点没有发现已有的 TCP 连接已经失效了。

交叉连接情况

在交叉连接的情况下,到目前为止,这在端点之间是完全对称的,但我们必须打破对称来解决使用哪个 TCP 连接以及需要关闭哪个。协议用来打破对称性的解决方案的对端点地址进行排序(以二进制字符串形式按字典顺序排序)。因此,每个节点必须采用的用来决定是否接受传入连接请求的规则是:ConnectionRequestAccepted,如果对等点的 ID 小于本地端点 ID,则应答,否则回复 ConnectionRequestCrossed,关闭 TCP 连接。

连接断开/重建请求

在第二种情况下,处理传入 TCP 连接的端点已经确定在两个端点之前已经存在已建立的连接,该协议如下。发送一个 ConnectionRequestCrossed 回复,关闭 TCP 连接。此外,端点尝试验证现有连接的活跃性,目的是验证它是否处于活动状态,或确定它不是为了关闭断开的链接(这将允许打开新连接)。

为了验证活跃性,接入点发送一个 ProbeSocket 信息。如果在实现定义的时间段内未收到 ProbeSocket 消息,则接入点应关闭 TCP 连接并相应地更新其本地状态,以使端点能够建立新的连接。

接收 ProbeSocket 消息的接入点应该使用 ProbeSocketAck 回复。

这些消息的编码很简单:

+-------------+
| ProbeSocket |
+-------------+
|    Int32    |

+----------------+
| ProbeSocketAck |
+----------------+
|     Int32      |

其中控制头消息的值分别是 4 和 5。

协议主体

一旦在两个端点之间建立了一个重量级连接,协议的主要部分就开始了。

两个端点之间的主要协议包括发送/接收一系列消息:控制消息和数据消息。每个都有一个标识消息的头部和适合消息类的主体部分。主协议的消息是用于创建和关闭轻量级连接的控制消息,以及用于在轻量级连接上发送数据的数据消息。

轻量级连接时单向的。在 TCP 连接的每个方向都有独立的轻量级连接集合。发送方管理每个方向的轻量级连接。接收方不能直接控制轻量级连接的分配。

轻量级连接由轻量级连接 ID 区分,这是一个 32 位的有符号整数。轻量级连接 ID 必须大于1024。轻量级连接 ID 号应该按顺序使用。

用于创建或关闭轻量级连接的控制消息只是简单的区分它们所处的轻量级连接 ID。同样,数据消息根据正在发送的轻量级连接标识 ID。

用于不同连接 ID 的消息可以任意交织(实现不同轻量级连接的复用)。唯一的约束是很显然的:对于任意连接 ID,消息序列必须是创建的连接消息,任意数量的数据消息以及关闭连接消息。

这些消息的格式如下:

+-----------+-----------+
| CreateCon |   LWCId   |
+-----------+-----------+
|   Int32   |   Int32   |

+-----------+-----------+
|  CloseCon |   LWCId   |
+-----------+-----------+
|   Int32   |   Int32   |

+-----------+-----------+-------------------+
|   LWCId   |    Len    |       Data        |
+-----------+-----------+-------------------+
|   Int32   |   Int32   |     Len-bytes     |

其中:

  • CreateCon 控制头是 0;
  • CloseCon 控制头是 1;
  • LWCId 是轻量级的连接ID, 它 >= 1024。

头部 Int32 是控制消息头部和数据消息的轻量级连接 ID 的别名,这就是为什么连接 ID 必须是1024或更大的原因。

数据消息由轻量级连接 ID 和以长度为前缀的数据帧组成。这个协议的实现可能希望最大化这些数据帧,例如为了因资源考虑而确保连接之前合理的复用。

请注意,这些数据边界和 TCP 套接字或数据包上的读取/写入之间不需要直接对应。为性能和网络效率考虑,在单一的写中管理连接开启,小数据消息和连接关闭是合理的。

关闭重量级连接

关闭重量级连接并不简单。这是因为只有在两个方向上的轻量级连接都关闭时,才能关闭重量级连接。鉴于轻量级连接的分配由每个端点独立控制,因此两个端点之间需要进行一些同步,以便两个端点在任意方向上不再有轻量级连接达到一致。

当一个端点确定它没有更多的输出的轻量级连接,并且它知道传入的连接集是空的,那么它可以启动协议来关闭重量级连接。它通过发送一个 CloseSocket 来实现。该信息携带了该端点能看到的最大传入轻量级连接 ID:即由本地端点迄今为止已看到的远程端点分配的最高连接 ID。本地端点更新它用于跟踪远程端点的状态,以表明它现在正在关闭。如果本地端点现在收到来自远程端点的创建连接消息,而远程端点被标记为处于关闭过程中,则它将状态重置为正常连接建立状态。如果远程端点在收到关闭套接字消息之前打开一个新的轻量级连接,则会发生这种情况,因此应该尝试关闭应被禁止的套接字。

当一个端点收到 CloseSocket 消息,检查其本地状态,已检查出站轻量级连接的数量以及它用于传出连接的最大轻量级连接 ID。如果仍然有出站连接,则关闭套接字消息将被忽略。此外,如果本地节点到目前为止使用的最大出站轻量级连接 ID 高于关闭套接字消息中收到的最大出站轻量级连接 ID,则关闭套接字消息将被忽略。即使出站连接数目前为零,如果出站连接被创建并在关闭套接字消息到达之前被关闭,也会发生这种情况。在这两种情况下发生的事情是,重量级连接再次变得活跃,而一方则由于不活跃而试图关闭它,因此放弃尝试关闭它是合适的。

另一方面,如果没有出站连接,并且远程端点看到的最后一个新的连接 ID 与本地相同,则双方都同意,并且应该关闭 TCP 连接。

消息结构是:

+-------------+-----------+
| CloseSocket |   LWCId   |
+-------------+-----------|
|    Int32    |   Int32   |

其中:

  • CloseSocket - 关闭连接控制消息,值为 2;
  • LWCId - 迄今使用的最大轻量级连接 ID。

流量控制和背压(TODO)

轻量级连接不提供任何超出 TCP 提供的流量控制。该协议不提供任何设施来拒绝传入的轻量级连接。任何这样的设施都必须在顶层,在应用层或另一个中间层。

实现应该考虑背压和头部堵塞问题。Head of line?是许多 TCP 协议层面的共同问题,例如 HTTP 1.x,其中一个较大的响应可以『阻塞』其他 URL 的较小的响应,因为这些响应式按顺序发送的。这个问题在这个传输协议中没那么严重,因为连接是复用的,所以小消息不需要被大消息阻塞。尽管如此,还是必须按顺序接收所有连接的多路复用数据流:不可能在整个重量级连接上返回一个轻量级连接。