Go RPC Remote procedure call

Today we will learn Remote Procedure Call (RPC) in Go language.

In distributed computing, remote procedure call is a computer communication protocol. This protocol allows a program running on one computer to call a subroutine in another address space without the programmer having to program for this interaction as if it were a native program. RPC is a server-client pattern. The classic implementation is a system that sends requests and receives responses for information interaction.

From WikiPedia

RPC allows the client to access the functions on the server side relatively directly. The “relatively direct” here means that we do not need to write something like Web services on the server side to provide the interface, and manually encode and decode various data on both sides.

This article consists of two parts. The first part introduces net/ RPC of the Golang standard library, and the second part implements a toy PRC framework for further understanding.

Part0. net/rpc

This part mainly refers to “Go Language Advanced Programming” 4.1 GETTING Started with RPC. If not, read the original text.

Net/RPC of the Go standard library implements basic RPC. It uses a Gob coding method unique to the Go language, so both the server and the client must use Golang and cannot be invoked across languages.

For the server, NET/RPC requires an exported structure to represent the RPC service. All methods in this structure that meet specific requirements are provided to the client to access:

type T struct {}

func (t *T) MethodName(argType T1, replyType *T2) error
Copy the code
  • The structure is an export.
  • Methods are exported.
  • The method takes two arguments, both of which are exported (or built-in) types.
  • The second argument to the method is a pointer.
  • Method returns error.

The server connects to the server through RPC.Dial (for the TCP service), and then invokes the method in the RPC service using Call:

rpc.Call("T.MethodName", argType T1, replyType *T2)
Copy the code

For example, use NET/RPC to implement a Hello World.

Hello World

The service side

First build a HelloService to represent the provided service:

// server.go

// HelloService is a RPC service for helloWorld
type HelloService struct {}

// Hello say hello to request
func (p *HelloService) Hello(request string, reply *string) error {
	*reply = "Hello, " + request
	return nil
}
Copy the code

To register and start the RPC service, we can use the HTTP service:

// server.go

func main (a) {
    // Register the RPC service with the name and the HelloService instance to be accessed by the client
	rpc.RegisterName("HelloService".new(HelloService))

	/ / HTTP service
	rpc.HandleHTTP()
	err := http.ListenAndServe(": 1234".nil)
	iferr ! =nil {
		log.Fatal("Http Listen and Serve:", err)
	}
}
Copy the code

You can also use TCP services, replacing lines 8 to 12 above:

	/ / TCP services
	listener, err := net.Listen("tcp".": 1234")
	iferr ! =nil {
		log.Fatal("ListenTCP error:", err)
	}

	conn, err := listener.Accept()
	iferr ! =nil {
		log.Fatal("Accept error:", err)
	}

	rpc.ServeConn(conn)
Copy the code

Note that the server accepts only one request and automatically closes after the client requests. If you want to keep processing, you can replace the second half of the code with:

    for {
        conn, err := listener.Accept()
        iferr ! =nil {
            log.Fatal("Accept error:", err)
        }

        go rpc.ServeConn(conn)
    }
Copy the code

The client

package main

import (
	"fmt"
	"log"
	"net/rpc"
)

func main(a) {
	// HTTP
	// client, err := rpc.DialHTTP("tcp", "localhost:1234")
	
    //TCP
	client, err := rpc.Dial("tcp"."localhost:1234")
	iferr ! =nil {
		log.Fatal("dialing:", err)
	}

	var reply string
	err = client.Call("HelloService.Hello"."world", &reply)
	iferr ! =nil {
		log.Fatal(err)
	}

	fmt.Println(reply)
}
Copy the code

Start the server first:

$ go run helloworld/server/server.go
Copy the code

Call the client on another terminal and you get the result:

$ go run helloworld/client/client.go
Hello, world
Copy the code

A more canonical RPC interface

The previous code server, client registration, invoking RPC services are written dead. With all of this work in one piece, it’s pretty hard to maintain, and you need to consider refactoring the HelloService service and the client implementation.

The service side

First, abstract the service interface with an interface:

// HelloServiceName is the name of HelloService
const HelloServiceName = "HelloService"

// HelloServiceInterface is a interface for HelloService
type HelloServiceInterface interface {
	Hello(request string, reply *string) error
}

// RegisterHelloService register the RPC service on svc
func RegisterHelloService(svc HelloServiceInterface) error {
	return rpc.RegisterName(HelloServiceName, svc)
}
Copy the code

When instantiating a service, the registration uses:

RegisterHelloService(new(HelloService))
Copy the code

The remaining specific service implementations remain unchanged.

The client

On the client side, consider wrapping the RPC details into a client object HelloServiceClient:

// HelloServiceClient is a client for HelloService
type HelloServiceClient struct {
	*rpc.Client
}

var _ HelloServiceInterface = (*HelloServiceClient)(nil)

// DialHelloService dial HelloService
func DialHelloService(network, address string) (*HelloServiceClient, error) {
	c, err := rpc.Dial(network, address)
	iferr ! =nil {
		return nil , err
	}
	return &HelloServiceClient{Client: c}, nil
}

// Hello calls HelloService.Hello
func (p *HelloServiceClient) Hello(request string, reply *string) error {
	return p.Client.Call(HelloServiceName + ".Hello", request, reply)
}
Copy the code

The details of handling RPCS are not exposed when called:

client, err := DialHelloService("tcp"."localhost:1234")
iferr ! =nil {
    log.Fatal("dialing:", err)
}

var reply string
err = client.Hello("world", &reply)
iferr ! =nil {
    log.Fatal(err)
}

fmt.Println(reply)
Copy the code

The instance

Using the above, make a simple calculator RPC service. The project directory is as follows:

Calc / ├── calcrpc. Go ├── client │ ├─ client. Go ├─ server ├─ calc. Go ├─ server.goCopy the code

First write a calcrPC. go that defines a common RPC interface for the server/client:

package calc

import "net/rpc"

// ServiceName Specifies the name of the calculator service
const ServiceName = "CalcService"

// ServiceInterface calculator ServiceInterface
type ServiceInterface interface {
	// CalcTwoNumber operates on two numbers
	CalcTwoNumber(request Calc, reply *float64)  error
	// GetOperators gets all the supported operations
	GetOperators(request struct{}, reply *[]string)  error
}

// RegisterCalcService register the RPC service on svc
func RegisterCalcService(svc ServiceInterface) error {
	return rpc.RegisterName(ServiceName, svc)
}

// Calc defines the calculator object, including two operands
type Calc struct {
	Number1 float64
	Number2 float64
	Operator string
}
Copy the code

Then write the server implementation, and write a general calculator abstract implementation in calc.go:

// Simple calculator implementation

package main

import (
	"errors"
)

/* Abstract calculation function type */

// Operation is an abstraction of computation
type Operation func(Number1, Number2 float64) float64

/* Add, subtract, multiply, and divide the specific Operation implementation */

// Add is the Operation implementation of addition
func Add(Number1, Number2 float64) float64 {
	return Number1 + Number2
}

// Sub is the Operation implementation of subtraction
func Sub(Number1, Number2 float64) float64 {
	return Number1 - Number2
}

// Mul is the Operation implementation of multiplication
func Mul(Number1, Number2 float64) float64 {
	return Number1 * Number2
}

// Div is the Operation implementation of division
func Div(Number1, Number2 float64) float64 {
	return Number1 / Number2
}

/ * * / factory

// Operators register all supported operations
var Operators = map[string]Operation {
	"+": Add,
	"-": Sub,
	"*": Mul,
	"/": Div,
}

// CreateOperation Obtains the appropriate Operation function from the string operator
func CreateOperation(operator string) (Operation, error) {
	var oper Operation
	if oper, ok := Operators[operator]; ok {
		return oper, nil
	}
	return oper, errors.New("Illegal Operator")}Copy the code

Next is the implementation of the RPC service, in server.go:

package main

import (
	"gorpctest/calc"
	"net/http"
	"net/rpc"
)

/* RPC service implementation */

// CalcService is an implementation of the calculator RPC service
type CalcService struct{}

