查看原文
其他

ML&DEV[10] | gRPC的应用

机智的叉烧 CS的陋室 2022-08-08


【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等,但是速度更快,体积更小。

他的结构是这样的:

  1. message Example1{

  2. optional string stringVal = 1;

  3. optional bytes bytesVal = 2;

  4. message EmbeddedMessage{

  5. int32 int32Val = 1;

  6. string stringVal = 2;

  7. }

  8. optional EmbeddedMessage embeddedExample1 = 3;

  9. repeated int32 repeatedInt32Val = 4;

  10. repeated string repeatedStringVal = 5;

  11. }

可以看到这个结构很像json,但是又不太一样,主要是他有了比较严谨的数据类型定义。

在这里,我们只要知道它的基本结构和编写方式即可。protobuf的具体格式和入门可以参照这个:https://developers.google.com/protocol-buffers/docs/cpptutorial,如果是打不开,可以看这个,中文版的:https://www.jianshu.com/p/d2bed3614259。

然后我们回过头来看helloworld的pb,这个的具体位置这里:grpc/examples/protos/helloworld.proto首先我们可以很明显的看到两个东西,分别是 HelloRequestHelloReply

  1. // The request message containing the user's name.

  2. message HelloRequest{

  3. string name = 1;

  4. }


  5. // The response message containing the greetings

  6. message HelloReply{

  7. string message = 1;

  8. }

这就是所谓的请求格式和返回格式,请求内容里需要一个 name字段,返回的结果中需要一个 message字段。

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

  1. // The greeting service definition.

  2. service Greeter{

  3. // Sends a greeting

  4. rpc SayHello(HelloRequest) returns (HelloReply) {}

  5. }

服务的名称是 Greeter,里面是一个 rpc服务,可以看到里面类似一个非常简单的函数定义,输入是 HelloRequest输出是 HelloReply,这里非常好理解,足够了,然后我们就可以开始构建项目了。

gRPC服务的构建

这里我主要用的是make的方式(一种编译c++的方法,g++的一种升级版)去编译构造,那肯定要从makefile去看了,回到helloworld项目里面去。

首先,我们要想办法把上面的proto插入到项目去,这个非常好理解。

  1. protoc -I ../../protos/ --grpc_out=. --plugin=protoc-gen-grpc=grpc_cpp_plugin ../../protos/helloworld.proto

  2. protoc -I ../../protos/ --cpp_out=. ../../protos/helloworld.proto

一个是grpcout,一个对应cppout。实质上,在makefile里面写的就比较通用了,直接看吧:

  1. .PRECIOUS: %.grpc.pb.cc

  2. %.grpc.pb.cc: %.proto

  3. $(PROTOC) -I $(PROTOS_PATH) --grpc_out=. --plugin=protoc-gen-grpc=$(GRPC_CPP_PLUGIN_PATH) $<


  4. .PRECIOUS: %.pb.cc

  5. %.pb.cc: %.proto

  6. $(PROTOC) -I $(PROTOS_PATH) --cpp_out=. $<

客户端

greeter_client.cc

首先还是看看比较简单的客户端。直接看main函数应该会比较舒服(不懂c++的看到这里已经觉得快睡着了吧,加油!这这里的main说白了就是个程序入口)。

  1. int main(int argc, char** argv) {

  2. // Instantiate the client. It requires a channel, out of which the actual RPCs

  3. // are created. This channel models a connection to an endpoint (in this case,

  4. // localhost at port 50051). We indicate that the channel isn't authenticated

  5. // (use of InsecureChannelCredentials()).

  6. GreeterClient greeter(grpc::CreateChannel(

  7. "localhost:50051", grpc::InsecureChannelCredentials()));

  8. std::string user("world");

  9. std::string reply = greeter.SayHello(user);

  10. std::cout << "Greeter received: "<< reply << std::endl;


  11. return0;

  12. }

  • 首先,是创建了个 GreeterClient的实例化对象,里面有端口和一个大家可以理解为备用端口的玩意。

  • 然后,就是要构建request了,这的request比较简单,就是一个string字符串(参考之前我们定义的protobuf)。

  • 然后就是去请求了, greeter.SayHello(user),注意这里要提前为他准备一个承接结果的变量(这里是reply,也是striing)。

  • 最后就是输出结果了(类似python的print)。

这里看起来都非常简单,毕竟有一个比较完善的类了,那么我们就来看看这个类里面有啥。

  1. classGreeterClient{

  2. public:

  3. GreeterClient(std::shared_ptr<Channel> channel)

  4. : stub_(Greeter::NewStub(channel)) {}


  5. // Assembles the client's payload, sends it and presents the response back

  6. // from the server.

  7. std::stringSayHello(const std::string& user) {

  8. // Data we are sending to the server.

  9. HelloRequest request;

  10. request.set_name(user);


  11. // Container for the data we expect from the server.

  12. HelloReply reply;


  13. // Context for the client. It could be used to convey extra information to

  14. // the server and/or tweak certain RPC behaviors.

  15. ClientContext context;


  16. // The actual RPC.

  17. Status status = stub_->SayHello(&context, request, &reply);


  18. // Act upon its status.

  19. if(status.ok()) {

  20. // std::cout << reply.message() << std::endl;

  21. return reply.message();

  22. } else{

  23. std::cout << status.error_code() << ": "<< status.error_message()

  24. << std::endl;

  25. return"RPC failed";

  26. }

  27. }


  28. private:

  29. std::unique_ptr<Greeter::Stub> stub_;

  30. };

