Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases now! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Building Microservices with Go

You're reading from   Building Microservices with Go Develop seamless, efficient, and robust microservices with Go

Arrow left icon
Product type Paperback
Published in Jul 2017
Publisher
ISBN-13 9781786468666
Length 358 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Nic Jackson Nic Jackson
Author Profile Icon Nic Jackson
Nic Jackson
Arrow right icon
View More author details
Toc

RPC in the Go standard library

As expected, the Go standard library has fantastic support for RPC right out-of-the-box. Let's look at a few examples of how we can use this.

Simple RPC example

In this simple example, we will see how we can use the standard RPC package to create a client and server that use a shared interface to communicate over RPC. We will follow the typical Hello World example that we ran through when learning the net/http package and see just how easy it is to build an RPC-based API in go:

rpc/server/server.go:

34 type HelloWorldHandler struct{} 
35
36 func (h *HelloWorldHandler) HelloWorld(args *contract.HelloWorldRequest, reply *contract.HelloWorldResponse) error {
37 reply.Message = "Hello " + args.Name
38 return nil
39 }

Like our example on creating REST APIs using the standard library for RPC, we will also define a handler. The difference between this handler and http.Handler is that it does not need to conform to an interface; as long as we have a struct field with methods on it we can register this with the RPC server:

func Register(rcvr interface{}) error 

The Register function, which is in the rpc package, publishes the methods that are part of the given interface to the default server and allows them to be called by clients connecting to the service. The name of the method uses the name of the concrete type, so in our instance if my client wanted to call the HelloWorld method, we would access it using HelloWorldHandler.HelloWorld. If we do not wish to use the concrete types name, we can register it with a different name using the RegisterName function, which uses the provided name instead:

func RegisterName(name string, rcvr interface{}) error 

This would enable me to keep the name of the struct field to whatever is meaningful to my code; however, for my client contract I might decide to use something different such as Greet:

