Remote Procedure Calls¶

Emil Sekerinski, McMaster University, Fall 2019¶


Message passing is well suited for one-directional communication, as in filters (e.g. the sorting network). When two-directional communication between clients and a server is needed, a channel for sending requests and a reply channel for each client needs to be introduced.

The remote procedure call (RPC) eliminates the need for channels in client-server communication. The server exports procedures that can be called, as with monitors. When a client calls a procedure, execution is delayed, as with synchronous communication. However, execution is delayed until the results of the procedure called are sent back to the client.

Consider the remote procedure call

r ← server.gcd(a1, a2)

and assume that server runs following process:

var args: channel[integer × integer]
var result: channel[integer]

process gcd
    var x, y: integer
    do true →
        args ? (x, y)
        do x > y → x := x - y
         ⫿  y > x → y := y - x
        result ! x

The remote procedure call is then equivalent to

args ! (a1, a2) ; result ? r

Unlike with message passing, the name of the server has to be known to the client.

RPC in Python¶

Following gcd server uses the standard xmlrpc library. The library encodes the parameters and results as XML structures for transmission. The parameter to SimpleXMLRPCServer is a tuple with the Internet address and the port number; the port must be opened for communication.

Note: The cell below goes into an infinite loop, so before running it, open a copy of this notebook in a separate window with the Jupyter server running on the same or a different computer.

In [ ]:
from xmlrpc.server import SimpleXMLRPCServer


def gcd(x, y):
  a, b = x, y
  while a != b:
    if a > b:
      a = a - b
    else:
      b = b - a
  return a


server = SimpleXMLRPCServer(('jhub3.cas.mcmaster.ca', 8020))
server.register_function(gcd, 'gcd')
server.serve_forever()

On the client, a server proxy has to be created:

In [ ]:
import xmlrpc.client

server = xmlrpc.client.ServerProxy('http://jhub3.cas.mcmaster.ca:8020')
server.gcd(81, 36)

Question: Suppose there is sequence of calls to server.gcd. Do the client and server run in parallel?

Answer. With the gcd server, either the server or the client would execute, but not both (and there could be period when neither executes due to the time for the transmission).

The xmlrpc library also allows objects to be remotely called. The parameter allow_none=True is needed when creating the server proxy to allow parameterless calls. (Reminder: open a new copy of the notebook before running next cell)

In [ ]:
from xmlrpc.server import SimpleXMLRPCServer


class Counter:
  def __init__(self):
    self.a, self.e = 0, True
    # e == even(a)

  def inc(self):
    self.a, self.e = self.a + 1, not self.e

  def even(self):
    return self.e


server = SimpleXMLRPCServer(('jhub3.cas.mcmaster.ca', 8026), allow_none=True)
server.register_instance(Counter())  # create Counter object, then register
server.serve_forever()

The corresponding client is:

In [ ]:
import xmlrpc.client

c = xmlrpc.client.ServerProxy('http://jhub3.cas.mcmaster.ca:8026')
c.inc()
c.even()

If you try to run a server on a port that is already in use, you get an "address in use" error. To check which ports are currently used, run:

In [ ]:
!netstat -atunlp

To check the status of a specific port, run:

In [ ]:
!netstat -atunlp | grep 8023

When running a Python RPC server from a notebook, it will only run as long as the notebook runs. To keep a server running after logging out, save the server to a file, say Counter.py, and run from the command line (not notebooks):

nohup python3 Counter.py &

The & starts a new process that runs in the background and nohup prevents that process from being terminated when logging out. To check the log produced by the server process, run:

cat nohup.out

Note that only one method at a time is executed, like with monitors, and following the definition of RPC in terms of channels. This guarantees that the invariant will be preserved without any additional means for mutual exclusion. However, this also reduced potential concurrent execution.

Python supports also multi-threaded RPC servers by creating a new server class that "mixes in" ThreadingMixIn.

RPC in Go¶

The net/rpc package allow remote calls to methods of the form

func (t *T) MethodName(argType T1, replyType *T2) error

Type error is predeclared as

type error interface {
	Error() string
}

By convention, returning nil means that no error occurred.

In Go, methods of a class do not have to be declared together with the fields. Rather, the fields are declared as a struct and methods separately, with the parameter before the method name being the receiver of the call. This allows methods to be added as needed without introducing new classes by inheritance.

In [ ]:
%%writefile counter.go
package main

type Counter struct{a int32; e bool}

func (self *Counter) Inc() {
    self.a += 1; self.e = !self.e
}

func (self *Counter) Even() bool {
    return self.e
}

