RPC学习笔记

RPC和REST协议的区别,这些协议都是建立在TCP基础上的可靠传输协议。

传输协议

HTTP协议

规定传输方式:

  • Method:方法:GET PUT POST DELETE …
  • URL:地址
  • Data:具体数据或者二进制data
  • Header:

RPC

借用HTTP协议,携带RPC数据。

  • Method:只有POST

  • URL:xxx.xxx.com/GetUser 地址和方法,方法名首字母大写(实现callID的指定)

  • Data:请求的数据

REST

在HTTP协议,根据HTTP框架设计的一种接口方式,用于CRUD

  • C:创建 POST xxx.xxx.com/user,内容放在Data中,Data以json的形式
  • R:获取 GET xxx.xxx.com/user/{id}
  • U:更新 PUT xxx.xxx.com/user/{id},更新内容放在Data中,以json的形式
  • D:删除 DELETE xxx.xxx.com/user/{id}

选择RPC或者REST需要根据服务工程化来决定,微服务下,服务之间通过RPC访问可以不用对外暴露接口,统一由RPC Gateway做反向代理转发。

RPC

Remote Procedure Call:远程过程调用。可以将远端的函数、方法当做本地的函数或方法使用,通过Protobuf实现跨语言通信。

img

RPC中解决的问题:

  1. Call的id映射(需要确定调用的远程函数具体是哪个函数)(在HTTP协议上,通过path代表Call ID)
  2. 序列化和反序列化(如何传输数据,使用rpc默认的序列化协议还是json还是protobuf)(在HTTP协议上,通过header头指定的序列化协议)
  3. 网络传输(通过tcp还是http传输数据,确保数据传输以及传输格式)(HTTP1协议默认短链接,过程中要经历三次握手和四次挥手)(gRPC框架基于http2.0,使用长连接流式传输,无论是网络连接还是序列化反序列化,性能更高)

preview

RPC通信的四个部分:

  1. 服务端
  2. 服务端存根
  3. 客户端
  4. 客户端存根

类似于一个HTTP服务

1
2
3
4
5
6
7
8
9
10
11
http.HandleFunc("/hello", func(writer http.ResponseWriter, request *http.Request) {
_ = request.ParseForm()
a := request.Form["a"]
b := request.Form["b"]
res := map[string]string{
"a + b": a[0] + b[0],
}
writer.Header().Add("content-type", "application/json")
datas, _ := json.Marshal(res)
writer.Write(datas)
})

请求时,通过/hello确定call ID,通过writer.Header().Add("content-type", "application/json")确定序列化方式,通过HTTP网络传输,这也是一种RPC。

通过RPC框架进行封装,则可以将Call ID进行封装,序列化和反序列化的方式进行封装,通信方式也进行封装,例如RPC协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type RPC struct {
}

// 将请求参数封装到req中,返回参数封装到reply中,RPC就是一个stub
func (r *RPC) Hello(req string, reply *string) error {
*reply = req + req
return nil
}

func main() {
err := rpc.RegisterName("hello", &RPC{}) // 注册服务
if err != nil {
log.Fatal(err)
}

listener, _ := net.Listen("tcp", ":8080") // 网络传输基于tcp
rpc.Accept(listener)
}

客户端调用

1
2
3
dial, _ := rpc.Dial("tcp", ":8080")
var reply string
err := dial.Call("hello.Hello", "nihao", &reply) // 通过hello指定服务,Hello指定方法,"nihao"为传入参数,reply为出参

RPC有标准的用法,步骤如下:

  • 服务端注册带名称的服务,第一个入参是请求,第二个入参是指针,代表返回内容
  • 服务端通过TCP套接字层监听并且接收请求
  • 客户端建立TCP连接
  • 客户端通过服务名称和两个参数调用连接

因此可以看到,rpc协议的序列化和反序列化,可以通过rpc自带的协议实现,也可以通过json实现,也可以通过http协议实现。

