RPC principle and Go RPC detailed explanation

RPC principle and Go RPC

What is RPC

RPC (Remote Procedure Call), that is, remote procedure call. It allows calling remote services as if they were local services.

RPC is a server-client (Client/Server) model, and the classic implementation is a system that exchanges information by sending requests and receiving responses.

The first counterpart to RPC (Remote Procedure Call) is the local call.

local calls

package main

import "fmt"

func add(x, y int) int {
    
    
	z := x + y
	return z
}

func main() {
    
    
	// 调用本地函数add
	a := 10
	b := 20
	ret := add(a, b)
	fmt.Println(ret)
}

Compile the above program into a binary file— app1and then run it, and the result 30 will be output.

app1The execution process of calling a function locally in a program can addbe understood as the following four steps.

  1. Push the values ​​of a and b onto the stack
  2. Find the add function through the function pointer, enter the function to take out the values ​​10 and 20 in the stack, and assign them to x and y
  3. Calculate x*y and store the result in z
  4. Push the value of z onto the stack and return from the add function
  5. Take the z return value from the stack and assign it to ret

RPC call

Local procedure calls happen in the same process— addthe code that defines the function and addthe code that calls the function share the same memory space, so the call executes normally.

But we can't directly in another program - app2中调用add `function, because they are two programs - the memory space is isolated from each other. (app1 and app2 may be deployed on the same server or on different servers on the Internet.)

RPC is to solve function/method calls like remote, cross-memory space, and so on. To realize RPC, the following three problems need to be solved.

  1. How to determine which function to execute? In the local call, the function body is specified by the function pointer function, and then the add function is called, and the compiler automatically determines the location of the add function in the memory through the function pointer function. But in RPC, calls cannot be done through function pointers, because their memory addresses may be completely different. Therefore, both the caller and the callee need to maintain a { ​​function <-> ID } mapping table to ensure that the correct function is called.
  2. How to express parameters? The parameters passed in the local procedure call are implemented through the stack memory structure, but RPC cannot directly use memory to pass parameters, so the parameters or return values ​​need to be serialized and converted into byte streams during transmission, and vice versa.
  3. How to carry out network transmission? The caller and callee of a function are usually connected through a network, that is, the function ID and the serialized byte stream need to be transmitted over the network, so as long as the transmission can be completed, the caller and the callee are not subject to a certain Network protocol limitations. For example, some RPC frameworks use the TCP protocol and some use HTTP.

In the past, when implementing cross-service calls, we used the RESTful API method. The called party will provide an HTTP interface to the outside world. The caller initiates an HTTP request as required and receives the response data returned by the API interface. The following example wraps adda function into a RESTful API.

HTTP call RESTful API

First, we write an HTTP-based server service, which will receive HTTP requests from other programs, execute specific programs and return the results.

// server/main.go

package main

import (
	"encoding/json"
	"log"
	"net/http"
)

type addParam struct {
    
    
	X int `json:"x"`
	Y int `json:"y"`
}

type addResult struct {
    
    
	Code int `json:"code"`
	Data int `json:"data"`
}

func add(x, y int) int {
    
    
	return x + y
}