代码还是比较长的,我们这里直接看我们最关心的 SayHello吧,这里面我们可以看到里面还有一个 stub_->SayHello,很明显这里面是一个更加接近真请求的函数,有 contextrequestreply三种, context之前没谈到,这里注释也说了是是不被用到的额外信息而已,传进去即可。这里需要注意的是, stub_->SayHello的结果不是通过返回值来返回,而是通过形参来传出,返回值是成功与否( &),另外补充说一下,这个 stub_->SayHello的声明(定义)在 make构建后的 helloworld.grpc.pb.h中( Greeter::Stub::SayHello)。

不往里说,这里已经足够,说白了我们只需要凑出protobuf所约定的数据结构的数据传过去,然后准备一个承接结果的变量接住返回接过即可。

服务端

greeter_server.cc

这里的main文件更加简单了。

  1. int main(int argc, char** argv) {

  2. RunServer();


  3. return0;

  4. }

没啥好讲的,但是这个 RunServer肯定有不少好东西。开始之前看看引入了什么东西吧:

  1. #include<iostream>

  2. #include<memory>

  3. #include<string>


  4. #include<grpcpp/grpcpp.h>


  5. #ifdef BAZEL_BUILD

  6. #include"examples/protos/helloworld.grpc.pb.h"

  7. #else

  8. #include"helloworld.grpc.pb.h"

  9. #endif


  10. using grpc::Server;

  11. using grpc::ServerBuilder;

  12. using grpc::ServerContext;

  13. using grpc::Status;

  14. using helloworld::HelloRequest;

  15. using helloworld::HelloReply;

  16. using helloworld::Greeter;

这里可以分为5块吧。

  • c++内置库。

  • grpccpp.h。grpc的核心内容。

  • helloworld.grpc.pb。protobuf构建的grpc服务构建。

  • grpc相关的构建工具。

  • helloworld中的是基于protobuf构建的相关数据类,大家非常熟悉这3个东西吧。

  1. voidRunServer() {

  2. std::string server_address("0.0.0.0:50051");

  3. GreeterServiceImpl service;


  4. ServerBuilder builder;

  5. // Listen on the given address without any authentication mechanism.

  6. builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());

  7. // Register "service" as the instance through which we'll communicate with

  8. // clients. In this case it corresponds to an *synchronous* service.

  9. builder.RegisterService(&service);

  10. // Finally assemble the server.

  11. std::unique_ptr<Server> server(builder.BuildAndStart());

  12. std::cout << "Server listening on "<< server_address << std::endl;


  13. // Wait for the server to shutdown. Note that some other thread must be

  14. // responsible for shutting down the server for this call to ever return.

  15. server->Wait();

  16. }

这里定义了一个服务的构造器 ServerBuilderbuilder,去构造一个服务,说白了就是需要一个“监听器”( AddListeningPort)(之所以叫监听器,是因为服务是一个触发机制,只有触发到对应的命令才会执行,而这个命令就是端口,这里是 0.0.0.0:50051),构造后就注册一个服务 RegisterService,然后把这个监听器放进去便开始监听。

这里还没谈到一个关键点,也是我们算法工程师最关心的,那就是服务接收请求后,怎么做我们想做的处理,答案就在这个 GreeterServiceImpl上,我们是在这里定义的。

  1. // Logic and data behind the server's behavior.

  2. classGreeterServiceImplfinal: publicGreeter::Service{

  3. StatusSayHello(ServerContext* context, constHelloRequest* request,

  4. HelloReply* reply) override{

  5. std::string prefix("Hello ");

  6. reply->set_message(prefix + request->name());

  7. returnStatus::OK;

  8. }

  9. };

这里可以明显的看到,服务的内容就是把用户发送过来的请求 request中的 name提取出来,前面加上 Hello而已。当然的,这里我们可以做更多的事情,一方面我们可以丰富这个类,当然我们也可以新建类来做更复杂的事情。

如此一来,服务就起来了,剩下就等客户端请求了。

开始执行

开始之前,肯定先做好准备工作,那就是编译,之前也提到,就是 make

首先需要把服务启动起来(可以理解为店铺开张了)。

  1. nohup ./greet_server &

这里用nohup是要保证这个服务持续运行,这个程序必须持续执行,所以把它放在后台。

然后就可以去请求了。

  1. ./greet_client

这时候你会看到返回结果。

  1. Greeter received: Hello world

这里的 Hello是服务端拼上去的, world就是客户端写上的, Greeterreceived:是在客户端请求完成后输出结果时加上去的。来给大家回放一下:

  1. // greeter_client.cc发出的请求

  2. std::string user("world");

  3. std::string reply = greeter.SayHello(user);


  4. // greeter_server.cc进行了处理

  5. std::string prefix("Hello ");

  6. reply->set_message(prefix + request->name());


  7. // greeter_client.cc的结果输出

  8. // Act upon its status.

  9. if(status.ok()) {

  10. // std::cout << reply.message() << std::endl;

  11. return reply.message();

  12. } else{

  13. std::cout << status.error_code() << ": "<< status.error_message()

  14. << std::endl;

  15. return"RPC failed";

这里要是想看服务失败的样子,可以把上面用 nohup启动的服务给kill掉就行。结果是这样的:

  1. 14: ConnectFailed

  2. 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

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



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存