ML&DEV[10] | gRPC的应用
【ML&DEV】
这是大家没有看过的船新栏目!ML表示机器学习,DEV表示开发,本专栏旨在为大家分享作为算法工程师的工作,机器学习生态下的有关模型方法和技术,从数据生产到模型部署维护监控全流程,预备知识、理论、技术、经验等都会涉及,近期内容以入门线路为主,敬请期待!
往期回顾:
上一期和大家谈到了gRPC的快速开始,我么哪知道了gRPC是什么以及怎么快速启动,那么现在,我们来看看这个玩意具体内部是怎么运作的,这里我们同样以helloworld这个为例子来去谈。首先上期内容在这里:
当然的,大家也可以直接去看官方的教程,这里会教你怎么去做,但是我会和大家谈更多的内部细节的东西,可别错过啦:https://github.com/grpc/grpc/tree/master/examples/cpp/helloworld
我发现这下面基本就是c++的东西了,这意味着c++的入门线路安排上了好吧。
基本结构
这里有必要和大家再啰嗦一下具体的架构模式。
左边是一个c++写的gRPC_server服务,服务大家可以理解为一个服务台,一个专门的服务台,你有特定的需求就找他的那种,他能给你提供相应的服务,他是并非万能,而是具备专项的能力。
右边是各种别的语言或者环境写的客户端,有Ruby的,也有Client的,当然也可以是c++的甚至是别的,客户端可以理解为一个客户,这个客户有特定需求需要访问服务端以满足特定的需求,此时就需要做出请求。
需要指出的是,客户端和服务端两者的关系是相对于这个任务而言的,你需要完成某个任务的时候,你要请求别人,那你就是客户端,但是你作为一个服务要被人请求的时候,你就是一个服务端,对于一个项目,他可能是服务端也可能是一个客户端(这么理解吧,你舔的女神可能是别人的舔狗)。
由于服务端的功能一般是单一的,它只能完成特定的功能,为了保证两者的沟通是有效的,服务端和客户端之间就要订一个协议,可以理解为沟通的暗号,这个协议在我的理解下要完成这几个功能:
客户端提供给服务端需要的数据,例如要验证账号密码正确,那肯定一个需要把账号密码传过去。要做计算,肯定你给要把相应的特征和参数传过去。
服务端计算完成的结果,即客户端需要的内容。
一般的,可能我们会用xml或者是json,在gRPC中,这个协议就是protobuf,下面会谈到。
还有两个需要指出的概念,客户端向服务端发出的信息被称为你请求,英文是request,服务端接收客户端的请求后,返回给客户端的信息成为返回,英文是response,有的地方也叫做reply。
protobuf
要谈到gRPC的运作,肯定就要谈到protobuf了,这是一个谷歌开源的语言无关、平台无关、可拓展的序列化数据结构方法,类似XML等,但是速度更快,体积更小。
他的结构是这样的:
message Example1{
optional string stringVal = 1;
optional bytes bytesVal = 2;
message EmbeddedMessage{
int32 int32Val = 1;
string stringVal = 2;
}
optional EmbeddedMessage embeddedExample1 = 3;
repeated int32 repeatedInt32Val = 4;
repeated string repeatedStringVal = 5;
}
可以看到这个结构很像json,但是又不太一样,主要是他有了比较严谨的数据类型定义。
在这里,我们只要知道它的基本结构和编写方式即可。protobuf的具体格式和入门可以参照这个:https://developers.google.com/protocol-buffers/docs/cpptutorial,如果是打不开,可以看这个,中文版的:https://www.jianshu.com/p/d2bed3614259。
然后我们回过头来看helloworld的pb,这个的具体位置这里:grpc/examples/protos/helloworld.proto
。首先我们可以很明显的看到两个东西,分别是 HelloRequest
和 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;
}
这就是所谓的请求格式和返回格式,请求内容里需要一个 name
字段,返回的结果中需要一个 message
字段。
另外,就是我们需要订立的服务了,可以理解为将 HelloRequest
和 HelloReply
定义为服务的定义。
// The greeting service definition.
service Greeter{
// Sends a greeting
rpc SayHello(HelloRequest) returns (HelloReply) {}
}
服务的名称是 Greeter
,里面是一个 rpc
服务,可以看到里面类似一个非常简单的函数定义,输入是 HelloRequest
输出是 HelloReply
,这里非常好理解,足够了,然后我们就可以开始构建项目了。
gRPC服务的构建
这里我主要用的是make的方式(一种编译c++的方法,g++的一种升级版)去编译构造,那肯定要从makefile去看了,回到helloworld项目里面去。
首先,我们要想办法把上面的proto插入到项目去,这个非常好理解。
protoc -I ../../protos/ --grpc_out=. --plugin=protoc-gen-grpc=grpc_cpp_plugin ../../protos/helloworld.proto
protoc -I ../../protos/ --cpp_out=. ../../protos/helloworld.proto
一个是grpcout,一个对应cppout。实质上,在makefile里面写的就比较通用了,直接看吧:
.PRECIOUS: %.grpc.pb.cc
%.grpc.pb.cc: %.proto
$(PROTOC) -I $(PROTOS_PATH) --grpc_out=. --plugin=protoc-gen-grpc=$(GRPC_CPP_PLUGIN_PATH) $<
.PRECIOUS: %.pb.cc
%.pb.cc: %.proto
$(PROTOC) -I $(PROTOS_PATH) --cpp_out=. $<
客户端
即 greeter_client.cc
。
首先还是看看比较简单的客户端。直接看main函数应该会比较舒服(不懂c++的看到这里已经觉得快睡着了吧,加油!这这里的main说白了就是个程序入口)。
int main(int argc, char** argv) {
// Instantiate the client. It requires a channel, out of which the actual RPCs
// are created. This channel models a connection to an endpoint (in this case,
// localhost at port 50051). We indicate that the channel isn't authenticated
// (use of InsecureChannelCredentials()).
GreeterClient greeter(grpc::CreateChannel(
"localhost:50051", grpc::InsecureChannelCredentials()));
std::string user("world");
std::string reply = greeter.SayHello(user);
std::cout << "Greeter received: "<< reply << std::endl;
return0;
}
首先,是创建了个
GreeterClient
的实例化对象,里面有端口和一个大家可以理解为备用端口的玩意。然后,就是要构建request了,这的request比较简单,就是一个string字符串(参考之前我们定义的protobuf)。
然后就是去请求了,
greeter.SayHello(user)
,注意这里要提前为他准备一个承接结果的变量(这里是reply,也是striing)。最后就是输出结果了(类似python的print)。
这里看起来都非常简单,毕竟有一个比较完善的类了,那么我们就来看看这个类里面有啥。
classGreeterClient{
public:
GreeterClient(std::shared_ptr<Channel> channel)
: stub_(Greeter::NewStub(channel)) {}
// Assembles the client's payload, sends it and presents the response back
// from the server.
std::stringSayHello(const std::string& user) {
// Data we are sending to the server.
HelloRequest request;
request.set_name(user);
// Container for the data we expect from the server.
HelloReply reply;
// Context for the client. It could be used to convey extra information to
// the server and/or tweak certain RPC behaviors.
ClientContext context;
// The actual RPC.
Status status = stub_->SayHello(&context, request, &reply);
// Act upon its status.
if(status.ok()) {
// std::cout << reply.message() << std::endl;
return reply.message();
} else{
std::cout << status.error_code() << ": "<< status.error_message()
<< std::endl;
return"RPC failed";
}
}
private:
std::unique_ptr<Greeter::Stub> stub_;
};
代码还是比较长的,我们这里直接看我们最关心的 SayHello
吧,这里面我们可以看到里面还有一个 stub_->SayHello
,很明显这里面是一个更加接近真请求的函数,有 context
、 request
和 reply
三种, context
之前没谈到,这里注释也说了是是不被用到的额外信息而已,传进去即可。这里需要注意的是, stub_->SayHello
的结果不是通过返回值来返回,而是通过形参来传出,返回值是成功与否( &
),另外补充说一下,这个 stub_->SayHello
的声明(定义)在 make
构建后的 helloworld.grpc.pb.h
中( Greeter::Stub::SayHello
)。
不往里说,这里已经足够,说白了我们只需要凑出protobuf所约定的数据结构的数据传过去,然后准备一个承接结果的变量接住返回接过即可。
服务端
即 greeter_server.cc
。
这里的main文件更加简单了。
int main(int argc, char** argv) {
RunServer();
return0;
}
没啥好讲的,但是这个 RunServer
肯定有不少好东西。开始之前看看引入了什么东西吧:
#include<iostream>
#include<memory>
#include<string>
#include<grpcpp/grpcpp.h>
#ifdef BAZEL_BUILD
#include"examples/protos/helloworld.grpc.pb.h"
#else
#include"helloworld.grpc.pb.h"
#endif
using grpc::Server;
using grpc::ServerBuilder;
using grpc::ServerContext;
using grpc::Status;
using helloworld::HelloRequest;
using helloworld::HelloReply;
using helloworld::Greeter;
这里可以分为5块吧。
c++内置库。
grpccpp.h。grpc的核心内容。
helloworld.grpc.pb。protobuf构建的grpc服务构建。
grpc相关的构建工具。
helloworld中的是基于protobuf构建的相关数据类,大家非常熟悉这3个东西吧。
voidRunServer() {
std::string server_address("0.0.0.0:50051");
GreeterServiceImpl service;
ServerBuilder builder;
// Listen on the given address without any authentication mechanism.
builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
// Register "service" as the instance through which we'll communicate with
// clients. In this case it corresponds to an *synchronous* service.
builder.RegisterService(&service);
// Finally assemble the server.
std::unique_ptr<Server> server(builder.BuildAndStart());
std::cout << "Server listening on "<< server_address << std::endl;
// Wait for the server to shutdown. Note that some other thread must be
// responsible for shutting down the server for this call to ever return.
server->Wait();
}
这里定义了一个服务的构造器 ServerBuilderbuilder
,去构造一个服务,说白了就是需要一个“监听器”( AddListeningPort
)(之所以叫监听器,是因为服务是一个触发机制,只有触发到对应的命令才会执行,而这个命令就是端口,这里是 0.0.0.0:50051
),构造后就注册一个服务 RegisterService
,然后把这个监听器放进去便开始监听。
这里还没谈到一个关键点,也是我们算法工程师最关心的,那就是服务接收请求后,怎么做我们想做的处理,答案就在这个 GreeterServiceImpl
上,我们是在这里定义的。
// Logic and data behind the server's behavior.
classGreeterServiceImplfinal: publicGreeter::Service{
StatusSayHello(ServerContext* context, constHelloRequest* request,
HelloReply* reply) override{
std::string prefix("Hello ");
reply->set_message(prefix + request->name());
returnStatus::OK;
}
};
这里可以明显的看到,服务的内容就是把用户发送过来的请求 request
中的 name
提取出来,前面加上 Hello
而已。当然的,这里我们可以做更多的事情,一方面我们可以丰富这个类,当然我们也可以新建类来做更复杂的事情。
如此一来,服务就起来了,剩下就等客户端请求了。
开始执行
开始之前,肯定先做好准备工作,那就是编译,之前也提到,就是 make
。
首先需要把服务启动起来(可以理解为店铺开张了)。
nohup ./greet_server &
这里用nohup是要保证这个服务持续运行,这个程序必须持续执行,所以把它放在后台。
然后就可以去请求了。
./greet_client
这时候你会看到返回结果。
Greeter received: Hello world
这里的 Hello
是服务端拼上去的, world
就是客户端写上的, Greeterreceived:
是在客户端请求完成后输出结果时加上去的。来给大家回放一下:
// greeter_client.cc发出的请求
std::string user("world");
std::string reply = greeter.SayHello(user);
// greeter_server.cc进行了处理
std::string prefix("Hello ");
reply->set_message(prefix + request->name());
// greeter_client.cc的结果输出
// Act upon its status.
if(status.ok()) {
// std::cout << reply.message() << std::endl;
return reply.message();
} else{
std::cout << status.error_code() << ": "<< status.error_message()
<< std::endl;
return"RPC failed";
这里要是想看服务失败的样子,可以把上面用 nohup
启动的服务给kill掉就行。结果是这样的:
14: ConnectFailed
Greeter received: RPC failed
小结
这里我们看到了一个gRPC原始项目的初步构建方法,那么我来圈几个我们算法工程师最需要关心的几个关键点吧。
知道项目的构成,和怎么编译构建。
设计项目的时候,需要知道我们的服务需要什么(Input)、要做什么(Process)、返回什么结果(Output)。
知道如何设计接口,理解protobuf。
知道我们的算法和逻辑该往哪写。
参考文献
简单归纳一下这里面的参考文献。
ProtoBuf在中C++使用介绍。https://blog.csdn.net/u011086209/article/details/92796669
深入理解Protobuf-简介。https://www.jianshu.com/p/a24c88c0526a
gRPC C++ Hello World Tutorial。https://github.com/grpc/grpc/tree/master/examples/cpp/helloworld
这次参考的内容不多,多来源于自己的源码阅读和思考,有错漏之处欢迎各位大神批评指正。