10 RMI 传输协议
10.1 概述
RMI 协议利用另外两个协议来进行数据传输:Java 对象序列化和 HTTP。对象序列化协议用于编组调用和返回数据。HTTP 协议用于在必要时“POST”远程方法调用并获取返回数据。每个协议都被记录为一个单独的语法。在生成规则中的非终端符号可能指向由另一个协议(对象序列化或 HTTP)管理的规则。当跨越协议边界时,随后的生成将使用嵌入的协议。
关于语法符号的说明
- 我们使用类似于《Java 语言规范》中使用的符号。
- 流中的控制代码由十六进制表示的字面值表示。
- 语法中的一些非终端符号表示在方法调用中提供的应用程序特定值。这样的非终端符号的定义包括其 Java 编程语言类型。将每个这样的非终端符号映射到其相应的类型的表跟随在语法之后。
10.2 RMI 传输协议
RMI 的传输格式由一个 Stream
表示。这里采用的术语反映了客户端的视角。 Out
指输出消息,In
指输入消息。传输头的内容不使用对象序列化格式化。
Stream:
Out
In
RMI 使用的输入和输出流是成对的。每个 Out
流都有一个对应的 In
流。语法中的 Out
流映射到套接字的输出流(从客户端的视角)。语法中的 In
流与相应套接字的输入流配对。由于输出和输入流是成对的,因此在输入流上唯一需要的头信息是关于协议是否被理解的确认;其他头信息(如魔数和版本号)可以由流配对的上下文隐含。
10.2.1 输出流的格式
RMI 中的输出流由传输 Header
信息后跟一系列 Messages
组成。或者,输出流可以包含嵌入在 HTTP 协议中的调用。
Out:
Header Messages
HttpMessage
Header:
0x4a 0x52 0x4d 0x49 Version Protocol
Version:
0x00 0x01
Protocol:
StreamProtocol
SingleOpProtocol
MultiplexProtocol
StreamProtocol:
0x4b
SingleOpProtocol:
0x4c
MultiplexProtocol:
0x4d
Messages:
Message
Messages Message
Messages
被包装在由 Protocol
指定的特定协议中。对于 SingleOpProtocol
,在 Header
之后可能只有一个 Message
,并且没有额外的数据将 Message
包装在其中。 SingleOpProtocol
用于嵌入在 HTTP 请求中的调用,其中不可能进行超出单个请求和响应的交互。
对于 StreamProtocol
和 MultiplexProtocol
,服务器必须用一个字节 0x4e
响应支持该协议,并且包含一个包含主机名和端口号的 EndpointIdentifier
,服务器可以看到客户端正在使用的。客户端可以使用此信息来确定其主机名,如果出于安全原因无法这样做。然后,客户端必须响应另一个包含客户端默认接受连接的 EndpointIdentifier
。在 MultiplexProtocol
情况下,服务器可以使用此信息来识别客户端。
对于 StreamProtocol
,在此端点协商之后,Messages
将通过输出流发送,而不需要对数据进行任何额外的包装。对于 MultiplexProtocol
,套接字连接用作多路复用连接的具体连接,如 第 10.6 节“RMI 的多路复用协议” 中所述。通过此多路复用连接发起的虚拟连接由一系列如下描述的 Messages
组成。
有三种类型的输出消息:Call
、Ping
和 DgcAck
。 Call
编码了一个方法调用。 Ping
是用于测试远程虚拟机活动性的传输级消息。 DgcAck
是发送到服务器的分布式垃圾回收器的确认,指示服务器返回值中的远程对象已被客户端接收。
Message:
Call
Ping
DgcAck
Call:
0x50 CallData
Ping:
0x52
DgcAck:
0x54 UniqueIdentifier
10.2.2 输入流的格式
目前有三种类型的输入消息:ReturnData
、HttpReturn
和 PingAck
。 ReturnData
是“正常”RMI 调用的结果。 HttpReturn
是嵌入在 HTTP 协议中的调用的返回结果。 PingAck
是对 Ping
消息的确认。
In:
ProtocolAck Returns
ProtocolNotSupported
HttpReturn
ProtocolAck:
0x4e
ProtocolNotSupported:
0x4f
Returns:
Return
Returns Return
Return:
ReturnData
PingAck
ReturnData:
0x51 ReturnValue
PingAck:
0x53
10.3 RMI 使用对象序列化
RMI 调用中的调用和返回数据使用 Java 对象序列化协议格式化。每个方法调用的 CallData
都写入一个包含 ObjectIdentifier
(调用目标)、一个 Operation
(表示要调用的方法的数字)、一个 Hash
(验证客户端存根和远程对象骨架使用相同的存根协议的数字),然后是零个或多个用于调用的 Arguments
的列表。
在 JDK1.1 存根协议中,Operation
表示由 rmic
分配的方法编号,而 Hash
是存根/骨架哈希,即存根的接口哈希。从 Java 2 存根协议开始(Java 2 存根使用 rmic
的 -v1.2
选项生成),Operation
的值为 -1,而 Hash
是表示要调用的方法的哈希。哈希在 “RemoteRef
接口” 中描述。
CallData:
ObjectIdentifier Operation Hash Arguments[opt]
ObjectIdentifier:
ObjectNumber UniqueIdentifier
UniqueIdentifier:
Number Time Count
Arguments:
Value
Arguments Value
Value:
Object
Primitive
RMI 调用的 ReturnValue
包括返回代码,指示正常返回或异常返回,一个用于标记返回值的 UniqueIdentifier
(如果需要,用于发送 DGCAck
),后跟返回结果:返回的 Value
或抛出的 Exception
。
ReturnValue:
0x01 UniqueIdentifier Value[opt]
0x02 UniqueIdentifier Exception
注意: ObjectIdentifier
、UniqueIdentifier
和 EndpointIdentifier
不使用默认序列化写出,而是每个使用自己特殊的 write
方法(这不是对象序列化使用的 writeObject
方法);每种标识符类型的 write
方法将其组件数据连续添加到输出流中。
10.3.1 类注释和类加载
RMI 重写了 ObjectOutputStream
和 ObjectInputStream
的 annotateClass
和 resolveClass
方法。每个类都使用代码库 URL(可以加载类的位置)进行注释。在 annotateClass
方法中,加载类的类加载器被查询其代码库 URL。如果类加载器是非 null
的,并且类加载器具有非 null
的代码库,则使用 ObjectOutputStream.writeObject
方法将代码库写入流;否则,使用 writeObject
方法将 null
写入流。注意:作为优化,位于“java
”包中的类不会被注释,因为它们对接收方始终可用。
类注释在反序列化期间使用 ObjectInputStream.resolveClass
方法解析。 resolveClass
方法首先通过 ObjectInputStream.readObject
方法读取注释。如果注释,即代码库 URL,是非 null
的,则获取该 URL 的类加载器并尝试加载类。通过使用 java.net.URLConnection
获取类字节来加载类(这与 Web 浏览器的小程序类加载器使用的机制相同)。
10.4 RMI 使用 HTTP POST 协议
10.5 RMI 的应用特定值
符号 | 类型 |
---|---|
计数 |
short |
异常 |
java.lang.Exception |
哈希 |
long |
主机名 |
UTF |
数字 |
int |
对象 |
java.lang.Object |
对象数字 |
long |
操作 |
int |
端口号 |
int |
基本类型 |
byte , int , short , long ... |
时间 |
long |
10.6 RMI的多路复用协议
多路复用的目的是提供一个模型,其中两个端点可以分别打开多个全双工连接到另一个端点,在只有一个端点能够使用其他设施(例如,TCP连接)打开这样的双向连接的环境中。RMI使用这种简单的多路复用协议,允许客户端在某些情况下连接到RMI服务器对象,而在其他情况下这是不可能的。例如,一些小程序环境的安全管理器禁止创建用于监听传入连接的服务器套接字,从而阻止这些小程序导出RMI对象并通过直接套接字连接提供远程调用的服务。然而,如果小程序可以打开到其代码库主机的正常套接字连接,那么它可以使用该连接上的多路复用协议,以允许代码库主机调用由小程序导出的RMI对象的方法。本节描述了多路复用协议的格式和规则。
10.6.1 定义
本节定义了在协议描述的其余部分中使用的一些术语。
一个端点是使用多路复用协议的连接的两个用户中的一个。
多路复用协议必须在一个现有的双向可靠字节流之上进行层叠,这个字节流通常是由一个端点向另一个端点发起的。在当前的RMI使用中,这总是一个使用java.net.Socket
对象建立的TCP连接。这个连接将被称为具体连接。
多路复用协议促进了虚拟连接的使用,这些连接本身是双向的、可靠的字节流,代表两个端点之间的特定会话。在单个具体连接上两个端点之间的虚拟连接集合构成了一个多路复用连接。使用多路复用协议,虚拟连接可以被任一端点打开和关闭。关于给定端点的虚拟连接的状态由通过具体连接发送和接收的多路复用协议的元素定义。这种状态涉及连接是否打开或关闭、已传输的实际数据以及相关的流量控制机制。如果没有另外限定,本节中使用的术语连接在其余部分中指的是虚拟连接。
在给定的多路复用连接中,每个虚拟连接由一个16位整数标识,称为连接标识符。因此,在一个多路复用连接中存在65,536个可能的虚拟连接。实现可能限制同时使用的这些虚拟连接的数量。
10.6.2 连接状态和流量控制
连接使用多路复用协议定义的各种操作进行操作。以下是协议定义的操作的名称:OPEN、CLOSE、CLOSEACK、REQUEST和TRANSMIT。所有操作的确切格式和规则在第10.6.3节“协议格式”中详细描述。
OPEN、CLOSE和CLOSEACK操作控制连接的打开和关闭,而REQUEST和TRANSMIT操作用于在流量控制机制的约束下在打开的连接中传输数据。
连接状态
对于特定端点,如果端点已发送针对该连接的OPEN操作,或者已接收到针对该连接的OPEN操作(并且随后未关闭),则虚拟连接对于该端点是打开的。下面描述了各种协议操作。
对于特定端点,如果端点已发送针对该连接的CLOSE操作,但尚未收到该连接的后续CLOSE或CLOSEACK操作,则虚拟连接对于该端点是挂起关闭的。
对于特定端点,如果连接从未打开过,或者已接收到针对该连接的CLOSE或CLOSEACK操作(并且随后未打开),则虚拟连接对于该端点是关闭的。
流量控制
多路复用协议使用简单的分组流量控制机制,允许多个虚拟连接并行存在于同一具体连接上。流量控制机制的高级要求是所有虚拟连接的状态是独立的;一个连接的状态不得影响其他连接的行为。例如,如果处理来自一个连接的数据的数据缓冲区变满,这不会阻止任何其他连接的数据传输和处理。如果一个连接的继续取决于另一个连接的使用完成,比如递归的RMI调用会发生的情况,这是必要的。因此,实现必须始终能够消耗和处理所有准备在具体连接上输入的多路复用协议数据(假设符合本规范)。
每个端点与每个连接关联两个状态值:端点请求但尚未接收的字节数(输入请求计数)以及另一个端点请求但尚未由该端点提供的字节数(输出请求计数)。
当端点从另一个端点接收到REQUEST操作时,其输出请求计数会增加,当其发送TRANSMIT操作时,它会减少。当端点发送REQUEST操作时,其输入请求计数会增加,当其接收到TRANSMIT操作时,它会减少。如果其中任何一个值变为负数,则违反了协议。
如果端点发送一个REQUEST操作,使其输入请求计数增加到超出当前可以处理而不阻塞的字节数,则违反了协议。但是,如果连接的用户正在等待读取数据,则应确保其输入请求计数大于零。
如果端点发送包含超出其输出请求计数的字节数的TRANSMIT操作,则违反了协议。它可以缓冲传出数据,直到连接的用户明确要求刷新写入连接的数据。然而,如果必须通过连接发送数据,无论是通过显式刷新还是因为实现的输出缓冲区已满,那么连接的用户可能会被阻塞,直到足够的TRANSMIT操作可以继续。
除了上述规则外,实现可以根据需要发送REQUEST和TRANSMIT操作。例如,一个端点可以请求连接的更多数据,即使其输入缓冲区不为空。
10.6.3 协议格式
多路复用协议的字节流格式由一系列可变长度记录组成。记录的第一个字节是标识记录操作的操作代码,并确定其余内容的格式。定义了以下合法的操作代码:
值 | 名称 |
---|---|
0xE1 | OPEN |
0xE2 | CLOSE |
0xE3 | CLOSEACK |
0xE4 | REQUEST |
0xE5 | TRANSMIT |
如果记录的第一个字节不是定义的操作代码之一,则违反了协议。以下各节描述了每个操作代码的记录格式。
OPEN操作
这是OPEN操作记录的格式:
大小(字节) | 名称 | 描述 |
---|---|---|
1 | 操作码 | 操作代码(OPEN) |
2 | ID | 连接标识符 |
端点发送OPEN操作以打开指定的连接。如果ID指向当前对于发送端点是打开或挂起关闭的连接,则违反了协议。连接打开后,对于两个端点,该连接的输入和请求计数状态均为零。
接收到一个OPEN操作表示另一端正在打开指定的连接。在连接打开后,对于连接的输入和输出请求计数状态,两个端点都为零。
为了防止两个端点之间的标识符冲突,有效连接标识符的空间被分成两半,取决于最高有效位的值。每个端点只允许使用特定高位值打开连接。发起具体连接的端点必须只能打开高位设置为标识符中的连接,而另一端点必须只能打开高位为零的连接。例如,如果一个无法创建服务器套接字的RMI小程序启动到其代码库主机的多路复用连接,该小程序可以在标识符范围0x8000-7FFF内打开虚拟连接,而服务器可以在标识符范围0-0x7FFF内打开虚拟连接。
CLOSE操作
这是CLOSE操作记录的格式:
大小(字节) | 名称 | 描述 |
---|---|---|
1 | 操作码 | 操作码(OPEN) |
2 | ID | 连接标识符 |
ID指的是当前对于发送端点已关闭或待关闭的连接(如果接收端点也发送了CLOSE操作),则这是一种协议违规。发送CLOSE后,连接对于发送端点变为待关闭状态。因此,在从另一端点接收到CLOSE或CLOSEACK之前,发送端点不能重新打开连接。
接收到CLOSE操作表示另一端点已关闭指定的连接,因此在接收端点上关闭。尽管接收端点可能不会再为此连接发送任何操作(直到再次打开),但它仍应向连接的读取器提供实现的输入缓冲区中的数据。如果连接之前是打开状态而不是待关闭状态,则接收端点必须对该连接响应CLOSEACK操作。
CLOSEACK操作
大小(字节) | 名称 | 描述 |
---|---|---|
1 | 操作码 | 操作码(OPEN) |
2 | ID | 连接标识符 |
接收到CLOSEACK操作将指定的连接状态从待关闭更改为关闭,因此将来可以重新打开连接。
REQUEST操作
大小(字节) | 名称 | 描述 |
---|---|---|
1 | 操作码 | 操作码(OPEN) |
2 | ID | 连接标识符 |
4 | 计数 | 请求的附加字节数量 |
ID不是对于发送端点处于打开状态的连接,则这是一种协议违规。端点的输入请求计数增加值为 计数。计数的值是一个带符号的32位整数,如果为负数或零,则这是一种协议违规。
计数。如果连接对于接收端点处于待关闭状态,则可以忽略任何REQUEST操作。
TRANSMIT操作
大小(字节) | 名称 | 描述 |
---|---|---|
1 | 操作码 | 操作码(OPEN) |
2 | ID | 连接标识符 |
4 | 计数 | 传输中的字节数量 |
计数 | 数据 | 传输数据 |
计数。计数的值是一个带符号的32位整数,如果为负数或零,则这是一种协议违规。如果TRANSMIT操作导致发送端点的输出请求计数变为负数,则这也是一种协议违规。
计数。如果这导致输入请求计数变为零且连接的用户正在尝试读取更多数据,则端点应该响应另一个REQUEST操作。如果连接对于接收端点处于待关闭状态,则可以忽略任何TRANSMIT操作。
协议违规
关闭。真实连接将被终止,所有虚拟连接将立即关闭。已经可供从虚拟连接读取的数据可以被连接的用户读取。