一文搞懂 gRPC:實現簡單的文件存儲服務
大家都知道,傳統的遠程過程調用(RPC)機制在應對複雜的分佈式環境時逐漸暴露出一些痛點。比如,在跨語言交互時的繁瑣配置、較差的性能表現以及難以有效管理大量微服務之間的通信等問題。
而 gRPC 則是對傳統 RPC 的一次革新,它憑藉着基於 HTTP/2 協議的優勢、高效的序列化格式(如 Protocol Buffers)以及豐富的功能特性,成功地解決了這些痛點。如果你正在爲構建分佈式系統中的通信模塊而煩惱,那麼 gRPC 絕對值得我們深入探討。
一、gRPC 概述
1.1RPC
(1) 什麼是 RPC ?
RPC(Remote Procedure Call Protocol)遠程過程調用協議,目標就是讓遠程服務調用更加簡單、透明。RPC 框架負責屏蔽底層的傳輸方式(TCP 或者 UDP)、序列化方式(XML/Json/ 二進制)和通信細節,服務調用者可以像調用本地接口一樣調用遠程的服務提供者,而不需要關心底層通信細節和調用過程。
(2) 爲什麼要用 RPC ?
當我們的業務越來越多、應用也越來越多時,自然的,我們會發現有些功能已經不能簡單劃分開來或者劃分不出來。此時可以將公共業務邏輯抽離出來,將之組成獨立的服務 Service 應用,而原有的、新增的應用都可以與那些獨立的 Service 應用 交互,以此來完成完整的業務功能。
所以我們急需一種高效的應用程序之間的通訊手段來完成這種需求,RPC 大顯身手的時候來了!
(3) 常用的 RPC 框架
-
gRPC:一開始由 google 開發,是一款語言中立、平臺中立、開源的遠程過程調用 (RPC) 系統。
-
Thrift:thrift 是一個軟件框架,用來進行可擴展且跨語言的服務的開發。它結合了功能強大的軟件堆棧和代碼生成引擎,以構建在 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml 這些編程語言間無縫結合的、高效的服務。
-
Dubbo:Dubbo 是一個分佈式服務框架,以及 SOA 治理方案,Dubbo 自 2011 年開源後,已被許多非阿里系公司使用。
-
Spring Cloud:Spring Cloud 由衆多子項目組成,如 Spring Cloud Config、Spring Cloud Netflix、Spring Cloud Consul 等,提供了搭建分佈式系統及微服務常用的工具。
(4)RPC 的調用流程
要讓網絡通信細節對使用者透明,我們需要對通信細節進行封裝,我們先看下一個 RPC 調用的流程涉及到哪些通信細節:
-
服務消費方(client)調用以本地調用方式調用服務;
-
client stub 接收到調用後負責將方法、參數等組裝成能夠進行網絡傳輸的消息體;
-
client stub 找到服務地址,並將消息發送到服務端;
-
server stub 收到消息後進行解碼;
-
server stub 根據解碼結果調用本地的服務;
-
本地服務執行並將結果返回給 server stub;
-
server stub 將返回結果打包成消息併發送至消費方;
-
client stub 接收到消息,並進行解碼;
-
服務消費方得到最終結果。
RPC 的目標就是要 2~8 這些步驟都封裝起來,讓用戶對這些細節透明,下面是網上的另外一幅圖,感覺一目瞭然:
1.2gRPC
gRPC 是由 google 開發的,是一款語言中立、平臺中立、開源的 RPC(Remote Procedure Call,遠程過程調用) 框架。在 gRPC 裏客戶端應用可以像調用本地對象一樣直接調用另一臺不同的機器上服務端應用的方法,使得您能夠更容易地創建分佈式應用和服務。與許多 RPC 框架類似,gRPC 也是基於以下理念:定義一個服務,指定其能夠被遠程調用的方法(包含參數和返回類型)。在服務端實現這個接口,並運行一個 gRPC 服務器來處理客戶端調用。
gRPC 使用 Protocol Buffers 作爲接口描述語言(IDL)以及底層的信息交換格式。Protocol Buffers 是一種靈活、高效的數據序列化格式,將結構化的數據序列化爲二進制格式,比其他傳輸協議如 JSON 和 XML 更快、更小、更簡單。它提供了一種定義和序列化數據結構的靈活方式,簡化了數據交互的複雜度,並且使用. proto 文件當做密碼本,記錄字段和編號的對應關係。
gRPC 提供了多種編程語言的類庫實現,服務定義文件和自動代碼生成功能。使用 protocol buffers 作爲接口描述語言,通過. proto 文件定義服務接口,其中包含消費者消費服務的方式、消費者能夠遠程調用的方法、調用這些方法所使用的參數和消息格式等。服務定義可以生成服務器端代碼和客戶端代碼,服務器端骨架通過提供低層級的通信抽象簡化服務器端邏輯,客戶端存根使用抽象簡化客戶端通信,爲不同的編程語言隱藏低層級的通信。
gRPC 具有多種服務類型,支持簡單 RPC、服務器流式 RPC、客戶端流式 RPC 和雙向流式 RPC,每種服務類型都有不同的使用場景和優點。
此外,gRPC 還具有一些擴展點,如調用管道可實現池化 tcp、tcp 探活等功能,還支持負載均衡、元數據 metadata 和攔截器等。它能夠在多種環境中運行和交互,從谷歌內部的服務器到個人筆記本,客戶端應用可以像調用本地對象一樣直接調用另一臺不同機器上服務端應用的方法,使得創建分佈式應用和服務更加容易。
gRPC 的特點
-
跨語言使用,支持 C++、Java、Go、Python、Ruby、C#、Node.js、Android Java、Objective-C、PHP 等編程語言;
-
基於 IDL 文件定義服務,通過 proto3 工具生成指定語言的數據結構、服務端接口以及客戶端 Stub;
-
通信協議基於標準的 HTTP/2 設計,支持雙向流、消息頭壓縮、單 TCP 的多路複用、服務端推送等特性,這些特性使得 gRPC 在移動端設備上更加省電和節省網絡流量;
-
序列化支持 PB(Protocol Buffer)和 JSON,PB 是一種語言無關的高性能序列化框架,基於 HTTP/2 + PB, 保障了 RPC 調用的高性能;
-
安裝簡單,擴展方便(用該框架每秒可達到百萬個 RPC)。
gRPC 交互過程
-
交換機在開啓 gRPC 功能後充當 gRPC 客戶端的角色,採集服務器充當 gRPC 服務器角色;
-
交換機會根據訂閱的事件構建對應數據的格式(GPB/JSON),通過 Protocol Buffers 進行編寫 proto 文件,交換機與服務器建立 gRPC 通道,通過 gRPC 協議向服務器發送請求消息;
-
服務器收到請求消息後,服務器會通過 Protocol Buffers 解譯 proto 文件,還原出最先定義好格式的數據結構,進行業務處理;
-
數據處理完後,服務器需要使用 Protocol Buffers 重編譯應答數據,通過 gRPC 協議向交換機發送應答消息;
-
交換機收到應答消息後,結束本次的 gRPC 交互。
簡單地說,gRPC 就是在客戶端和服務器端開啓 gRPC 功能後建立連接,將設備上配置的訂閱數據推送給服務器端。我們可以看到整個過程是需要用到 Protocol Buffers 將所需要處理數據的結構化數據在 proto 文件中進行定義。
1.3Protocol Buffers
你可以理解 ProtoBuf 是一種更加靈活、高效的數據格式,與 XML、JSON 類似,在一些高性能且對響應速度有要求的數據傳輸場景非常適用。
ProtoBuf 在 gRPC 的框架中主要有三個作用:定義數據結構、定義服務接口,通過序列化和反序列化方式提升傳輸效率。
爲什麼 ProtoBuf 會提高傳輸效率呢?
我們知道使用 XML、JSON 進行數據編譯時,數據文本格式更容易閱讀,但進行數據交換時,設備就需要耗費大量的 CPU 在 I/O 動作上,自然會影響整個傳輸速率。Protocol Buffers 不像前者,它會將字符串進行序列化後再進行傳輸,即二進制數據。
可以看到其實兩者內容相差不大,並且內容非常直觀,但是 Protocol Buffers 編碼的內容只是提供給操作者閱讀的,實際上傳輸的並不會以這種文本形式,而是序列化後的二進制數據,字節數會比 JSON、XML 的字節數少很多,速率更快。
gPRC 如何支撐跨平臺,多語言呢 ?
Protocol Buffers 自帶一個編譯器也是一個優勢點,前面提到的 proto 文件就是通過編譯器進行編譯的,proto 文件需要編譯生成一個類似庫文件,基於庫文件才能真正開發數據應用。
具體用什麼編程語言編譯生成這個庫文件呢?由於現網中負責網絡設備和服務器設備的運維人員往往不是同一組人,運維人員可能會習慣使用不同的編程語言進行運維開發,那麼 Protocol Buffers 其中一個優勢就能發揮出來——跨語言。
從上面的介紹,我們得出在編碼方面 Protocol Buffers 對比 JSON、XML 的優點:
-
標準的 IDL 和 IDL 編譯器,這使得其對工程師非常友好;
-
序列化數據非常簡潔,緊湊,與 XML 相比,其序列化之後的數據量約爲 1/3 到 1/10;
-
解析速度非常快,比對應的 XML 快約 20-100 倍;
-
提供了非常友好的動態庫,使用非常簡單,反序列化只需要一行代碼。
-
Protobuf 也有其侷限性:
-
由於 Protobuf 產生於 Google,所以目前其僅支持 Java、C++、Python 三種語言;
-
Protobuf 支持的數據類型相對較少,不支持常量類型;
-
由於其設計的理念是純粹的展現層協議(Presentation Layer),目前並沒有一個專門支持 Protobuf 的 RPC 框架。
Protobuf 適用場景:
-
Protobuf 具有廣泛的用戶基礎,空間開銷小以及高解析性能是其亮點,非常適合於公司內部的對性能要求高的 RPC 調用;
-
由於 Protobuf 提供了標準的 IDL 以及對應的編譯器,其 IDL 文件是參與各方的非常強的業務約束;
-
Protobuf 與傳輸層無關,採用 HTTP 具有良好的跨防火牆的訪問屬性,所以 Protobuf 也適用於公司間對性能要求比較高的場景;
-
由於其解析性能高,序列化後數據量相對少,非常適合應用層對象的持久化場景;
-
主要問題在於其所支持的語言相對較少,另外由於沒有綁定的標準底層傳輸層協議,在公司間進行傳輸層協議的調試工作相對麻煩。
1.4 基於 HTTP 2.0 標準設計
除了 Protocol Buffers 之外,從交互圖中和分層框架可以看到, gRPC 還有另外一個優勢——它是基於 HTTP 2.0 協議的。
由於 gRPC 基於 HTTP 2.0 標準設計,帶來了更多強大功能,如多路複用、二進制幀、頭部壓縮、推送機制。
這些功能給設備帶來重大益處,如節省帶寬、降低 TCP 連接次數、節省 CPU 使用等,gRPC 既能夠在客戶端應用,也能夠在服務器端應用,從而以透明的方式實現兩端的通信和簡化通信系統的構建。
HTTP 1.X 定義了四種與服務器交互的方式,分別爲 GET、POST、PUT、DELETE,這些在 HTTP 2.0 中均保留,我們看看 HTTP 2.0 的新特性:雙向流、多路複用、二進制幀、頭部壓縮。
1.5 性能對比
與採用文本格式的 JSON 相比,採用二進制格式的 protobuf 在速度上可以達到前者的 5 倍!
Auth0 網站所做的性能測試結果顯示,protobuf 和 JSON 的優勢差異在 Java、Python 等環境中尤爲明顯,下圖是 Auth0 在兩個 Spring Boot 應用程序間所做的對比測試結果。
結果顯示,protobuf 所需的請求時間最多隻有 JSON 的 20% 左右,即速度是其 5 倍!
下面看一下性能和空間開銷對比:
從上圖可得出如下結論:
-
XML 序列化(Xstream)無論在性能和簡潔性上比較差。
-
Thrift 與 Protobuf 相比在時空開銷方面都有一定的劣勢。
-
Protobuf 和 Avro 在兩方面表現都非常優越。
二、C++ 開發 gRPC 服務端和客戶端
2.1 安裝依賴
Protocol Buffers (protobuf):gRPC 使用 Protocol Buffers 作爲其序列化工具,需要先安裝它。可以從 Protocol Buffers 的官方網站下載並安裝。
gRPC C++:安裝支持 C++ 的 gRPC 庫。可以按照 gRPC 的官方文檔進行安裝,通常需要從源代碼編譯安裝或者使用包管理工具(如 Conan 等)進行安裝。
2.2 定義服務接口
創建一個.proto文件來定義 gRPC 服務的接口。例如:
syntax = "proto3";
package example;
service Calculator {
// 定義加法運算方法
rpc Add (Request) returns (Response) {}
// 定義減法運算方法
rpc Subtract (Request) returns (Response) {}
}
message Request {
int32 a = 1;
int32 b = 2;
}
message Response {
int32 result = 1;
}
在這個例子中,定義了一個名爲 Calculator 的服務,包含 Add 和 Subtract 兩個遠程過程調用方法,以及 Request 和 Response 兩個消息類型。
2.3 生成代碼
使用 protoc 工具和 gRPC 的 C++ 插件來生成 C++ 代碼。假設 .proto 文件名爲 calculator.proto,在命令行中執行以下命令:
protoc -I=<proto文件所在的目錄> --cpp_out=<輸出的C++代碼目錄> --grpc_out=<輸出的gRPC C++代碼目錄> <proto文件路徑>
2.4 實現服務端
#include "calculator.grpc.pb.h"
#include <grpcpp/grpcpp.h>
#include <memory>
class CalculatorServiceImpl final : public example::Calculator::Service {
public:
grpc::Status Add(grpc::ServerContext* context, const example::Request* request,
example::Response* response) override {
response->set_result(request->a() + request->b());
return grpc::Status::OK;
}
grpc::Status Subtract(grpc::ServerContext* context, const example::Request* request,
example::Response* response) override {
response->set_result(request->a() - request->b());
return grpc::Status::OK;
}
};
以上代碼定義了一個 CalculatorServiceImpl 類,它繼承自生成的 example::Calculator::Service 類,並實現了 Add 和 Subtract 方法。在這些方法中,根據請求中的參數計算結果,並設置到響應中。然後創建一個 gRPC 服務器:
int main() {
std::string server_address("0.0.0.0:50051");
CalculatorServiceImpl service;
grpc::ServerBuilder builder;
builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
builder.RegisterService(&service);
std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
std::cout << "Server listening on " << server_address << std::endl;
server->Wait();
return 0;
}
在 main 函數中,創建了一個 ServerBuilder 對象,設置服務器的監聽地址和認證信息,註冊服務實現類,然後構建並啓動服務器。最後,使用 server->Wait() 讓服務器一直運行,等待客戶端的請求。
2.5 實現客戶端
#include "calculator.grpc.pb.h"
#include <grpcpp/grpcpp.h>
#include <iostream>
int main() {
std::shared_ptr<grpc::Channel> channel = grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials());
example::Calculator::Stub stub(channel);
example::Request request;
request.set_a(3);
request.set_b(2);
example::Response response;
grpc::ClientContext context;
grpc::Status status = stub.Add(&context, request, &response);
if (status.ok()) {
std::cout << "Result: " << response.result() << std::endl;
} else {
std::cout << "Error: " << status.error_code() << ": " << status.error_message() << std::endl;
}
return 0;
}
在客戶端代碼中,首先創建一個 grpc::Channel 對象,指定連接的服務器地址和認證信息。然後,使用這個通道創建一個 example::Calculator::Stub 對象,它是客戶端的代理對象,用於調用遠程服務。
創建請求對象,設置請求參數,然後調用 stub 的相應方法(這裏是 Add 方法),傳遞請求對象和響應對象。最後,檢查調用的狀態,如果成功,則打印結果;如果失敗,則打印錯誤信息。
三、原理解析
3.1 服務定義與代碼生成原理
(1)proto 文件
-
在 gRPC 中,.proto 文件是服務定義的核心。它使用 Protocol Buffers 的語法來定義服務接口、消息結構等內容。
-
對於服務接口,如前面示例中的 Calculator 服務,它明確了可供遠程調用的方法(如 Add 和 Subtract),這些方法的定義類似於函數聲明,指定了輸入參數(Request 類型)和返回值(Response 類型)。
-
消息結構(如 Request 和 Response)定義了數據的組織方式。每個消息中的字段都有唯一的編號(如 int32 a = 1; 中的 1),這有助於在序列化和反序列化時準確識別字段。
(2) 代碼生成
-
當使用 protoc 工具結合 gRPC 的 C++ 插件處理. proto 文件時,它會根據定義生成對應的 C++ 代碼。
-
對於服務接口,會生成一個抽象基類(如 example::Calculator::Service),其中包含純虛函數,這些純虛函數對應着. proto 文件中定義的遠程過程調用方法。這個抽象基類定義了服務端必須實現的接口。
-
對於消息結構,會生成對應的 C++ 類(如 example::Request 和 example::Response),這些類提供了設置和獲取消息字段值的方法,並且內部實現了序列化和反序列化邏輯,以便在網絡傳輸中能夠將數據轉換爲二進制格式並進行解析。
3.2 服務端原理
(1) 服務實現類
-
服務端需要創建一個類(如 CalculatorServiceImpl)來繼承由代碼生成的抽象基類(example::Calculator::Service)並實現其中的虛函數。
-
在實現的虛函數(如 Add 和 Subtract)內部,服務端根據業務邏輯處理請求數據,然後將結果填充到響應對象中。這個過程涉及到從請求消息對象中獲取輸入數據,進行相應的計算或操作,再通過響應消息對象的方法設置結果。
(2) 服務器構建與啓動
-
grpc::ServerBuilder 是構建 gRPC 服務器的關鍵類。
-
通過 AddListeningPort 方法設置服務器監聽的地址和端口,以及使用的安全憑證(如 grpc::InsecureServerCredentials 表示不使用加密的簡單模式)。
-
使用 RegisterService 方法將服務實現類註冊到服務器構建器中,這樣服務器就知道如何處理特定服務的請求。
-
最後,BuildAndStart 方法構建並啓動服務器實例。server->Wait 使服務器進入阻塞狀態,持續監聽客戶端的連接請求並處理請求。
3.3 客戶端原理
(1) 通道創建
客戶端首先創建一個 grpc::Channel 對象(如 grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials()))。這個通道表示與服務器的連接,它包含了服務器的地址、端口以及連接的安全配置等信息。通道負責建立和管理與服務器的網絡連接,並且在底層處理諸如連接建立、重連等操作。
(2) 存根創建與請求調用
-
使用創建的通道創建一個 example::Calculator::Stub 對象,這個存根是客戶端代理,它隱藏了與服務器通信的底層細節。
-
客戶端創建請求消息對象(如 example::Request),設置請求的參數值。然後通過存根對象調用對應的遠程過程調用方法(如 stub.Add),傳遞 grpc::ClientContext(用於設置請求的上下文信息,如超時等)、請求消息對象和一個用於接收響應的消息對象(如 example::Response)。
-
當調用存根的方法時,客戶端將請求消息對象序列化,通過通道發送到服務器。然後等待服務器的響應,一旦收到響應,客戶端將對響應消息進行反序列化,並根據 grpc::Status 對象判斷調用是否成功,如果成功則可以從響應消息對象中獲取結果。
四、案例分析
4.1 使用場景
(1) 微服務架構中的服務間通信
場景描述
在微服務架構中,不同的服務通常由不同的團隊開發,可能使用不同的技術棧。gRPC 的多語言支持特性使其成爲微服務間通信的理想選擇。例如,一個電商系統可能包含用戶服務、訂單服務、商品服務等多個微服務。用戶服務可能用 C++ 開發,用於處理用戶註冊、登錄等功能;訂單服務可能用 Java 開發,負責訂單的創建、查詢和管理;商品服務可能用 Python 開發,管理商品的信息和庫存。
C++ 開發的 gRPC 服務可以作爲這些微服務中的一部分,與其他語言開發的微服務進行高效通信。例如,用戶服務(C++)可能需要調用訂單服務(Java)來查詢用戶的訂單歷史,或者商品服務(Python)需要調用用戶服務(C++)來驗證用戶的權限。
優勢分析
高性能:gRPC 基於 HTTP/2 協議,具有低延遲、高吞吐量的特點。在微服務架構中,大量的服務間調用需要快速響應,gRPC 能夠滿足這一需求。例如,在高併發的電商促銷活動期間,大量的訂單查詢和商品信息查詢等操作需要快速完成,gRPC 可以確保服務間通信的高效性。
強類型接口:通過使用 Protocol Buffers 定義服務接口,C++ 開發的 gRPC 服務能夠提供清晰、嚴格的接口定義。這有助於減少服務間集成時的錯誤,提高代碼的可維護性。不同語言的開發團隊可以根據定義好的. proto 文件準確地實現服務的客戶端和服務器端,避免了因接口不清晰導致的通信問題。
(2) 實時數據處理系統
場景描述
在實時數據處理系統中,如金融交易數據處理、物聯網傳感器數據採集與分析等場景,數據需要在不同組件之間快速傳遞和處理。例如,在金融交易系統中,交易數據從前端界面(可能是 C++ 編寫的高性能交易客戶端)流向交易服務器,交易服務器可能需要對數據進行驗證、路由到不同的交易處理模塊(如訂單匹配、風險評估等),並且將處理結果及時反饋給客戶端。
物聯網場景中,傳感器(如溫度、溼度傳感器)不斷採集數據,通過網絡傳輸到數據處理中心。數據處理中心可能用 C++ 開發基於 gRPC 的服務來接收這些數據,進行實時分析、存儲等操作。
優勢分析
雙向流支持:gRPC 的雙向流特性非常適合實時數據處理。在金融交易系統中,客戶端可以持續向服務器發送交易請求,同時服務器也可以不斷地向客戶端推送交易狀態更新、市場行情等信息。在物聯網場景中,傳感器可以持續向數據處理中心發送數據流,數據處理中心也可以向傳感器發送控制指令,如調整傳感器的採集頻率等。
高效的序列化:Protocol Buffers 的高效序列化和反序列化能力,使得數據在傳輸過程中的開銷較小。對於實時數據處理系統,大量的數據需要快速傳輸,這一特性可以減少網絡傳輸的延遲,提高系統的整體性能。
(3) 分佈式計算系統
場景描述
在分佈式計算系統中,如大規模科學計算(例如氣象模擬、基因測序分析等),計算任務被分解成多個子任務,分佈在不同的計算節點上進行處理。這些計算節點可能用 C++ 編寫以提高計算效率,並且需要相互通信來協調計算任務、交換中間結果等。
例如,在氣象模擬中,不同區域的氣象數據計算任務可能分配到不同的計算節點。這些節點之間需要通過網絡通信來共享邊界條件、合併計算結果等。
優勢分析
跨平臺和跨語言:gRPC 允許不同平臺和語言編寫的計算節點進行通信。即使在分佈式計算系統中存在多種語言編寫的組件,C++ 開發的 gRPC 服務也能夠與它們無縫對接。例如,部分計算節點可能是用 Python 編寫的數據分析模塊,C++ 編寫的計算節點可以通過 gRPC 與它們進行通信,實現數據和任務的交互。
可擴展性:隨着計算需求的增加,可以方便地向分佈式計算系統中添加新的計算節點。新節點只需要實現相應的 gRPC 服務接口,就可以融入現有的系統中。這種可擴展性對於大規模分佈式計算任務非常重要,例如在基因測序分析中,隨着測序數據量的不斷增加,可以添加更多的計算資源來加速分析過程。
4.2 案例分析:簡單的文件存儲服務
(1) 案例詳情
需求描述:構建一個簡單的文件存儲服務,客戶端可以上傳文件到服務器,並且能夠從服務器下載文件。服務需要支持多個客戶端同時操作,並且要保證數據傳輸的高效性和可靠性。
gRPC 的適用性分析
-
多客戶端併發:gRPC 基於 HTTP/2 協議,具有多路複用的特性,可以在一個連接上處理多個併發請求,非常適合多個客戶端同時與服務器交互的場景,如多個用戶同時上傳或下載文件。
-
高效性:使用 Protocol Buffers 進行數據序列化,相比於傳統的文本格式(如 JSON),可以減少網絡傳輸的數據量,提高文件傳輸的效率。
-
可靠性:gRPC 提供了錯誤處理機制,在文件傳輸過程中如果出現網絡故障或其他錯誤,可以進行適當的處理,如重試上傳或下載操作。
(2) 代碼實現
①定義服務接口(.proto 文件)
syntax = "proto3";
package file_service;
// 定義文件元數據消息
message FileMetadata {
string name = 1;
int64 size = 2;
}
// 定義文件塊消息
message FileChunk {
bytes data = 1;
int32 chunk_number = 2;
}
// 定義文件存儲服務
service FileStorageService {
// 上傳文件方法
rpc UploadFile(stream FileChunk) returns (FileMetadata);
// 下載文件方法
rpc DownloadFile(FileMetadata) returns (stream FileChunk);
}
在這個. proto 文件中,首先定義了 FileMetadata 消息類型,用於描述文件的基本信息,如名稱和大小。FileChunk 消息類型用於表示文件的塊,包含文件塊的數據(以字節流形式)和塊的編號。FileStorageService 是定義的服務,包含 UploadFile 和 DownloadFile 兩個方法。
UploadFile 方法接收一個文件塊的流,因爲文件可能較大,需要分塊上傳,最後返回上傳文件的元數據;DownloadFile 方法接收文件的元數據,然後返回一個文件塊的流,用於下載文件。
②服務端代碼實現
#include <grpcpp/grpcpp.h>
#include <fstream>
#include "file_service.pb.h"
// 實現文件存儲服務類
class FileStorageServiceImpl final : public file_service::FileStorageService::Service {
public:
grpc::Status UploadFile(grpc::ServerContext* context,
grpc::ServerReader<file_service::FileChunk>* reader,
file_service::FileMetadata* response) override {
std::ofstream outfile;
file_service::FileChunk chunk;
int chunk_count = 0;
while (reader->Read(&chunk)) {
if (chunk_count == 0) {
// 創建新文件
outfile.open(chunk.data(), std::ios::binary);
} else {
// 追加文件塊
outfile.write(chunk.data().data(), chunk.data().size());
}
chunk_count++;
}
outfile.close();
response->set_name("uploaded_file.txt");
response->set_size(1024); // 這裏假設文件大小爲1024字節,實際應根據寫入情況計算
return grpc::Status::OK;
}
grpc::Status DownloadFile(grpc::ServerContext* context, const file_service::FileMetadata* request,
grpc::ServerWriter<file_service::FileChunk>* writer) override {
std::ifstream infile(request->name(), std::ios::binary);
if (!infile) {
return grpc::Status(grpc::StatusCode::NOT_FOUND, "File not found");
}
int chunk_number = 0;
while (infile) {
file_service::FileChunk chunk;
chunk.set_chunk_number(chunk_number);
std::vector<char> buffer(1024);
infile.read(buffer.data(), buffer.size());
chunk.set_data(buffer.data(), infile.gcount());
writer->Write(chunk);
chunk_number++;
}
infile.close();
return grpc::Status::OK;
}
};
在 UploadFile 方法中:
-
首先創建一個 ofstream 對象用於寫入文件。
-
然後通過 ServerReader 逐塊讀取客戶端發送的文件塊。如果是第一個塊,則創建新文件;否則,將塊追加到文件中。
-
最後關閉文件,設置響應的文件元數據(這裏簡單設置了文件名和大小),並返回 OK 狀態。
在 DownloadFile 方法中:
-
根據客戶端請求的文件元數據中的文件名創建 ifstream 對象用於讀取文件。如果文件不存在,則返回 NOT_FOUND 狀態。
-
然後逐塊讀取文件內容,將每塊數據設置到 FileChunk 消息中,並通過 ServerWriter 發送給客戶端。
-
最後關閉文件,返回 OK 狀態。
③客戶端代碼實現
#include <grpcpp/grpcpp.h>
#include <iostream>
#include "file_service.pb.h"
// 上傳文件函數
void UploadFile(grpc::Channel* channel) {
file_service::FileStorageService::Stub stub(channel);
grpc::ClientContext context;
std::vector<file_service::FileChunk> chunks;
// 這裏假設已經將文件分塊存儲在chunks向量中
file_service::FileMetadata response;
grpc::Status status;
{
grpc::ClientWriter<file_service::FileChunk> writer = stub.UploadFile(&context, &response);
for (const auto& chunk : chunks) {
status = writer.Write(chunk);
if (!status.ok()) {
break;
}
}
writer.Close();
status = writer.Finish();
}
if (status.ok()) {
std::cout << "File uploaded successfully. Name: " << response.name() << ", Size: " << response.size() << std::endl;
} else {
std::cout << "File upload failed. Error: " << status.error_message() << std::endl;
}
}
// 下載文件函數
void DownloadFile(grpc::Channel* channel) {
file_service::FileStorageService::Stub stub(channel);
grpc::ClientContext context;
file_service::FileMetadata request;
request.set_name("uploaded_file.txt");
grpc::Status status;
{
grpc::ClientReader<file_service::FileChunk> reader = stub.DownloadFile(&context, request);
file_service::FileChunk chunk;
while (reader.Read(&chunk)) {
// 這裏可以將接收到的文件塊進行合併存儲等操作
std::cout << "Received chunk number: " << chunk.chunk_number() << std::endl;
}
status = reader.Finish();
}
if (status.ok()) {
std::cout << "File downloaded successfully." << std::endl;
} else {
std::cout << "File download failed. Error: " << status.error_message() << std::endl;
}
}
int main() {
// 創建不安全的通道,實際應用中可使用安全通道
std::shared_ptr<grpc::Channel> channel = grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials());
// 上傳文件
UploadFile(channel);
// 下載文件
DownloadFile(channel);
return 0;
}
在 UploadFile 函數中:
-
首先創建服務存根 stub,然後創建 ClientContext 和用於存儲文件元數據的 response 對象。
-
通過 stub.UploadFile 獲取 ClientWriter,逐塊將文件塊寫入到服務器。如果寫入過程中出現錯誤,則停止寫入。寫入完成後關閉 ClientWriter 並獲取最終狀態。
-
根據最終狀態判斷文件是否上傳成功,並輸出相應信息。
在 DownloadFile 函數中:
-
同樣先創建存根和 ClientContext,設置請求的文件元數據(這裏指定了文件名)。
-
通過 stub.DownloadFile 獲取 ClientReader,逐塊讀取服務器發送的文件塊,並進行簡單的輸出(實際應用中可進行文件合併存儲操作)。讀取完成後獲取最終狀態,根據狀態判斷文件是否下載成功並輸出相應信息。
-
在 main 函數中,創建與服務器連接的通道,然後分別調用 UploadFile 和 DownloadFile 函數進行文件的上傳和下載操作。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/ViGP7Cau8BKZm9GXEs10vg