一文搞懂 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 框架

(4)RPC 的調用流程

要讓網絡通信細節對使用者透明,我們需要對通信細節進行封裝,我們先看下一個 RPC 調用的流程涉及到哪些通信細節:

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 的特點

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 的優點:

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 倍!

下面看一下性能和空間開銷對比:

從上圖可得出如下結論:

二、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 文件

(2) 代碼生成

3.2 服務端原理

(1) 服務實現類

(2) 服務器構建與啓動

3.3 客戶端原理

(1) 通道創建

客戶端首先創建一個 grpc::Channel 對象(如 grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials()))。這個通道表示與服務器的連接,它包含了服務器的地址、端口以及連接的安全配置等信息。通道負責建立和管理與服務器的網絡連接,並且在底層處理諸如連接建立、重連等操作。

(2) 存根創建與請求調用

四、案例分析

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 的適用性分析

(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 方法中:

在 DownloadFile 方法中:

③客戶端代碼實現

#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 函數中:

在 DownloadFile 函數中:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ViGP7Cau8BKZm9GXEs10vg