Chapter 18 ■ rpC
345
for linenum, line in enumerate(fileobj.readlines()):
function(line)
return linenum + 1
if name == 'main':
main()
At first, the client might look like a rather standard program using an RPC service. After all, it calls a generically
named connect() function with a network address and then accesses methods of the returned proxy object as though the
calls were being performed locally. However, if you look closer, you will see some startling differences! The first argument
to the RPC function is actually a live file object that does not necessarily exist on the server. The other argument is a
function; another live object instead of the kind of inert data structure that RPC mechanisms usually support.
The server exposes a single method that takes the proffered file object and callable function. It uses these
exactly as you would in a normal Python program that was happening inside a single process. It calls the file object’s
readlines(), and it expects the return value to be an iterator over which a for loop can repeat. Finally, the server calls
the function object that has been passed in without any regard for where the function actually lives (namely, in the
client). Note that RPyC’s new security model dictates that, absent any special permission, it will only allow clients to
call methods that start with the special prefix exposed_.
It is especially instructive to look at the output generated by running the client, assuming that a small
testfile.txt indeed exists in the current directory and that it has a few words of wisdom inside:
$ python rpyc_client.py
Noisy: 'Simple\n'
Noisy: 'is\n'
Noisy: 'better\n'
Noisy: 'than\n'
Noisy: 'complex.\n'
The number of lines in the file was 5
Equally startling here are two facts. First, the server was able to iterate over multiple results from readlines(),
even though this required the repeated invocation of file–object logic that lived on the client. Second, the server didn’t
somehow copy the noisy() function’s code object so that it could run the function directly; instead, it repeatedly
invoked the function, with the correct argument each time, on the client side of the connection!
How is this happening? Quite simply, RPyC takes exactly the opposite approach from the other RPC mechanisms
examined previously. Whereas all of the other techniques try to serialize and send as much information across the
network as possible and then leave the remote code to either succeed or fail with no further information, the RPyC
scheme only serializes completely immutable items, such as Python integers, floats, strings, and tuples. For everything
else, it passes across a remote object identifier that lets the remote side reach back into the client to access attributes
and invoke methods on those live objects.
This approach results in quite a bit of network traffic. It can also result in a significant delay if lots of object
operations have to pass back and forth between the client and server before an operation is complete. Establishing
proper security is also an issue. To give the server permission to call things like readlines() on the client’s own
objects, I chose to make the client connection with a blanket assertion of allow_public_attrs. But if you are not
comfortable giving your server code such complete control, then you might have to spend a bit of time getting the
permissions exactly right for your operations to work without exposing too much potentially dangerous functionality.
So the technique can be expensive, and security can be tricky if the client and server do not trust each other.
But when you need it, there is really nothing like RPyC for letting Python objects on opposite sides of a network
boundary cooperate with each other. You can even let more than two processes play the game; check out the RPyC
documentation for more details!