浅谈gRPC

gRPC

什么是gRPC

gRPC是一个高性能、开源和通用的RPC框架,面向服务端和移动端,基于HTTP/2协议设计。

gRPC目前提供 C、Java 和 Go 语言版本,分别是:grpc, grpc-java, grpc-go. 其中 C 版本支持 C, C++, Node.js, Python, Ruby, Objective-C, PHP 和 C#。 本文当主要针对grpc C语言版本进行说明。

在 gRPC 里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,使得您能够更容易地创建分布式应用和服务。与许多 RPC 系统类似,gRPC 也是基于以下理念:定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个接口,并运行一个 gRPC 服务器来处理客户端调用。在客户端拥有一个存根能够像服务端一样的方法。

avatar

gRPC 客户端和服务端可以在多种环境中运行和交互 - 从 google 内部的服务器到你自己的笔记本,并且可以用任何 gRPC 支持的语言来编写。所以,你可以很容易地用 Java 创建一个 gRPC 服务端,用 Go、Python、Ruby 来创建客户端。此外,Google 最新 API 将有 gRPC 版本的接口,使你很容易地将 Google 的功能集成到你的应用里。

gRPC特点

  1. 语言中立,支持多种语言;
  2. 基于IDL文件定义服务,通过proto3工具生成指定语言的数据结构、服务端接口以及客户端Stub;
  3. 通信协议基于标准的HTTP/2设计,支持双向流、消息头压缩、单TCP的多路复用、服务端推送等特性,这些特性使得gRPC在移动端设备上更加省电和节省网络流量;
  4. 编码格式序列化支持PB(Protocol Buffer)和JSON,PB是一种语言无关的高性能序列化框架,基于HTTP/2+PB,保障了RPC调用的高性能。

通信协议HTTP/2

gRPC基于HTTP/2标准设计,所以相对于其他RPC框架,gRPC带来了更多强大功能,如双向流、头部压缩、多复用请求等。这些功能给移动设备带来重大益处,如节省带宽、降低TCP链接次数、节省CPU使用和延长电池寿命等。同时,gRPC还能够提高了云端服务和Web应用的性能。gRPC既能够在客户端应用,也能够在服务器端应用,从而以透明的方式实现客户端和服务器端的通信和简化通信系统的构建。

编码协议

gRPC编码格式序列化支持PB(Protocol Buffer)和JSON。默认使用PB。

protocol buffer简介

protobuffer是google开发的一种数据描述语言,它能够将结构化的数据序列化,并切可以将序列化的数据进行反序列化恢复原有的数据结构。一般用于数据存储以及通信协议方面。

官方文档:https://developers.google.com/protocol-buffers/docs/overview

protocol buffer特点

  1. 平台无关、语言无关;
  2. 二进制格式、数据自描述,相比于xml和json的文本格式,体积更小,解析速度更快;
  3. 使用proto Buffer的编译器,可以生成方便在编程中使用的数据访问代码,从而提供了完整详细的操作API;
  4. 具有更好的兼容性,很好的支持向上或向下兼容的特性;
  5. 提供多种序列化的出口和入口,如文件流,string流,array流等等。

gRPC安装

gRPC依赖于ProtoBuf,在linux系统下,通过以下步骤就可以安装好gRPC和ProtoBuf。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1. sudo yum install build-essential autoconf libtool libgflags-dev libgtest-dev clang libc++-dev pkg-config unzip

2. git clone -b $(curl -L http://grpc.io/release) https://github.com/grpc/grpc

3. cd grpc

4. git submodule update --init

5. make

6. sudo make install

7. cd third_party/protobuf

8. sudo ./autogen.sh

9. sudo ./configure

10. make

11. sudo make install

服务定义

  • 简单RPC,客户端使用存根发送请求到服务器并等待响应返回,就像平常的函数调用一样

    1
    2
    // Obtains the feature at a given position.
    rpc GetFeature(Point) returns (Feature) {}
  • 服务器端流式RPC,客户端发送请求到服务器,拿到一个流去读取返回的消息序列。 客户端读取返回的流,直到里面没有任何消息。从例子中可以看出,通过在 响应 类型前插入stream关键字,可以指定一个服务器端的流方法。

    1
    2
    3
    4
    5
    // Obtains the Features available within the given Rectangle.  Results are
    // streamed rather than returned at once (e.g. in a response message with a
    // repeated field), as the rectangle may cover a large area and contain a
    // huge number of features.
    rpc ListFeatures(Rectangle) returns (stream Feature) {}
  • 客户端流式RPC , 客户端写入一个消息序列并将其发送到服务器,同样也是使用流。一旦客户端完成写入消息,它等待服务器完成读取返回它的响应。通过在 请求 类型前指定stream关键字来指定一个客户端的流方法。

    1
    2
    3
    // Accepts a stream of Points on a route being traversed, returning a
    // RouteSummary when traversal is completed.
    rpc RecordRoute(stream Point) returns (RouteSummary) {}
  • 双向流式RPC,双方使用读写流去发送一个消息序列.两个流独立操作,因此客户端和服务器可以以任意喜欢的顺序读写:比如, 服务器可以在写入响应前等待接收所有的客户端消息,或者可以交替的读取和写入消息,或者其他读写的组合。 每个流中的消息顺序被预留。你可以通过在请求和响应前加stream关键字去制定方法的类型。

    1
    2
    3
    // Accepts a stream of RouteNotes sent while a route is being traversed,
    // while receiving other RouteNotes (e.g. from other users).
    rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}

