RPC
远程过程调用(Remote Procedure Call,RPC)是一个计算机通信协议
该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程
如果涉及的软件采用面向对象编程,那么远程过程调用亦可称作远程调用或远程方法调用
gRPC
- gRPC由google开发,是一款语言中立、平台中立、开源的远程过程调用系统
- gRPC客户端和服务端可以在多种环境中运行和交互,例如用java写一个服务端,可以用go语言写客户端调用
Protobuf
- rotobuf是由Google开发的一套对数据结构进行序列化的方法,可用做通信协议,数据存储格式,等等。其特点是不限语言、不限平台、扩展性强,就像XML一样。
特点:
1、操作更简单。
2、序列化后生成的代码体积更小
3、解析速度更快。(比xml快两个数量级)
4、与XML相比,protobuf的缺点是不易读。众所周知,XML是一种自描述语言,一看就可知道其作用,见文知意。而protobuf序列化后是一串二进制代码,如果没有对应的协议格式(即.proto文件),想要读懂它难如登天。另外,如果目标是一种基于文版的标签式文档(如html),则XML更具优势
关于protobuf的历史及其特点,可参考Google网站:点击打开链接
语法规则
protobuf协议的文件后缀名为.proto。一个简单的protobuf协议如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| message Person { required string name = 1; required int32 id = 2; optional string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { required string number = 1; optional PhoneType type = 2 [default = HOME]; } repeated PhoneNumber phone = 4; } message AddressBook { repeated Person person = 1; }
|
1、标识符
protobuf协议的标识符为message或enum,如示例中的Person和PhoneType。message标识一条消息,enum标识一个枚举类型。使用protobuf编译器将协议文件编译后,message和enum都会生成一个类。
2、字段
协议字段格式如下:
1
| role type name = tag [default value];
|
role有三种取值:
required:表示该字段必须有值,不能为空,否则message被认为是未初始化的。如果试图build一个未初始化的message将会抛出RuntimeException。解析未初始化的message会抛出IOException。
optional:表示该段为可选值,可以不进行设置。如果不设置,会设置一个默认值。可以设置一个默认值,正如示例中的type字段。否则使用系统默认值,数字类型默认为0;字符串类型默认为空字符串;逻辑类型默认为false;内部自定义的message,默认值通常是message的实例或原型。
repeated:表示该字段可以重复,可等同于动态数组。
注意:使用required字段一定要小心,因为该字段是永久性的。如果以后因为某种原因,想不用该字段,或者要将该字段改成optional或者repeated,那么使用旧接口读取新的协议时,如果发现没有该字段,他们会认为该字段是不完整的,会拒接接收该消息,或者直接丢弃。
3、编码
wireType |
编码方式 |
编码长度 |
储存方式 |
代码数据类型 |
0 |
Varint |
变长(1-10个字节) |
T-V |
int32,int64,uint32,uint64,bool,enum (负数使用sint32,sint64,) |
1 |
64-bit |
固定8字节 |
T-V |
fixed64,sfixed64,double |
2 |
Length-delimi |
变长 |
T-L-V |
String |
3 |
Start group |
弃用 |
弃用 |
弃用 |
4 |
End group |
弃用 |
弃用 |
弃用 |
5 |
32-bit |
固定4字节 |
T-V |
Fixed32,sfixed32,float |
T - V 编码举例:
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
| message request { int63 user_id = 1; // tagNum = 1, wireType = 0, }
{"user_id":2} json需要: 14个字符 112字节 protobuf: 2个字节 相差52倍
假设 value为 2, 则编码出的T-V为: +-----+---+-----------------+ |00001|000|00000010| +-----+---+-----------------+ tagNum type data
{"user_id":300} json需要: 16个字符 128字节 protobuf: 3个字节 差42倍
假设 value为 300, 则编码出的T-V为: 300(00000001 00101100) 第一个字节 第二 第三 +-----+---+-----------------------+ |00001|000| 10101100 00000010| 下个T-V +-----+---+-----------------------+ tagNum type data
Tag高位=0: 一个byte data的第一个字节最高位为1,说明下一个字节还要继续读
|
T - L - V 就是在上面的基础上增加了length,用来表达变长的内容:
嵌套对象编码举例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| message request { User user = 1; // tagNum = 1, wireType = 2, } message User { int64 user_id = 1; // tagNum = 1 }
假设 request = { user_id: 2}, 则编码出的T-L-V为: Tag length value Tag value +---------+--------+---------+--------- |00001010 |00000010|000010000|00000010| 1<<3 | 2 2 byte 1<<3 | 0 2
通过解request 知道第一个字段是User,再拿到第一个字段的value去解User, 知道User第一个字段是int64,解析出data为2。 一个嵌套对象即解析完毕
|
案例
流程:
- 编写
.proto
描述文件
- 编译生成
.pb.go
文件
- 服务端实现约定的接口并提供服务
- 客户端按照约定调用
.pb.go
文件中的方法请求服务
项目结构:
1 2 3 4 5 6 7 8 9
| |—— hello/ |—— client/ |—— main.go // 客户端 |—— server/ |—— main.go // 服务端 |—— proto/ |—— hello/ |—— hello.proto // proto描述文件 |—— hello.pb.go // proto编译后文件
|
编写描述文件:hello.proto
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| syntax = "proto3"; package hello;
option go_package = "./hello";
service Hello { rpc SayHello(HelloRequest) returns (HelloResponse) {} }
message HelloRequest { string name = 1; }
message HelloResponse { string message = 1; }
|
hello.proto
文件中定义了一个Hello Service,该服务包含一个SayHello
方法,同时声明了HelloRequest
和HelloResponse
消息结构用于请求和响应。客户端使用HelloRequest
参数调用SayHello
方法请求服务端,服务端响应HelloResponse
消息。一个最简单的服务就定义好了。
编译生成.pb.go
文件
1 2 3 4
| $ cd proto/hello
$ protoc -I . --go_out=plugins=grpc:. ./hello.proto
|
在当前目录内生成的hello.pb.go
文件,按照.proto
文件中的说明,包含服务端接口HelloServer
描述,客户端接口及实现HelloClient
,及HelloRequest
、HelloResponse
结构体。
注意:不要手动编辑该文件
实现服务端接口 server/main.go
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 42 43 44 45 46
| package main
import ( "fmt" "net"
pb "rpc-demo/proto/hello" "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/grpclog" )
const ( Address = "127.0.0.1:50052" )
type helloService struct{}
var HelloService = helloService{}
func (h helloService) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) { resp := new(pb.HelloResponse) resp.Message = fmt.Sprintf("Hello %s.", in.Name)
return resp, nil }
func main() { listen, err := net.Listen("tcp", Address) if err != nil { grpclog.Fatalf("Failed to listen: %v", err) }
s := grpc.NewServer()
pb.RegisterHelloServer(s, HelloService)
grpclog.Println("Listen on " + Address) s.Serve(listen) }
|
服务端引入编译后的proto
包,定义一个空结构用于实现约定的接口,接口描述可以查看hello.pb.go
文件中的HelloServer
接口描述。实例化grpc Server并注册HelloService,开始提供服务。
运行:
1 2
| $ go run main.go Listen on 127.0.0.1:50052 //服务端已开启并监听50052端
|
实现客户端调用 client/main.go
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
| package main
import ( pb "rpc-demo/proto/hello" "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/grpclog" )
const ( Address = "127.0.0.1:50052" )
func main() { conn, err := grpc.Dial(Address, grpc.WithInsecure()) if err != nil { grpclog.Fatalln(err) } defer conn.Close()
c := pb.NewHelloClient(conn)
req := &pb.HelloRequest{Name: "gRPC"} res, err := c.SayHello(context.Background(), req)
if err != nil { grpclog.Fatalln(err) }
grpclog.Println(res.Message) }
|
客户端初始化连接后直接调用hello.pb.go
中实现的SayHello
方法,即可向服务端发起请求,使用姿势就像调用本地方法一样。
运行:
1 2
| $ go run main.go Hello gRPC. // 接收到服务端响应
|
prev
last update time 2022-05-06
next