ML&DEV[10] | gRPC的应用

【ML&DEV】

这是大家没有看过的船新栏目!ML表示机器学习,DEV表示开发,本专栏旨在为大家分享作为算法工程师的工作,机器学习生态下的有关模型方法和技术,从数据生产到模型部署维护监控全流程,预备知识、理论、技术、经验等都会涉及,近期内容以入门线路为主,敬请期待!

往期回顾:

上一期和大家谈到了gRPC的快速开始,我么哪知道了gRPC是什么以及怎么快速启动,那么现在,我们来看看这个玩意具体内部是怎么运作的,这里我们同样以helloworld这个为例子来去谈。首先上期内容在这里:

ML&DEV[9] | gRPC初体验

当然的,大家也可以直接去看官方的教程,这里会教你怎么去做,但是我会和大家谈更多的内部细节的东西,可别错过啦: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。首先我们可以很明显的看到两个东西,分别是 HelloRequestHelloReply

// 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字段。

另外,就是我们需要订立的服务了,可以理解为将 HelloRequestHelloReply定义为服务的定义。

// 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,很明显这里面是一个更加接近真请求的函数,有 contextrequestreply三种, 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

这次参考的内容不多,多来源于自己的源码阅读和思考,有错漏之处欢迎各位大神批评指正。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值