Simple RPC Python Module Part II
Let’s have a look at the client implementation. On the client side, we mainly use two objects to communicate with the server: the client object and the attribute wrapper object.
The client object appears, to the user, as pretty much identical to the server object: all the (public) methods of the server object can be accessed on the client object as well, and — for as far as possible — the results of these methods should be identical as well. The client object’s communication with the server object should be as transparent as possible. As such, the implementation of the client object may strike as rather simple:
class PyRPCClient(object):
def __init__(self, address, debug=False):
"""
Connection to an RPC server. Initialized with a given address as
expected by the socket module.
If the optional flag `debug' is set to true, when an exception occurs,
before raising it, the client will print the formatted traceback from
the server side. This is useful for debugging.
"""
self.address = address
self.debug = debug
def __getattribute__(self, attribute):
"""
Overloaded attribute accessor, to transfer local attribute access to
RPC calls.
"""
# NOTE: Because we overload __getattribute__, we have to access our
# OWN attributes by using the __getattribute__ method of our parent
# class, `object'. Otherwise we end up in an infinite loop.
return PyRPCAttributeWrapper(object.__getattribute__(self, "address"),
attribute, object.__getattribute__(self, "debug"))
There’s really nothing special about it. All this implementation does, is to
return a PyRPCAttributeWrapper instance for each and every
attribute that is being accessed on the client object. It is this
attribute wrapper object that does all the magic. So let’s move on to
that.
When instantiated, all the attribute wrapper object is given, are the address of the server object, the attribute to call, and a debugging flag. The constructor is rather trivial, it simply stores these values into members of the instance, for later reference.
As I explained in the previous post, the limitations of this design impose
that only methods of the server object can be accessed. As such, the only
other method of the attribute wrapper, is the __call__() method.
This special Python construct allows an instance of this class to be executed
like a function or — in this case — a method.
def __call__(self, *args, **kwargs):
# We need to loop here, in case the server is too busy to serve us
# now.
loop = True
while loop:
data = pickle.dumps((self.attribute, args, kwargs))
# Ten connection attempts.
for i in range(10):
# Instantiation here for repeated connection attempts.
try:
self.__socket = socket.socket(socket.AF_INET,
socket.SOCK_STREAM)
self.__socket.connect(self.address)
except:
# Connection failed, wait a bit and then retry.
self.__socket = None
time.sleep(0.001)
else:
# Connection successful! Exit the for loop.
break
if not self.__socket:
# Ten connection attempts failed.
raise Exception("Failed to connect to %s (call: %s)", (
self.address, (self.attribute, args, kwargs)))
length = self.__socket.send(data)
while length < len(data):
data = data[length:]
length = self.__socket.send(data)
# If the following call fails with a "Connection reset by peer"
# error, we actually need to start all over again.
try:
response = self.__socket.recv(1024)
except socket.error, err:
if err[0] == 104: # Connection reset by peer.
# We should try again.
self.__socket.close() # Close to reconnect again.
del self.__socket # Clear up memory right away.
# XXX Should we limit this loop to a certain amount of
# iterations?
# Wait a little before re-trying.
time.sleep(0.001)
else:
# All other exceptions should still be raised.
raise err
else:
loop = False
break
# End of while loop.
loop = True
while loop:
try:
result = pickle.loads(response)
loop = False
except:
# Not enough, read more.
response += self.__socket.recv(1024)
self.__socket.close()
# Check for exceptions.
if isinstance(result, PyRPCException):
if self.debug:
# Print the traceback info.
print "Server side traceback:\n" + result.traceback
raise result.exception
else:
return result
There is no need to query the signature of the method on the server side,
because Python allows us to simply catch any possible signature using the two
special arguments *args and **kwargs, to catch any
fixed or named argument, respectively (note that the names are simply
conventions, the important bits are the asterisks).
The main execution consists of two loops. In the first loop, the call data is
serialised using the pickle module and then ten attempts are made
to connect to the server object. Please note that the pickle
module is not safe as such, but as I explained in the previous post,
security was never an issue when designing this module.
Once connected, the data is transmitted to the server object. This data consists of a simple tuple, containing the name of the attribute being called and all fixed and named arguments. I found that in the setup I was deploying this module, I sometimes would get a “connection reset by peer” error, which meant that the transmission had to be repeated.
The second loop deals with receiving the server’s response. Other than
reading the data back over the connection, this part of the code only deals
with the special case in which the returned object is an instance of the
PyRPCException class. This is a special class the server object
uses to represent exceptions that occurred while executing the requested
method. If such an object is received by the client, the given exception is
raised to — again — transparently mimic the server object’s behaviour.
Otherwise, the object that was received is returned.
Next time, we shall look at the implementation of the exception class as well as some details of the server implementation.
in Python