Client-Server

Rserve is currently the default solution when looking for a server solution for R, but rpy2 can be used to develop very easily one’s own server, tailored to answer specific requirements. Such requirements can include for example access restriction, a security model, access to subsets of the R engine, distribution of jobs to other servers, all of which are currently difficult or impossible to achieve with R serve.

The pyRserve package addresses the connection to Rserve from Python, and although it frees one from handling the R server is also constrains one to use Rserve.

Simple socket-based server and client

Server

An implementation of a simplistic socket server listening on a given port for a string to evaluate as R code is straightforward with Python’s SocketServer module.

Our example server will be in a file rpyserve.py, containing the following code.

import socketserver
import sys
import rpy2.robjects as robjects

class MyTCPHandler(socketserver.StreamRequestHandler):

    def handle(self):
        # verbose server
        print("%s wrote:" % self.client_address[0])

        # self.rfile is a file-like object created by the handler;
        # we can now use e.g. readline() instead of raw recv() calls
        encoding = self.rfile.readline().strip()
        encoding = str(encoding, 'ASCII')
        print('    encoding: %s' % encoding)

        size = int.from_bytes(self.rfile.read(8), 'little')
        print('    size: %i bytes' % size)
        
        rcv = self.rfile.read(size)
        rcv = str(rcv, encoding) 

        # verbose server
        print('    R code string:')
        print(rcv)

        # evaluate the data passed as a string of R code
        results = robjects.r(rcv)

        # return the result of the evaluation as a string
        # to the client
        results = bytes(str(results), encoding)
        size_res = len(results).to_bytes(8, 'little')
        print('    Result size: %i' % len(results))
        self.wfile.write(size_res +
                         results)


if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser()
    parser.add_argument('-p', '--port', 
                        type=int,
                        default=8080,
                        help='port')
    parser.add_argument('--hostname',
                        default='localhost')
    options = parser.parse_args()

    # Create the server, binding to localhost on port 9999
    server = socketserver.TCPServer((options.hostname, options.port),
                                    MyTCPHandler)

    print('Server listening on %s:%i' % (options.hostname, options.port))
    # Activate the server; this will keep running until you
    # interrupt the program with Ctrl-C
    server.serve_forever()

Running a server listening on port 9090 is then:

python rpyserve.py --hostname localhost

Client

Using Python’s socket module, implementing a client is just as easy. We write the code for ours into a file rpyclient.py:

import socket
import sys
import locale
import argparse


def send_R_code(rcode, hostname, port):
    """
    Evaluate the R code in `rcode` (on a possibly remote machine)
    """
    # Create a socket (SOCK_STREAM means a TCP socket)
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # Connect to server and send data
    sock.connect((hostname, port))
    size_send = len(rcode).to_bytes(8, 'little')
    sock.send(bytes(encoding, 'ASCII') + b'\n' + \
              size_send + \
              rcode)

    # Receive data from the server and shut down
    print("Received:")
    size = int.from_bytes(sock.recv(8), 'little') # 64 bits max
    print("    size: %i bytes" % size)
    received = sock.recv(size)
    sock.close()
    print("    R output:")
    print(str(received, encoding))


if __name__ == '__main__':
    
    parser = argparse.ArgumentParser()
    parser.add_argument('-p', '--port',
                        type=int,
                        default=8080)
    parser.add_argument('--hostname',
                        default='localhost')
    
    options = parser.parse_args()

    # read R code from STDIN
    rcode = sys.stdin.read()
    encoding = locale.getlocale()[1]
    rcode = bytes(rcode, encoding)

    send_R_code(rcode, options.hostname, options.port)

Evaluating R code on a local server as defined in the previous section, listening on port 9090 is then:

echo 'R.version' | python rpyclient.py --hostname localhost

In this example, the client is querying the R version.