rpc协议的通信,可以继续tcp的socket变成实现,也可以通过http协议实现。具体例子可以看另外一篇文档:golang中的网络编程

Protobuf

全称是Protocl Buffers,是一种数据描述语言,类似jsonxml,可通过附带工具生成代码并实现将结构化数据序列化的功能。例如通过proto文件生成golang文件

1
2
3
4
5
6
7
8
9
10
syntax = "proto3"; // 版本

package gostudy; //
option go_package = "gostudy/proto/gen/go;userpb"; // 生成的代码位置和package

message User {
int64 id = 1; // 由于二进制,需要用id表示位置
string name = 2;
int64 duty_d = 3;
}

通过protoc生成go代码

1
protoc --go_out=plugins=grpc:. hello.proto

生成之后的结构体

1
2
3
4
5
6
7
8
9
type User struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// 可被外部访问的参数有两个tag,一个是protobuf,另一个是json
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
DutyD int64 `protobuf:"varint,3,opt,name=duty_d,json=dutyD,proto3" json:"duty_d,omitempty"`
}

通过proto序列化和反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

import (
"encoding/json"
userpb "gostudy/proto/gen/go" // 通过protobuf文件中指定的package引用
)

func main() {
user := userpb.User{
Id: 1,
Name: "mitaka",
DutyD: 5,
}
fmt.Println(&user)

// 通过proto序列化
bytes, err := proto.Marshal(&user)
if err != nil {
panic(err)
}
fmt.Printf("%X\n", bytes)
// 通过proto反序列化
user1 := userpb.User{}
err = proto.Unmarshal(bytes, &user1)
if err != nil {
panic(err)
}
fmt.Println(&user1)
// 通过json序列化
b, err := json.Marshal(&user)
if err != nil {
panic(err)
}
fmt.Printf("%s\n", b)
// 通过json反序列化
user2 := userpb.User{}
err = json.Unmarshal(b, &user2)
if err != nil {
panic(err)
}
fmt.Println(&user2)
}

复合类型和枚举

1
2
3
4
5
6
7
8
9
10
11
12
message User {
int64 id = 1;
Book now_book = 4;
repeated Book carry_books =5;
string name = 2;
int64 duty_d = 3;
}

message Book {
int64 id = 1;
string name = 2;
}

生成代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Book struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields

Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
}