19 func StartServer() { 
20 helloWorld := &HelloWorldHandler{}
21 rpc.Register(helloWorld)
22
23 l, err := net.Listen("("tcp", fmt.Sprintf(":%(":%v", port))
24 if err != nil {
25 log.Fatal(fmt.Sprintf("("Unable to listen on given port: %s", err))
26 }
27
28 for {
29 conn, _ := l.Accept()
30 go rpc.ServeConn(conn)
31 }
32 }

In the StartServer function, we first create a new instance of our handler and then we register this with the default RPC server.

Unlike the convenience of net/http where we can just create a server with ListenAndServe, when we are using RPC we need to do a little more manual work. In line 23, we are creating a socket using the given protocol and binding it to the IP address and port. This gives us the capability to specifically select the protocol we would like to use for the server, tcp, tcp4, tcp6, unix, or unixpacket:

func Listen(net, laddr string) (Listener, error) 

The Listen() function returns an instance that implements the Listener interface:

type Listener interface { 
// Accept waits for and returns the next connection to the listener.
Accept() (Conn, error)

// Close closes the listener.
// Any blocked Accept operations will be unblocked and return errors.
Close() error

// Addr returns the listener's network address.
Addr() Addr
}

To receive connections, we must call the Accept method on the listener. If you look at line 29, you will see that we have an endless for loop, this is because unlike ListenAndServe which blocks for all connections, with an RPC server we handle each connection individually and as soon as we deal with the first connection we need to continue to again call Accept to handle subsequent connections or the application would exit. Accept is a blocking method so if there are no clients currently attempting to connect to the service then Accept will block until one does. Once we receive a connection then we need to call the Accept method again to process the next connection. If you look at line 30 in our example code, you will see we are calling the ServeConn method:

func ServeConn(conn io.ReadWriteCloser) 

The ServeConn method runs the DefaultServer method on the given connection, and will block until the client completes. In our example, we are using the go statement before running the server so that we can immediately process the next waiting connection without blocking for the first client to close its connection.

In terms of communication protocol, ServeConn uses the gob wire format https://golang.org/pkg/encoding/gob/, we will see when we look at JSON-RPC how we can use a different encoding.

The gob format was specifically designed to facilitate Go to Go-based communication and was designed around the idea of something easier to use and possibly more efficient than the likes of protocol buffers, this comes at a cost of cross language communication.

With gobs, the source and destination values and types do not need to correspond exactly, when you send struct, if a field is in the source but not in the receiving struct, then the decoder will ignore this field and the processing will continue without error. If a field is present in the destination that is not in the source, then again the decoder will ignore this field and will successfully process the rest of the message. Whilst this seems like a minor benefit, it is a huge advancement over the RPC messages of old such as JMI where the exact same interface must be present on both the client and server. This level of inflexibility with JMI introduced tight coupling between the two code bases and caused no end of complexity when it was required to deploy an update to our application.

To make a request to our client we can no longer simply use curl as we are no longer are using the HTTP protocol and the message format is no longer JSON. If we look at the example in rpc/client/client.go we can see how to implement a connecting client:

13 func CreateClient() *rpc.Client {
14 client, err := rpc.Dial("tcp", fmt.Sprintf("localhost:%v", port))
15 if err != nil {
16 log.Fatal("dialing:", err)
17 }
18
19 return client
20 }

 

The previous block shows how we need to setup rpc.Client, the first thing we need to do on line 14 is to create the client itself using the Dial() function in the rpc package:

func Dial(network, address string) (*Client, error)

We then use this returned connection to make a request to the server:

22 func PerformRequest(client *rpc.Client) 
contract.HelloWorldResponse {
23 args := &contract.HelloWorldRequest{Name: "World"}
24 var reply contract.HelloWorldResponse
25
26 err := client.Call("HelloWorldHandler.HelloWorld", args, &reply)
27 if err != nil {
28 log.Fatal("error:", err)
29 }
30
31 return reply
32 }

 

In line 26, we are using the Call() method on the client to invoke the named function on the server:

func (client *Client) Call(serviceMethod string, args interface{}, reply interface{}) error

Call is a blocking function which waits until the server sends a reply writing the response assuming there is no error to the reference of our HelloWorldResponse passed to the method and if an error occurs when processing the request this is returned and can be handled accordingly.

RPC over HTTP

In the instance that you need to use HTTP as your transport protocol then the rpc package can facilitate this by calling the HandleHTTP method.

The HandleHTTP method sets up two endpoints in your application:

const ( 
// Defaults used by HandleHTTP
DefaultRPCPath = "/_goRPC_"
DefaultDebugPath = "/debug/rpc"
)

If you point your browser at the DefaultDebugPath you can see details for the registered endpoints, there are two things to note:

  • This does not mean you can communicate easily with your API from a web browser. The messages are still gob encoded so you would need to write a gob encoder and decoder in JavaScript, which I am not actually sure is possible. It was certainly never the intent of the package to support this capability and therefore I would not advise this action, a JSON or JSON-RPC based message is much better suited to this use case.
  • The debug endpoint is not going to provide you with auto-generated documentation for your API. The output is fairly basic and the intention seems to be so you can track the number of calls made to an endpoint.

All that said there may be a reason why you need to use HTTP, possibly your network does not allow any other protocol or potentially you have a load balancer that is not capable of dealing with pure TCP connections. We can also take advantage of HTTP headers and other metadata which is not available using a pure TCP request.

rpc_http/server/server.go

22 func StartServer() { 
23 helloWorld := &HelloWorldHandler{}
24 rpc.Register(helloWorld)
25 rpc.HandleHTTP()
26
27 l, err := net.Listen("tcp", fmt.Sprintf(":%v", port))
28 if err != nil {
29 log.Fatal(fmt.Sprintf("Unable to listen on given port: %s", err))
30 }
31
32 log.Printf("Server starting on port %v\n", port)
33
34 http.Serve(l, nil)
35 }

If we look at line 25, in the preceding example, we can see we are calling the rpc.HandleHTTP method, this is a requirement using HTTP with RPC as it will register the HTTP handlers we mentioned earlier with the DefaultServer method. We then call the http.Serve method and pass it the listener we are creating in line 27, we are setting the second parameter to be nil as we wish to use the DefaultServer method. This is exactly the same method that we looked at in the previous examples when we were looking at RESTful endpoints.

JSON-RPC over HTTP

In this last example, we will look at the net/rpc/jsonrpc package that provides a built-in codec for serializing and deserializing to the JSON-RPC standard. We will also look at how we can send these responses over HTTP, whilst you may ask why not just use REST, and to some extent I will agree with you, it is an interesting example to be able to see how we can extend the standard framework.

The StartServer method contains nothing we have not seen before it is the standard rpc server setup, the main difference is line 42 where instead of starting the RPC server we are starting an http server and passing the listener to it along with a handler:

rpc_http_json/server/server.go

33 func StartServer() { 
34 helloWorld := new(HelloWorldHandler)
35 rpc.Register(helloWorld)
36
37 l, err := net.Listen("tcp", fmt.Sprintf(":%v", port))
38 if err != nil {
39 log.Fatal(fmt.Sprintf("Unable to listen on given port: %s", err))
40 }
41
42 http.Serve(l, http.HandlerFunc(httpHandler))
43 }

The handler we are passing to the server is where the magic happens:

45 func httpHandler(w http.ResponseWriter, r *http.Request) { 
46 serverCodec := jsonrpc.NewServerCodec(&HttpConn{in: r.Body, out: w})
47 err := rpc.ServeRequest(serverCodec)
48 if err != nil {
49 log.Printf("Error while serving JSON request: %v", err)
50 http.Error(w, "Error while serving JSON request, details have been logged.", 500)
51 return
52 }
53 }

In line 46, we are calling the jsonrpc.NewServerCodec function and passing to it a type that implements io.ReadWriteCloser. The NewServerCodec method returns a type that implements rpc.ClientCodec, which has the following methods:

type ClientCodec interface { 
// WriteRequest must be safe for concurrent use by multiple goroutines.
WriteRequest(*Request, interface{}) error
ReadResponseHeader(*Response) error
ReadResponseBody(interface{}) error

Close() error
}

A ClientCodec type implements the writing of RPC request and reading RPC responses. To write a request to the connection a client calls the WriteRequest method. To read the response, the client must call ReadResponseHeader and ReadResponseBody as a pair. Once the body has been read, it is the client's responsibility to call the Close method to close the connection. If a nil interface is passed to ReadResponseBody then the body of the response should be read and then discarded:

17 type HttpConn struct { 
18 in io.Reader
19 out io.Writer
20 }
21
22 func (c *HttpConn) Read(p []byte) (n int, err error) { return c.in.Read(p) }
23 func (c *HttpConn) Write(d []byte) (n int, err error) { return c.out.Write(d) }
24 func (c *HttpConn) Close() error { return nil }

The NewServerCodec method requires that we pass it a type that implements the ReadWriteCloser interface. As we do not have such a type passed to us as parameters in the httpHandler method we have defined our own type, HttpConn, which encapsulates the http.Request body, which implements io.Reader, and the ResponseWriter method, that implements io.Writer. We can then write our own methods that proxy the calls to the reader and writer creating a type that has the correct interface.

And that is it for our short intro to RPC with the standard libraries; we will see when we look at some frameworks more in depth in Chapter 3, Introducing Docker, how these can be used to build a production microservice.

You have been reading a chapter from
Building Microservices with Go
Published in: Jul 2017
Publisher:
ISBN-13: 9781786468666
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime