This paper aims at describing several core problems in RPC framework design and their solutions, and constructs a simple RPC framework based on Golang reflection technology.

Project address: tiny-RPC

RPC

Remote Procedure Call (RPC), also known as Remote Procedure Call, can be understood as: service A wants to Call the function of service B in different memory space, but it cannot be called directly because it is not in the same memory space. Therefore, the semantics of the Call and the data of the Call need to be expressed through the network.

The service side

The RPC server needs to solve two problems:

  • Since the client is passing RPC function names, how does the server maintain the mapping between function names and function entities
  • How does the server invoke the corresponding function entity based on the function name

The core processes

  • Maintains the mapping of function names to functions
  • After receiving the function name and parameter list from the client, the parameter list is parsed as reflected values and the corresponding function is executed
  • Encode the result of function execution and return it to the client

Methods registration

The server needs to maintain the mapping between RPC function names and RPC function entities. We can use the Map data structure to maintain the mapping.

type Server struct {
	addr  string
	funcs map[string]reflect.Value
}

// Register a method via name
func (s *Server) Register(name string, f interface{}) {
	if _, ok := s.funcs[name]; ok {
		return
	}
	s.funcs[name] = reflect.ValueOf(f)
}
Copy the code

Execution call

Generally speaking, the client sends the function name and parameter list as request data to the server when calling RPC.

Since we use map[string] reflect.value to maintain the mapping between function names and function entities, we can Call the corresponding function with value.call ().

The code address: https://play.golang.org/p/jaPHviCbe5K

package main

import (
	"fmt"
	"reflect"
)

func main(a) {
	// Register methods
	funcs := make(map[string]reflect.Value)
	funcs["add"] = reflect.ValueOf(add)

	// When receives client's request
	req := []reflect.Value{reflect.ValueOf(1), reflect.ValueOf(2)}
	vals := funcs["add"].Call(req)
	var rsp []interface{}
	for _, val := range vals {
		rsp = append(rsp, val.Interface())
	}

	fmt.Println(rsp)
}

func add(a, b int) (int, error) {
	return a + b, nil
}
Copy the code

The specific implementation

Due to space constraints, the server implementation code is not posted here, please see the project address for details.

The client

The RPC client needs to solve one problem:

  • Since the specific implementation of a function is on the server side, the client only has the prototype of the function. How does the client call its function entity through the prototype

The core processes

  • Encode the function parameters passed by the caller and pass them to the server
  • The server response data is decoded and returned to the caller

Generate calls

We can bind a function entity to the specified function prototype with reflect.makefunc.

The code address: https://play.golang.org/p/AaedlW9U-6n

package main

import (
	"fmt"
	"reflect"
)

func main(a) {
	add := func(args []reflect.Value) []reflect.Value {
		result := args[0].Interface().(int) + args[1].Interface().(int)
		return []reflect.Value{reflect.ValueOf(result)}
	}

	var addptr func(int.int) int
	container: =reflect.ValueOf(&addptr).Elem(a)
	v: =reflect.MakeFunc(container.Type().add)
	container.Set(v)

	fmt.Println(addptr(1, 2))}Copy the code

The specific implementation

Due to space constraints, the client implementation code is not posted here, please see the project address for details.

Data transmission format

We need to define the data format in which the server interacts with the client.

type Data struct {
	Name string        // service name
	Args []interface{} // request's or response's body except error
	Err  string        // remote server error
}
Copy the code

Encoding and decoding functions corresponding to interactive data.

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

func decode(b []byte) (Data, error) {
	buf := bytes.NewBuffer(b)
	decoder := gob.NewDecoder(buf)
	var data Data
	iferr := decoder.Decode(&data); err ! =nil {
		return Data{}, err
	}
	return data, nil
}
Copy the code

At the same time, we need to define a simple TLV protocol (fixed length message header + variable length message body) to standardize data transmission.

// Transport struct
type Transport struct {
	conn net.Conn
}

// NewTransport creates a transport
func NewTransport(conn net.Conn) *Transport {
	return &Transport{conn}
}

// Send data
func (t *Transport) Send(req Data) error {
	b, err := encode(req) // Encode req into bytes
	iferr ! =nil {
		return err
	}
	buf := make([]byte.4+len(b))
	binary.BigEndian.PutUint32(buf[:4].uint32(len(b))) // Set Header field
	copy(buf[4:], b)                                    // Set Data field
	_, err = t.conn.Write(buf)
	return err
}

// Receive data
func (t *Transport) Receive(a) (Data, error) {
	header := make([]byte.4)
	_, err := io.ReadFull(t.conn, header)
	iferr ! =nil {
		return Data{}, err
	}
	dataLen := binary.BigEndian.Uint32(header) // Read Header filed
	data := make([]byte, dataLen)              // Read Data Field
	_, err = io.ReadFull(t.conn, data)
	iferr ! =nil {
		return Data{}, err
	}
	rsp, err := decode(data) // Decode rsp from bytes
	return rsp, err
}
Copy the code

The relevant data

  • Project address: tiny-RPC
  • Go RPC source code analysis