这几天一直在研究grpc,谷歌的远程调用系统,这套系统谷歌开源在了github上,直接搜索grpc,star最高的就是项目本尊,我怀着好奇的心情去研究它,却被它蹂躏的很惨,道一句“珍爱生命,远离谷歌”。本篇大概分为这几部分:grpc项目的编译、简单样例的使用、grpc自动生成通讯代码浅谈、总结。
目录
环境准备:
1. git
2. go语言
3. Cmake(需要将其加入环境变量)
4. yasm(编译工具)
5. Visual Studio 2015或2017
首先下载grpc,我推荐直接git下载,网速正常的话大概十几分钟,但是注意third_party内部的部分内容不会下载下来,需要进行子模块的克隆下载,把第三方依赖也都下载下来,这个过程略微耗时。
- git clone https://github.com/grpc/grpc.git
- git submodule update --init
或者直接一步解决:
git clone --recurse-submodules https://github.com/grpc/grpc
third下面大概有这几项:
如果有些文件夹下的文件没有git下来,需要进去删除掉里面所有文件,记住是所有。比如zlib一直缺失,需要进去手动将.git或者其他什么文件全部删除,删除过后进行:
git submodule update --init
这时,我默认你的grpc代码已经全部download下来了。
这里需要用到Cmake,下载下来并将其加入环境变量,具体看如下教程:
yasm下载地址(选择win64.exe):
windows下go语言安装教程:
对于visual studio来说,我本人使用的是2017版本,SDK10以上。这就够了。
以上是完成编译环境的介绍。
完成环境的安装后,就可以编译代码了,我们打开cmd进入grpc目录下,一步一步运行如下操作(注意17和15的区别):
- mkdir .build
-
- cd .build
-
- cmake .. -G "Visual Studio 15 2017 Win64"
-
- or
-
- cmake .. -G "Visual Studio 14 2015 Win64"
下一步:
cmake --build . --config Debug
大约等待20-30分钟,视你的电脑速度而定。这个时候如果你的代码还有缺失,请回到一、处重新git代码,一般是不会有问题。编译好后你便可以使用VS打开解决方案,里面会有很多解决方案,不过大多都是依赖项,不要慌。
这个时候你的编译就完成了。
样例调用的样例全部来自grpc的样例,其在grpc目录下的examples中,选择自己要测试的语言即可,我这里选择cpp。在cpp文件下选择helloworld,里面有greeter_async_client和greeter_async_client2、greeter_async_server。这是几个简单的通讯例子,如何将其带起来呢?
VS新建工程,在附加包含目录中加入如下:
- C:\grpc\third_party\abseil-cpp
- C:\grpc\third_party\protobuf\src
- C:\grpc\third_party
- C:\grpc\include
同样在链接器->常规的附加库目录中加入如下:
- C:\grpc\.build\third_party\boringssl-with-bazel\Debug
- C:\grpc\.build\third_party\re2\Debug
- C:\grpc\.build\third_party\abseil-cpp\absl\types\Debug
- C:\grpc\.build\third_party\abseil-cpp\absl\time\Debug
- C:\grpc\.build\third_party\abseil-cpp\absl\synchronization\Debug
- C:\grpc\.build\third_party\abseil-cpp\absl\strings\Debug
- C:\grpc\.build\third_party\abseil-cpp\absl\status\Debug
- C:\grpc\.build\third_party\abseil-cpp\absl\random\Debug
- C:\grpc\.build\third_party\abseil-cpp\absl\profiling\Debug
- C:\grpc\.build\third_party\abseil-cpp\absl\numeric\Debug
- C:\grpc\.build\third_party\abseil-cpp\absl\hash\Debug
- C:\grpc\.build\third_party\abseil-cpp\absl\flags\Debug
- C:\grpc\.build\third_party\abseil-cpp\absl\debugging\Debug
- C:\grpc\.build\third_party\abseil-cpp\absl\container\Debug
- C:\grpc\.build\third_party\abseil-cpp\absl\base\Debug
- C:\grpc\.build\Debug
- C:\grpc\.build\third_party\cares\cares\lib\Debug
- C:\grpc\.build\third_party\protobuf\Debug
- C:\grpc\.build\third_party\zlib\Debug
在连接器->输入->附加依赖项里填入如下:
- absl_random_seed_gen_exception.lib
- absl_random_seed_sequences.lib
- absl_status.lib
- absl_statusor.lib
- absl_cord.lib
- absl_cord_internal.lib
- absl_cordz_functions.lib
- absl_cordz_handle.lib
- absl_cordz_info.lib
- absl_cordz_sample_token.lib
- absl_str_format_internal.lib
- absl_strings.lib
- absl_strings_internal.lib
- absl_graphcycles_internal.lib
- absl_synchronization.lib
- absl_civil_time.lib
- absl_time.lib
- absl_time_zone.lib
- absl_bad_any_cast_impl.lib
- absl_bad_optional_access.lib
- absl_bad_variant_access.lib
- re2.lib
- testing.lib
- crypto.lib
- ssl.lib
以上过程很漫长,很心酸。
在grpc的VS工程里找到grpc_cpp_plugin和protoc,分别点击生成,我们需要生成的两个exe文件,然后将grpc_cpp_plugin.exe放入protoc目录下,确保与protoc.exe同目录,同时将helloworld.proto文件带进来,内容如下:
- syntax = "proto3";
- option java_package = "io.grpc.examples";
- package helloworld;
-
- // The greeter service definition.
- service Greeter {
- // Sends a greeting
- rpc SayHello (HelloRequest) returns (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;
- }
然后开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的附加到进程登操作。
- int main(int argc, char* argv[]) {
-
- return PROTOBUF_NAMESPACE_ID::compiler::ProtobufMain(argc, argv);
- }
我们输入protoc命令后,命令个数在argc中,具体命令在数组argv[ ]中。
RUN的代码如下,这部分解析这些参数的代码。:
- int CommandLineInterface::Run(int argc, const char* const argv[]) {
- Clear();
- switch (ParseArguments(argc, argv)) {
- case PARSE_ARGUMENT_DONE_AND_EXIT:
- return 0;
- case PARSE_ARGUMENT_FAIL:
- return 1;
- case PARSE_ARGUMENT_DONE_AND_CONTINUE:
- break;
- }
继续寻找关键代码,这个GenerateOutput任务是将proto文件流化并按照规则解析,解析的内容为生成代码的元素:
- if (!GenerateOutput(parsed_files, output_directives_[i],
- generator.get())) {
- return 1;
- }
点进去,继续找到如下代码:
- if (!GeneratePluginOutput(parsed_files, plugin_name, parameters,
- generator_context, &error)) {
- std::cerr << output_directive.name << ": " << error << std::endl;
- return false;
- }
继续点进去可以发现protoc部分调用grpc_cpp_plugin部分的进程启动相关代码:
- // Invoke the plugin.
- Subprocess subprocess;
-
- if (plugins_.count(plugin_name) > 0) {
- subprocess.Start(plugins_[plugin_name], Subprocess::EXACT_NAME);
- } else {
- subprocess.Start(plugin_name, Subprocess::SEARCH_PATH);
- }
-
- std::string communicate_error;
- if (!subprocess.Communicate(request, &response, &communicate_error)) {
- *error = strings::Substitute("$0: $1", plugin_name, communicate_error);
- return false;
- }
这部分可以看我写的CreateProcess api的东西:
在 Communicate处启动进程来调用代码生成插件,并将自己解析的proto生成代码元素通过request传过去,response负责接收插件已经生成好的代码,这就是它大体的工作机制。整个过程插件只是接收素材,生成代码,写入文件的操作也是由protoc来完成的。
- int main(int argc, char* argv[]) {
- ::Sleep(1000 * 20);
- //
- CppGrpcGenerator generator;
- return grpc::protobuf::compiler::PluginMain(argc, argv, &generator);
- }
与protoc的通信是通过管道进行的:
- #ifdef _WIN32
- setmode(STDIN_FILENO, _O_BINARY);
- setmode(STDOUT_FILENO, _O_BINARY);
- #endif
然后通过request接收解析好的素材:
- CodeGeneratorRequest request;
- if (!request.ParseFromFileDescriptor(STDIN_FILENO)) {
- std::cerr << argv[0] << ": protoc sent unparseable request to plugin."
- << std::endl;
- return 1;
- }
通过response部分导出生成的代码,再通过管道传给protoc:
- std::string error_msg;
- CodeGeneratorResponse response;
- if (GenerateCode(request, *generator, &response, &error_msg)) {
- if (!response.SerializeToFileDescriptor(STDOUT_FILENO)) {
- std::cerr << argv[0] << ": Error writing to stdout." << std::endl;
- return 1;
- }
- }
到这,grpc自动生成代码已然清晰。其实最详细的代码如何生成的部分不能详细写出,我们要将其加入产品,等到反盗版策略下发公司股价再涨一波。。。
这个任务目前是我做的最难的任务吧,如果没有他人帮助估计得再做一周。谷歌不愧是全球顶尖的互联网公司,其工程师写出的代码拓展性、兼容性、稳定性都是让人拍案叫绝,目前还有一些我不知道它为什么这么写的代码,真的是让自己发现自己的黑洞。
总之,grpc的代码生成机制是通过进程间的管道通信、共享的代码生成类,通过request和response两个对象来交互,request和response这两个对象以及其复杂的内部结构成功将我绕晕,这个感兴趣的大家debug进去自己找,我是歇了。
说起对象,程序员缺对象吗?一套代码下来想生成几个对象还不是看自己心情么?请求上天赏赐给我一个对象吧,哈哈哈哈哈。