1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329
|
/**
* @page rpc_system OLA RPC System
*
* [TOC]
*
* @section rpc_Overview Overview
* A common question that comes up is "How can I communicate with olad using
* something other than C++, Python or Java?".
*
* The bad news is that unless someone has written a client library for your
* language, this requires a bit of work. The good news is that there are
* people willing to help and if you do a good job when writing the client
* library, we'll incorporate it into the OLA codebase and maintain it going
* forward.
* @section rpc_Background Background
* This section provides an introduction to Remote Procedure Calls (RPC).
* If you have used RPCs in other situations (JMI, CORBA, Thrift, etc.) you may
* want to skip this bit.
*
* <a href="http://en.wikipedia.org/wiki/Remote_procedure_call">Remote
* procedure call</a> has some good background reading.
*
* On the client side there is a stub, usually a class. Each stub method takes
* some request data and some sort of callback function that is run when the
* remote call completes. The stub class is auto-generated from the protocol
* specification. The stub class is responsible for serializing the request
* data, sending it to the remote end, waiting for the response, deserializing
* the response and finally invoking the callback function.
*
* On the server side there is a usually a 'Service' class that has to be
* provided. Usually an abstract base class is generated from the protocol
* specification and the programmer subclasses this and provides an
* implementation for each method that can be called.
*
* Because the client stub is autogenerated their can be a lot of duplicated
* code which constructs the request message, sets up the completion callbacks
* and invokes the method. Rather than forcing the users of the client library
* to do duplicate all this work, there is usually a layer in between that
* provides a friendly API and calls into the stub. This layer can be thin
* (all it does is wrap the stub functions) or thick (a single API call may
* invoke multiple RPCs).
*
* These client APIs can be blocking or asynchronous. Blocking clients force the
* caller to wait for the RPC to complete before the function call returns.
* Asynchronous APIs take a callback, in much the same way as the stub itself
* does.
*
* @section rpc_Proto Use of Protocol Buffers
*
* OLA uses <a href="https://code.google.com/p/protobuf/">Protocol Buffers</a>
* as the data serialization format. Protocol buffers allow us to wrap the
* request and response data up into a message (similar to a C struct) and then
* serialize the message to binary data in a platform independent way. It's
* worth reading the
* <a href="https://developers.google.com/protocol-buffers/docs/overview">
* Protocol Buffers documentation</a> before going further. You don't need to
* understand the binary format, but you should be familiar with the Protocol
* Buffer definition format.
*
* As well as data serialization, protobufs also provide a basic framework for
* building RPC services. Early versions of protobufs came with a <a
* href="https://developers.google.com/protocol-buffers/docs/reference/cpp-generated#service">
* service generator</a>. The generic service generator has been deprecated
* since the 2.4.0 release since code generated was rather inflexible (in
* trying to be all things to all people it left many needs un-addressed).
*
* Let's look at a simple example for a dictionary service, Dictionary.proto.
* The client sends the word its looking for and the server replies with the
* definition and maybe some extra information like the pronunciation and a
* list of similar words.
* ~~~~~~~~~~~~~~~~~~~~~
message WordQuery {
required string word = 1;
}
message WordDefinition {
required string definition = 1;
optional string pronunciation = 2;
repeated string similar_words = 3;
}
service DictionaryLookup {
rpc Lookup(WordQuery) returns(WordDefinition);
}
~~~~~~~~~~~~~~~~~~~~~
*
* We can generate the C++ code by running:
*
* protoc --plugin=protoc-gen-cppservice=protoc/ola_protoc_plugin \
* --cppservice_out ./ Dictionary.proto
*
* The generated C++ stub class definition in Dictionary.pb.h looks something
* like:
~~~~~~~~~~~~~~~~~~~~~
class DictionaryLookup_Stub {
public:
DictionaryLookup_Stub(::google::protobuf::RpcChannel* channel);
void Lookup(\::google::protobuf::RpcController* controller,
const \::WordQuery* request,
\::WordDefinition* response,
\::google::protobuf::Closure* done);
};
~~~~~~~~~~~~~~~~~~~~~
*
* As you can see the 2nd and 3rd arguments are the request and response
* messages (WordQuery and WordDefinition respectively). The 4th argument is
* the completion callback and the 1st argument keeps track of the outstanding
* RPC.
*
* An implementation of DictionaryLookup_Stub::Lookup(...) is generated in
* Dictionary.pb.cc. Since the only thing that differs between methods is the
* request / response types and the method name, the implementation just calls
* through to the RpcChannel class:
*
~~~~~~~~~~~~~~~~~~~~~
void DictionaryLookup_Stub::Lookup(\::google::protobuf::RpcController* controller,
const \::WordQuery* request,
\::WordDefinition* response,
\::google::protobuf::Closure* done) {
channel_->CallMethod(descriptor()->method(0),
controller, request, response, done);
}
~~~~~~~~~~~~~~~~~~~~~
*
* Protocol buffers doesn't provide an implementation of RpcChannel, since it's
* implementation specific. Most of the time the RpcChannel uses a TCP
* connection but you can imagine other data passing mechanisms.
*
* As part of writing a new client you'll need to write an implementation of
* an RpcChannel. The C++ implementation is ola::rpc::RpcChannel.
*
* Putting it all together produces the flow shown in the diagram below. The
* yellow code is auto-generated, the blue code has to be written.
*
* \image html rpc.png "The control flow from the Client to the Server"
*
*
* @section rpc_OLA_RPC OLA's Model
*
* By default, olad listens on localhost:9010 . Clients open a TCP connection
* to this ip:port and write serialized protobufs (see \ref rpc_RPCLayer) to
* the connection.
*
* Why TCP and not UDP? Mostly because the original developer was lazy and
* didn't want to deal with chunking data into packets, when TCP handles this
* for us. However TCP has it's own problems, with [Head of line
* blocking](http://en.wikipedia.org/wiki/Head-of-line_blocking). For this
* reason RPC traffic should only be used on localhost, where packet loss is
* unlikely. If you want cross-host RPC you should probably be using E1.31
* (sACN) or the Web APIs.
*
* The OLA client libraries follow the thin, asynchronous client model.
* There is usually a 1:1 mapping between API methods and the underlying RPC
* calls. The clients take the arguments supplied to the API method, construct
* a request protobuf and invoke the stub method.
*
* The exception is the Java library, which could do with some love.
*
* The Python client continues to use the service generator that comes with
* Protocol Buffers. The C++ client and olad (the server component) has
* switched to our own generator, which can be found in protoc/ . If you're
* writing a client in a new language we suggest you build your own generator,
* protoc/CppGenerator.h is a good starting point.
*
* If you don't know C++ don't worry, we're happy to help. Once you know what
* your generated code needs to look like we can build the generator for you.
*
* @section rpc_RPCLayer RPC Layer
*
* To make the RPC system message-agnostic we wrap the serialized request /
* response protobufs in an outer layer protobuf. This outer layer also
* contains the RPC control information, like what method to invoke, the
* sequence number etc.
*
* The outer layer is defined in common/rpc/Rpc.proto and looks like:
~~~~~~~~~~~~~~~~~~~~~
enum Type {
REQUEST = 1;
RESPONSE = 2;
...
};
message RpcMessage {
required Type type = 1;
optional uint32 id = 2;
optional string name = 3;
optional string name = 3;
optional bytes buffer = 4;
}
~~~~~~~~~~~~~~~~~~~~~
*
* The fields are as follows:
* <dl><dt>type</dt>
* <dd>The type of this RPC message, usually REQUEST or RESPONSE</dd>
* <dt>id</dt>
* <dd>the sequence number of the message. The sequence number of response messages
* must match the number used in the request.</dd>
* <dt>name</dt>
* <dd>the name of the remote method to call</dd>
* <dt>buffer</dt>
* <dd>the serialized inner protocol buffer.</dd>
* </dl>
*
* So putting it all together, the RpcChannel::CallMethod() should look
* something like:
~~~~~~~~~~~~~~~~~~~~~
void RpcChannel::CallMethod(const MethodDescriptor *method,
RpcController *controller,
const Message *request,
Message *reply,
SingleUseCallback0<void> *done) {
RpcMessage message;
message.set_type(REQUEST);
message.set_id(m_sequence.Next());
message.set_name(method->name());
string output;
request->SerializeToString(&output);
message.set_buffer(output);
// Send the message over the TCP connection.
return SendMsg(&message);
}
~~~~~~~~~~~~~~~~~~~~~
*
* That's it on the sender side at a high level, although there is a bit of
* detail that was skipped over. For instance you'll need to store the sequence
* number, the reply message, the RpcController and the callback so you can do
* the right thing when the response arrives.
*
* @section rpc_RPCHeader RPC Header
*
* Finally, since protobufs don't contain length information, we prepend a
* 4 byte header to each message. The first 4 bits of the header is a version
* number (currently 1) while the remaining 28 bits is the length of the
* serialized RpcMessage.
*
* \image html rpc-message.png "The wire format of a single RPC message"
*
* When writing the code that sends a message, it's very important to enable
* TCP_NODELAY on the socket and write the 4 byte header and the serialized
* RpcMessage in a single call to write(). Otherwise, the write-write-read
* pattern can introduce delays of up to 500ms to the RPCs, <a
* href="http://en.wikipedia.org/wiki/Nagle's_algorithm">Nagle's algorithm</a>
* has a detailed analysis of why this is so. Fixing this in the C++ & Python
* clients led to a 1000x improvement in the RPC latency and a 4x speedup in
* the RDM Responder Tests, see commit
* a80ce0ee1e714ffa3c036b14dc30cc0141c13363.
*
* @section rpc_OLAServices OLA Methods
*
* The methods exported by olad are defined in common/protocol/Ola.proto . If
* you compare the RPC methods to those provided by the ola::client::OlaClient
* class (the C++ API) you'll notice they are very similar.
*
* @section rpc_NewClient Writing a new Client
*
* To write a client in a new language you'll need the following:
* - An implementation of Protocol Buffers for your language. Luckily there
* are <a href="https://code.google.com/p/protobuf/wiki/ThirdPartyAddOns">
* many implementations</a> available. It doesn't have to have a service
* generator (see the first point below).
* - A sockets library.
* - If you're writing an asynchronous client (and we recommend you do) you'll
* either need non-blocking I/O or a threading library.
*
* I'd tackle the problem in the following steps:
* - Write (or ask us to help write) the service generator for your language.
* This will allow you to generate the OlaServerService_Stub.
* - Write an implementation of RpcChannel and RpcController.
* - Write the code to establish a connection to localhost:9010 and read from
* the network socket when new data arrives.
*
* Once that's complete you should have a working RPC implementation. However
* it'll still require the callers to deal with protobufs, stub objects and the
* network code. The final step is to write the thin layer on top that presents
* a clean, programmer friendly API, and handles the protobuf creation
* internally.
*
* The last part is the least technically challenging, but it does require good
* API design so that new methods and arguments can be added later.
*
* @section rpc_SimpleExample A Very Simple Example
*
* A quick example in Python style pseudo code. This constructs a DmxData
* message, sends it to the server and the extracts the Ack response.
~~~~~~~~~~~~~~~~~~~~~
# connect to socket
server = socket.connect("127.0.0.1", 9010)
# Build the request
Ola_pb2.DmxData dmx_data
dmx_data.universe = 1
dmx_data.data = .....
# Wrap in the outer layer
Rpc_pb2.RpcMessage rpc_message
rpc_message.type = RpcMessage::REQUEST
rpc_message.id = 1
rpc_message.name = 'UpdateDmxData'
rpc_message.buffer = dmx_data.Serialize()
server.write(rpc_message.Serialize())
# wait for response on socket
response_data = server.recv(1000)
header = struct.unpack('<L', response_data)[0]
# grab the first 4 bytes which is the header
version, size = ((header & 0xf0000000) >> 28, header & 0x0ffffff)
if version != 1:
# Bad reply!
return
response = Rpc_pb2.RpcMessage()
response.ParseFromString(response_data[4:size])
if message.type != RpcMessage::RESPONSE:
# not a response
return
if message.id != 1:
# not the response we're looking for
return
Ola_pb2.Ack ack
ack.ParseFromString(response.buffer)
~~~~~~~~~~~~~~~~~~~~~
*
*/
|