Article directory
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— app1
and then run it, and the result 30 will be output.
app1
The execution process of calling a function locally in a program can add
be understood as the following four steps.
- Push the values of a and b onto the stack
- 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
- Calculate x*y and store the result in z
- Push the value of z onto the stack and return from the add function
- Take the z return value from the stack and assign it to ret
RPC call
Local procedure calls happen in the same process— add
the code that defines the function and add
the 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.
- 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.
- 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.
- 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 add
a 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(¶m)
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 ServiceA
type and define an exportable Add
method for it. And ServiceA
register 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 representsclient
a synchronous call to a remote method named "ServiceA.Add" using the connection object, passing asargs
a parameter, and storing the result inreply
.- If there is an error in the call,
log.Fatal
the error message is output by.
b. Asynchronous call:
client.Go("ServiceA.Add", args, &reply2, nil)
: This line of code representsclient
an asynchronous call to a remote method named "ServiceA.Add" using the connection object, passing asargs
a parameter, and storing the result inreply2
. The Go method is used here, which immediately returns anrpc.Call
object representing the state of the asynchronous call.<-divCall.Done
: By using<-
the operator, we wait for the completion of the asynchronous call, heredivCall.Done
is 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.
① 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.