A Remote Procedure Call (RPC) is a subroutine in distributed computing. The remote implementation of RPC is similar to local calls but usually not identical. RPC usually requires that the object name, function name, or parameter are passed to remote servers, and the servers then return the processed result(s) back to client-side (request-response). RPC can be communicated through TCP, UDP, or HTTP protocols.
There are three types of implementation in Golang, namely:
net/rpc
net/rpc/jsonrpc
gRPC
The Golang official documentation uses encoding/gob
in the net/rpc
package as encoding or decoding methods, supporting TCP or HTTP protocols. However, because gob
encoding is only used in Golang, it only supports servers and client-side interactions written in Golang.
Example of server-side net/rpc
:
package mainimport ( "fmt" "log" "net" "net/rpc")type Listener int
type Reply struct { Data string}func (l *Listener) GetLine(line []byte, reply *Reply) error { rv := string(line) fmt.Printf("Receive: %v\n", rv) *reply = Reply{rv} return nil}func main() { addy, err := net.ResolveTCPAddr("tcp", "0.0.0.0:12345") if err != nil { log.Fatal(err) } inbound, err := net.ListenTCP("tcp", addy) if err != nil { log.Fatal(err) } listener := new(Listener) rpc.Register(listener) rpc.Accept(inbound)}
In this example, we noticed that the GetLine
function is added to Listener
. This function will return an error
type, while expecting a content line and reply from client-side. It also must be a pointer, so a Reply
struct is declared to store the corresponding Data
.
In the main function, first we use net.ResolveTCPAddr
and net.ListenTCP
to establish a TCP connection, listening to 12345 port from all addresses. Lastly, we use rpc.Register
to register the connection to be listened to, accepting all requests from the abovesaid TCP connections.
Example of client-side net/rpc
:
package mainimport ( "bufio" "log" "net/rpc" "os")type Reply struct { Data string}func main() { client, err := rpc.Dial("tcp", "localhost:12345") if err != nil { log.Fatal(err) }
in := bufio.NewReader(os.Stdin) for { line, _, err := in.ReadLine() if err != nil { log.Fatal(err) } var reply Reply err = client.Call("Listener.GetLine", line, &reply) if err != nil { log.Fatal(err) } log.Printf("Reply: %v, Data: %v", reply, reply.Data) }}
Client-side will use rpc.Dial
to establish a connection to the server and ports, and it’s an infinite for
loop with ReadLine
function that accepts input from receiving ports. If there are any breaks in the line in between, this will trigger client.Call
and start the GetLine
function. With this process, reply
will be stored in the database, and we can call it out with reply.Data
(basically, this means what we input is what we get in output). Let’s try to run the code:
❯ go run simple_server.go
Receive: hi
Receive: haha❯ go run simple_client.go
hi
2019/12/05 18:19:14 Reply: {hi}, Data: hi
haha
2019/12/05 18:19:15 Reply: {haha}, Data: haha
net/rpc
only supports Golang, so Go library uses net/rpc/jsonrpc
to support RPC in cross-language platforms. To implement the same application as above, we just need to change rpc.Accept
in the main()
function.
Example of server-side net/rpc/jsonrpc
:
import "net/rpc/jsonrpc"func main() { addy, err := net.ResolveTCPAddr("tcp", "0.0.0.0:12345") if err != nil { log.Fatal(err) } inbound, err := net.ListenTCP("tcp", addy) if err != nil { log.Fatal(err) } listener := new(Listener) rpc.Register(listener) for { conn, err := inbound.Accept() if err != nil { continue } jsonrpc.ServeConn(conn) }}
Example of client-side net/rpc/jsonrpc
:
func main() { client, err := jsonrpc.Dial("tcp", "localhost:12345") //Only change this if err != nil { log.Fatal(err) } in := bufio.NewReader(os.Stdin) for { line, _, err := in.ReadLine() if err != nil { log.Fatal(err) } var reply Reply err = client.Call("Listener.GetLine", line, &reply) if err != nil { log.Fatal(err) } log.Printf("Reply: %v, Data: %v", reply, reply.Data) }}
json-rpc
is based on the TCP protocol and currently doesn’t support the HTTP method yet. The results will be the same as in the previous example:
❯ go run simple_server.go
Receive: hi
Receive: haha❯ go run simple_client.go
hi
2019/12/05 20:20:19 Reply: {hi}, Data: hi
haha
2019/12/05 20:20:20 Reply: {haha}, Data: haha
The JSON object in the request has two corresponding structs: clientRequest
and serverRequest
.
type serverRequest struct { Method string `json:"method"` Params *json.RawMessage `json:"params"` Id *json.RawMessage `json:"id"`}type clientRequest struct { Method string `json:"method"` Params [1]interface{} `json:"params"` Id uint64 `json:"id"`}
We may use this struct in other programming languages to send a message. Let’s try it in the command line:
❯ echo -n "hihi" |base64 # Parameters must be base64 encodedaGloaQ==~/strconv.code/rpc master*❯ echo -e '{"method": "Listener.GetLine","params": ["aGloaQ=="], "id": 0}' | nc localhost 12345{"id":0,"result":{"Data":"hihi"},"error":null}
The fact that jsonRPC
can support other languages but not the HTTP method limits its application in real life. Therefore, for a production environment, we normally use alternatives like Thrift
or gRPC
to overcome this.
gRPC is a high-performance, widely used open-source RPC framework by Google. It’s mainly designed for concurrency in modern applications based on HTTP/2 standard protocol. It was developed in a Protobuf serialised protocol and supports popular languages such as Python, Golang, and Java.
Protobuf is the abbreviation of Protocol Buffers, which is Google’s language-neutral, platform-neutral, extensible mechanism for serialising structured data, similar to XML or the JSON format. It is light-weight and fast, very suitable for storing data or exchanging data in an RPC network.
First, install Protobuf:
❯ brew install protobuf❯ protoc --versionlibprotoc 3.7.1go get -u github.com/golang/protobuf/{proto,protoc-gen-go}
Then, write sample data based on proto3:
syntax = "proto3";package simple;// Requestmessage SimpleRequest {string data = 1;}// Responsemessage SimpleResponse {string data = 1;}// rpc methodservice Simple { rpc GetLine (SimpleRequest) returns (SimpleResponse);}
request
and response
in the above example only have one data
string. The Simple
service only has one GetLine
method with SimpleRequest
as input, and it returns SimpleResponse
. Let’s try it out:
❯ mkdir src/simple❯ protoc --go_out=plugins=grpc:src/simple simple.proto❯ ll src/simpletotal 8.0K-rw-r--r-- 1 xiaoxi staff 7.0K Dec 05 21:43 simple.pb.go
This successfully creates a simple.pb.go
file under src/simple
to support gRPC.
First, install gRPC:
❯ go get -u google.golang.org/grpc
Then, import src/simple
into code:
package mainimport ( "fmt" "log" "net" pb "./src/simple" "golang.org/x/net/context" "google.golang.org/grpc")type Listener intfunc (l *Listener) GetLine(ctx context.Context, in *pb.SimpleRequest) (*pb.SimpleResponse, error) { rv := in.Data fmt.Printf("Receive: %v\n", rv) return &pb.SimpleResponse{Data: rv}, nil}func main() { addy, err := net.ResolveTCPAddr("tcp", "0.0.0.0:12345") if err != nil { log.Fatal(err) } inbound, err := net.ListenTCP("tcp", addy) if err != nil { log.Fatal(err) } s := grpc.NewServer() listener := new(Listener) pb.RegisterSimpleServer(s, listener) s.Serve(inbound)}
We noticed that pb "./src/simple"
is imported as a package and renamed as pb
.
The first parameter for GetLine
function is context.Context
. The second param is *pb.Simple-Request
(with the request defined in the .proto
file). This function will return (*pb.SimpleResponse, error)
, where pb.SimpleResponse
corresponds to the definition in the .proto
file. On the other hand, despite SimpleRequest
and SimpleResponse
being in camel case in the .proto
file, they need to be in capitals when being used.
Client-side:
package mainimport ( "bufio" "log" "os" pb "./src/simple" "golang.org/x/net/context" "google.golang.org/grpc")func main() { conn, err := grpc.Dial("localhost:12345", grpc.WithInsecure()) if err != nil { log.Fatal(err) } c := pb.NewSimpleClient(conn) in := bufio.NewReader(os.Stdin) for { line, _, err := in.ReadLine() if err != nil { log.Fatal(err) } reply, err := c.GetLine(context.Background(), &pb.SimpleRequest{Data: string(line)}) if err != nil { log.Fatal(err) } log.Printf("Reply: %v, Data: %v", reply, reply.Data) }}
First, establish a connection using grpc.Dial("localhost:12345", rpc.WithInsecure())
. Then use pb.NewSimpleClient
to create a new simpleClient
instance, with the format of XXXClient
. (XXX was defined in the .proto
file previously, for the simple
in service Simple
).
Use the following command to use RPC:
reply, err := c.GetLine(context.Background(), &pb.SimpleRequest{Data: string(line)})
GetLine
is defined in the .proto
file ( rpc GetLine(SimpleRequest) returns (SimpleResponse)
). The first parameter is context.Background()
. The second param is request
. Because the line is in[]byte
type, it needs to be converted into string
. The response reply
is an instance of SimpleReponse
and can be obtained from reply.Data
:
❯ go run grpc_server.go
Receive: hi
Receive: Haha
Receive: vvv❯ go run grpc_client.go
hi2019/12/06 07:57:48 Reply: data:"hi" , Data: hi
Haha
2019/12/06 07:57:51 Reply: data:"Haha" , Data: Haha
vvv
2019/12/06 07:57:53 Reply: data:"vvv" , Data: vvv
In this piece, we have explained RPC (Remote Procedure Call) and three types of implementation in Golang. Also, we covered the example of codes for net/rpc
, net/jsonrpc
, and grpc
. Happy coding!