// CalcTwoNumber adds, subtracts, multiplies and divides two numbers
func (c *CalcService) CalcTwoNumber(request calc.Calc, reply *float64) error {
	oper, err := CreateOperation(request.Operator)
	iferr ! =nil {
		return err
	}
	*reply = oper(request.Number1, request.Number2)
	return nil
}

// GetOperators gets all the supported operations
func (c *CalcService) GetOperators(request struct{}, reply *[]string) error {
	opers := make([]string.0.len(Operators))
	for key := range Operators {
		opers = append(opers, key)
	}
	*reply = opers
	return nil
}

/* Run RPC service */

func main(a) {
	calc.RegisterCalcService(new(CalcService))
	rpc.HandleHTTP()
	http.ListenAndServe(": 8080".nil)}Copy the code

Then there’s the client implementation, in client.go:

package main

import (
	"gorpctest/calc"
	"log"
	"net/rpc"
)

/* Define the client implementation */

// CalcClient is a client for CalcService
type CalcClient struct {
	*rpc.Client
}

var _ calc.ServiceInterface = (*CalcClient)(nil)

// DialCalcService dial CalcService
func DialCalcService(network, address string) (*CalcClient, error) {
	c, err := rpc.DialHTTP(network, address)
	iferr ! =nil {
		return nil , err
	}
	return &CalcClient{Client: c}, nil
}

// CalcTwoNumber operates on two numbers
func (c *CalcClient) CalcTwoNumber(request calc.Calc, reply *float64)  error {
	return c.Client.Call(calc.ServiceName + ".CalcTwoNumber", request, reply)
}

// GetOperators gets all the supported operations
func (c *CalcClient) GetOperators(request struct{}, reply *[]string)  error {
	return c.Client.Call(calc.ServiceName + ".GetOperators", request, reply)
}

/* Use the client to invoke the RPC service */

func main (a) {
	client, err := DialCalcService("tcp"."localhost:8080")
	iferr ! =nil {
		log.Fatal("Err Dial Client:", err)
	}

    // Test GetOperators
	var opers []string
	err = client.GetOperators(struct{}{}, &opers)
	iferr ! =nil {
		log.Println(err)
	}
	log.Println(opers)

    // Test CalcTwoNumber
	testAdd := calc.Calc {
		Number1: 2.0,
		Number2: 3.14,
		Operator: "+",}var result float64
	client.CalcTwoNumber(testAdd, &result)
	log.Println(result)
}
Copy the code

net/rpc/jsonrpc

Net/RPC allows custom encoding and decoding of RPC data when packaged by plug-ins:

// Code of the service segment
rpc.ServeCodec(SomeServerCodec(conn)) // SomeServerCodec is an encoder

// Client decoding
conn, _ := net.Dial("tcp"."localhost:1234")
client := rpc.NewClientWithCodec(SomeClientCodec(conn)) // SomeClientCodec is a decoder
Copy the code

Net/RPC/jSONRPC is one such implementation, which uses JSON rather than Gob encoding and can be used for cross-language RPC. In real use, NET/RPC/JSONRPC internally encapsulates the above mentioned encoding and decoding implementation and provides roughly the same API as NET/RPC.

The server uses JSON RPC by changing the last line of main (not counting}) :

// Instead of `go rpc.ServeConn(conn)`
go jsonrpc.ServeConn(conn)
Copy the code

The implementation of jsonrpc.ServeConn is RPC.ServeCodec(jsonrpc.NewServerCodec(conn))

When called, change DialHelloService to connect to the service code to use:

// Instead of `c, err := rpc.Dial(network, address)`
c, err := jsonrpc.Dial(network, address)
Copy the code

It can also be used here:

conn, _ := net.Dial("tcp"."localhost:1234")
client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
Copy the code

The open service is based on TCP. We can close the server program, run NC -L 1234 to start a TCP service, and then run the client program again. The NC will output what the client requested:

$ nc -l 1234
{"method":"HelloService.Hello"."params": ["world"]."id": 0}Copy the code

You can see that the request body is JSON data. Conversely, mimicking the request body, we can manually send the mock request to the running client and see the response body:

$ echo -e '{"method":"HelloService.Hello","params":["JSON-RPC"],"id":1}' | nc localhost 1234
{"id": 1,"result":"Hello, JSON-RPC"."error":null}
Copy the code

