profile picture

Parth Desai

Blog about Software engineering

Linkedin Github StackOverflow

© 2020. Parth Desai All rights reserved.

How to build RPC server in golang (step by step with examples)

Overview

Remote procedure call (RPC) is basically a form of inter-process communication. It is widely used in distributed computing.

In this blog post, We will build a simple RPC server step by step.

1. Define an interface and shared structs

For sake of simplicity, we will choose an interface with two methods: Multiply and Divide, which perform * and / operations respectively.

There will be only two shared structs called Args and Quotient that will be used to pass arguments from client to server and represent the output of Multiply and Divide respectively.

package shared
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
package shared
type Arith interface {
Multiply(args *Args, reply *int) error
Divide(args *Args, quo *Quotient) error
}
view raw interface.go hosted with ❤ by GitHub

2. Write implementation for the interface

Now, we will write struct which implements two methods we mentioned above: Multiply and Divide.

package main
import (
"shared" //Path to the package contains shared struct
)
// Every method that we want to export must have
// (1) the method has two arguments, both exported (or builtin) types
// (2) the method's second argument is a pointer
// (3) the method has return type error
type Arith int
func (t *Arith) Multiply(args *shared.Args, reply *int) error {
*reply = args.A * args.B
return nil
}
func (t *Arith) Divide(args *shared.Args, quo *shared.Quotient) error {
if args.B == 0 {
return errors.New("divide by zero")
}
quo.Quo = args.A / args.B
quo.Rem = args.A % args.B
return nil
}

3. Implement a RPC server

Two ways we can implement a RPC server in golang:

3.1 HTTP RPC

In this case server will be listening for incoming connection using HTTP protocol, and will switch to RPC protocol afterwards.

Benefit of this approach is, you can perform authentication of client easily, before allowing RPC, using any authentication method supported by HTTP.

package main
import (
"errors"
"log"
"net"
"net/http"
"net/rpc"
"shared" //Path to the package contains shared struct
)
func registerArith(server *rpc.Server, arith shared.Arith) {
// registers Arith interface by name of `Arithmetic`.
// If you want this name to be same as the type name, you
// can use server.Register instead.
server.RegisterName("Arithmetic", arith)
}
func main() {
//Creating an instance of struct which implement Arith interface
arith := new(Arith)
// Register a new rpc server (In most cases, you will use default server only)
// And register struct we created above by name "Arith"
// The wrapper method here ensures that only structs which implement Arith interface
// are allowed to register themselves.
server := rpc.NewServer()
registerArith(server, arith)
// registers an HTTP handler for RPC messages on rpcPath, and a debugging handler on debugPath
server.HandleHTTP("/", "/debug")
// Listen for incoming tcp packets on specified port.
l, e := net.Listen("tcp", ":1234")
if e != nil {
log.Fatal("listen error:", e)
}
// This statement starts go's http server on
// socket specified by l.
http.Serve(l, nil)
}
view raw server_http.go hosted with ❤ by GitHub

Internally, server listens for HTTP CONNECT method, and then uses http.Hijacker to hijack the connection.

3.2 TCP RPC

In this case server will be listening for incoming connection directly, instead of relying on HTTP protocol.

package main
import (
"errors"
"log"
"net"
"net/rpc"
"shared" //Path to the package contains shared struct
)
func registerArith(server *rpc.Server, arith shared.Arith) {
// registers Arith interface by name of `Arithmetic`.
// If you want this name to be same as the type name, you
// can use server.Register instead.
server.RegisterName("Arithmetic", arith)
}
func main() {
//Creating an instance of struct which implement Arith interface
arith := new(Arith)
// Register a new rpc server (In most cases, you will use default server only)
// And register struct we created above by name "Arith"
// The wrapper method here ensures that only structs which implement Arith interface
// are allowed to register themselves.
server := rpc.NewServer()
registerArith(server, arith)
// Listen for incoming tcp packets on specified port.
l, e := net.Listen("tcp", ":1234")
if e != nil {
log.Fatal("listen error:", e)
}
// This statement links rpc server to the socket, and allows rpc server to accept
// rpc request coming from that socket.
server.Accept(l)
}
view raw server.go hosted with ❤ by GitHub

4. Implement a RPC client

We need to implement RPC client, based on which way we chose to build our server.

4.1 HTTP RPC

package main
import (
"fmt"
"log"
"net/rpc"
"shared" //Path to the package contains shared struct
)
type Arith struct {
client *rpc.Client
}
func (t *Arith) Divide(a, b int) shared.Quotient {
args := &shared.Args{a, b}
var reply shared.Quotient
err := t.client.Call("Arithmetic.Divide", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
return reply
}
func (t *Arith) Multiply(a, b int) int {
args := &shared.Args{a, b}
var reply int
err := t.client.Call("Arithmetic.Multiply", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
return reply
}
func main() {
// Tries to connect to localhost:1234 using HTTP protocol (The port on which rpc server is listening)
client, err := rpc.DialHTTP("tcp", "localhost:1234")
if err != nil {
log.Fatal("dialing:", err)
}
// Create a struct, that mimics all methods provided by interface.
// It is not compulsory, we are doing it here, just to simulate a traditional method call.
arith := &Arith{client: client}
fmt.Println(arith.Multiply(5, 6))
fmt.Println(arith.Divide(500, 10))
}
view raw client_http.go hosted with ❤ by GitHub

4.2 TCP RPC

package main
import (
"fmt"
"log"
"net"
"net/rpc"
"shared" //Path to the package contains shared struct
)
type Arith struct {
client *rpc.Client
}
func (t *Arith) Divide(a, b int) shared.Quotient {
args := &shared.Args{a, b}
var reply shared.Quotient
err := t.client.Call("Arithmetic.Divide", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
return reply
}
func (t *Arith) Multiply(a, b int) int {
args := &shared.Args{a, b}
var reply int
err := t.client.Call("Arithmetic.Multiply", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
return reply
}
func main() {
// Tries to connect to localhost:1234 (The port on which rpc server is listening)
conn, err := net.Dial("tcp", "localhost:1234")
if err != nil {
log.Fatal("Connectiong:", err)
}
// Create a struct, that mimics all methods provided by interface.
// It is not compulsory, we are doing it here, just to simulate a traditional method call.
arith := &Arith{client: rpc.NewClient(conn)}
fmt.Println(arith.Multiply(5, 6))
fmt.Println(arith.Divide(500, 10))
}
view raw client.go hosted with ❤ by GitHub