1. RPC框架
RPC框架是什么
RPC 框架说白了就是让你可以像调用本地方法一样调用远程服务提供的方法,而不需要关心底层的通信细节。简单地说就让远程服务调用更加简单、透明。 RPC包含了客户端(Client)和服务端(Server)
业界主流的 RPC 框架整体上分为三类:
1> 支持多语言的RPC框架,比较成熟的有Google的gRPC、Apache(Facebook)的Thrift;
2> 只支持特定语言的RPC框架,例如新浪微博的Motan;
3> 支持服务治理等服务化特性的分布式服务框架,其底层内核仍然是 RPC 框架, 例如阿里的 Dubbo
2. GRPC简介
在GRPC里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,使得您能够更容易地创建分布式应用和服务。与许多RPC系统类似,gRPC也是基于以下理念:定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个接口,并运行一个GRPC服务器来处理客户端调用。在客户端拥有一个存根能够像服务端一样的方法。
GRPC调用流程:
1> 客户端(gRPC Stub)调用 A 方法,发起 RPC 调用。
2> 对请求信息使用 Protobuf 进行对象序列化压缩(IDL)。
3> 服务端(gRPC Server)接收到请求后,解码请求体,进行业务逻辑处理并返回。
4> 对响应结果使用 Protobuf 进行对象序列化压缩(IDL)。
5> 客户端接受到服务端响应,解码请求体。回调被调用的 A 方法,唤醒正在等待响应(阻塞)的客户端调用并返回响应结果
===================================================================
概念部分讲完,grpc支持多语言跨平台,这里接下来咱们重点讲解c++如何使用grpc,方法很多,咱们这里只分析两种使用方式:
1> mingw源代码编译使用的方式
2> Qt调用grpc
3. 下载grpc
git clone [-b v1.50.0] https://github.com/grpc/grpc
git submodule update --init
注意:
1> 这里[-b v1.50.0]表示可以指定对应的版本号,也可以不加,默认下载master的grpc源码
2> submodule比较慢的情况下可以考虑换个源,修改grpc/.gitmodules,将内容替换为:
[submodule "third_party/abseil-cpp"]
path = third_party/abseil-cpp
url = https://gitee.com/suuair/abseil-cpp.git
[submodule "third_party/benchmark"]
path = third_party/benchmark
url = https://gitee.com/chahan/benchmark.git
[submodule "third_party/bloaty"]
path = third_party/bloaty
url = https://gitee.com/GgBoy_ssz/bloaty.git
[submodule "third_party/boringssl-with-bazel"]
path = third_party/boringssl-with-bazel
url = https://gitee.com/GgBoy_ssz/boringssl.git
[submodule "third_party/cares/cares"]
path = third_party/cares/cares
url = https://gitee.com/RicLee/c-ares.git
[submodule "third_party/envoy-api"]
path = third_party/envoy-api
url = https://gitee.com/RicLee/data-plane-api.git
[submodule "third_party/googleapis"]
path = third_party/googleapis
url = https://gitee.com/GgBoy_ssz/googleapis.git
[submodule "third_party/googletest"]
path = third_party/googletest
url = https://gitee.com/bosspoi/googletest.git
[submodule "third_party/libuv"]
path = third_party/libuv
url = https://gitee.com/RicLee/libuv.git
[submodule "third_party/opencensus-proto"]
path = third_party/opencensus-proto
url = https://gitee.com/RicLee/opencensus-proto.git
[submodule "third_party/opentelemetry"]
path = third_party/opentelemetry
url = https://gitee.com/EBServerStudy/opentelemetry-proto.git
[submodule "third_party/protobuf"]
path = third_party/protobuf
url = https://gitee.com/EBServerStudy/protobuf.git
[submodule "third_party/protoc-gen-validate"]
path = third_party/protoc-gen-validate
url = https://gitee.com/arzhe/protoc-gen-validate.git
[submodule "third_party/re2"]
path = third_party/re2
url = https://gitee.com/GgBoy_ssz/re2.git
[submodule "third_party/xds"]
path = third_party/xds
url = https://gitee.com/EBServerStudy/xds.git
[submodule "third_party/zlib"]
path = third_party/zlib
url = https://gitee.com/RicLee/zlib.git
# When using CMake to build, the zlib submodule ends up with a
# generated file that makes Git consider the submodule dirty. This
# state can be ignored for day-to-day development on gRPC.
ignore = dirty
替换完成之后执行命令:
git submodule sync
git submodule update --init
4. 编译安装
1> 在终端中进入grpc文件夹,输入mkdir -p cmake/build创建编译文件夹
2> 构建代码,输入cmake …/… -G "MinGW Makefiles"生成makefile和相关文件
3> 编译代码,输入 make -j
4> 安装grpc, 输入make install -j默认安装到"C:\Program Files (x86)\grpc"这个目录下
以上步骤的基础在于已经安装好了cmake和mingw编译环境;这里不详细展开怎么安装;不清楚的童鞋可以参考:
https://blog.csdn.net/qq_37434641/article/details/127561281
我这里实际上已经安装了qt环境,qt环境下自带了mingw编译环境,不需要安装w64devkit;但是编译命令需要更换成mingw32-make -j && mingw32-make install -j;包括命令找不到的话需要配置环境变量,熟悉linux编译的应该都知道这些,这里不展开,自行百度
[ps]如果熟悉qt creator的童鞋觉得这种命令行编译的方式太繁琐,可以使用qt creator设置好构建套件,然后打开make install开关,可以直接编译cmakelist.txt进行编译;如图:
5. 编译输出
正常编译完成会在C:\Program Files (x86)\grpc目录下生成如下内容:
6. 运行示例
切换到源代码E:\grpc\grpc\examples\cpp\helloworld目录,修改cmakelist.txt文件
修改完成后执行以下命令:
mkdir -p cmake/build
cd cmake/build
cmake ../.. -DCMAKE_PREFIX_PATH="C:/Program Files (x86)/grpc/" -G "MinGW Makefiles"
make -j
不报错的情况下会生成服务端的greeter_server.exe和greeter_client.exe执行文件;命令行分别执行可以看到对应的打印
7. 使用.proto文件
接下来定义一个简单的.proto文件,生成客户端服务端代码,写个简单的客户端服务端程序看通信是否正常;
syntax = "proto3";
package helloworld;
// 定义服务
service Greeter {
// 定义rpc方法
rpc SayHello(HelloRequest) returns (HelloReply) {}
}
// 消息定义
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
1> 执行命令生成grpc服务代码:
protoc.exe -I . --grpc_out=. --plugin=protoc-gen-grpc=grpc_cpp_plugin.exe helloworld.proto
这个命令需要注意一点:如果grpc_cpp_plugin.exe不在执行路径下的话需要填写绝对路径,不然可能报错:protoc-gen-grpc: The system cannot find the file specified
2> 执行命令生成c++代码
protoc.exe -I . --cpp_out=. helloworld.proto
8. 服务端代码
#include <iostream>
using namespace std;
#include <grpcpp/grpcpp.h>
#include "helloworld.grpc.pb.h"
namespace grpc_helloworld {
class GreeterService final : public helloworld::Greeter::Service {
grpc::Status SayHello(grpc::ServerContext* context,
const helloworld::HelloRequest* request,
helloworld::HelloReply* reply) override {
std::string prefix("Hello ");
reply->set_message(prefix + request->name());
return grpc::Status::OK;
}
};
}
// main.cpp
#include <iostream>
#include <memory>
#include <string>
using grpc::Server;
using grpc::ServerBuilder;
using grpc_helloworld::GreeterService;
using helloworld::HelloRequest;
using helloworld::HelloReply;
int main(int argc, char** argv) {
grpc::ServerBuilder builder;
int port = 50051;
builder.AddListeningPort("0.0.0.0:" + std::to_string(port), grpc::InsecureServerCredentials());
// 创建服务实现对象
std::unique_ptr<GreeterService::Service> service(new GreeterService());
// 使用NewService方法来添加服务
builder.RegisterService(service.get());
// 构建并运行服务器
std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
std::cout << "Server listening on port " << port << std::endl;
server->Wait();
return 0;
}
9. 客户端代码
#include <iostream>
#include <memory>
#include <string>
#include <grpcpp/grpcpp.h>
#include "helloworld.grpc.pb.h"
using grpc::Channel;
using grpc::ClientContext;
using grpc::Status;
using helloworld::Greeter;
using helloworld::HelloRequest;
using helloworld::HelloReply;
class GreeterClient {
public:
GreeterClient(std::shared_ptr<Channel> channel)
: stub_(Greeter::NewStub(channel)) {}
std::string SayHello(const std::string& name) {
// Data we are sending to the server.
HelloRequest request;
request.set_name(name);
// Container for 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()) {
return reply.message();
} else {
std::cerr << "RPC failed: " << status.error_code() << std::endl;
return "RPC failed";
}
}
private:
std::unique_ptr<Greeter::Stub> stub_;
};
int main(int argc, char** argv) {
// Instantiate the client. It requires a channel, out of which the actual RPCs
// are created. This channel models the connection to the service in gRPC and is
// used by the stub that represents the client. We'll create one channel for the
// entire application lifetime.
GreeterClient client(grpc::CreateChannel(
"localhost:50051", grpc::InsecureChannelCredentials()));
// Contact the server and receive a reply.
std::string reply = client.SayHello("world");
std::cout << "Greeter received: " << reply << std::endl;
return 0;
}
10. 链接顺序
需要注意的是编译grpc默认生成的是一对静态库;链接的时候静态库的顺序不对可能会导致很多的未定义错误;可以调整顺序慢慢试;网上有很多调整过的链接顺序,我试了一些并不一定都能编译过,可能具体环境需要具体分析,我这里提供一个mingw下链接正常的链接顺序给大家参考(ps:具体环境具体分析,编译不过需要自己调整)
INCLUDEPATH += "C:/Program Files (x86)/grpc/include"
LIBS += -L"C:/Program Files (x86)/grpc/lib" -lgrpc++ \
-lgrpc \
-lprotobuf \
-lssl \
-lz \
-lcares \
-lre2 \
-lgpr \
-lupb \
-laddress_sorting \
-labsl_hash \
-labsl_status \
-labsl_statusor \
-labsl_strerror \
-labsl_strings \
-labsl_strings_internal \
-labsl_str_format_internal \
-labsl_time \
-labsl_time_zone \
-labsl_random_internal_randen \
-labsl_random_internal_pool_urbg \
-labsl_random_distributions \
-labsl_random_internal_randen_hwaes \
-labsl_random_internal_randen_hwaes_impl \
-labsl_random_internal_randen_slow \
-labsl_random_internal_seed_material \
-labsl_random_seed_gen_exception \
-labsl_random_internal_platform \
-labsl_random_seed_sequences \
-labsl_synchronization \
-labsl_symbolize \
# -labsl_debugging_internal \
-labsl_low_level_hash \
-labsl_base \
-labsl_malloc_internal \
-labsl_graphcycles_internal \
-labsl_cord \
-labsl_cord_internal \
-labsl_cordz_info \
-labsl_cordz_handle \
-labsl_cordz_functions \
-labsl_exponential_biased \
-labsl_bad_optional_access \
-labsl_bad_variant_access \
-labsl_raw_hash_set \
-labsl_raw_logging_internal \
-labsl_stacktrace \
-labsl_spinlock_wait \
-labsl_city \
-labsl_throw_delegate \
-labsl_int128 \
-lpthread \
-lcrypto \
-lbcrypt \
-ldbghelp \
-lws2_32
11. Qt使用grpc
上述介绍的源代码编译的方式其他平台类似,不赘述;这里介绍除了这种编译源代码通用的方式以为另外一种比较简单的方式,就是使用Qt框架来做,Qt框架封装了grpc用起来会更方便,缺点是需要安装Qt
11.1 确认Qt包含GRPC库
首先需要确保你的Qt勾选了grpc的安装,可以打开MaintenanceTool.exe看一下,路径就在Qt的默认安装路径下,我这里是D:\Qt
11.2 生成Qt支持的grpc文件
与上面生成的方法类似,需要用到Qt的两个执行文件qtprotobufgen.exe和qtgrpcgen.exe
protoc.exe --plugin=protoc-gen-qtprotobuf=D:\Qt\6.5.2\mingw_64\bin\qtprotobufgen.exe -I . --qtprotobuf_out=. helloworld.proto
protoc.exe --plugin=protoc-gen-qtgrpc=D:\Qt\6.5.2\mingw_64\bin\qtgrpcgen.exe -I . --qtgrpc_out=. helloworld.proto
pause
11.2 .pro文件添加对grpc的支持
将生成的文件加入到项目中,并且添加QT += grpc内容;
QT = core grpc network
CONFIG += c++17 cmdline
# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
helloworld.qpb.cpp \
helloworld_client.grpc.qpb.cpp \
helloworld_protobuftyperegistrations.cpp \
main.cpp
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
HEADERS += \
helloworld.qpb.h \
helloworld_client.grpc.qpb.h
11.3 客户端代码
#include <QCoreApplication>
#include <QGrpcInsecureChannelCredentials>
#include "helloworld_client.grpc.qpb.h"
#include <QGrpcHttp2Channel>
#include <QThread>
#include <QAbstractSocket>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
helloworld::Greeter::Client client;
auto channel = std::shared_ptr<QAbstractGrpcChannel>(new QGrpcHttp2Channel(
QUrl("http://192.168.23.113:50051", QUrl::StrictMode), QGrpcInsecureChannelCredentials() | QGrpcInsecureCallCredentials()));
client.attachChannel(channel);
helloworld::HelloRequest req;
helloworld::HelloReply rep;
req.setName("world");
std::shared_ptr<QGrpcCallReply> grpcReply = client.SayHello(req);
QAbstractSocket::connect(grpcReply.get(), &QGrpcCallReply::errorOccurred, [=](const QGrpcStatus &status)
{
qDebug().noquote() << "sayHello errorOccurred : " << status.code() << status.message();
});
QAbstractSocket::connect(grpcReply.get(), &QGrpcCallReply::finished, [=, &rep]()
{
rep = grpcReply->read<helloworld::HelloReply>();
qDebug().noquote() << "finished: " << rep.message();
});
return a.exec();
}
这里有个注意点:
如果服务端使用的c++的服务端如果使用的是localhost的地址,c++客户端同样使用localhost好使;但是Qt编写的客户端使用localhost并不好使,原因时间问题没细研究,估计是要配置哪里的localhost和地址的映射关系;可以自行研究;