随便聊聊gRPC

本文涉及的产品
数据传输服务 DTS,数据迁移 small 3个月
推荐场景:
MySQL数据库上云
数据传输服务 DTS,数据同步 small 3个月
推荐场景:
数据库上云
数据传输服务 DTS,数据同步 1个月
简介: 在介绍gRPC之前,首先先来聊一下gRPC当中用到的一个编码Protobuf。

gRPC


%N12C6MDC_{)KP9(Z4K8[6A.jpg

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

image.gifJ7$5K}47ZKKVRCTFY})SD33.png

什么是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但是它对于空间和时间开销是相对来说比较低的。

编码规则

FD~S6F54E3ME4T`YF6F8U9J.png

对于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编码只占用一个字节

[W_H{P]ON6JLNGJO%DGQF)C.png

0x80(>127数字代表)

这里如果说数字大小占用超过7bit,那么会在最高位补1然后继续往下去取,直到剩余bit全为0,最终组合得到最终的编码。

B$MD)~F]9)MS6OXMRKZ}XCT.png

然后简单的来聊一下Varint的解码过程,咱们按照小端来做,从前往后去读,如果最高位是1,那么最低位就取剩余的8bit,然后继续往下去找,如果最高位依然是1继续重复之前的操作,直到最高位为0,最终拼接所有bit得到原始的数字。

-1(负数)

对于负数,一般我们采用补码表示,这导致了,如果我们保存一个负数的话,采用Varint编码之后,会占用5个字节,这对于网络流量寸土寸金的时代,这肯定是不能忍受的,因此对于负数,protobuf定义了sint32和sint64来表示负数,对于负数采用Zigzag编码之后,在执行Varint编码。

%LV}QW%[FU@HLY5][VCBABV.png

image.gif

从上面的例子我们可以看出来,采用Varint编码之后,每个字段所占用的长度是可以计算出来的,因此呢就不需要存长度了。

字符串编码

字符串属于变长类型,因此对于Tag当中的wire_type为2,我们来看一下在最上面提到过的一个例子。

message Info {
  required string name = 1;
}

下面的例子是name的值为LittleQ的时候对应的编码的值。

0A 07 4C 69 74 74 6C 65 51

同样的,我们用图来手动解析一下上面的值。

430@{0_P{N]CQ~~WS}DG4_K.png

image.gif

数组编码

对于数组来说,存在两种形式。一种是添加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

image.gifT]8RVTR_V[Z3{(4F_[12~F1.png

使用方法

因为protobuf在数据传输过程当中是没有保存具体的字段信息的,因此需要通信双方预先定义好所需要通信的数据格式,一般在.proto当中定义一个或者多个消息类型,从上面讲解的编码类型来看,实际上对应的字段只要类型和序号对了,对于具体的格式可能有多种情况,所以交互双方要去约定具体的.proto文件。


Http2

因为gRPC是基于http2的,在这里不展开http2的解释了,有兴趣的读者可以自行查阅一下相关的rfc。


gRPC

前面啰嗦了这么多,现在终于回到了本篇文章的主题,正式开聊有关gRPC的相关知识。

image.gifAEY76UV4LZS60I(ED](VN$5.png

下面我们来通过一个例子来实现一下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.gogreeter_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对于省空间这件事情应该是做的还是相当不错的,虽然说编码之后的内容咱们开发者可能一眼无法看穿,但是对于计算机来说还是有好的。

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
Sqoop 企业级大数据迁移方案实战
Sqoop是一个用于在Hadoop和关系数据库服务器之间传输数据的工具。它用于从关系数据库(如MySQL,Oracle)导入数据到Hadoop HDFS,并从Hadoop文件系统导出到关系数据库。 本课程主要讲解了Sqoop的设计思想及原理、部署安装及配置、详细具体的使用方法技巧与实操案例、企业级任务管理等。结合日常工作实践,培养解决实际问题的能力。本课程由黑马程序员提供。
相关文章
|
JSON 前端开发 Java
SpringBoot项目Http406错误问题解决
一、背景 1、自定义了返回类 2、控制器使用@ResponseBody注解标记
若依修改,http和https的两种写法,部署成功的两种写法
若依修改,http和https的两种写法,部署成功的两种写法
|
编解码 Rust 自然语言处理
gRPC源码分析(三):从Github文档了解gRPC的项目细节
从这里可以看出,gRPC虽然是支持多语言,但原生的实现并不多。如果想在一些小众语言里引入gRPC,还是有很大风险的,有兴趣的可以搜索下TiDB在探索rust的gRPC的经验分享。
191 1
|
6月前
|
C++
vs2017下gRPC的编译以及简单使用
vs2017下gRPC的编译以及简单使用
107 0
|
Java 编译器 API
gRPC 初探与简单使用
gRPC 初探与简单使用
59 0
|
测试技术 Go
gRPC阅读日记(五)创建grpc的服务端和客户端
gRPC阅读日记(五)创建grpc的服务端和客户端
|
Go 流计算
gRPC阅读日记(七)客户端的RPC构建2
gRPC阅读日记(七)客户端的RPC构建2
|
Go 流计算
gRPC阅读日记(六)来看看客户端的rpc请求如何实现
gRPC阅读日记(六)来看看客户端的rpc请求如何实现
|
XML JSON 网络协议
gRPC笔记与相关问题
gRPC笔记与相关问题
|
Java Go
Go Web编程实战(8)----创建HTTP与HTTPS服务器端
Go Web编程实战(8)----创建HTTP与HTTPS服务器端
218 0
Go Web编程实战(8)----创建HTTP与HTTPS服务器端