VI. 网络操作的非正式描述
这里我们叙述网络使用的三个主要阶段中所进行的操作: 建立、流量控制和关闭。
A. 建立
为建立用于数据传输的连接, 必须交换一对 RFC。一条 RTS 必须从接收端发往发送端, 一条 STR 必须由发送端发给接收端。此外, 接收端必须在其 RTS 中指定一个链路号。这些 RFC(RFC 是涵盖 RTS 和 STR 的统称)可以按任意时间顺序发出。还必须为待处理呼叫(即用户程序尚未处理的 RFC)的排队做出安排。这样, 当用户用完一个连接后, 可以选择检查来自另一进程的下一个待处理呼叫, 并决定接受或拒绝该连接请求。问题在于用户可能不去检查其待处理呼叫;这样它们只会白白占用 NCP 中的队列空间。稍后将提到这一问题的几种替代解决方案。
利用上文所述原型系统调用的框架, 我们设想至少有四种时间顺序可以成功建立连接:
- 用户可以发出 LISTEN, 表示愿意考虑与任何向其发送 RFC 的对象建立连接。当 RFC 到来时, 用户会得到通知。然后用户决定是否希望与该套接字连接, 并据此发出 ACCEPT 或 CLOSE。CLOSE 会“拒绝”(refuses)该连接, 详见“关闭”一节。ACCEPT 表示用户愿意连接;此时发出一条 RFC, 连接完全建立。
- 在处理用户的 LISTEN 请求时, NCP 发现该本地套接字存在一个待处理呼叫。用户会立即得到通知, 并可如上所述执行 ACCEPT 或 CLOSE。
- 用户发出 CONNECT, 指定其希望连接的某个特定外部套接字。随即发出一条 RFC。如果外部进程接受该请求, 它会回送一条 RFC 作为应答。收到这条确认 RFC 后, 连接即建立。
- 在处理 CONNECT 时, NCP 可能发现存在一个从指定外部套接字发往该本地套接字的待处理呼叫。此时发出一条确认 RFC, 连接即建立。
在上述所有情况下, 连接建立时用户都会得到通知, 但在分配缓冲区空间并发送 ALL 命令之前, 数据无法开始流动。
如“关闭”一节所述, 如果收到 CLS, 上述任何一种连接过程都会被中断。
1. 待处理呼叫队列
必须为待处理的 RFC 实现某种形式的排队。要理解这一点, 一个简单的方法是考察典型的 LISTEN-CONNECT 序列。一方发出 LISTEN, 另一方发出 CONNECT。如果 LISTEN 在来自远端 CONNECT 的 RFC 到达之前发出, 一切正常。然而, 由于网络的异步特性, 我们永远无法保证事件会按此顺序发生。如果不对呼叫排队, 那么 RFC 在 LISTEN 发出之前到达就会被拒绝;如果在之后到达, 则会被接受。这样就形成了极其含糊的局面。
除非拥有无限的队列空间, 否则最好有某种机制来清除队列中用户从未理会的旧 RFC。一种显而易见但非正式的方法是记下每个 RFC 进入队列的时间, 然后定期拒绝所有超过某个任意时限的 RFC。另一个想法(可能应纳入任何方案之中)是: 当用户注销或崩溃时, 由 NCP 对所有未完成的连接或待处理呼叫发送 CLS。
本描述所采用的方案乍看之下可能不太直观;但我们认为它比其他提议更切合实际。基本上, 当发出 CONNECT 时, NCP 假定该套接字希望与指定的外部套接字、且仅与该套接字通信。因此, 它通过回送 CLS 将所有不匹配的 RFC 从待处理呼叫队列中清除。同样, 当连接处于 RFC-SEND 状态(已发出 CONNECT)时, 所有不匹配的 RFC 都会被拒绝。如果执行的是 LISTEN-ACCEPT 或 LISTEN-CLOSE 序列, 则其余待处理呼叫不会从队列中移除, 因为用户将来可能希望接受这些请求。
尽管后一种方法可能显得武断和/或不必要地严格, 但假定我们面对的是一位称职的程序员(即对竞态条件和网络的异步特性保持警惕的人), 我们尚未设想出会被该方法禁止的场景。当然, 某个站点选择何种方案高度依赖于具体实现;我们建议为 RFC 的排队提供一定时长的保留, 该时长至少应与上述 CONNECT 清除方案中 RFC 被保留的时间处于同一数量级。
B. 流量控制
只有当连接完全建立(即已交换两条 RFC 且尚未开始关闭)时, 有意义的数据才能在该连接上流动。我们假定 NCP 有一个用于接收传入数据的缓冲区, 并且存在某个有意义的量, 可以(按每个连接)通告其能处理的消息大小。我们进一步假定发送端根据这些大小通告来调节其传输。
连接建立时, 一个单元(称为 'Their Size')被置为零。接收端会决定能分配多少空间, 并发送一条指明该空间的 ALL 消息。发送端将 'Their Size' 增加所分配的空间, 随后便可以发送长度小于或等于 'Their Size' 的消息。每传输一条消息, 就从 'Their Size' 中减去该消息的长度。当接收端分配更多缓冲区空间时(例如用户取走一条消息, 从而释放了一些系统缓冲区空间), 所释放的位数会通过 ALL 消息发送给发送端。
因此, 'Their Size' 永远不允许变为负数, 且当 'Their Size' 等于零时不能进行任何传输。
注意, ALL 消息中指定的长度是增量, 而不是接收缓冲区的绝对大小。这是由流量控制协议的全双工特性所决定的。ALL 消息的长度字段可以长达 32 位(注: 这是一个无符号整数), 因而实际上可以提供一个无限的“位汇”(bit sink), 如果将来需要的话。
C. 关闭
正如建立连接需要两条 RFC 一样, 关闭连接需要两条 CLS。关闭在各种情况下发生, 并有多种用途。为简化对竞态条件的分析, 我们区分四种情况: 中止、拒绝、由接收方终止、由发送方终止。
当用户发出 CONNECT, 然后在 CONNECT 被确认之前发出 CLOSE 时, 就是“中止”(aborts)了连接。通常用户会在长时间等待确认后中止;如果用户崩溃, 其系统也可能代为中止。
当用户发出 LISTEN, 并在被告知有潜在呼叫方后发出 CLOSE 时, 就是“拒绝”(refuses)了连接。对于正在等待某个特定套接字呼叫的套接字, 任何发往它的连接请求也都会被拒绝。
连接建立之后, 任何一方都可以终止连接。所需的事件顺序表明, 接收端的 CLOSE 尝试应被视为“请求”(requests), 发送端总是尽快予以满足。任何尚未传递给用户或仍在网络中传输的数据都将被丢弃。发送端的 CLOSE 请求在所有数据传输完成后即予满足。
1. 中止
我们可以区分三种情况:
- a) 在最简单的情况下, 我们发送一条 RFC, 随后发送一条 CLS。对方以 CLS 应答, 连接尝试结束。
- b) 外部进程可能在本地进程中止连接的同时接受该连接。在这种情况下, 外部进程会认为本地进程正在终止一个已建立的连接。
- c) 外部进程可能在本地进程中止连接的同时拒绝该连接。在这种情况下, 外部进程会认为本地进程是在确认它的拒绝。
2. 拒绝
收到 RFC 后, 本地主机可以回应 RFC 或 CLS, 也可能不作回应。(本地主机可能已经发出了自己的 RFC, 等等。)如果本地主机发送 CLS, 就称本地主机在“拒绝”(refusing)该连接请求。
我们要求通过交换 CLS 命令来关闭连接, 因此本地主机必须保留会合表条目, 直到收到确认 CLS 为止。
3. 由发送方终止
当发送端用户发出 CLOSE 系统调用时, 其 NCP 必须立即接受, 但在本地缓冲区中的所有数据都已传给外部主机之前, 不得发出 CLS 命令。因此, 在发送 CLS 命令之前, 必须同时检测 'buffer-empty' 和 'RFNM-received'。与往常一样, CLS 必须得到确认后才能删除条目。
4. 由接收方终止
当接收端用户发出 CLOSE 系统调用时, 其 NCP 接受并立即发送 CLS 命令。不过数据仍可能到达, 这些数据应被丢弃。发送端收到 CLS 后, 应立即终止数据流。