智慧 + 毅力 = 无所不能

正确性、健壮性、可靠性、效率、易用性、可读性、可复用性、兼容性、可移植性...

导航

protobuffer、gRPC、restful gRPC的相互转化

Posted on 2018-03-09 18:32  Bill Yuan  阅读(8448)  评论(0编辑  收藏  举报

转自:https://studygolang.com/articles/12510

文档

protobuf

 Google Protocol Buffer(简称 Protobuf)是一种轻便高效的结构化数据存储格式,平台无关、语言无关、可扩展,可用于通讯协议和数据存储等领域。

优点

  • 平台无关,语言无关,可扩展;
  • 提供了友好的动态库,使用简单;
  • 解析速度快,比对应的XML快约20-100倍;
  • 序列化数据非常简洁、紧凑,与XML相比,其序列化之后的数据量约为1/3到1/10。

安装

参考 golang使用protobuf

https://github.com/google/protobuf/releases // 下载并编译、安装
go get github.com/golang/protobuf/proto   // golang的protobuf库文件

// 插件
go get github.com/golang/protobuf/protoc-gen-go  // 用于根据protobuf生成golang代码,语法 protoc --go_out=. *.proto

语法

book/book.proto

syntax="proto3";
package book;

// import "xxx/xx.proto"

// 出版社
message Publisher{
    required string name = 1
}  
// 书籍信息
message Book {
     required string name = 1;
    message Author {
        required string name = 1;
        required string address = 1;
    }
    required Author author = 2;

    enum BookType{
        SCIENCE = 1 ;
        LITERATURE = 2;
    }

    optional BookType type = 3;
    optional Publisher publisher = 4
}
  • syntax="proto3":指定protobuf的版本
  • package book:声明一个报名,一般与文件目录名相同
  • import "xxx/xx.proto":导入其他的包,这样你就可以使用其他的包的数据结构
  • required、optional、repeated:表示该字段是否必须填充;required表示必须指定且只能指定一个;当optional表示可选,可指定也可不指定,但不可超过一个不指定值的时候会采用空值,如string类型的字段会用字符串表示;repeated表示可以重复,类似与编程语言中的list
  • message Author:在一个message体内定义一个message结构体
  • enum:是枚举类型结构体
  • 数字:字段的标识符,不可重复
  • 数据类型: int32、int64、uint32、uint64、sint32、sint64、double、float、 string、bool、bytes、enum、message等等

在golang使用

protobuf采用以上的book.proto文件

并使用以下命令生成go文件

protoc --go_out=. *.proto

在代码中使用

package main

import (
    b "book"
    "github.com/golang/protobuf/proto"
)

func main(){
    ...
    // 将实例转为proto编码
    var b = &b.Book{Name:"xxx", Author:b.Author{Name:"yyy"}}
    protoBook, err := proto.Marshal(b)
    ...
    // 讲proto编码转化为实例
    var b2 b.Book
    err = proto.Unmarshal(protoBook, &b2)    
    ...
}

grpc

gRPC是由Google主导开发的RPC框架,使用HTTP/2协议并用ProtoBuf作为序列化工具。其客户端提供Objective-C、Java接口,服务器侧则有Java、Golang、C++等接口。使用grpc可以方便的调用其他进程的方法,调用需要传输的数据使用的是proto编码。这对于大型项目来说,可以有效的提高数据的解编码效率和数据传输率。

proto service定义

一个RPC service就是一个能够通过参数和返回值进行远程调用的method,我们可以简单地将它理解成一个函数。因为gRPC是通过将数据编码成protocal buffer来实现传输的。因此,我们通过protocal buffers interface definitioin language(IDL)来定义service method,同时将参数和返回值也定义成protocal buffer message类型。具体实现如下所示,包含下面代码的文件叫helloworld.proto:

syntax = "proto3";
 
 package helloworld;
 
// 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;
}