To summarize, the structure of the request and response looks like this:

type Request struct {
    Method string           `json:"method"`
    Params *json.RawMessage `json:"params"`
    Id     *json.RawMessage `json:"id"`
}

type Response struct {
    Id     uint64           `json:"id"`
    Result *json.RawMessage `json:"result"`
    Error  interface{}      `json:"error"`
}
Copy the code

(In the actual implementation, the client and server requests and responses are defined slightly differently.)

With other languages, you can communicate with Go’s RPC services as long as you follow this request/response structure.

JSON-RPC in HTTP

The implementation is based on TCP, which is sometimes inconvenient, and we might prefer to use the familiar HTTP protocol.

Net/RPC RPC service is built on the abstract IO.ReadWriteCloser interface (CONN), so you can set up RPC on different communication protocols. Net/RPC/jsonRPC on HTTP server

func main(a) {
	RegisterHelloService(new(HelloService))

	/ / HTTP service
	http.HandleFunc("/jsonrpc".func(w http.ResponseWriter, r *http.Request) {
		var conn io.ReadWriteCloser = struct {
			io.Writer
			io.ReadCloser
		} {
			ReadCloser: r.Body,
			Writer: w,
		}

		rpc.ServeRequest(jsonrpc.NewServerCodec(conn))
	})

	http.ListenAndServe(": 1234".nil)}Copy the code

RPC services can then be easily accessed from different languages via HTTP:

curl -X POST http://localhost:1234/jsonrpc  --data '{"method":"HelloService.Hello","params":["world"],"id":0}'
{"id": 0."result":"Hello, world"."error":null}
Copy the code

However, the problem is that it is not convenient to write the client using Go. You will need to build a client implementation to complete the encoding of the request, sending and decoding of the response and binding 😂. Alternatively, you can use a jSON-RPC library.

Part1. Implementation of simple RPC

For this part, refer to the video “Learning microservice Framework from 0” in station B. P9~P14 RPC. If not, you can learn the original video.

To further understand, let’s write a simple RPC service, from custom protocol, encoding, decoding, RPC server, client implementation.

Let’s write a package RPC to implement this:

/ RPC ├── key.go ├── Key.go ├── Key.goCopy the code

Network communication

We use the following custom protocol for communication based on TCP:

Network byte stream Header Data
The size of the uint32(Fixed length: 4 bytes) []byte(Length indicated by Header)
instructions The length of Data The specific data

We implement this basic protocol using a Session structure:

// session.go PART 0

// Session is a Session connection for RPC communication
type Session struct {
	conn net.Conn
}

// NewSession Creates a Session from the network connection
func NewSession(conn net.Conn) *Session {
	return &Session{conn: conn}
}
Copy the code

Subsequent RPC communications use the Session to read and write data to the TCP connection:

// session.go PART 1

// Write Write data to the Session
func (s *Session) Write(data []byte) error {
	buf := make([]byte.4+len(data))
	// Header
	binary.BigEndian.PutUint32(buf[:4].uint32(len(data)))
	// Data
	copy(buf[4:], data)

	_, err := s.conn.Write(buf)

	return err
}

// Read Reads data from the Session
func (s *Session) Read(a) ([]byte, error) {
	// Read the Header to get the Data length information
	header := make([]byte.4)
	if_, err := io.ReadFull(s.conn, header); err ! =nil {
		return nil, err
	}
	dataLen := binary.BigEndian.Uint32(header)

	// Read Data according to dataLen
	data := make([]byte, dataLen)
	if_, err := io.ReadFull(s.conn, data); err ! =nil {
		return nil, err
	}
	return data, nil
}
Copy the code

The codec

In the process of RPC, we need to transfer the parameters and results of the function according to a certain format. We can define RPCData as follows to format the content of RPC communication:

// codec.go PART 0

// RPCData Defines the data format for RPC communication
type RPCData struct {
	Func string        // The function to access
	Args []interface{} // The parameters of the function
}
Copy the code

In the whole RPC, all network communications use Session to transmit the []byte encoded by RPCData. To encode RPCData into bytes on one end and decode the original Go data type on the other end, use encoding/gob:

