III - 网络控制程序
每台主机都实现一个称为网络控制程序 (NCP) 的模块, 它控制涉及该主机的所有网络通信。NCP 构成的网络形成一个分布式通信系统, 在各进程之间实现通信路径。NCP 协议的问题包括: (i) 这些通信路径的定义, 以及 (ii) 一套在维护这些通信路径方面协调分布式 NCP 系统的机制。下面讨论这两点。
socket
两个进程之间的通信是通过两个 socket 之间的单工连接进行的: 一个连接到一个进程的发送 socket 和一个连接到另一个进程的接收 socket。socket 具有以下特征:
socket 标识符 - 全网用 socket 标识符来唯一标识一个 socket。它由 48 位组成, 包含以下各个部分:
-
用户号 (24 位) - 连接到一个进程的 socket 通过一个用户号被标识为属于该进程, 该用户号由 8 位的 "本机" 主机码加上 16 位的、由本机主机分配的用户码组成。对于同一个用户在任何主机上的任何进程所连接的所有 socket, 这个用户号都相同。
-
实例标记 (8 位) - 属于同一个用户的多个进程可以同时存在于单台主机之内。实例标记标识一个 socket 所属的那个特定进程。按惯例, 一个用户在某一主机上第一个使用网络的进程获得实例标记 = 0。
-
主机号 (8 位) - 这是所连接的进程所在主机的编码。
-
socket 码 (8 位) - 该码为每个进程提供 128 个发送 socket 和 128 个接收 socket。最低位决定这是一个 "发送" (= 1) 还是 "接收" (= 0) socket。
socket 的状态 - 每个 socket 都有一个相关联的状态。NCP 可以实现更多过渡性的 socket 状态, 但以下三种具有概念上的重要性。
-
非活动 - 当前不存在任何已告知 NCP 它希望监听该 socket 的进程。其他任何进程都无法成功地与一个非活动的 socket 通信。
-
打开 - 某个进程已同意监听与该 socket 有关的事件, 但尚未建立连接。
-
已连接 - 该 socket 当前已连接到另一个 socket。
socket 事件队列 - 每个打开或已连接的 socket 都维护一个有待向拥有它的进程揭示的事件队列。它由一份按时间先后排列的列表组成, 表中是试图连接或断开该 socket 的一个或多个外部进程的动作所生成的某些事件。事件队列中的一个表项由事件类型加上所涉及的外部 socket 的标识符组成。定义了以下事件类型:
-
"request" - 一个外部 socket 请求连接。(若本地 socket 已连接则不排队)
-
"accept" - 一个外部 socket 接受所请求的连接。
-
"reject" - 一个外部 socket 拒绝所请求的连接。
-
"close" - 一个外部 socket 断开一条现有的连接。
当 "request" 事件被接受或被拒绝时, 它就从队列中移除。其他事件则在被揭示给拥有该 socket 的进程时从队列中移除。
有些事件对拥有该 socket 的进程而言是透明的, 它们不会在事件队列中生成表项。
虽然事件队列在概念上是不受限制的, 但看来有必要对它的长度设定某种实际限制。当一个 socket 的事件队列已满时, 任何会使队列增长的新到事件都应被丢弃, 并通知发送方 NCP (通过下文所述的 ERR 命令)。
NCP 控制通信
NCP 网络通过在各组成部件之间传递控制命令来协调其活动。这些命令一般涉及由收到命令的 NCP 所控制的 socket 连接的创建和操纵。一条控制命令通过作为消息经由链路编号 1 (被指定为控制链路) 发往某主机的相应 NCP 而被导向该特定 NCP, 该链路专供此用。IMP 网络并不区分这些消息与通过 socket 连接实现通信的普通数据消息。
定义了以下 NCP 控制命令:
-
请求连接
RFC <local socket> <foreign socket> [<link no.>]NCP 把该命令导向一个外部 NCP, 以尝试在本地 socket 与外部 socket 之间发起一条连接。如果外部 socket 处于打开状态, 外部 NCP 就向该 socket 的事件队列中放入一个 "request" 事件, 以便向拥有该 socket 的进程揭示。如果外部进程接受, 外部 NCP 以另一条 RFC 的形式返回肯定确认。它通过发出 CLS 命令 (见下) 来拒绝连接。如果外部 socket 不处于打开状态 (非活动或已连接), 那么 RFC 将在不征询拥有该 socket 的进程的情况下被自动拒绝。发往同一 socket 的多条 RFC 按到达顺序放入其事件队列。一旦拥有该 socket 的进程决定接受一条连接, 队列中所有排队的 RFC 都会被 NCP 自动拒绝。控制着这一可能被连接的 socket 对中 "接收" socket 的 NCP 指定一个用于传输消息的链路编号。
-
关闭连接
CLS <local socket> <foreign socket>NCP 发出该网络命令以断开一条现有的连接, 或者对一条 RFC 作出否定确认。如果一个 NCP 关闭本地发送 socket, 就可能存在竞态问题: CLS 命令可能在该 socket 连接上的最后一条消息之前到达外部 NCP。这一竞态通过遵守两条准则来防止: (i) 对本地发送 socket 的 CLS 命令要等到发往外部 socket 的最后一条消息的 RFNM 返回之后才发送, 并且 (ii) 外部 NCP 按接收顺序处理所有到达的消息。
-
阻塞一条连接上的输出
BLK <foreign send socket>一个进程通过接收 socket 读取数据的速度可能慢于消息到达的速度, 因此 NCP 的缓冲区可能趋于堵塞。NCP 向外部 NCP 发出该命令, 以阻塞该 socket 对上的进一步传输, 直到接收进程赶上进度。
-
恢复一条被阻塞连接上的输出
RSM <foreign send socket>NCP 发出该命令以解除先前被阻塞的一条连接的阻塞。
-
中断连接到一条连接的进程
INT <foreign socket>收到该消息会使外部 NCP 立即中断连接到 <foreign socket>、且与某个本地 socket 相连的外部进程。已经在 NCP 网络内部沿被中断的连接传输的数据仍将被传送到目的 socket。"中断" 的含义是: 该进程将立即中止其当前的执行, 并执行某种标准过程。该过程不在本协议层次上定义。
-
向外部 NCP 报告一条错误命令
ERR <code> <command length> <command in error>该命令用于报告虚假的网络命令或消息, 或者妨碍处理该命令的过载状况。<code> 指明错误类型。如果 <code> 指明的是一条错误的网络命令, 则 <command in error> 就是该命令 (不包括 IMP 首部), 而 <command length> 是一个整数, 指出其以位计的长度。如果 <code> 指明的是一条错误的消息, 则 <command in error> 只包含传输该错误消息所用的链路编号。(这相对 NWG/RFC 40 中的规定略有修改。)
-
网络测试命令
ECO <48 bit code> <echo switch>NCP 可以通过向一个外部 NCP 发送一条带有任意 <48 bit code> (与 socket 标识符长度相同) 和 <echo switch> 'on' 的 ECO 命令, 来测试它与该外部 NCP 之间通信的质量。收到这样一条 ECO 命令的 NCP 应当立即向发起方 NCP 发送一条确认 ECO, 其中带有相同的 <48 bit code> 和 <echo switch> 'off'。NCP 不确认带有 <echo switch> 'off' 的 ECO。我们认为, 该命令对整个网络的初期调试会有相当大的帮助。
-
空操作命令
NOPNCP 在收到该命令后将其丢弃。
NCP 的用户接口
每台主机上的 NCP 都有一个接口, 本地进程可以通过它在 NCP 的控制之下使用网络。该接口的确切规定不是网络协议的问题, 因为每个站点都会有适合自身特定要求的接口。对 NCP 的用户接口的协议要求是: 它提供所有预期的网络功能, 且不提供非法的特权。这类非法特权的例子包括: 冒充另一个进程、窃听并非发给它的通信, 或者诱使 NCP 发出虚假的网络命令或消息。
这里我们勾勒一个基于 Carr、Crocker 和 Cerf 提案的接口, 它足以充分利用网络。虽然这组特定的调用主要意在举例说明, 但它表明了所需的功能类型。
可以使用以下对 NCP 的调用:
-
LISTEN <my 8 bit socket code>
用户打开该 socket, 为它创建一个空的事件队列。这个 LISTEN 调用可能阻塞等待第一个 "request" 事件, 也可能立即返回。
-
INIT <my socket code> <foreign socket>
用户尝试把 <my socket> 连接到 <foreign socket>。本地 NCP 向外部 NCP 发送一条 RFC, 请求创建该连接。返回的确认要么是一条 RFC (请求被接受), 要么是一条 CLS (请求被拒绝)。按调用方的选择, INIT 调用可以阻塞等待预期的 "accept" 或 "reject" 事件, 也可以不等待而立即返回。在后一种情况下, 用户必须在稍后调用 STATUS (见下), 以确定外部 NCP 所作的处理。当被阻塞的 INIT 调用返回时, "accept" 或 "reject" 事件从事件队列中移除。
-
STATUS <my socket code>
该调用报告 <my socket> 队列中最早的一个此前未报告过的事件。如果该类事件在揭示后即可删除, 则 STATUS 调用会把该事件从队列中删除。
-
ACCEPT <my socket code>
用户接受与这样一个外部 socket 的连接: 它的 "request" 事件在 <my socket> 的事件队列中最早。一条确认用的 RFC 被发往被接受的外部 socket, 并且该 "request" 事件从事件队列中删除。如果队列中还存在其他 "request" 事件, NCP 会通过发出一条 CLS 命令并删除该事件来自动拒绝连接。
-
REJECT <my socket code>
用户拒绝与这样一个外部 socket 的连接: 它的 "request" 事件在 <my socket> 的事件队列中最早。NCP 发出一条 CLS 命令, 并从队列中删除该 "request" 事件。
-
CLOSE <my socket code>
用户指示 NCP 断开与该 socket 的任何活动连接, 并使该 socket 变为非活动。如果曾经存在过连接, NCP 会向外部 socket 发出一条 CLS 命令。一旦 "close" 事件被揭示给外部进程, 外部 socket 的状态也就变为关闭。
-
INTERRUPT <my socket code>
用户指示 NCP 向连接到 <my socket> 的外部 socket 发出一条 INT 命令。
-
TRANSMIT <my socket code> <pointer> <nbits>
用户希望把 <nbits> 的数据读入 (<my socket> 是接收) 由 <pointer> 指向的区域, 或从该区域写出 (<my socket> 是发送)。写调用在 NCP 已把数据排队以便通过连接发送一条消息之后立即返回。只有在连接被阻塞、或者本地 NCP 负载过重无法立即处理该请求时, 写调用才会阻塞。要通过一条连接传输的数据被格式化为一条或多条最大长度为 8095 位的 IMP 消息, 并经由控制着接收方连接的那个 NCP 所发出的 RFC 中指定的链路编号传送到外部主机。当 TRANSMIT 动作发生时, <my socket> 的事件队列中的一个 "close" 事件被揭示。写调用会立即揭示 "close" 事件。读调用则在所有数据都读完之后揭示它。
从用户视角看一条连接的历程
一个示例
假设主机 A 上的进程 'a' 希望与主机 B 上的进程 'b' 建立连接。在通信能够进行之前, 必须满足两个条件:
-
进程 'a' 必须能够向它的 NCP 指明 'b' 的 socket 空间中它想要连接的一个 socket。
-
进程 'b' 必须已经在该 socket 上 LISTEN。
1. 建立连接
-
进程 'b' 对 socket 'Bb9' 执行 LISTEN。
-
进程 'a' 把 'Bb9' INIT 到它的 'Aa12'。A 处的 NCP 生成一条 RFC, 指明链路编号 = 47, 该编号是它从可用链路集合中选出的。这就是在该连接被进程 'b' ACCEPT 之后它将用来接收消息的那条链路。
-
进程 'b' 被告知 A 的 INIT 请求。它可以 REJECT 该连接 (NCP B 返回一条 CLS), 也可以 ACCEPT (NCP B 返回一条 RFC)。
-
如果进程 'b' 执行 ACCEPT, 那么确认的 RFC 就建立起该连接, 消息现在可以流动了。
HOST A | HOST B
INITIATOR | ACCEPTOR
PROCESS 'a' | PROCESS 'b'
|
|
| a. LISTEN 'socket code 9'
|
|
b. INIT 'socket code 12' 'Bb9' |
RFC 'AA12' 'Bb9' 'link 47' ==========>
|
| c. ACCEPT 'socket code 9'
| RFC 'Bb9' 'Aa12'
|
| d. TRANSMIT 'send buffer' 'len'
| 'socket 9'
<============== IMP message 'link 47' 'send buffer'
|
e. TRANSMIT 'rec buffer' 'length'
'socket 12' ============>
|
| f. CLOSE 'socket code 9'
|
last RFNM ===>
<============== CLS 'Bb9' 'Aa12'
closes socket 'Aa12' |
|
图 2: 建立 socket 连接并通过其进行通信
2. 通过一条连接发送消息
-
进程 'b' 发出 TRANSMIT 调用以通过该连接发送数据。NCP B 把它格式化为一条 IMP 消息, 并按照 A 的 RFC 所规定, 以链路编号 = 47 把它发送给 NCP A。
-
NCP A 收到来自 NCP B 的、链路编号 = 47 的原始消息。NCP A 用这个链路编号来判断预期的接收者是谁, 并把该消息存入供接收进程使用的缓冲区。
-
进程 'a' 可以在任意时刻对 socket 码 12 发出读 (TRANSMIT) 调用。如果该 socket 没有待处理的数据, 读调用就会阻塞。读调用会取走经由 socket 码 12 传输的指定数量的位, 这可能需要跨越一条 IMP 消息的边界。IMP 消息的边界对读调用而言是不可见的。
-
如果进程 'b' 通过该连接发送数据的速度快于进程 'a' 取走数据的速度, 那么当 A 的缓冲区开始填满时, NCP A 可以向 NCP B 发出 BLK 命令。稍后, 当进程 'a' 赶上来时, NCP A 可以通过 RSM 命令让 B 恢复传输。
3. 进程 'b' 关闭连接
-
进程 'b' 决定关闭该连接, 并向 NCP B 发出 CLOSE 调用。为了避免竞态问题, B 等待该连接上前一条消息的 RFNM, 然后向 NCP A 发送 CLS 命令。当 CLS 命令消息的 RFNM 返回时, NCP B 从它的各张表中清除 socket 'Bb9', 从而在它这一端完成关闭, 并使 'Bb9' 变为非活动。
-
由于 NCP A 内部是顺序处理的, 可以保证在来自 NCP B 的 CLS 到达之前, 发往 socket 'Aa12' 的最后一条消息已经被导向某个进程。在收到来自 B 的 CLS 后, NCP A 把 socket 'Aa12' 标记为 "close pending", 并向 'Aa12' 的事件队列中放入一个 "close" 事件。
-
在仍有缓冲数据待处理期间, 进程 'a' 仍然可以对 socket 'Aa12' 发出读调用。当缓冲区已被取空之后 'a' 发出读调用时, "close" 事件被揭示, 以告知 'a' 连接已关闭, 同时 socket 'Aa12' 被从 NCP A 的各张表中清除。
4. 进程 'a' 关闭连接
-
让我们回到第 2 步, 并假设进程 'a' 想从它这一端关闭连接。这里不存在竞态问题, 因为我们假定一旦 'a' 发出 CLOSE 调用, 它就不再想通过该 socket 读取消息。
-
假设进程 'a' 对 socket 'Aa12' 发出 CLOSE 调用。NCP A 立即向 NCP B 发出一条 CLS 命令, 并把 socket 'Aa12' 标记为 "close pending"。为读取而缓冲在 'Aa12' 上的任何数据都被丢弃。为了让已经从进程 'b' 发出、仍在途中的剩余消息能够穿过 IMP 网络到达 NCP A 并被无错地丢弃, NCP A 在收到 CLS 命令的 RFNM 之后, 把 'Aa12' 在它的各张表中保留一段适当的时间。在此期间, NCP A 丢弃通过这条正在关闭的连接收到的所有消息。在留出合理的时间让这些死消息到达之后, NCP A 从它的各张表中清除 'Aa12', 从而有效地关闭连接并使 'Aa12' 变为非活动。此后发往 socket 'Aa12' 的消息会使 NCP A 向发起方 NCP 发送一条 ERR "erroneous command"。
-
当 NCP B 收到 CLS 命令时, socket 'Bb9' 被标记为 "close pending", 并且 CLS 事件被放入 'Bb9' 的事件队列。下一次进程 'b' 想通过该 socket 写入时, CLS 事件被揭示以告知它连接已关闭, 并且 socket 'Bb9' 从 NCP B 的各张表中移除。