接着,根据上述定义的service,我们可以利用protocal buffer compiler ,即protoc生成相应的服务器端和客户端的GoLang代码。生成的代码中包含了客户端能够进行RPC的方法以及服务器端需要进行实现的接口。

假设现在所在的目录是$GOPATH/src/helloworld/helloworld,我们将通过如下命令生成gRPC对应的GoLang代码:

protoc --go_out=plugins=grpc:. helloworld.proto

此时,将在目录下生成helloworld.pb.go文件

server

server.go

package main
 
// server.go
 
import (
    "log"
    "net"
 
    "golang.org/x/net/context"
    "google.golang.org/grpc"
    pb "helloworld/helloworld"
)
 
const (
    port = ":50051"
)
 
type server struct {}

// 当接收到请求的时候回调用该方法
// 参数由grpc自己根据请求进行构造 
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}
 
func main() {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatal("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})
    s.Serve(lis)
}

其中pb是我们刚才根据proto生成的go文件的包

client

package main
 
//client.go
 
import (
    "log"
    "os"
 
    "golang.org/x/net/context"
    "google.golang.org/grpc"
    pb "helloworld/helloworld"
)
 
const (
    address     = "localhost:50051"
    defaultName = "world"
)
 
func main() {
    // 建立一个grpc连接
    conn, err := grpc.Dial(address, grpc.WithInsecure())
    if err != nil {
        log.Fatal("did not connect: %v", err)
    }
    defer conn.Close()
    // 新建一个客户端,方法为:NewXXXClinent(conn),XXX为你在proto定义的服务的名字
    c := pb.NewGreeterClient(conn)
 
    name := defaultName
    if len(os.Args) >1 {
        name = os.Args[1]
    }
    // 调用远程,并得到返回
    r, err := c.SayHello(context.Background(), &pb.HelloRequest{Name: name})
    if err != nil {
        log.Fatal("could not greet: %v", err)
    }
    log.Printf("Greeting: %s", r.Message)
}

restful转grpc

使用grpc的优点很多,二进制的数据可以加快传输速度,基于http2的多路复用可以减少服务之间的连接次数,和函数一样的调用方式也有效的提升了开发效率。不过使用grpc也会面临一个问题,我们的微服务对外一定是要提供Restful接口的,如果内部调用使用grpc,在某些情况下要同时提供一个功能的两套API接口,这样就不仅降低了开发效率,也增加了调试的复杂度。于是就想着有没有一个转换机制,让Restful和gprc可以相互转化。

grpc-gateway应运而生

安装

首先你得要根据本文之前的步骤安装proto和grpc,然后如下安装一些库

go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger
go get -u github.com/golang/protobuf/protoc-gen-go

用法

定义service的proto文件

syntax = "proto3";
 package example;

 import "google/api/annotations.proto";

 message StringMessage {
   string value = 1;
 }
 
 service YourService {
   rpc Echo(StringMessage) returns (StringMessage) {
     option (google.api.http) = {
       post: "/v1/example/echo"
       body: "*"
     };
   }
 }

option 表示处理哪些path的请求以及如何处理请求体(参数),见https://cloud.google.com/serv...

生成go文件

protoc -I/usr/local/include -I. \
  -I$GOPATH/src \
  -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
  --go_out=plugins=grpc:. \
  path/to/your_service.proto

protoc -I/usr/local/include -I. \
  -I$GOPATH/src \
  -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
  --grpc-gateway_out=logtostderr=true:. \
  path/to/your_service.proto

以上生成的两个文件,第一个是pb.go文件,给grpc server用的;第二个是pb.gw.go文件,给grpc-gateway用的,用于grpc和restful的相互转化

服务器

package main

import (
  "flag"
  "net/http"

  "github.com/golang/glog"
  "golang.org/x/net/context"
  "github.com/grpc-ecosystem/grpc-gateway/runtime"
  "google.golang.org/grpc"
    
  gw "path/to/your_service_package"
)

var (
  echoEndpoint = flag.String("echo_endpoint", "localhost:9090", "endpoint of YourService")
)

func run() error {
  ctx := context.Background()
  ctx, cancel := context.WithCancel(ctx)
  defer cancel()

  mux := runtime.NewServeMux()
  opts := []grpc.DialOption{grpc.WithInsecure()}
  err := gw.RegisterYourServiceHandlerFromEndpoint(ctx, mux, *echoEndpoint, opts)
  if err != nil {
    return err
  }

  return http.ListenAndServe(":8080", mux)
}

func main() {
  flag.Parse()
  defer glog.Flush()

  if err := run(); err != nil {
    glog.Fatal(err)
  }
}

测试

curl -X POST -k http://localhost:8080/v1/example/echo -d '{"name": " world"}

{"message":"Hello  world"}

流程如下:curl用post向gateway发送请求,gateway作为proxy将请求转化一下通过grpc转发给greeter_server,greeter_server通过grpc返回结果,gateway收到结果后,转化成json返回给前端。

 

Grpc+Grpc Gateway实践

https://segmentfault.com/a/1190000013339403

假定我们有一个项目需求,希望用Rpc作为内部API的通讯,同时也想对外提供Restful Api,写两套又太繁琐不符合

 

于是我们想到了Grpc以及Grpc Gateway,这就是我们所需要的

 

介绍与环境安装

准备环节

在正式开始我们的Grpc+Grpc Gateway实践前,我们需要先配置好我们的开发环境

  • Grpc
  • Protoc Plugin
  • Protocol Buffers
  • Grpc-gateway

Grpc

是什么

Google对Grpc的定义:

A high performance, open-source universal RPC framework

也就是Grpc是一个高性能、开源的通用RPC框架,具有以下特性:

  • 强大的IDL,使用Protocol Buffers作为数据交换的格式,支持v2v3(推荐v3
  • 跨语言、跨平台,也就是Grpc支持多种平台和语言
  • 支持HTTP2,双向传输、多路复用、认证等

安装

1、官方推荐(需科学上网)

go get -u google.golang.org/grpc

2、通过github.com

进入到第一个$GOTPATH目录(因为go get 会默认安装在第一个下)下,新建google.golang.org目录,拉取golanggithub上的镜像库:

cd /usr/local/go/path/src   

mkdir google.golang.org

cd google.golang.org/

git clone https://github.com/grpc/grpc-go

mv grpc-go/ grpc/

目录结构:

google.golang.org/
└── grpc
    ...

而在grpc下有许多常用的包,例如:

  • metadata:定义了grpc所支持的元数据结构,包中方法可以对MD进行获取和处理
  • credentials:实现了grpc所支持的各种认证凭据,封装了客户端对服务端进行身份验证所需要的所有状态,并做出各种断言
  • codes:定义了grpc使用的标准错误码,可通用

Protoc Plugin

是什么

编译器插件

安装

go get -u github.com/golang/protobuf/protoc-gen-go

Protoc Plugin的可执行文件从$GOPATH中移动到$GOBIN下

mv /usr/local/go/path/bin/protoc-gen-go /usr/local/go/bin/

Protocol Buffers v3

是什么

Protocol buffers are a flexible, efficient, automated mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages. You can even update your data structure without breaking deployed programs that are compiled against the "old" format.

Protocol BuffersGoogle推出的一种数据描述语言,支持多语言、多平台,它是一种二进制的格式,总得来说就是更小、更快、更简单、更灵活,目前分别有v2v3的版本,我们推荐使用v3

建议可以阅读下官方文档的介绍,本系列会在使用时简单介绍所涉及的内容

安装

wget https://github.com/google/protobuf/releases/download/v3.5.1/protobuf-all-3.5.1.zip
unzip protobuf-all-3.5.1.zip
cd protobuf-3.5.1/
./configure
make
make install

检查是否安装成功

protoc --version

如果出现报错

protoc: error while loading shared libraries: libprotobuf.so.15: cannot open shared object file: No such fileor directory

则执行ldconfig后,再次运行即可成功

为什么要执行ldconfig

我们通过控制台输出的信息可以知道,Protocol Buffers Libraries的默认安装路径在/usr/local/lib

Libraries have been installed in:
   /usr/local/lib

If you ever happen to want to link against installed libraries
in a given directory, LIBDIR, you must either use libtool, and
specify the full pathname of the library, or use the `-LLIBDIR'
flag during linking and do at least one of the following:
   - add LIBDIR to the `LD_LIBRARY_PATH' environment variable
     during execution
   - add LIBDIR to the `LD_RUN_PATH' environment variable
     during linking
   - use the `-Wl,-rpath -Wl,LIBDIR' linker flag
   - have your system administrator add LIBDIR to `/etc/ld.so.conf'

See any operating system documentation about shared libraries for
more information, such as the ld(1) and ld.so(8) manual pages.

而我们安装了一个新的动态链接库,ldconfig一般在系统启动时运行,所以现在会找不到这个lib,因此我们要手动执行ldconfig让动态链接库为系统所共享,它是一个动态链接库管理命令,这就是ldconfig命令的作用

protoc使用

我们按照惯例执行protoc --help(查看帮助文档),我们抽出几个常用的命令进行讲解

1、-IPATH, --proto_path=PATH:指定import搜索的目录,可指定多个,如果不指定则默认当前工作目录

2、--go_out:生成golang源文件

参数

若要将额外的参数传递给插件,可使用从输出目录中分离出来的逗号分隔的参数列表:

protoc --go_out=plugins=grpc,import_path=mypackage:. *.proto
  • import_prefix=xxx:将指定前缀添加到所有import路径的开头
  • import_path=foo/bar:如果文件没有声明go_package,则用作包。如果它包含斜杠,那么最右边的斜杠将被忽略。
  • plugins=plugin1+plugin2:指定要加载的子插件列表(我们所下载的repo中唯一的插件是grpc)
  • Mfoo/bar.proto=quux/shme: M参数,指定.proto文件编译后的包名(foo/bar.proto编译后为包名为quux/shme

Grpc支持

如果proto文件指定了RPC服务,protoc-gen-go可以生成与grpc相兼容的代码,我们仅需要将plugins=grpc参数传递给--go_out,就可以达到这个目的

protoc --go_out=plugins=grpc:. *.proto

Grpc-gateway

是什么

grpc-gateway is a plugin of protoc. It reads gRPC service definition, and generates a reverse-proxy server which translates a RESTful JSON API into gRPC. This server is generated according to custom options in your gRPC definition.

grpc-gateway是protoc的一个插件。它读取gRPC服务定义,并生成一个反向代理服务器,将RESTful JSON API转换为gRPC。此服务器是根据gRPC定义中的自定义选项生成的。

安装

go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway

如果出现以下报错,我们分析错误提示可得知是连接超时(大概是被墙了)

package google.golang.org/genproto/googleapis/api/annotations: unrecognized import path "google.golang.org/genproto/googleapis/api/annotations" (https fetch: Get https://google.golang.org/genproto/googleapis/api/annotations?go-get=1: dial tcp 216.239.37.1:443: getsockopt: connection timed out)

有两种解决方法,

1、科学上网

2、通过github.com

进入到第一个$GOPATH目录的google.golang.org目录下,拉取genprotogithub上的go-genproto镜像库:

cd /usr/local/go/path/src/google.golang.org

git clone https://github.com/google/go-genproto.git

mv go-genproto/ genproto/

在安装完毕后,我们将grpc-gateway的可执行文件从$GOPATH中移动到$GOBIN

mv /usr/local/go/path/bin/protoc-gen-grpc-gateway /usr/local/go/bin/

到这里我们这节就基本完成了,建议多反复看几遍加深对各个组件的理解!

参考

示例代码