Hey,

I’ve been exploring the new Go support for AWS Lambda that has been announced by AWS early this year (see Announcing Go Support for AWS Lambda), and there’s a little detail of it that I found very interesting: at its core, it uses Go’s net/rpc package as a mean of communication between the AWS infrastructure and your code.

Having never used net/rpc myself, I decided to explore it.

Architecting an RPC hello world

As net/rpc gives us ways of making a piece of code communicate with another piece of code over the wire, a natural example is a “Hello World” in a client-server architecture.

To achieve that, I created this repository: cirocosta/sample-rpc-go.

The file structure looks like the following:

.
├── Makefile            # instructions for building and running the
|                       # example
|
├── client              # RPC client
│   └── client.go       # provides the interface which executes the
|                       # command remotely.
|
├── core                # Shared functionality.
│   └── core.go         # - implements the actual handler that gets
|                       #   executed in the server;
|                       # - provides the go representation of the 
|                       #   messages exchanged between client and server.
|
├── main.go             # CLI
|                       #
└── server              # RPC server - provides the interface which
    |                   # allows a client to communicate with it
    └── server.go       # over the wire.

That allow us to keep the project really simple as main executes either the server or client code based on a flag (-server) and all the rest of the functionality is implemented independently.

Create a client and a server, and you’ll be able to communicate.

Go RPC (remote procedure call) hello world architecture of a client communicating with the server

Let’s do that, then!

Creating a Go RPC Handler

The main piece of an RPC server is the methods that get executed when the client makes a call to them, the p part of the name - procedures.

For those accustomed to HTTP, in RPC land the equivalent of an endpoint (like, POST /say-hello) is a service/procedure name (HelloSayer.Say). Go constructs a map of these service endpoints by taking an exported struct that has a set of exported methods and making them available.

Naturally, these methods must obey to a certain interface so that net/rpc can properly provide (de)serialization of the request and response objects and perform the right method execution:

  • it must take two arguments and return an error object;
  • the first argument is the request and second is the response;
  • the second (response) must be a pointer - in case of failure (an error in the service), error is returned, and the response is sent as nil.

Looking at the Go source code (src/net/rpc/server.go), we can understand how it’s done behind the scenes.

// Register publishes in the server the set of methods of the
// receiver value that satisfies the following conditions:
//	- exported method of exported type
//	- two arguments, both of exported type
//	- the second argument is a pointer
//	- one return value, of type error
// It returns an error if the receiver is not an exported type or has
// no suitable methods. It also logs the error using package log.
// The client accesses each method using a string of the form "Type.Method",
// where Type is the receiver's concrete type.
func (server *Server) Register(rcvr interface{}) error {
	return server.register(rcvr, "", false)
}

// ciro: `register` does the hard work of taking the struct and then analyzing
// its methods to register them.
//
// In the end, `net/rpc` constructs a map that maps endpoint names 
// (`Struct.Method`) to the actual implementations, as well as  
// performing the (de)serialization accordingly  such that the methods 
// are properly called.
func (server *Server) register(rcvr interface{}, name string, useName bool) error {
	s := new(service)
	s.typ = reflect.TypeOf(rcvr)
	s.rcvr = reflect.ValueOf(rcvr)
	s.name = reflect.Indirect(s.rcvr).Type().Name()

	// Install the methods
        // ciro: this inspects the methods and verify (for each) whether they 
        // adhere to the "interface" that net/rpc expects - first arg is 
        // represents the request and is not a pointer ; second is the response 
        // and must be  pointer, and then it also has an error return.
	s.method = suitableMethods(s.typ, true)

        // store the service with the given service name
	if _, dup := server.serviceMap.LoadOrStore(s.name, s); dup {
		return errors.New(
                        "rpc: service already defined: " + 
                        sname)
	}
	return nil
}

So, for a Hello World I end up with this:

type Response struct {
	Message string
}

type Request struct {
	Name string
}

type Handler struct {}

func (h *Handler) Execute(req Request, res *Response) (err error) {
	if req.Name == "" {
		err = errors.New("A name must be specified")
		return
	}

	res.Message = "Hello " + req.Name
	return
}

It adheres to the interface that net/rpc expects and is simple enough for our example.

Creating a Go RPC Server and Client

Once the hander has been defined, creating the server is easy if you’ve set up an HTTP or TCP server before. The only difference is that before actually listening for incoming connections, you have to call rpc.Register and pass an instance of the struct that carries the exported methods you define (i.e., your service handlers).

// Publish our Handler methods
rpc.Register(&core.Handler{})

// Create a TCP listener that will listen on `Port`
listener, _ = net.Listen("tcp", ":"+strconv.Itoa(Port))

// Close the listener whenever we stop
defer listener.Close()

// Wait for incoming connections
rpc.Accept(listener)

on the client side, it’s almost the same as establishing a TCP connection:

var (
        addr     = "127.0.0.1:" + strconv.Itoa(Port)
        request  = &core.Request{Name: Request}
        response = new(core.Response)
)


// Establish the connection to the adddress of the
// RPC server
client, _ = rpc.Dial("tcp", addr)
defer c.client.Close()


// Perform a procedure call (core.HandlerName == Handler.Execute)
// with the Request as specified and a pointer to a response
// to have our response back.
_ = c.client.Call(core.HandlerName, request, response)
fmt.Println(response.Message)

Let’s see how it works in practice.

Go RPC in Practice

Given the simplicity of having a client and server, I got curious about how the underlying communication works.

By default, the Go RPC mechanism uses encoding/gob to (de)serialize the messages that come and go from net/rpc. This means that we’d not have an easy time deciphering what’s going on by inspecting the wire. Nonetheless, net/rpc is pluggable enough that other encoding mechanisms can be used - for instance, the official jsonrpc which is much simpler to inspect.

Go RPC (remote procedure call) hello world architecture of a client communicating with the server

First, a connection is established (plain TCP), and the request is sent:

{
    "id": 0,
    "method": "Handler.Execute",
    "params": [
        {
            "Name": "ciro"
        }
    ]
}

As no error happens, the response cames back with error: null and the result (our Response message):

{
    "error": null,
    "id": 0,
    "result": {
        "Message": "Hello ciro"
    }
}

That’s it!

Closing thoughts

Having never used net/rpc before, I felt that there’s a lot of value having a working RPC system so fast.

At the same time, it’s very hard to find great resources about this package online.

I started searching why and it turns out that net/rpc is in a “freeze” state - no more development is meant to go on in this package - and you can clearly see that: no method of this package takes context, although it makes a lot of sense for some of them (e.g., the client’s .Call method could easily have request cancellation with a context just like you have with net/http).

If you’re curious about whether people use net/rpc out there, the answer is: yes. Most notably, minio, which saw a decrease in performance when evaluating gRPC. Pretty interesting.

What about you? Have you ever used it? Did you, like me, went straight to gRPC without checking net/rpc? I’d love to know!

Reach me at Twitter @cirowrc at any time.

Have a good one!

finis