type User struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields

Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
NowBook *Book `protobuf:"bytes,4,opt,name=now_book,json=nowBook,proto3" json:"now_book,omitempty"`
CarryBooks []*Book `protobuf:"bytes,5,rep,name=carry_books,json=carryBooks,proto3" json:"carry_books,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
DutyD int64 `protobuf:"varint,3,opt,name=duty_d,json=dutyD,proto3" json:"duty_d,omitempty"`
}

可以看到复合类型和枚举都是指针。

字段可选性

1
2
3
4
5
6
7
8
9
10
11
12
message User {
...
BookStatus status = 6;
}

enum BookStatus {
BS_NOT_SPECIFIED = 0; // 没有这个字段时,默认值
NOT_STARTED = 1;
IN_PROCESS = 2;
FINISHED = 3;
PAID = 4;
}

限定Userstatus,只能在这些状态。

结构体中所有的字段都是可选的,当这个字段不填,则是零值。如果要区分是零值还是没有填写,那么需要增加一个字段,is_xxx bool,需要确保false的含义,与老版本匹配。

gRPC

为了解决RPC协议中,对客户端存根和服务端存根的封装,而且可以实现跨语言,则需要通过一种框架实现,RPC框架中,使用的最为广泛的,就是gRPC框架。

gRPC是基于ProtoBuf开发的跨语言的开源RPC框架。

客户端和服务端通信,中间经过网络,需要确定网络协议(tcpudp),确定服务端地址(域名、ip和端口),服务路径(url)服务参数(bodyquerypath),数据类型(stringint),数据编码(jsonxml),安全性(ssl加密,token),错误处理(error response)。使用HTTP通信可以满足上述所有条件,使用gRPC协议可以满足。gRPC使用HTTP/2协议,数据传输使用二进制,流式传输streaming,多路复用一个TCP连接。

Concept Diagram

定义服务方法、请求参数、返回参数

1
2
3
4
5
6
7
8
9
10
11
12
message GetUserRequest {         // 输入参数
int64 id = 1;
}

message GetUserResponse { // 返回参数
int64 id = 1;
Book book = 2;
}

service UserService { // 方法、功能
rpc GetUser (GetUserRequest) returns (GetUserResponse);
}

生成代码,通过plugins=grpc指定plgins

1
protoc -I=. --go_out=plugins=grpc,paths=source_relative:gen/go user.proto

生成的方法

1
2
3
type UserServiceServer interface {
GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error)
}

服务端实现interface

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Service struct {
}

func (*Service) GetUser(ctx context.Context, in *userpb.GetUserRequest) (*userpb.GetUserResponse, error) {
user := userpb.User{
Id: 1,
Name: "mitaka",
DutyD: 5,
}
return &userpb.GetUserResponse{
Id: in.Id,
User: &user,
}, nil
}

服务端开启socket监听

1
2
3
4
listen, err := net.Listen("tcp", ":8081") // net监听
s := grpc.NewServer() // grpc server
userpb.RegisterUserServiceServer(s, &userservice.Service{}) // UserService服务注册
err = s.Serve(listen) // 开启监听

客户端连接

1
2
3
dial, err := grpc.Dial("localhost:8081", grpc.WithInsecure()) // grpc调用端口,设置为不安全传输,练习环境,不加密
userclient := userpb.NewUserServiceClient(dial) // 调用服务
user, err := userclient.GetUser(context.Background(), &userpb.GetUserRequest{Id: 1}) // 传输数据

GRPC GATEWAY

为了对外兼容gRPC请求和HTTP请求,而且不需要写两份代码,维护两份逻辑,可以通过gRPC-gatewag实现一种代理功能,在gRPC协议外层代理一层HTTP协议。

image-20220706224053892

通过grpc-gateway反向代理,服务器内部通信使用grpc,外部访问通过http

user.yaml

1
2
3
4
5
6
7
type: google.api.Service
config_version: 3

http:
rules:
- selector: gostudy.UserService.GetUser
get: /user/{id}

生成gateway文件

1
protoc -I=. --grpc-gateway_out=paths=source_relative,grpc_api_configuration=user.yaml:gen/go user.proto

生成的文件

1
2
3
4
5
/*
Package userpb is a reverse proxy. // 反向代理

It translates gRPC into RESTful JSON APIs. // 将gRPC转换成RESTful风格的JSON APIs
*/

这个过程其实是内部调用grpc,并且启动一个http服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
mux := runtime.NewServeMux()
err = userpb.RegisterUserServiceHandlerFromEndpoint( // 调用本地8081端口的grpc服务
ctx, mux, "0.0.0.0:8081", []grpc.DialOption{grpc.WithInsecure()})
if err != nil {
log.Fatalf("register handler error: %v", err)
}

err = http.ListenAndServe(":8080", mux) // 监听8080端口的http服务
if err != nil {
log.Fatalf("grpc gateway listen error: %v", err)
}
1
2
 curl http://127.0.0.1:8080/user/1
{"id":"1", "user":{"id":"1", "nowBook":null, "carryBooks":[], "name":"mitaka", "dutyD":"5", "status":"BS_NOT_SPECIFIED"}}

从结果可以看到:

  1. "dutyD":"5",int64转换成string,需要改成int32

  2. status的value不是指定的常量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    mux := runtime.NewServeMux(
    runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.JSONPb{
    MarshalOptions: protojson.MarshalOptions{
    UseEnumNumbers: true, // 将enum变量转变成数字
    },
    UnmarshalOptions: protojson.UnmarshalOptions{},
    },
    ),
    )

    引申

grpc-gateway

grpc-gateway wiki