• gRPC(Google远程过程调用)根据.proto文件生成代码机制(grcp_cpp_plugin插件)详解。


            这几天一直在研究grpc,谷歌的远程调用系统,这套系统谷歌开源在了github上,直接搜索grpc,star最高的就是项目本尊,我怀着好奇的心情去研究它,却被它蹂躏的很惨,道一句“珍爱生命,远离谷歌”。本篇大概分为这几部分:grpc项目的编译、简单样例的使用、grpc自动生成通讯代码浅谈、总结。

    目录

    一、Windows平台下编译grpc

    1. 代码下载

    2. 代码编译

    、 简单样例的使用

    1. VS内依赖的配置

    2. demo的调用过程 

     三、代码生成机制

     1. Protoc部分

    2. 代码生成的插件部分(grpc_cpp_plugin)

    总结


    一、Windows平台下编译grpc

            环境准备:

                    1. git

                    2. go语言

                    3. Cmake(需要将其加入环境变量)

                    4. yasm(编译工具)

                    5. Visual Studio 2015或2017

    1. 代码下载

            首先下载grpc,我推荐直接git下载,网速正常的话大概十几分钟,但是注意third_party内部的部分内容不会下载下来,需要进行子模块的克隆下载,把第三方依赖也都下载下来,这个过程略微耗时。

    1. git clone https://github.com/grpc/grpc.git
    2. git submodule update --init

            或者直接一步解决:

    git clone --recurse-submodules https://github.com/grpc/grpc

          third下面大概有这几项:

            

            如果有些文件夹下的文件没有git下来,需要进去删除掉里面所有文件,记住是所有。比如zlib一直缺失,需要进去手动将.git或者其他什么文件全部删除,删除过后进行:

    git submodule update --init

            这时,我默认你的grpc代码已经全部download下来了。

    2. 代码编译

            这里需要用到Cmake,下载下来并将其加入环境变量,具体看如下教程:

            Windows下CMake的下载与安装详解_wendy_ya的博客-CSDN博客_cmake下载

            yasm下载地址(选择win64.exe):

     The Yasm Modular Assembler Project 

            windows下go语言安装教程:

          Go语言安装(windows)_Mr Scarecrow的博客-CSDN博客_go语言安装 window 

            对于visual studio来说,我本人使用的是2017版本,SDK10以上。这就够了。 

            以上是完成编译环境的介绍。

            完成环境的安装后,就可以编译代码了,我们打开cmd进入grpc目录下,一步一步运行如下操作(注意17和15的区别):

    1. mkdir .build
    2. cd .build
    3. cmake .. -G "Visual Studio 15 2017 Win64"
    4. or
    5. cmake .. -G "Visual Studio 14 2015 Win64"

    下一步:

    cmake --build . --config Debug

            大约等待20-30分钟,视你的电脑速度而定。这个时候如果你的代码还有缺失,请回到一、处重新git代码,一般是不会有问题。编译好后你便可以使用VS打开解决方案,里面会有很多解决方案,不过大多都是依赖项,不要慌。

            这个时候你的编译就完成了。

    二、 简单样例的使用

    1. VS内依赖的配置

            样例调用的样例全部来自grpc的样例,其在grpc目录下的examples中,选择自己要测试的语言即可,我这里选择cpp。在cpp文件下选择helloworld,里面有greeter_async_client和greeter_async_client2、greeter_async_server。这是几个简单的通讯例子,如何将其带起来呢?

           VS新建工程,在附加包含目录中加入如下:

    1. C:\grpc\third_party\abseil-cpp
    2. C:\grpc\third_party\protobuf\src
    3. C:\grpc\third_party
    4. C:\grpc\include

    同样在链接器->常规的附加库目录中加入如下:

    1. C:\grpc\.build\third_party\boringssl-with-bazel\Debug
    2. C:\grpc\.build\third_party\re2\Debug
    3. C:\grpc\.build\third_party\abseil-cpp\absl\types\Debug
    4. C:\grpc\.build\third_party\abseil-cpp\absl\time\Debug
    5. C:\grpc\.build\third_party\abseil-cpp\absl\synchronization\Debug
    6. C:\grpc\.build\third_party\abseil-cpp\absl\strings\Debug
    7. C:\grpc\.build\third_party\abseil-cpp\absl\status\Debug
    8. C:\grpc\.build\third_party\abseil-cpp\absl\random\Debug
    9. C:\grpc\.build\third_party\abseil-cpp\absl\profiling\Debug
    10. C:\grpc\.build\third_party\abseil-cpp\absl\numeric\Debug
    11. C:\grpc\.build\third_party\abseil-cpp\absl\hash\Debug
    12. C:\grpc\.build\third_party\abseil-cpp\absl\flags\Debug
    13. C:\grpc\.build\third_party\abseil-cpp\absl\debugging\Debug
    14. C:\grpc\.build\third_party\abseil-cpp\absl\container\Debug
    15. C:\grpc\.build\third_party\abseil-cpp\absl\base\Debug
    16. C:\grpc\.build\Debug
    17. C:\grpc\.build\third_party\cares\cares\lib\Debug
    18. C:\grpc\.build\third_party\protobuf\Debug
    19. C:\grpc\.build\third_party\zlib\Debug

            在连接器->输入->附加依赖项里填入如下:

    1. absl_random_seed_gen_exception.lib
    2. absl_random_seed_sequences.lib
    3. absl_status.lib
    4. absl_statusor.lib
    5. absl_cord.lib
    6. absl_cord_internal.lib
    7. absl_cordz_functions.lib
    8. absl_cordz_handle.lib
    9. absl_cordz_info.lib
    10. absl_cordz_sample_token.lib
    11. absl_str_format_internal.lib
    12. absl_strings.lib
    13. absl_strings_internal.lib
    14. absl_graphcycles_internal.lib
    15. absl_synchronization.lib
    16. absl_civil_time.lib
    17. absl_time.lib
    18. absl_time_zone.lib
    19. absl_bad_any_cast_impl.lib
    20. absl_bad_optional_access.lib
    21. absl_bad_variant_access.lib
    22. re2.lib
    23. testing.lib
    24. crypto.lib
    25. ssl.lib

            以上过程很漫长,很心酸。 

    2. demo的调用过程 

            在grpc的VS工程里找到grpc_cpp_plugin和protoc,分别点击生成,我们需要生成的两个exe文件,然后将grpc_cpp_plugin.exe放入protoc目录下,确保与protoc.exe同目录,同时将helloworld.proto文件带进来,内容如下:

    1. syntax = "proto3";
    2. option java_package = "io.grpc.examples";
    3. package helloworld;
    4. // The greeter service definition.
    5. service Greeter {
    6. // Sends a greeting
    7. rpc SayHello (HelloRequest) returns (HelloReply) {}
    8. }
    9. // The request message containing the user's name.
    10. message HelloRequest {
    11. string name = 1;
    12. }
    13. // The response message containing the greetings
    14. message HelloReply {
    15. string message = 1;
    16. }

            然后开cmd运行如下命令以生成消息文件:

    protoc -I=. --cpp_out=. helloworld.proto

            同样紧跟其后运行如下命令生成通讯文件:

    protoc --grpc_out=./ --plugin=protoc-gen-grpc=grpc_cpp_plugin.exe helloworld.proto  

             这时你将有四个文件,他们分别是:

            同样,新建一个工程将他们引进来即可,找到我们上面说的client与server的代码,分别让他们编译,生出三个exe即可,两个客户端的,一个服务端的。把制作的demo都放到一起,带上zlibd.dll。

            首先运行服务端,他在监听0.0.0.0:50051:

            

             再开第一个客户端,它收到了消息hello world:

            

            继续开第二个客户端,它同样接连不断的接收到了消息:

            

            这里的demo就展示完毕。

     三、代码生成机制

             我们知道protobuf的流化需要pb.h和pb.cc文件,远程调用同样需要类似的.grpc.pb.h和.grpc.pb.cc,并且这两者文件通过.proto文件自动生成的。下面我们来探索这个机制的流程,需要两套代码,VS的附加到进程登操作。

     1. Protoc部分

    1. int main(int argc, char* argv[]) {
    2. return PROTOBUF_NAMESPACE_ID::compiler::ProtobufMain(argc, argv);
    3. }

            我们输入protoc命令后,命令个数在argc中,具体命令在数组argv[ ]中。

             RUN的代码如下,这部分解析这些参数的代码。:

    1. int CommandLineInterface::Run(int argc, const char* const argv[]) {
    2. Clear();
    3. switch (ParseArguments(argc, argv)) {
    4. case PARSE_ARGUMENT_DONE_AND_EXIT:
    5. return 0;
    6. case PARSE_ARGUMENT_FAIL:
    7. return 1;
    8. case PARSE_ARGUMENT_DONE_AND_CONTINUE:
    9. break;
    10. }

            继续寻找关键代码,这个GenerateOutput任务是将proto文件流化并按照规则解析,解析的内容为生成代码的元素:

    1. if (!GenerateOutput(parsed_files, output_directives_[i],
    2. generator.get())) {
    3. return 1;
    4. }

           

            点进去,继续找到如下代码:        

    1. if (!GeneratePluginOutput(parsed_files, plugin_name, parameters,
    2. generator_context, &error)) {
    3. std::cerr << output_directive.name << ": " << error << std::endl;
    4. return false;
    5. }

            继续点进去可以发现protoc部分调用grpc_cpp_plugin部分的进程启动相关代码:

    1. // Invoke the plugin.
    2. Subprocess subprocess;
    3. if (plugins_.count(plugin_name) > 0) {
    4. subprocess.Start(plugins_[plugin_name], Subprocess::EXACT_NAME);
    5. } else {
    6. subprocess.Start(plugin_name, Subprocess::SEARCH_PATH);
    7. }
    8. std::string communicate_error;
    9. if (!subprocess.Communicate(request, &response, &communicate_error)) {
    10. *error = strings::Substitute("$0: $1", plugin_name, communicate_error);
    11. return false;
    12. }

             这部分可以看我写的CreateProcess api的东西:

    CreatProcess - WINDOWS API 第一弹 进程相关_Thomas_Lbw的博客-CSDN博客

            在 Communicate处启动进程来调用代码生成插件,并将自己解析的proto生成代码元素通过request传过去,response负责接收插件已经生成好的代码,这就是它大体的工作机制。整个过程插件只是接收素材,生成代码,写入文件的操作也是由protoc来完成的。

    2. 代码生成的插件部分(grpc_cpp_plugin)

    1. int main(int argc, char* argv[]) {
    2. ::Sleep(1000 * 20);
    3. //
    4. CppGrpcGenerator generator;
    5. return grpc::protobuf::compiler::PluginMain(argc, argv, &generator);
    6. }

           

            与protoc的通信是通过管道进行的:

    1. #ifdef _WIN32
    2. setmode(STDIN_FILENO, _O_BINARY);
    3. setmode(STDOUT_FILENO, _O_BINARY);
    4. #endif

            然后通过request接收解析好的素材:

    1. CodeGeneratorRequest request;
    2. if (!request.ParseFromFileDescriptor(STDIN_FILENO)) {
    3. std::cerr << argv[0] << ": protoc sent unparseable request to plugin."
    4. << std::endl;
    5. return 1;
    6. }

            通过response部分导出生成的代码,再通过管道传给protoc:

    1. std::string error_msg;
    2. CodeGeneratorResponse response;
    3. if (GenerateCode(request, *generator, &response, &error_msg)) {
    4. if (!response.SerializeToFileDescriptor(STDOUT_FILENO)) {
    5. std::cerr << argv[0] << ": Error writing to stdout." << std::endl;
    6. return 1;
    7. }
    8. }

             到这,grpc自动生成代码已然清晰。其实最详细的代码如何生成的部分不能详细写出,我们要将其加入产品,等到反盗版策略下发公司股价再涨一波。。。

    总结

            这个任务目前是我做的最难的任务吧,如果没有他人帮助估计得再做一周。谷歌不愧是全球顶尖的互联网公司,其工程师写出的代码拓展性、兼容性、稳定性都是让人拍案叫绝,目前还有一些我不知道它为什么这么写的代码,真的是让自己发现自己的黑洞。

            总之,grpc的代码生成机制是通过进程间的管道通信、共享的代码生成类,通过request和response两个对象来交互,request和response这两个对象以及其复杂的内部结构成功将我绕晕,这个感兴趣的大家debug进去自己找,我是歇了。

            说起对象,程序员缺对象吗?一套代码下来想生成几个对象还不是看自己心情么?请求上天赏赐给我一个对象吧,哈哈哈哈哈。

  • 相关阅读:
    Vue学习:el 与data的两种写法
    uniapp:如何实现点击图片可以全屏展示预览
    十六、模型构建器(ModelBuilder)快速提取城市建成区——理论介绍
    【Python】数据可视化利器PyCharts在测试工作中的应用
    interface中的clocking
    java-Arrays
    算法设计与分析复习--回溯(一)
    shell脚本执行jps时:-bash: jps: command not found
    android-AP6212配置
    IT职业规划:大公司VS小公司,怎样选择更有前途?
  • 原文地址:https://blog.csdn.net/weixin_44120785/article/details/127776277