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.
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:
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)
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:
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:
!netstat -atunlp
To check the status of a specific port, run:
!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.
%%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())
}
!go run counter.go
%%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)
}
!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
:
%%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)
}
!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:
%%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)
}
!go run gcdserver.go
On the client, the parameters and result value has to be converted in an analogous way:
%%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
%%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)
}
!go run gcdserver.go
%%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))
}