ProtoBuf 是一套接口描述语言(Interface Definition Language,IDL ),类似 Apache 的 Thrift 。
相关处理工具主要是 protoc ,基于 C++ 语言实现。
用户写好 .proto
描述文件,之后便可以使用 protoc 自动编译生成众多计算机语言(C++、Java、Python、C#、Go 等)的接口代码。这些代码可以支持 gRPC,也可以不支持。
gRPC 是 Google 开源的 RPC 框架和库,已支持主流计算机语言。底层通信采用 HTTP2 协议,比较适合互联网场景。gRPC 在设计上考虑了跟 ProtoBuf 的配合使用。
两者分别解决不同问题,可以配合使用,也可以分开单独使用。
典型的配合使用场景是,写好 .proto
描述文件定义 RPC 的接口,然后用 protoc(带 gRPC 插件)基于 .proto
模板自动生成客户端和服务端的接口代码。
ProtoBuf
需要工具主要包括:
运行环境:各种语言的 protobuf 库,不同语言有不同的安装来源;
语法类似 C++ 语言,可以参考 ProtoBuf 语言规范:https://developers.google.com/protocol-buffers/docs/proto。
比较核心的,message
是代表数据结构(里面可以包括不同类型的成员变量,包括字符串、数字、数组、字典……),service
代表 RPC 接口。变量后面的数字是代表进行二进制编码时候的提示信息,1~15 表示热变量,会用较少的字节来编码。另外,支持导入。
默认所有变量都是可选的(optional),repeated 则表示数组。主要 service rpc 接口接受单个 message 参数,返回单个 message。
参考官方给出示例,如下所示:
复制 syntax = "proto3" ;
package helloworld ;
// The greeting 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 ;
}
编译最关键的参数是输出语言格式参数,例如,python 为 --python_out=OUT_DIR
。
一些还没有官方支持的语言,可以通过安装 protoc 对应的 plugin 来支持。例如,对于 Go 语言,可以安装
复制 $ go get -u github.com/golang/protobuf/{protoc-gen-go,proto} // 前者是 plugin;后者是 go 的依赖库
之后,正常使用 protoc --go_out=./ ./hello.proto
命令调用 protoc-gen-go
插件来生成 hello.pb.go。
ProtoBuf 提供了 Marshal/Unmarshal
方法来将数据结构进行序列化操作。所生成的二进制文件在存储效率上比 XML 高 310 倍,并且处理性能高 1 2 个数量级。
gRPC
相关工具主要包括:
运行时库:各种不同语言有不同的安装方法 ,主流语言的包管理器都已支持。
protoc,以及 gRPC 插件和其它插件:采用 ProtoBuf 作为 IDL 时,对 .proto 文件进行编译处理。
类似其它 RPC 框架,gRPC 的库在服务端提供一个 gRPC Server,客户端的库是 gRPC Stub。典型的场景是客户端发送请求,同步或异步调用服务端的接口。客户端和服务端之间的通信协议是基于 HTTP2 的 gRPC 协议,支持双工的流式保序消息,性能比较好,同时也很轻。
采用 ProtoBuf 作为 IDL,则需要定义 service 类型。生成客户端和服务端代码。用户自行实现服务端代码中的调用接口,并且利用客户端代码来发起请求到服务端。一个完整的例子可以参考 https://github.com/grpc/grpc-go/blob/master/examples/helloworld 。
以上面 proto 文件为例,需要执行时添加 gRPC 的 plugin:
复制 $ protoc --go_out=plugins=grpc:. hello.proto
执行后会生成对应的 .pb.go 文件,其中包括了 proto 文件中指定的结构和 protobuf 协议相关转换代码。
gRPC 更多原理可以参考官方文档:http://www.grpc.io/docs 。
实现服务端代码
生成的 .pb.go 文件中,服务端相关的核心代码如下,主要定义了 GreeterServer 接口,用户可以自行修改或编写实现代码。
复制 type GreeterServer interface {
SayHello ( context . Context , * HelloRequest ) ( * HelloReply , error )
}
func RegisterGreeterServer (s * grpc . Server , srv GreeterServer ) {
s. RegisterService ( & _Greeter_serviceDesc, srv)
}
用户需要自行实现服务端接口,代码如下。
比较重要的,创建并启动一个 gRPC 服务的过程:
创建监听套接字:lis, err := net.Listen("tcp", port)
;
注册服务:pb.RegisterGreeterServer()
;
复制 package main
import (
"context"
"fmt"
"log"
"net"
"google.golang.org/grpc"
pb "hello/hello"
)
const (
port = ":50051"
)
type server struct { }
// 这里实现服务端接口中的方法。
func (s * server ) SayHello (ctx context . Context , in * pb . HelloRequest ) ( * pb . HelloReply , error ) {
log. Printf ( "Received: %v \n" , in. GetName ())
return & pb . HelloReply {Message: "Hello " + in. GetName ()}, nil
}
// 创建并启动一个 gRPC 服务的过程:创建监听套接字、创建服务端、注册服务、启动服务端。
func main () {
lis, err := net. Listen ( "tcp" , port)
if err != nil {
log. Fatalf ( "failed to listen: %v " , err)
}
s := grpc. NewServer ()
pb. RegisterGreeterServer (s, & server {})
fmt. Printf ( "Starting listen on port: %s " , port)
if err := s. Serve (lis); err != nil {
log. Fatalf ( "failed to serve: %v " , err)
}
}
编译并启动服务端。
复制 $ go run server/server.go
Starting listen on port: :50051
生成客户端代码
生成的 Go 文件中客户端相关代码如下,主要和实现了 HelloServiceClient 接口。用户可以通过 gRPC 来直接调用这个接口。
复制 type GreeterClient interface {
// Sends a greeting
SayHello (ctx context . Context , in * HelloRequest , opts ... grpc . CallOption ) ( * HelloReply , error )
}
type greeterClient struct {
cc * grpc . ClientConn
}
func NewGreeterClient (cc * grpc . ClientConn ) GreeterClient {
return & greeterClient {cc}
}
func (c * greeterClient ) SayHello (ctx context . Context , in * HelloRequest , opts ... grpc . CallOption ) ( * HelloReply , error ) {
out := new ( HelloReply )
err := c.cc. Invoke (ctx, "/helloworld.Greeter/SayHello" , in, out, opts ... )
if err != nil {
return nil , err
}
return out, nil
}
用户直接调用接口方法:创建连接、创建客户端、调用接口。
复制 package main
import (
"context"
"log"
"os"
"time"
"google.golang.org/grpc"
pb "hello/hello"
)
const (
address = "localhost:50051"
defaultName = "world"
)
func main () {
// Set up a connection to the server.
conn, err := grpc. Dial (address, grpc. WithInsecure (), grpc. WithBlock ())
if err != nil {
log. Fatalf ( "did not connect: %v " , err)
}
defer conn. Close ()
c := pb. NewGreeterClient (conn)
// Contact the server and print out its response.
name := defaultName
if len (os.Args) > 1 {
name = os.Args[ 1 ]
}
ctx, cancel := context. WithTimeout (context. Background (), time.Second)
defer cancel ()
r, err := c. SayHello (ctx, & pb . HelloRequest {Name: name})
if err != nil {
log. Fatalf ( "could not greet: %v " , err)
}
log. Printf ( "Greeting: %s " , r. GetMessage ())
}
编译并启动客户端,可以查看到服务端返回的消息。
复制 $ go run client/client.go
Greeting: Hello world