使用Protobuf创建简单的gRpc服务
程序开发过程中,最重要的一步是数据交互,也就是服务之间的数据通信,涉及到通讯需要有通讯协议,数据的序列化与反序列化,从早期使用的xml,到现今流行的json,再到protobuf,这些都是为了解决通讯的效率问题。
Protobuf 简介
Protobuf 全称是Protocol buffers,是Google开发的一种协议,允许对结构化数据进行序列化和反序列化。Google开发它的目的也是为了解决通讯效率问题。
为什么已经有了xml 和 json 还要使用Protobuf?
对于小量数据序列化与反序列化来说,三者的性能并没有太大区别,但是数据量变大的时候,Protobuf序列化后的大小是json的十分之一,是xml格式的二十分之一,而且性能是他们的5~100倍。总的来说,Protobuf的特征就是小且快。
性能对比:
来源
如何使用Protobuf?
概念
首先得理解序列化,序列化的通俗解释是:把内存的一段数据转化成二进制并存储或者通过网络传输,而读取磁盘或另一端收到后可以在内存中重建这段数据。
而protobuf协议是跨语言跨平台的序列化协议,对于json与xml是一种序列化的方式,是不需要提前定义接口描述语言(IDL),并且具备可读性,但是传输体积比较大。
所以使用Protobuf前是需要写好IDL的,关于IDL语法怎么写?官方提供比较全的文档。
IDL编写
官网上查询到IDL文档支持以上语言,protobuf中最基本的类型是message,每一个message 都有多个字段。
-
Protobuf基本数据类型:
doble,folate,int32,int64,uint32,uint64,sint32,sint64,fixed32,fixed64,sfixed32,sfixed64,bool,string -
Protobuf 关键字说明:
关键字 | 字段说明 |
---|---|
syntax | 指定 Protobuf 的版本,Protobuf 目前有 proto2 和 proto3 两个常用版本,如果没有声明,则默认是proto2 |
package | 指定文件包名 |
import | 导包,和 Java 的 import 类似 |
message | 定义消息类,和 Java 的 class 关键字类似,消息类之间可以嵌套 |
repeated | 定义一个集合,和 Java 的集合类似 |
reserved | 保留字段,如果使用了这个关键字修饰,用户就不能使用这个字段编号或字段名 |
option | option 可以用在 Protobuf 的 scope 中,或者 message、enum、service 的定义中,Protobuf 定义的 option 有 java_package,java_outer_classname,java_multiple_files 等等 |
optional | 表示该字段是可选的 |
java_package | 指定生成类所在的包名 |
java_outer_classname | 定义当前文件的类名,如果没有定义,则默认为文件的首字母大写名称 |
java_multiple_files | 指定编译过后 Java 的文件个数,如果是 true,那么将会一个 Java 对象一个类,如果是 false,那么定义的Java 对象将会被包含在同一个文件中 |
- 实践
需要将IDL文件编译成对应平台使用的文件
-
配置protobuf环境
for mac
brew intall protobuf
for window
下载对应的二进制文件包
https://github.com/google/protobuf/releases 解压后配置到环境变量中。 -
安装对应语言类型插件
protoc -h 查看支持默认的语言类型
如果需要其他的语言需要安装插件
go语言插件 $ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest $ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest for mac brew安装 brew install protoc-gen-go brew install protoc-gen-go-grpc
-
按照IDL语法写一个简单的IDL文件:
-
生成go
进入proto 文件所在的目录执行命令生成go 对应的文件protoc --go_out=. --go-grpc_out=. hello.proto
执行命令后会看到多出了一个 hello_grpc.pb.go 文件,这个就是go能直接使用的文件
grpc 通讯模式
服务类型 | 解释 | 应用场景 |
---|---|---|
简单 RPC | 一般的rpc调用,传入一个请求对象,返回一个返回对象 | 通用rpc场景 |
服务端流式 RPC | 传入一个请求对象,服务端可以返回多个结果对象 | 股票实时数据流 |
客户端流式 RPC | 客户端传入多个请求对象,服务端返回一个结果对象 | 物联网终端上送数据 |
双向流式 RPC | 结合客户端流式RPC和服务端流式RPC,可以传入多个请求对象,返回多个结果对象 | 聊天应用 |
我们选择简单RPC通讯模式。
编写服务端代码
service.go
package main
import (
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
"log"
"net"
hello "x-go/src/main/bp/idl"
)
// server 用来实现 hello.HelloServer
type helloServer struct {
hello.UnimplementedHelloServer
}
// SayHello 实现 hello.SayHello 方法
func (s *helloServer) SayHello(ctx context.Context, in *hello.HelloRequest) (*hello.HelloReply, error) {
return &hello.HelloReply{Message: "Hello " + in.Name, Code: 200}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
return
}
s := grpc.NewServer()
hello.RegisterHelloServer(s, &helloServer{})
//在 server 中 注册 gRPC 的 reflection service
reflection.Register(s)
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
*** 需要注意的是应用路径为上一步通过命令生成的go文件所在目录 ***
编写客户端代码
client.go
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"google.golang.org/grpc"
"log"
"net/http"
hello "x-go/src/main/bp/idl"
)
func main() {
r := gin.Default()
r.GET("/rpc/hello", func(c *gin.Context) {
sayHello(c)
})
// Run http server
if err := r.Run(":8052"); err != nil {
log.Fatalf("could not run server: %v", err)
}
}
func sayHello(c *gin.Context) {
// Set up a connection to the server.
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
client := hello.NewHelloClient(conn)
name := c.DefaultQuery("name", "hello go !!")
req := &hello.HelloRequest{Name: name}
res, err := client.SayHello(c, req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"result": fmt.Sprint(res.Message),
"code": fmt.Sprint(res.Code),
})
}
启动服务
先启动service端服务,再启动client 服务,访问client地址会看到数据通讯效果。
小结
上文中使用的go作为服务端与客户端示例,对于protobuf 来说,它只是一种协议,与语言并无关系,所以在任何语言中我们都能用protobuf作为通讯协议,提高通讯效率。