一文搞懂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 相比,其序列化之后的数据量约为 13 到 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函数进行文件的上传和下载操作。