已經折騰grpc幾天了,也基本搞明白了怎么用,這里做一個簡單的記錄,以便日后需要的時候有個參考。
按照順序,先寫同步服務的簡單實例,然后寫異步服務的,最后寫4中服務類型的使用。
grpc源碼的example目錄下都有相關的實例,但是講的不夠清楚,特別是異步服務這一塊,注釋說明不夠詳盡,CallData的封裝也不利於理接異步服務的流程結構。所以我這里不按照官方的示例來做了。
參考資料
1、編寫proto文件,定義服務
如何編寫proto文件,可以看Protobuf3 語法指南 (英文原文地址https://developers.google.com/protocol-buffers/docs/proto3)中的說明,這里就不再敘述了。
我這里寫一個簡單的simple.proto文件,定義三個簡單的服務接口,流式接口以后再說。
syntax="proto3";
// 包名是生成代碼的使用的namespace,所有代碼都在這個下面
package Simple;
// 指定服務的名稱,作為生成代碼里面的二級namespace
service Server {
// 測試接口一
rpc Test1(TestRequest) returns (TestNull ){}
// 測試接口二
rpc Test2(TestNull ) returns (TestReply) {}
// 測試接口三
rpc Test3(TestRequest) returns (TestReply) {}
}
message TestNull {
}
message TestRequest {
string name = 1; // 客戶端名稱
int32 id = 2; // 客戶端IP
double value = 3; // 附帶一個值
}
message TestReply {
int32 tid = 1; // 服務線程ID
string svrname = 2; // 服務名稱
double takeuptime = 3; // 請求處理耗時
}
上面的接口中,必須有參數和返回值,如果不需要參數或者返回值,也必須定義一個空的(沒有成員)message,否則無法通過編譯。
2、編譯proto文件,生成代碼
安裝好grpc之后,可以使用grpc的相關命令行程序,來使用proto文件生成C++代碼(也可以生成別的語言的),這里需要分兩步,一是生成protobuf(反)序列化的代碼,二是生成基本服務框架代碼。
# 1、生成protobuf序列化代碼
# 執行下面命令后,將在當前目錄下生成 simple.pb.h 和 simple.pb.cc 文件
protoc -I ./ --cpp_out=. simple.proto
# 2、生成服務框架代碼
# 執行下面命令后,將在當前目錄下生成 simple.grpc.pb.h 和 simple.grpc.pb.cc 文件
protoc -I ./ --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` simple.proto
# 上面的 `which grpc_cpp_plugin` 也可以替換為 grpc_cpp_plugin 程序的路徑(如果不在系統PATH下)
# 生成的代碼需要依賴第一步生成序列化代碼,所以在使用的時候必須都要有(生成的時候不依賴)
如果自己生成比較麻煩,可以在這個網站上生成 protobuf compiler

3、編寫服務端代碼
查看上一步驟生成的代碼(simple.grpc.pb.cc),可以看到它已經生成了服務代碼Simple::Server::Service類,里面已經有三個Test函數的空實現,這三個函數都是虛函數,所以可以繼承這個類,並實現這三個函數,也可以直接修改生成的代碼,把這三個函數的實現改為自己的實現。

server.cpp 代碼
#include "simple.grpc.pb.h"
#include <grpcpp/grpcpp.h>
#include <memory>
#include <iostream>
#include <strstream>
// 繼承自生成的Service類,實現三個虛函數
class ServiceImpl:
public Simple::Server::Service {
public:
// Test1 實現都是差不都的,這里只是為了測試,就隨便返回點數據了
grpc::Status Test1(grpc::ServerContext* context,
const Simple::TestRequest* request,
Simple::TestNull* response)
override
{
std::ostrstream os;
os << "Client Name = " << request->name() << '\n';
os << "Clinet ID = " << request->id() << '\n';
os << "Clinet Value= " << request->value()<< '\n';
std::string message = os.str();
// grpc狀態可以設置message,所以也可以用來返回一些信息
return grpc::Status(grpc::StatusCode::OK,message);
}
// Test2
grpc::Status Test2(grpc::ServerContext* context,
const Simple::TestNull* request,
Simple::TestReply* response)
override
{
response->set_tid(100);
response->set_svrname("Simple Server");
response->set_takeuptime(0.01);
return grpc::Status::OK;
}
// Test3
grpc::Status Test3(grpc::ServerContext* context,
const Simple::TestRequest* request,
Simple::TestReply* response)
override
{
std::ostrstream os;
os << "Client Name = " << request->name() << '\n';
os << "Clinet ID = " << request->id() << '\n';
os << "Clinet Value= " << request->value()<< '\n';
std::string message = os.str();
response->set_tid(__LINE__);
response->set_svrname(__FILE__);
response->set_takeuptime(1.234);
// grpc狀態可以設置message
return grpc::Status(grpc::StatusCode::OK,std::move(message));
}
};
int main()
{
// 服務構建器,用於構建同步或者異步服務
grpc::ServerBuilder builder;
// 添加監聽的地址和端口,后一個參數用於設置認證方式,這里選擇不認證
builder.AddListeningPort("0.0.0.0:33333",grpc::InsecureServerCredentials());
// 創建服務對象
ServiceImpl service;
// 注冊服務
builder.RegisterService(&service);
// 構建服務器
std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
std::cout<<"Server Runing"<<std::endl;
// 進入服務處理循環(必須在某處調用server->Shutdown()才會返回)
server->Wait();
return 0;
}
編譯
寫完代碼之后,需要進行編譯。編譯的命令如下:
g++ -o server server.cpp simple.grpc.pb.cc simple.pb.cc \
-std=c++11 -I. `pkg-config --cflags protobuf grpc` \
`pkg-config --libs protobuf grpc++ grpc` \
-pthread -Wl,--no-as-needed -lgrpc++_reflection -Wl,--as-needed -ldl
# 因為我這里沒有grpc.pc文件,導致pkg-config找不到相關配置,所以我實際使用下面命令編譯
g++ -o service service.cpp simple.grpc.pb.cc simple.pb.cc -std=c++11 -I. -lgrpc++ -lgrpc -lprotobuf -lgpr -lz -lcares -laddress_sorting -lpthread -Wno-deprecated
這里自己編譯的grpc或者安裝的可能沒有grpc.pc文件,導致pkg-config程序找不到相關的配置,如果沒有就直接鏈接以下的庫即可:
# VC下使用
grpc.lib grpc++.lib libprotobuf.lib gpr.lib zlib.lib cares.lib address_sorting.lib ws2_32.lib
# gcc/clang下使用
-lgrpc++ -lgrpc -lprotobuf -lgpr -lz -lcares -laddress_sorting -lpthread
4、編寫客戶端代碼
客戶端代碼比服務端簡單多了,下面直接給出代碼(因為比較簡單,這里就只寫Test3的調用代碼了)。
關於客戶端,可以看一下這篇文章從grpc源碼講起(Client端的消息發送)
client.cpp代碼
客戶端這里只寫了一個接口的測試,另外兩個也都是差不多的,就不寫了。
#include "simple.grpc.pb.h"
#include <grpcpp/grpcpp.h>
#include <memory>
#include <iostream>
int main()
{
// 創建一個連接服務器的通道
std::shared_ptr<grpc::Channel> channel =
grpc::CreateChannel("localhost:33333", grpc::InsecureChannelCredentials());
// 創建一個stub
std::unique_ptr<Simple::Server::Stub> stub = Simple::Server::NewStub(channel);
// 上面部分可以復用,下面部分復用的話要自己考慮多線程安全問題
{
// 創建一個請求對象,用於打包要發送的請求數據
Simple::TestRequest request;
// 創建一個響應對象,用於解包響要接收的應數據
Simple::TestReply reply;
// 創建一個客戶端上下文。它可以用來向服務器傳遞附加的信息,以及可以調整某些RPC行為
grpc::ClientContext context;
// 發送請求,接收響應
grpc::Status st = stub->Test3(&context,request,&reply);
if(st.ok()){
// 輸出下返回數據
std::cout<< "tid = " << reply.tid()
<< "\nsvrname = " << reply.svrname()
<< "\ntakeuptime = " << reply.takeuptime() << std::endl;
}
else {
// 返回狀態非OK
std::cout<< "StatusCode = "<< st.error_code()
<<"\nMessage: "<< st.error_message() <<std::endl;
}
}
}
客戶端的編譯和服務是一樣的,只是把server.cpp改為client.cpp即可。
g++ -o client client.cpp simple.grpc.pb.cc simple.pb.cc -std=c++11 -I. -lgrpc++ -lgrpc -lprotobuf -lgpr -lz -lcares -laddress_sorting -lpthread -Wno-deprecated
5、簡單測試一下
編譯了server和client后,都運行起來測試了一下,可行。
服務端這里輸出,是因為我在代碼里面加了一行輸出,在Test3函數中輸出了一下函數名(__func__)和行號(__LINE__)。

這里推薦一個工具BloomRPC ,這是一個GRPC服務的可視化界面客戶端程序,可以直接加載proto文件,發送請求並接收返回。(我這里無法發送到非本地服務器,不知道是不是這個軟件的原因)

