使用Protobuf创建简单的gRpc服务

125 阅读5分钟

程序开发过程中,最重要的一步是数据交互,也就是服务之间的数据通信,涉及到通讯需要有通讯协议,数据的序列化与反序列化,从早期使用的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保留字段,如果使用了这个关键字修饰,用户就不能使用这个字段编号或字段名
optionoption 可以用在 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文件编译成对应平台使用的文件

  1. 配置protobuf环境

    for mac brew intall protobuf

    for window 下载对应的二进制文件包 github.com/google/prot… 解压后配置到环境变量中。

  2. 安装对应语言类型插件 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 
    
  3. 按照IDL语法写一个简单的IDL文件:

  4. 生成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作为通讯协议,提高通讯效率。