func main(){
    c := new(Counter); c.a, c.e = 0, true
    c.Inc(); println(c.Even())
    c.Inc(); println(c.Even())
}
In [ ]:
!go run counter.go
In [ ]:
%%writefile point.go
package main
import "math"

type Point struct{x, y float64}

func (p *Point) Distance() float64 {
    return math.Sqrt(p.x * p.x + p.y * p.y)
}

func (p *Point) Scale(factor float64) {
    p.x *= factor; p.y *= factor
}

func main(){
    q := new(Point); q.x, q.y = 3, 4
    l := q.Distance(); println(l)
    q.Scale(2); println(q.x, q.y)
}
In [ ]:
!go run point.go

For a GCD server, the function for computing the GCD has to be written as a method. As methods can be attached to (almost) any type, we define a new type Gcd to be type int:

In [ ]:
%%writefile gcdmethod.go
package main

type GCDArg struct{X, Y int}
type Gcd int

func (t *Gcd) ComputeGCD(arg *GCDArg, reply *int ) error {
    a, b := arg.X, arg.Y
    for a != b {
        if a > b {a = a - b} else {b = b - a}
    }
    *reply = a
    return nil
}

func main(){
    g := new(Gcd); println(g); println(*g)
    a := GCDArg{81, 36}
    var r int
    g.ComputeGCD(&a, &r)
    println(r)
    h := new(Gcd); println(h); println(*h)
}
In [ ]:
!go run gcdmethod.go

Question: What is the output of the println statements?

The server registers a new Gcd object under a name, here Algorithms and then accepts incoming requests:

In [ ]:
%%writefile gcdserver.go
package main
import ("net"; "net/rpc")

type GCDArg struct{X, Y int}
type Gcd int

func (t *Gcd) ComputeGCD(arg *GCDArg, reply *int ) error {
    println(&t)
    a, b := arg.X, arg.Y
    for a != b {
        if a > b {a = a - b} else {b = b - a}
    }
    *reply = a
    return nil
}

func main(){
    server := rpc.NewServer()
    server.RegisterName("Algorithms", new(Gcd))

    ln, err := net.Listen("tcp", ":8012")
    println(err) // if err != nil {panic(e)}
    server.Accept(ln)
}
In [ ]:
!go run gcdserver.go

On the client, the parameters and result value has to be converted in an analogous way:

In [ ]:
%%writefile gcdclient.go
package main
import ("net"; "net/rpc")

type GcdClient struct{client *rpc.Client}
type GCDArg struct{X, Y int}

func (t *GcdClient) gcd(a, b int) int {
    args := &GCDArg{a, b}
    var reply int
    err := t.client.Call("Algorithms.Compute_GCD", args, &reply)
    if err != nil {panic(err)}
    return reply
}
func main() {
    conn, err := net.Dial("tcp", "jhub3.cas.mcmaster.ca:8020")
    if err != nil {panic(err)}
    algorithms := &GcdClient{client: rpc.NewClient(conn)}

    println(algorithms.gcd(10, 4))
    println(algorithms.gcd(81, 36))
}

Counter

In [ ]:
%%writefile counterserver.go
package main
import ("net"; "net/rpc")

type Gcd struct{a int, e bool}
type IncArg struct{X, Y int}

func (t *Gcd) ComputeGCD(arg *GCDArg, reply *int ) error {
    println(&t)
    a, b := arg.X, arg.Y
    for a != b {
        if a > b {a = a - b} else {b = b - a}
    }
    *reply = a
    return nil
}

func main(){
    server := rpc.NewServer()
    server.RegisterName("Algorithms", new(Gcd))

    ln, err := net.Listen("tcp", ":8012")
    println(err) // if err != nil {panic(e)}
    server.Accept(ln)
}
In [ ]:
!go run gcdserver.go
In [ ]:
%%writefile gcdclient.go
package main
import ("net"; "net/rpc")

type GcdClient struct{client *rpc.Client}
type GCDArg struct{X, Y int}

func (t *GcdClient) gcd(a, b int) int {
    args := &GCDArg{a, b}
    var reply int
    err := t.client.Call("Algorithms.Compute_GCD", args, &reply)
    if err != nil {panic(err)}
    return reply
}
func main() {
    conn, err := net.Dial("tcp", "jhub3.cas.mcmaster.ca:8020")
    if err != nil {panic(err)}
    algorithms := &GcdClient{client: rpc.NewClient(conn)}

    println(algorithms.gcd(10, 4))
    println(algorithms.gcd(81, 36))
}