// codec.go PART 1

// encode to encode RPCData
func encode(data RPCData) ([]byte, error) {
	var buf bytes.Buffer
	encoder := gob.NewEncoder(&buf)
	iferr := encoder.Encode(data); err ! =nil {
		return nil, err
	}
	return buf.Bytes(), nil
}

// decode decodes the data into RPCData
func decode(data []byte) (RPCData, error) {
	buf := bytes.NewBuffer(data)
	decoder := gob.NewDecoder(buf)

	var rpcData RPCData
	err := decoder.Decode(&rpcData)
	return rpcData, err
}
Copy the code

With a solution for network communication and a way to encode and decode, you can begin to build a server-side framework and client-side implementation of RPC services.

The service side

At its core, the RPC server maintains a mapping of a function name to a local function. Implementing this mapping and starting a network service enables the client to call the server function by giving the function name and parameters.

Here we can simply define a service as follows:

// server.go PART 0

// Server is a simple RPC service
type Server struct {
	funcs map[string]reflect.Value
}

func NewServer(a) *Server {
	return &Server{funcs: map[string]reflect.Value{}}
}
Copy the code

The reflection mechanism is used to map funS:

// server.go PART 1

// Register to Register the functions bound to RPC services
// Match the function name to the function
func (s *Server) Register(name string, function interface{}) {
	// If it already exists, skip it
	if _, ok := s.funcs[name]; ok {
		return
	}
	fVal := reflect.ValueOf(function)
	s.funcs[name] = fVal
}
Copy the code

The next step is to enable network services, listen for TCP connections, and service access:

// server.go PART 2

ListenAndServe listens for the address and runs the RPC service
func (s *Server) ListenAndServe(address string) error {
	listener, err := net.Listen("tcp", address)
	iferr ! =nil {
		return err
	}
	for {
		conn, err := listener.Accept()
		iferr ! =nil {
			log.Println("accept error:", err)
			continue
		}
		s.handleConn(conn)
	}
}
Copy the code

The connection is handled in the handleConn. Create an RPC session with the CONN, decode the request body, and get the functions and parameters that the client wants to request. Call the local function to do the work, encode the return value, and return it to the client:

// server.go PART 3

// handleConn Handles conn requests for RPC services
func (s *Server) handleConn(conn net.Conn) {
	// Create a session
	srvSession := NewSession(conn)

	// Read and decode data
	data, err := srvSession.Read()
	iferr ! =nil {
		log.Println("session read error:", err)
		return
	}
	requestRPCData, err := decode(data)
	iferr ! =nil {
		log.Println("data decode error:", err)
		return
	}

	// Get the function
	f, ok := s.funcs[requestRPCData.Func]
	if! ok { log.Printf("unexpected rpc call: function %s not exist", requestRPCData.Func)
		return
	}

	// Get the parameter
	inArgs := make([]reflect.Value, 0.len(requestRPCData.Args))
	for _, arg := range requestRPCData.Args {
		inArgs = append(inArgs, reflect.ValueOf(arg))
	}

	// Reflect the call method
	returnValues := f.Call(inArgs)

	// Construct the result
	outArgs := make([]interface{}, 0.len(returnValues))
	for _, ret := range returnValues {
		outArgs = append(outArgs, ret.Interface())
	}
	replyRPCData := RPCData{
		Func: requestRPCData.Func,
		Args: outArgs,
	}
	replyEncoded, err := encode(replyRPCData)
	iferr ! =nil {
		log.Println("reply encode error:", err)
		return
	}

	// Write the result
	err = srvSession.Write(replyEncoded)
	iferr ! =nil {
		log.Println("reply write error:", err)
	}
}
Copy the code

The client

One characteristic of RPC clients is that they call remote functions as if they were local functions. The function to be called is not implemented locally, but we want it to work like a local function. Reflection provides this “fool yourself” feature.

First, we write out the client structure, which is basically a wrapper for a network connection:

// client.go PART 0

// Client is the Client of RPC
type Client struct {
	conn net.Conn
}

func NewClient(conn net.Conn) *Client {
	return &Client{conn: conn}
}
Copy the code

