RPC with Python using RPyC
RPyC provides remote procedure call, or more precisely, Remote Python Call between two computers.
On both machines we need to have Python and the RPyC package installed and on the server we need to launch a process before we can talk to it. The nice thing is that it runs on all the 3 major Operating systems: Linux, Mac OSX, and MS Windows, and you can make RPyC between machines running different OS-es.
Install RPyC
It is simple just type
pip install rpyc
Run the RPyC server
Nothing special needed. just run the rpyc_classic.py script that came with the RPyC installation:
rpyc_classic.py
It will print something like this to the console, and then it will wait for a client to connect. As the client access the server we will see further output appearing on the console.
INFO:SLAVE/18812:server started on [0.0.0.0]:18812
Simple module use
examples/python/rpyc_modules.py
import rpyc import sys if len(sys.argv) < 2: exit("Usage {} SERVER".format(sys.argv[0])) server = sys.argv[1] conn = rpyc.classic.connect(server) rsys = conn.modules.sys print(rsys.version) ros = conn.modules.os print(ros.uname())
This example, as all the other examples here require the name or IP address of remote server on the command line. So you'd run
python rpyc_modules.py remote_machine
If you wanted to run this on remote_machine
or you'd run
python rpyc_modules.py localhost
if you had only one computer and wanted to see how you can talk to yourself.
In this simple script we connect to a remote server using the following call:
conn = rpyc.classic.connect(server)
Then we can use the connection object stored in conn to execute commands on the remote server.
We can attache a local name to a remote Python object. For example this line connects the rsys name on our client to the sys object on the server. (It automatically imports the sys module on the server.)
rsys = conn.modules.sys
Once we have made that call we can use the rsys on the client just as we would use sys on the server. For example we can access its attributes and print them on the console of the client:
print(rsys.version)
The same with the os module in the above code.
Persistent variables on the server
On the next example we use the execute and eval methods of the connection object. The execute method will, execute arbitrary code on the server. In our example first we create a variable with a new value. Then we change the content of the variable.
Finally we use the eval method to evaluate an expression and return the results.
examples/python/rpyc_variables.py
import rpyc import sys if len(sys.argv) < 2: exit("Usage {} SERVER".format(sys.argv[0])) server = sys.argv[1] conn = rpyc.classic.connect(server) conn.execute('x = 21') conn.execute('x *= 2') print(conn.eval('x')) # 42 conn.execute('scores = { "Foo" : 10 }') conn.execute('scores["Foo"] += 1') conn.execute('scores["Bar"] = 42') local_scores = conn.eval('scores') print(local_scores) # {'Foo': 11, 'Bar': 42} print(local_scores['Foo']) # 11 conn.namespace["scores"]["Bar"] += 58 print(conn.eval('scores')) # {'Foo': 11, 'Bar': 100}
The super nice thing is that it will take any data structure we have on the server and replicate it locally. So in the second set of calls we create a dictionary on the server, we modify it, and then we copy it back to the client using the eval method.
On the client we get a copy of the remote object.
Better yet, we can use the namespace attribute of the connection object which is a dictionary where the keys are the object in on the remote server. For example the variables we created there. Thus conn.namespace["scores"] refers to the object called scores on the remote machine. We can access and even change the object and its attributes with regular Python code like this:
conn.namespace["scores"]["Bar"] += 58
Remote exception handling
examples/python/rpyc_exception.py
import rpyc import sys if len(sys.argv) < 2: exit("Usage {} SERVER".format(sys.argv[0])) server = sys.argv[1] conn = rpyc.classic.connect(server) conn.execute('x = 42') conn.execute('y = 0') conn.execute('z = x/y') print("Hello")
If an exception occurs on the server, it is printed to the console of the server and the exception itself is propagated to the client.
Meaning that the last print-statement will not be executed.
You can catch the remote exception locally the same way you'd catch local exceptions.
Traceback (most recent call last): File "rpyc_exception.py", line 12, in <module> conn.execute('z = x/y') File "/Users/gabor/venv3/lib/python3.6/site-packages/rpyc/core/netref.py", line 199, in __call__ return syncreq(_self, consts.HANDLE_CALL, args, kwargs) File "/Users/gabor/venv3/lib/python3.6/site-packages/rpyc/core/netref.py", line 72, in syncreq return conn.sync_request(handler, oid, *args) File "/Users/gabor/venv3/lib/python3.6/site-packages/rpyc/core/protocol.py", line 523, in sync_request raise obj ZeroDivisionError: division by zero ========= Remote Traceback (1) ========= Traceback (most recent call last): File "/home/gabor/venv3/lib/python3.6/site-packages/rpyc/core/protocol.py", line 347, in _dispatch_request res = self._HANDLERS[handler](self, *args) File "/home/gabor/venv3/lib/python3.6/site-packages/rpyc/core/protocol.py", line 624, in _handle_call return self._local_objects[oid](*args, **dict(kwargs)) File "/home/gabor/venv3/lib/python3.6/site-packages/rpyc/core/service.py", line 154, in exposed_execute execute(text, self.exposed_namespace) File "<string>", line 1, in <module> ZeroDivisionError: division by zero
Upload code - run function
Based on the above example we could upload and execute any Python code.
If that was a function definition then we would effectively create a new function on
the remote server which can be later executed.
For clarity I've moved the code that should be executed remotely to a separate file.
It is a simple implementation of the Fibonacci function:
examples/python/remote_code.py
Then there is code that will load this file in the client as a plain text, then
call execute on the connection object to execute the function definition
on the remote machine.
After we executed the code, we can use the namespace attribute of the connection
to access the function and call it with any parameter.
examples/python/rpyc_exception.py
examples/python/rpyc_timeout.py
examples/python/rpyc_reconnect_objects.py
examples/python/rpyc_ping.py
examples/python/rpyc_with_reconnect.py
Modules problem, might be solved with properties
L109
def fib(n):
if n == 1:
return [1]
if n == 2:
return [1, 1]
values = [1, 1]
for _ in range(2, n):
values.append(values[-1] + values[-2])
return values
import rpyc
import sys
if len(sys.argv) < 2:
exit("Usage {} SERVER".format(sys.argv[0]))
server = sys.argv[1]
conn = rpyc.classic.connect(server)
conn.execute('x = 42')
conn.execute('y = 0')
conn.execute('z = x/y')
print("Hello")
import rpyc
import sys
if len(sys.argv) < 2:
exit("Usage {} SERVER".format(sys.argv[0]))
server = sys.argv[1]
conn = rpyc.classic.connect(server)
#conn = rpyc.classic.factory.connect(server, 18812, rpyc.classic.SlaveService, config={ 'sync_request_timeout' : 1 }, ipv6 = False, keepalive = False)
my_code = '''
import time
def wait_a_bit(n):
start = time.time()
#for _ in range(n):
# time.sleep(1)
time.sleep(n)
end = time.time()
return { "start" : start, "end" : end, "diff" : int(end - start) }
'''
conn.execute(my_code)
rf = conn.namespace['wait_a_bit']
print(rf(50))
import sys
import time
from rpyc_with_reconnect import ReConn
if len(sys.argv) < 2:
exit("Usage {} SERVER".format(sys.argv[0]))
conn = ReConn(sys.argv[1])
conn.execute('x = 21')
print(conn.eval('x')) # 21
print("You have 10 sec to restart the RPyC server")
time.sleep(10)
conn.execute('y = 42')
print(conn.eval('y')) # 42
print("You have 10 sec to restart the RPyC server")
time.sleep(10)
print(conn.eval('6*7')) # 42
#conn.execute('x = 21')
#print(conn.eval('x')) # 42
#print("assigned")
#time.sleep(80)
#print("waiting a bit more")
##time.sleep(20)
#conn.reconnect()
#conn.execute('x *= 2')
#print(conn.eval('x')) # 42
import rpyc
import sys
import time
if len(sys.argv) < 2:
exit("Usage {} SERVER".format(sys.argv[0]))
server = sys.argv[1]
conn = rpyc.classic.connect(server)
rsys = conn.modules.sys
print(rsys.version)
for _ in range(10):
print("pinging")
conn.ping()
#try:
# conn.ping()
#except Exception as e:
# print(e)
# conn = rpyc.classic.connect(server)
time.sleep(3)
print("after wait")
time.sleep(3)
print("after more wait")
# raise EOFError("connection closed by peer")
ros = conn.modules.os
print(ros.uname())
# ConnectionRefusedError: [Errno 61] Connection refused
import rpyc
class ReConn(object):
def __init__(self, server):
self.server = server
self.connect()
def connect(self):
print("connecting")
self.conn = rpyc.classic.connect(self.server)
def execute(self, code):
try:
self.conn.execute(code)
except EOFError:
self.connect()
self.conn.execute(code)
def eval(self, code):
try:
return self.conn.eval(code)
except EOFError:
self.connect()
return self.conn.eval(code)
Published on 2018-12-15