func addHandler(w http.ResponseWriter, r *http.Request) {
    
    
	// Check for the HTTP method to be POST
	if r.Method != http.MethodPost {
    
    
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	// Parse the request body
	var param addParam
	err := json.NewDecoder(r.Body).Decode(&param)
	if err != nil {
    
    
		http.Error(w, "Invalid request body", http.StatusBadRequest)
		return
	}

	// Perform the business logic
	ret := add(param.X, param.Y)

	// Return the response
	resp := addResult{
    
    Code: 0, Data: ret}
	w.Header().Set("Content-Type", "application/json")
	err = json.NewEncoder(w).Encode(resp)
	if err != nil {
    
    
		log.Println("Error encoding response:", err)
	}
}

func main() {
    
    
	http.HandleFunc("/add", addHandler)
	log.Fatal(http.ListenAndServe(":9090", nil))
}

We write a client to request the above HTTP service, pass two integers x and y, and wait for the result to be returned.

// client/main.go

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

type Param struct {
    
    
	X int `json:"x"`
	Y int `json:"y"`
}

type Result struct {
    
    
	Code int `json:"code"`
	Data int `json:"data"`
}

func main() {
    
    
	// 通过HTTP请求调用其他服务器上的add服务
	url := "http://127.0.0.1:9090/add"
	param := Param{
    
    
		X: 10,
		Y: 20,
	}

	paramBytes, err := json.Marshal(param)
	if err != nil {
    
    
		fmt.Println("Error marshalling request body:", err)
		return
	}

	resp, err := http.Post(url, "application/json", bytes.NewReader(paramBytes))
	if err != nil {
    
    
		fmt.Println("Error making HTTP POST request:", err)
		return
	}
	defer resp.Body.Close()

	respBytes, err := io.ReadAll(resp.Body)
	if err != nil {
    
    
		fmt.Println("Error reading response body:", err)
		return
	}

	var respData Result
	err = json.Unmarshal(respBytes, &respData)
	if err != nil {
    
    
		fmt.Println("Error unmarshalling response body:", err)
		return
	}

	fmt.Println(respData.Data) // 30
}

This mode is a common cross-service or cross-language service call mode based on RESTful API. Since the use of API calls can also achieve the purpose of similar remote calls, why use RPC?

The purpose of using RPC is to allow us to call remote methods as indiscriminately as calling local methods. And based on the RESTful API is usually based on the HTTP protocol, data transmission using JSON and other text protocols, compared to RPC directly using the TCP protocol, data transmission mostly using binary protocols, RPC usually has better performance than RESTful API.

RESTful APIs are mostly used for data transmission between front and back ends, and RPC calls are mostly used between microservices under the current microservice architecture.

net/rpc

Basic RPC Example

The rpc package of the Go language provides access to object methods exported over the network or other I/O connections. The server registers an object and makes it visible as a service (the service name is the type name). Once registered, the object's exported methods will support remote access. A server can register multiple objects (services) of different types, but does not support registering multiple objects of the same type.

In the code below we define a ServiceAtype and define an exportable Addmethod for it. And ServiceAregister the type as a service, and its Add method supports RPC calls.

// rpc demo/service.go

package main

type Args struct {
    
    
	X, Y int
}

// ServiceA 自定义一个结构体类型
type ServiceA struct{
    
    }

// Add 为ServiceA类型增加一个可导出的Add方法
func (s *ServiceA) Add(args *Args, reply *int) error {
    
    
	*reply = args.X + args.Y
	return nil
}

func main() {
    
    
	service := new(ServiceA)
	rpc.Register(service) // 注册RPC服务
	rpc.HandleHTTP()      // 基于HTTP协议
	l, e := net.Listen("tcp", ":9091")
	if e != nil {
    
    
		log.Fatal("listen error:", e)
	}
	http.Serve(l, nil)
}

At this point, the client side can see a "ServiceA" service with the "Add" method. To call this service, you need to use the following code to connect to the server side first and then execute the remote call.

// rpc demo/client.go

package main

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

type ClientArgs struct {
    
    
	X, Y int
}

func main() {
    
    
	// 建立HTTP连接
	client, err := rpc.DialHTTP("tcp", "127.0.0.1:9091")
	if err != nil {
    
    
		log.Fatal("dialing:", err)
	}

	// 同步调用
	args := &ClientArgs{
    
    10, 20}
	var reply int
	err = client.Call("ServiceA.Add", args, &reply)
	if err != nil {
    
    
		log.Fatal("ServiceA.Add error:", err)
	}
	fmt.Printf("ServiceA.Add: %d+%d=%d\n", args.X, args.Y, reply)

	// 异步调用
	var reply2 int
	divCall := client.Go("ServiceA.Add", args, &reply2, nil)
	replyCall := <-divCall.Done // 接收调用结果
	fmt.Println(replyCall.Error)
	fmt.Println(reply2)
}

a. Synchronous call:

  • client.Call("ServiceA.Add", args, &reply): This line of code represents clienta synchronous call to a remote method named "ServiceA.Add" using the connection object, passing as argsa parameter, and storing the result in reply.
  • If there is an error in the call, log.Fatalthe error message is output by.

b. Asynchronous call:

  • client.Go("ServiceA.Add", args, &reply2, nil): This line of code represents clientan asynchronous call to a remote method named "ServiceA.Add" using the connection object, passing as argsa parameter, and storing the result in reply2. The Go method is used here, which immediately returns an rpc.Callobject representing the state of the asynchronous call.
  • <-divCall.Done: By using <-the operator, we wait for the completion of the asynchronous call, here divCall.Doneis a channel, which will receive a notification when the asynchronous call is completed.
  • replyCall.Error: Get the error message (if any) of the result of the asynchronous call.
  • reply2: Get the return value of the asynchronous call.

Execute the above two programs to see the result of the RPC call.

You will see the following output.

ServiceA.Add: 10+20=30
<nil>
30

RPC based on TCP protocol

Of course, the rpc package also supports direct use of the TCP protocol instead of the HTTP protocol.

The server-side code is modified as follows.

// rpc demo/service.go

package main

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

type Args struct {
    
    
	X, Y int
}

// ServiceA 自定义一个结构体类型
type ServiceA struct{
    
    }

// Add 为ServiceA类型增加一个可导出的Add方法
func (s *ServiceA) Add(args *Args, reply *int) error {
    
    
	*reply = args.X + args.Y
	return nil
}

func main() {
    
    
	service := new(ServiceA)
	rpc.Register(service) // 注册RPC服务
	l, e := net.Listen("tcp", ":9091")
	if e != nil {
    
    
		log.Fatal("listen error:", e)
	}
	for {
    
    
		conn, _ := l.Accept()
		rpc.ServeConn(conn)
	}
}

The client code is modified as follows.

// rpc demo/client.go

package main

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

type ClientArgs struct {
    
    
	X, Y int
}

func main() {
    
    
	// 建立TCP连接
	client, err := rpc.Dial("tcp", "127.0.0.1:9091")
	if err != nil {
    
    
		log.Fatal("dialing:", err)
	}

	// 同步调用
	args := &ClientArgs{
    
    10, 20}
	var reply int
	err = client.Call("ServiceA.Add", args, &reply)
	if err != nil {
    
    
		log.Fatal("ServiceA.Add error:", err)
	}
	fmt.Printf("ServiceA.Add: %d+%d=%d\n", args.X, args.Y, reply)

	// 异步调用
	var reply2 int
	divCall := client.Go("ServiceA.Add", args, &reply2, nil)
	replyCall := <-divCall.Done // 接收调用结果
	fmt.Println(replyCall.Error)
	fmt.Println(reply2)
}

RPC using the JSON protocol

By default, the rpc package uses the gob protocol to serialize/deserialize the transmitted data, which has limitations. The code below will attempt to serialize and deserialize the transfer data using the JSON protocol.

The server-side code is modified as follows.

// rpc demo/service.go

package main

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

type Args struct {
    
    
	X, Y int
}

// ServiceA 自定义一个结构体类型
type ServiceA struct{
    
    }

// Add 为ServiceA类型增加一个可导出的Add方法
func (s *ServiceA) Add(args *Args, reply *int) error {
    
    
	*reply = args.X + args.Y
	return nil
}

func main() {
    
    
	service := new(ServiceA)
	rpc.Register(service) // 注册RPC服务
	l, e := net.Listen("tcp", ":9091")
	if e != nil {
    
    
		log.Fatal("listen error:", e)
	}
	for {
    
    
		conn, _ := l.Accept()
		// 使用JSON协议
		rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
	}
}

The client code is modified as follows.

// rpc demo/client.go

package main

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

type ClientArgs struct {
    
    
	X, Y int
}

func main() {
    
    
	// 建立TCP连接
	conn, err := net.Dial("tcp", "127.0.0.1:9091")
	if err != nil {
    
    
		log.Fatal("dialing:", err)
	}
	// 使用JSON协议
	client := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
	// 同步调用
	args := &ClientArgs{
    
    10, 20}
	var reply int
	err = client.Call("ServiceA.Add", args, &reply)
	if err != nil {
    
    
		log.Fatal("ServiceA.Add error:", err)
	}
	fmt.Printf("ServiceA.Add: %d+%d=%d\n", args.X, args.Y, reply)

	// 异步调用
	var reply2 int
	divCall := client.Go("ServiceA.Add", args, &reply2, nil)
	replyCall := <-divCall.Done // 接收调用结果
	fmt.Println(replyCall.Error)
	fmt.Println(reply2)
}

Python calls RPC

The following code demonstrates how to use the python client to remotely call the Add method of serviceA in the above Go server.

import socket
import json

request = {
    
    
    "id": 0,
    "params": [{
    
    "x":10, "y":20}],  # 参数要对应上Args结构体
    "method": "ServiceA.Add"
}

client = socket.create_connection(("127.0.0.1", 9091),5)
client.sendall(json.dumps(request).encode())

rsp = client.recv(1024)
rsp = json.loads(rsp.decode())
print(rsp)

Output result:

{'id': 0, 'result': 30, 'error': None}

RPC principle

RPC makes remote calls just like local calls, and the call process can be disassembled into the following steps.

image-20230804131507445

① The service caller (client) calls the service locally;

② After receiving the call, the client stub is responsible for assembling methods, parameters, etc. into a message body that can be transmitted over the network;

③ The client stub finds the service address and sends the message to the server;

④ The server receives the message;

⑤ The server stub decodes the message after receiving it;

⑥ The server stub calls the local service according to the decoding result;

⑦ The local service executes and returns the result to the server stub;

⑧ The server stub packs the returned result into a message body capable of network transmission;

⑨ Send the message to the caller by address;

⑩ The client receives the message;

⑪ The client stub receives the message and decodes it;

⑫ The caller gets the final result.

The goal of using the RPC framework is to only care about the first step and the last step, and the other steps in the middle are all encapsulated, so that users do not need to care. For example, various RPC frameworks (grpc, thrift, etc.) in the community are designed to make RPC calls more convenient.

References:https://www.liwenzhou.com/posts/Go/golang-menu/

Guess you like

Origin blog.csdn.net/m0_63230155/article/details/132107279
RPC
RPC