Then implement a Call method to bring the original function locally via RPC:

// client.go PART 1

func (c *Client) Call(name string, funcPtr interface{}) {
	// Reflection initializes the funcPtr prototype
	fn := reflect.ValueOf(funcPtr).Elem()
    
    // RPC calls remote functions
	f := func(args []reflect.Value) []reflect.Value {
		/ / parameters
		inArgs := make([]interface{}, 0.len(args))
		for _, arg := range args {
			inArgs = append(inArgs, arg.Interface())
		}
        
		// Connect the service
		cliSession := NewSession(c.conn)

		/ / request
		requestRPCData := RPCData{
			Func: name,
			Args: inArgs,
		}
		requestEncoded, err := encode(requestRPCData)
		iferr ! =nil {
			panic(err)
		}
		iferr := cliSession.Write(requestEncoded); err ! =nil {
			panic(err)
		}

		/ / response
		response, err := cliSession.Read()
		iferr ! =nil {
			panic(err)
		}
		respRPCData, err := decode(response)
		iferr ! =nil {
			panic(err)
		}
		outArgs := make([]reflect.Value, 0.len(respRPCData.Args))
		for i, arg := range respRPCData.Args {
			if arg == nil {
				outArgs = append(outArgs, reflect.Zero(fn.Type().Out(i)))
			} else {
				outArgs = append(outArgs, reflect.ValueOf(arg))
			}
		}
        
        // Return the value of the remote function
		return outArgs
	}

	// Assign the RPC call function to fn
	v := reflect.MakeFunc(fn.Type(), f)
	fn.Set(v)
}
Copy the code

This function takes two arguments, name being the name of the function provided by the RPC server, and funcPtr being the prototype of the function to be called. The result of this function run is to “assign” funcPtr a “function that encapsulates a remote function called by RPC,” turning funcPtr from an empty prototype with its tables into a real callable function, calling it equivalent to calling the corresponding function on the server side through RPC.

For example, we implement and register a function on the server:

func queryUser(uid int) (User, error){...// queryUser implementation
}
Copy the code

On the client side, we can use a queryUser function prototype to capture its capabilities:

var query func(int) (User, error) // queryqueryUserThe prototype of the
client.Call("queryUser", &query) // "get" the remote queryUser function
u, err := query(1) // Use the remote function as if it were a local function
Copy the code

If you are not familiar with reflection and have trouble understanding the code implementation, this can be a bit confusing. Let’s look at an example of a specific call:

// rpc_test.go
package rpc

import (
	"encoding/gob"
	"fmt"
	"net"
	"testing"
)

// User User structure for testing
type User struct {
	Name string
	Age  int
}

// queryUser simulates the method of querying a user
func queryUser(uid int) (User, error) {
	// Fake data
	user := make(map[int]User)
	user[0] = User{Name: "Foo", Age: 12}
	user[1] = User{Name: "Bar", Age: 13}
	user[2] = User{Name: "Joe", Age: 14}

	// Fake query
	if u, ok := user[uid]; ok {
		return u, nil
	}
	return User{}, fmt.Errorf("user wiht id %d is not exist", uid)
}

func TestRPC(t *testing.T) {
	gob.Register(User{}) // The goB code must be registered to code the structure

	addr := ": 8080"

	/ / the server
	srv := NewServer()
	srv.Register("queryUser", queryUser)
	go srv.ListenAndServe(addr)

	/ / the client
	conn, err := net.Dial("tcp", addr)
	iferr ! =nil {
		t.Error(err)
	}
	cli := NewClient(conn)
	var query func(int) (User, error)
	cli.Call("queryUser", &query)

	u, err := query(1)
	iferr ! =nil {
		t.Error(err)
	}
	fmt.Println(u)
}
Copy the code

TestRPC simulates the invocation of RPC services by both the server and the client.

At this point, a complete toy version of RPC is complete, it is interesting to write this thing yourself. The complete code I put in this Gist CDFMLR /toy-rpc-golang:

  • Gist.github.com/cdfmlr/a1e2…

By("CDFMLR"."2020-09-12")
/ / See you 💪
Copy the code