How to build RPC server in golang (step by step with examples)
20 May 2016Overview
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 Arith interface { | |
Multiply(args *Args, reply *int) error | |
Divide(args *Args, quo *Quotient) error | |
} |
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) | |
} |
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) | |
} |
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)) | |
} |
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)) | |
} |