This evening I decided to give some thought about a good RPC design, with a simple semantics and efficient wire format. There is a lot of prior art, e.g. SUN RPC, SMB, CORBA, SOAP, XMLRPC, and I don't want to go into the detail of any one of them. The XML based wire format is not efficient. The ASN.1 or XDR based wire format are fine, but JSON is simpler. It also has an efficient wire format called
MessagePack. However, the RPC specification that came with MessagePack isn't satisfactory. One prominent missing feature is the streaming RPC. Its IDL is also tied to the underlying data representation. What I'm looking for is a simply typed way to describe a JSON value. The overall RPC semantics should be like simply-typed JavaScript.
Here is an informal proposal for the type of a simply-typed JSON value.
key-type := number | boolean | string | enum-type
key := number-value | boolean-value | string-value | enum-value
opt-key := key | optional key
type := key-type | object
| [type] // array
| (type1, type2, ..., typen) // tuple
| // associative map
{opt-key1: type1, opt-key2: type2, ..., opt-keyn: typen}
| nullable type
The type describes a JSON object. It can be:
- One of the primitive types "number," "boolean," or "string," or it could be simply "object" for any JSON object without more refined type description.
- The array syntax is for describing an array where all items are of the same type.
- The tuple is for describing a sequence of objects of various types, even though in JSON it would be represented as an array as well.
- The associative map is a key-value mapping from a concrete value of the key to a type. This is the refined description of a complex JSON object. The concrete key values can be a number, a boolean, or a string. The key values can be optionally prefixed by "optional" to indicate that the key could be missing.
- A type with the "nullable" qualifier which indicates that the value could be null.
Furthermore, for the ease of defining constants or symbolic names of associative map, we allow the user to define enums which map a symbolic name to a number. The enum type's name could be used wherever a type is expected, and the enum symbols could be used whenever a key is expected.
An interface is a collection of function calls with the signature:
typearg -> typeret throws type1, type2, ..., typen
where type
arg is the type of the function argument, type
ret is the type of the return value, and the list of types after "throws" are the exception types that could be raised by the function.
What is unique about this proposal is the unified treatment of bidirectional and streaming RPC. Typically, streaming RPC is a way for the server to send data in multiple responses, asynchronously back to the client. Client can always stream data to the server by making a call to server as many times as necessary. Here the streaming RPC is generalized to the concept of a co-interface. Whenever server needs to make a part of the response available, it would invoke one of the functions in the co-interface. A co-interface is supposed to be implemented by the client for receiving calls from the server, in order to connect to the server. Hence, the complete service description consists of a server-side interface and a client-side co-interface, which is how bidirectional RPC can be made.
Update: June 1, 2012. I've decided to get rid of exception (throw) and instead use cointerface for signaling alternative continuation.