项目示例

项目背景

一个大型系统需要集成许多微服务,其中包括大华的视频监控。系统框架为Python Django,大华视频提供c++版本SDK。所以使用gRPC框架,定义C++大华视频服务端,系统使用Python客户端调用视频服务端提供接口。

本文档只实现简单的登录大华网络摄像头(球机),登出,及控制其向上移动功能。

定义消息文件及服务

首先需要创建服务,定义我们需要的接口:

* 登录
* 登出
* 向上移动开始
* 向上移动结束

然后定义服务接口所需要的消息,一般包括:

* 客户端发过来的请求消息
* 服务端处理完毕后返回的回复消息

我们可以将服务和消息放在不同的proto文件中,也可以将它们放在一个文件中。

创建proto文件:dahua.proto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
syntax = "proto3";
package dahua;

//服务及接口
service Dahua {
rpc Login(LoginRequest) returns (LoginResponse) {} // 设备登录
rpc Logout(LogoutRequest) returns (LogoutResponse) {} // 设备登出
rpc UpBegin(UpBeginRequest) returns (UpBeginResponse) {} // 视角向上移动开始
rpc UpEnd(UpEndRequest) returns (UpEndResponse) {} // 视角向上移动结束
}

//消息
message DeviceInfo {
string addr = 1;
int32 port = 2;
string username = 3;
string password = 4;
}

message LoginRequest {
string session = 1;
DeviceInfo device = 2;
}

message LoginResponse {
int32 status = 1;
}

message LogoutRequest {
string session = 1;
}

message LogoutResponse {
int32 status = 1;
}

message UpBeginRequest {
string session = 1;
}

message UpBeginResponse {
int32 status = 1;
}

message UpEndRequest {
string session = 1;
}

message UpEndResponse {
int32 status = 1;
}

编译proto文件

因为服务端使用C++语言,客户端使用Python语言,所以需要使用ProtoBuf的编译器编译proto文件,来生成对应语言所能使用的文件。

编译前目录结构:

