gRPC
❝gRPC is a modern open source high performance Remote Procedure Call (RPC) framework that can run in any environment. It can efficiently connect services in and across data centers with pluggable support for load balancing, tracing, health checking and authentication. It is also applicable in last mile of distributed computing to connect devices, mobile applications and browsers to backend services.
❞
在介绍gRPC之前,首先先来聊一下gRPC当中用到的一个编码Protobuf。
ProtoBuf
什么是ProtoBuf
「Protocol Buffers」(简称:「ProtoBuf」)是一种开源跨平台的序列化数据结构的协议。其对于存储资料或在网络上进行通信的程序是很有用的。这个方法包含一个接口描述语言,描述一些数据结构,并提供程序工具根据这些描述产生代码,这些代码将用来生成或解析代表这些数据结构的字节流。
前言
Protobuf有特殊的编码规则。在正式介绍Protobuf的编码规则之前,我们先来回顾一下,之前我们利用网络传输数据常用采取的格式。
JSON
{ "name": "LittleQ" }
相信各位读者对于JSON格式应该是相当的熟悉,无论是写过客户端,服务端,爬虫等等吧,涉及到网络的数据传输,大家大概率首选的格式应该就是JSON格式了。
XML
<info> <name>LittleQ</name> <info>
对于XML来说,作为另一个数据传输的格式,相信各位读者至少应该是见过,我们可以看到上面这个例子,里面有个info的对象,里面有一条属性,name他的值为LittleQ
。
我们来观察上面的两种格式,我们可以非常容易的知道我们所要传输的数据是什么,但是呢,我们来思考这么一个问题,对于网络传输来说,一般都是两个实体进行的数据交换(不考虑哪种多个实体交换的情况,只考虑一个客户端和一个服务端进行通信)。那么双方所能发送或者接受的数据内容应该是提前协商好的,那么我们来看JSON当中的name和xml当中的<info> <name>
这些,似乎是可以省略一下,如果说通信的双方协商好了,我可以接收那些字段这样就能节省一大笔空间的开销。
考虑到上面所说的这一点,我们来看一下protobuf的表示格式:
protobuf
0A 07 4C 69 74 74 6C 65 51
protobuf采用了二进制直接传输数据,因此在可读性上面不及JSON或者说XML但是它对于空间和时间开销是相对来说比较低的。
编码规则
对于Protobuf的第一部分,也就是Tag,前半部分是一个序号,范围是从1到 然后后面三个是类型位,目前只有6种,因此需要3个比特就够了,具体类型如下:
Type | Meaning | Used For |
0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 64-bit | fixed64, sfixed64, double |
2 | Length-delimited | string, bytes, embedded messages, packed repeated fields |
3 | Start group | groups (deprecated) |
4 | End group | groups (deprecated) |
5 | 32-bit | fixed32, sfixed32, float |
上面的表格来自于Google的官方文档,我们可以看到3和4是已经废弃了的,因此呢我们只需要看剩下的4中类型就可以了,对于0、1和5这三种类型,是不需要长度的,因为他们的长度可以通过类型本身长度或者某种方式计算得来,后文在给读者来描述Varint类型长度是如何计算的,而对于2这种类型,存的是一个变长类型,因此需要存储长度。
Varint编码规则
Varint是一种紧凑的数字表示方法,它通过一个或者多个字节来表示一个数字,数字越小所使用的字节数越少,这样在某些情况下,比如说数字可能是非常小,也可能是非常大的情况,采用这种方式在数字小的时候就可以少占用字节数,Varint是一种变长编码。下面我们来看几个具体的varint的例子。
0x8(<128数字代表)
对于小于128的数字来说,Varint编码只占用一个字节
0x80(>127数字代表)
这里如果说数字大小占用超过7bit,那么会在最高位补1然后继续往下去取,直到剩余bit全为0,最终组合得到最终的编码。
然后简单的来聊一下Varint的解码过程,咱们按照小端来做,从前往后去读,如果最高位是1,那么最低位就取剩余的8bit,然后继续往下去找,如果最高位依然是1继续重复之前的操作,直到最高位为0,最终拼接所有bit得到原始的数字。
-1(负数)
对于负数,一般我们采用补码表示,这导致了,如果我们保存一个负数的话,采用Varint编码之后,会占用5个字节,这对于网络流量寸土寸金的时代,这肯定是不能忍受的,因此对于负数,protobuf定义了sint32和sint64来表示负数,对于负数采用Zigzag编码之后,在执行Varint编码。
从上面的例子我们可以看出来,采用Varint编码之后,每个字段所占用的长度是可以计算出来的,因此呢就不需要存长度了。
字符串编码
字符串属于变长类型,因此对于Tag当中的wire_type为2,我们来看一下在最上面提到过的一个例子。
message Info { required string name = 1; }
下面的例子是name的值为LittleQ
的时候对应的编码的值。
0A 07 4C 69 74 74 6C 65 51
同样的,我们用图来手动解析一下上面的值。
数组编码
对于数组来说,存在两种形式。一种是添加packed,另一种是不添加packed,对于不添加packed的来说,因为对于Tag的值是冗余的,因此添加之后可以节省一些空间,同样的,我们来看一个具体的例子。
message Info { repeated int32 a = 1; repeated int32 b = 2 [packed=true]; }
下面的例子是对于
{ "a": [ 1, 2, 3 ], "b": [ 1, 2, 3 ] }
的protobuf的编码表示
08 01 08 02 08 03 12 03 01 02 03
使用方法
因为protobuf在数据传输过程当中是没有保存具体的字段信息的,因此需要通信双方预先定义好所需要通信的数据格式,一般在.proto
当中定义一个或者多个消息类型,从上面讲解的编码类型来看,实际上对应的字段只要类型和序号对了,对于具体的格式可能有多种情况,所以交互双方要去约定具体的.proto
文件。
Http2
因为gRPC是基于http2的,在这里不展开http2的解释了,有兴趣的读者可以自行查阅一下相关的rfc。
gRPC
前面啰嗦了这么多,现在终于回到了本篇文章的主题,正式开聊有关gRPC的相关知识。
下面我们来通过一个例子来实现一下gRPC的服务调用,这里服务端我们选择用go来写吧,采用官网给出的一个.proto
格式。
// The greeter service definition. service Greeter { // Sends a greeting rpc SayHello (HelloRequest) returns (HelloReply) {} } // The request message containing the user's name. message HelloRequest { string name = 1; } // The response message containing the greetings message HelloReply { string message = 1; }
GoLang实现gRPC的服务端/客户端
首先来安装一下相关的依赖
go get -u github.com/golang/protobuf/proto go get -u google.golang.org/grpc
然后我们就可以愉快的编写代码了,最终的代码结构如下
. ├── client │ └── main.go ├── go.mod ├── go.sum ├── protos │ ├── greeter.pb.go │ ├── greeter.proto │ └── greeter_grpc.pb.go └── server └── main.go
在创建完成.proto
文件之后,需要利用protoc工具来生成对应语言的文件,对于Go来说,具体的命令如下:
protoc --go_out=. --go_opt=paths=source_relative \ --go-grpc_out=. --go-grpc_opt=paths=source_relative \ protos/greeter.proto
执行完成之后,这里会生成greeter.pb.go
和greeter_grpc.pb.go
两个文件。生成之后,我们就可以来先来编写服务端代码了。
package main import ( "context" "flag" "fmt" "log" "net" pb "gRPCServer/protos" "google.golang.org/grpc" ) var ( port = flag.Int("port", 10088, "The server port") ) type server struct { pb.UnimplementedGreeterServer } func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { log.Printf("Received: %v", in.GetName()) return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil } func main() { flag.Parse() lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", *port)) if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() pb.RegisterGreeterServer(s, &server{}) log.Printf("server listening at %v", lis.Addr()) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
之后,我们来编写客户端代码,简单调用一下我们上面刚才所写的服务。
package main import ( "context" "flag" "log" "time" pb "gRPCServer/protos" "google.golang.org/grpc" ) var ( addr = flag.String("addr", "localhost:10088", "the address to connect to") ) func main() { flag.Parse() conn, err := grpc.Dial(*addr, grpc.WithInsecure()) if err != nil { log.Fatalf("did not connect: %v", err) } defer conn.Close() c := pb.NewGreeterClient(conn) ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "LittleQ"}) if err != nil { log.Fatalf("could not greet: %v", err) } log.Printf("Greeting: %s", r.GetMessage()) }
之后,运行一下就可以成功的使用gRPC了。突然发现好像也没什么能说的了,所以呢,本文到这里就结束了。
总结
本文呢主要是聊了一下有关gRPC的使用和其数据传输主要才用的protobuf的编码规则,总体来说,protobuf对于省空间这件事情应该是做的还是相当不错的,虽然说编码之后的内容咱们开发者可能一眼无法看穿,但是对于计算机来说还是有好的。