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