1
2
3
4
5
$ tree
├── cpp
├── python
└── protos
└── dahua.proto

  • 编译C++所需文件,执行命令:

    1
    2
    3
    protoc -I protos --grpc_out=cpp --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` protos/dahua.proto

    protoc -I protos --cpp_out=cpp protos/dahua.proto
  • 编译Python需要文件,执行命令:

    1
    2
    3
    protoc -I protos --grpc_out=python --plugin=protoc-gen-grpc=`which grpc_python_plugin` protos/dahua.proto

    protoc -I protos --python_out=python protos/dahua.proto

编译后目录结构:

1
2
3
4
5
6
7
8
9
10
11
$ tree
├── cpp
│ ├── dahua.grpc.pb.cc #包含服务类的实现
│ ├── dahua.grpc.pb.h #声明生成的服务类的头文件
│ ├── dahua.pb.cc #包含消息类的实现
│ └── dahua.pb.h #声明生成的消息类的头文件
├── python
│ ├── dahua_pb2.py #消息类实现
│ └── dahua_pb2_grpc.py #服务类实现
└── protos
└── dahua.proto

可以看到,protoc通过grpc_cpp_plugin在cpp目录下生成C++项目所能引入的.h文件和.cc文件,通过grpc_python_plugin在python目录下生成了Python项目所能导入的.py文件

grpc_cpp_plugingrpc_python_plugin,在正确安装完gRPC时,应该就在linux环境下生成了。

生成服务端接口

创建server.h文件, 通过继承dahua.grpc.pb.h文件中声明的服务类,重写其服务接口,来完成我们实际需要提供的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include <cstdlib>
#include <stdio.h>
#include <iostream>
#include <mutex>
#include <map>
#include <grpc/grpc.h>
#include <grpc++/server.h>
#include <grpc++/server_builder.h>
#include <grpc++/server_context.h>
#include <grpc++/security/server_credentials.h>
#include "dhagent.h" //引入自定义后的大华sdk接口
#include "dahua.grpc.pb.h" //引入服务类
#include "dahua.pb.h" //引入消息类

using dahua::Dahua;
using dahua::DeviceInfo;
using dahua::LoginRequest;
using dahua::LoginResponse;
using dahua::LogoutRequest;
using dahua::LogoutResponse;
using dahua::UpBeginRequest;
using dahua::UpBeginResponse;
using dahua::UpEndRequest;
using dahua::UpEndResponse;
using namespace std;

//继承服务类
class DahuaServer final : public Dahua::Service
{
public:
DahuaServer() {
}

~DahuaServer() {
}

//重写服务类方法
grpc::Status Login(grpc::ServerContext *context, const LoginRequest *request,
LoginResponse *response) override
{
DeviceInfo device = request->device();
string session = request->session();
string addr = device.addr();
if (session != addr) {
printf("addr not match");
}

int error = mAgent.Login(addr, device.username(), device.port(), device.password());
response->set_status(error);
return grpc::Status::OK;
}

grpc::Status Logout(grpc::ServerContext *context, const LogoutRequest *request,
LogoutResponse *response) override
{
int status = 0;
string addr = request->session();
bool error = mAgent.Logout(addr);
if (!error) {
status = 1;
}

response->set_status(status);
return grpc::Status::OK;
}

grpc::Status UpBegin(grpc::ServerContext *context, const UpBeginRequest *request,
UpBeginResponse *response) override
{
std::cout << "stub up begin" << std::endl;
string addr = request->session();
mAgent.UpBegin(addr);

return grpc::Status::OK;

}

grpc::Status UpEnd(grpc::ServerContext *context, const UpEndRequest *request,
UpEndResponse *response) override
{
std::cout << "stub up end" << std::endl;
string addr = request->session();
mAgent.UpEnd(addr);
return grpc::Status::OK;
}

private:
DahuaAgent mAgent;
}

在这个场景下,我们实现的是 同步 版本的接口调用,它提供了 gRPC 服务器缺省的行为。同时,也有可能去实现一个异步的接口 Dahua::AsyncService,它允许你进一步定制服务器线程的行为。

启动服务器

一旦我们实现了所有的方法,我们还需要启动一个gRPC服务器,这样客户端才可以使用服务。

创建main.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <cstdlib>
#include <stdio.h>
#include <unistd.h>
#include <iostream>
#include <grpc/grpc.h>
#include <grpc++/server.h>
#include <grpc++/server_builder.h>
#include <grpc++/server_context.h>
#include <grpc++/security/server_credentials.h>
#include "server.h"
#include <string>

int main(int argc, char *argv[])
{
std::string addr = "0.0.0.0:5000";
DahuaServer service;
grpc::ServerBuilder builder;
builder.AddListeningPort(addr, grpc::InsecureServerCredentials());
builder.RegisterService(&service);
auto server = builder.BuildAndStart();
std::cout << "Server listening on " << addr << std::endl;
server->Wait();

return 0;
}

我们通过使用ServerBuilder去构建和启动服务器。为了做到这点,我们需要:

  1. 创建我们的服务实现类 RouteGuideImpl 的一个实例。
  2. 创建工厂类 ServerBuilder 的一个实例。
  3. 在生成器的 AddListeningPort() 方法中指定客户端请求时监听的地址和端口。
  4. 用生成器注册我们的服务实现。
  5. 调用生成器的 BuildAndStart() 方法为我们的服务创建和启动一个RPC服务器。
  6. 调用服务器的 Wait() 方法实现阻塞等待,直到进程被杀死或者 Shutdown() 被调用。

将C++服务项目编译成二进制文件server,运行服务,它将监听5000端口。

创建客户端

为了能调用服务的方法,我们得先创建一个存根

1
2
conn = grpc.insecure_channel('localhost', 5000)
client = dahua_pb2_grpc.DahuaStub(channel=conn)

因为客户端使用Python,首先得导入grpc的python包。

1
2
3
pip install grpcio==1.14.1
pip install protobuf==3.6.1
pip install grpcio-tools==1.14.1

创建client.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import grpc
from python import dahua_pb2, dahua_pb2_grpc

_HOST = '127.0.0.1'
_PORT = '5000'

def login(addr, port, username, password):
conn = grpc.insecure_channel(_HOST + ':' + _PORT)
client = dahua_pb2_grpc.DahuaStub(channel=conn)
device = dahua_pb2.DeviceInfo(addr=addr, port=port, username=username, password=password)
request = dahua_pb2.LoginRequest(session=addr, device=device)

try:
response = client.Login(request)
except:
return -1

print("received: " + str(response.status))
return response.status

def logout(addr):
conn = grpc.insecure_channel(_HOST + ':' + _PORT)
client = dahua_pb2_grpc.DahuaStub(channel=conn)
request = dahua_pb2.LogoutRequest(session=addr)

try:
response = client.Logout(request)
except:
return -1

print("received: " + str(response.status))
return response.status

def UpBegin(addr):
conn = grpc.insecure_channel(_HOST + ':' + _PORT)
client = dahua_pb2_grpc.DahuaStub(channel=conn)
request = dahua_pb2.UpBeginRequest(session=addr)

try:
response = client.UpBegin(request)
except:
return -1

print("received: " + str(response.status))
return response.status

def UpEnd(addr):
conn = grpc.insecure_channel(_HOST + ':' + _PORT)
client = dahua_pb2_grpc.DahuaStub(channel=conn)
request = dahua_pb2.UpEndRequest(session=addr)

try:
response = client.UpEnd(request)
except:
return -1

print("received: " + str(response.status))
return response.status

访问上述接口,即可调用服务端对应的接口。

项目示例基本只实现了最简单的RPC接口调用。