gRPC & Protocol Buffer 构建高性能接口实践

protobuf message定义_Protobuf语言指南 什么是 ProtobufProtobuf是Protocol Buffers的简称,它是Google公司开发的一种数据描述语言,用于描述一种轻便高效的结构化数据存储格式,并于2008年对外开源。Protobuf可以用于结构化数据串行化,或者说序列化。它的设计非常适用于在网络通讯中的数据载体,很适合做数据存储或 RPC 数据交换格式,它序列化出来的数据量少再加上以 K-V 的方式来存储数据,对消息的版... 阅读详情

介绍如何使用 gRPC 和 ProtoBuf,快速了解 gRPC 可以参考这篇文章第一段:gRPC quick Start。

接口开发是软件开发占据举足轻重的地位,是现代软件开发之基石。体现在无论是前后端分离的 Web 前端还是移动客户端,乃至基于不同系统、编程语言构建的软件系统之间,API 都是充当桥梁的作用把不同端的系统链接在一起从而形成了一套稳固的商用系统。

基于 Web 的接口通常都是 RESTful API 结合 JSON 在前后端之间传递信息,这种模式比较适合于前后端分离及移动客户端于后端通信;但对于承载大规模并发、高性能要求的微服务架构,基于 JSON 传输的 RESTful 是否还适用于高并发、伸缩性强及业务逻辑复杂的软件架构吗?基于 RESTful 架构是否能够简单是想双向流 (bidrectional stream) 的接口。gRPC 和 protocol buffer 就是解决上述问题。

关于 gRPC 和 Protobuf 的简介可以看看这篇文章:Google Protocol Buffer 和 gRPC 简介

gRPC & Protocol Buffer 实践

我本地的 GOPATH 目录为 /Users/hww/work/go ,给我们的 demo 项目新建一个目录 cd $GOPATH/src && mkdir rpc-protobuf

定义 Protocol Buffer 的消息类型和服务

在项目根目录 rpc-protobuf 新建文件目录 customer。首先给 Protocol Bufffer 文件定义服务接口和 paylaod 信息的数据结构,$GOPATH/scr/rpc-protobuf/customer/customer.proto:

syntax = "proto3";
package customer;

// The Customer sercie definition
service Customer {
    // Get all Customers with filter - A server-to-client streaming RPC.
    rpc GetCustomers(CustomerFilter) returns (stream CustomerRequest) {}

    // Create a new Customer - A simple RPC
    rpc CreateCustomer (CustomerRequest) returns (CustomerResponse) {}
}

message CustomerRequest {
    int32 id = 1;   // Unique ID number for a Customer.
    string name = 2;
    string email = 3;
    string phone = 4;

    message Address {
        string street = 1;
        string city = 2;
        string state = 3;
        string zip = 4;
        bool isShippingAddress = 5;
    }

    repeated Address addresses = 5;
}

message CustomerResponse {
    int32 id = 1;
    bool success = 2;
}

message CustomerFilter {
    string keyword = 1;
}

在 .proto 文件,第一行代码为版本号,在这里我们使用了 proto3 ;第二行代码为包名,通过该文件生成的 Go 源码包名和这里的一致为 customer

我们定义了消息类型和服务接口。标准数据类型有 int32, float, double, 或 string 这些常见的类型。一种消息类型就是多个字段的集合,每个字段都被一个在该消息中唯一的整数标记;Customer 服务中有两个 RPC 方法:

service Customer {
    // Get all Customers with filter - A server-to-client streaming RPC.
    rpc GetCustomers(CustomerFilter) returns (stream CustomerRequest) {}

    // Create a new Customer - A simple RPC
    rpc CreateCustomer (CustomerRequest) returns (CustomerResponse) {}
}

解释 Customer 服务之前,我们首先来大概了解一下 gRPC 中的三种类型的 RPC 方法。

  • simple RPC
    应用于常见的典型的 Request/Response 模型。客户端通过 stub 请求 RPC 的服务端并等待服务端的响应。
  • Server-side streaming RPC
    客户端给服务端发送一个请求并获取服务端返回的流,用以读取一连串的服务端响应。stream 关键字在响应类型的前面。// 例子
    rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){}
  • Client-side streaming RPC
    客户端发送的请求 payload 有一连串的的信息,通过流给服务端发送请求。stream 关键字在请求类型前面。// 例子
    rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {}
  • Bidirectional streaming RPC
    服务端和客户端之间都使用 read-write stream 进行通信。stream 关键字在请求类型和响应类型前面。// 例子
    rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){}

理解 gRPC 提供的四种类型 RPC 方法之后,回到 Customer 的例子中。在 Customer 服务提供了两种类型的 RPC 方法,分别是 simple RPC(CreateCustomer) 和 server-side streaming(GetCustomers) 。CreateCustomer 遵循标准 Request/Response 规范新建一个用户;GetCustomers 方法中,服务端通过 stream 返回多个消费者信息的列表。

基于 proto 文件生成服务端和客户端的 Go 代码

定义好 proto 文件之后,然后生成你需要的编程语言源代码,这些源代码是服务端和客户端业务逻辑代码的接口。客户端代码通过消息类型和服务接口调用 RPC 方法。
protocol buffer 编译器通过 gRPC 的 Go 插件生成客户端和服务端的代码。在项目根目录下运行命令:

protoc -I customer/ customer/customer.proto --go_out=plugins=grpc:customer

在 customer 目录下生成了 customer.pb.go 文件。该源码包含三大类功能:

  • 读写和序列化请求和响应消息类型
  • 提供定义在 proto 文件中定义的客户端调用方法接口
  • 提供定义在 proto 文件中定义的服务端实现方法接口

新建 gRPC 服务

以下代码片段新建依据 proto 文件中定义的服务新建 gRPC 服务端。

// server/main.go
package main

import (
    "log"
    "net"
    "strings"

    "golang.org/x/net/context"
    "google.golang.org/grpc"

    pb "rpc-protobuf/customer"
)

const (
    port = ":50051"
)

// server is used to implement customer.CustomerServer.
type server struct {
    savedCustomers []*pb.CustomerRequest
}

// CreateCustomer creates a new Customer
func (s *server) CreateCustomer(ctx context.Context, in *pb.CustomerRequest) (*pb.CustomerResponse, error) {
    s.savedCustomers = append(s.savedCustomers, in)
    return &pb.CustomerResponse{Id: in.Id, Success: true}, nil
}

// GetCustomers returns all customers by given filter
func (s *server) GetCustomers(filter *pb.CustomerFilter, stream pb.Customer_GetCustomersServer) error {
    for _, customer := range s.savedCustomers {
        if filter.Keyword != "" {
            if !strings.Contains(customer.Name, filter.Keyword) {
                continue
            }
        }
        if err := stream.Send(customer); err != nil {
            return err
        }
    }
    return nil
}

func main() {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatal("failed to listen: %v", err)
    }

    //Create a new grpc server
    s := grpc.NewServer()
    pb.RegisterCustomerServer(s, &server{})
    s.Serve(lis)
}

服务端源码中,server 结构体定义在 customer.pb.go 中的 CustomerServer 接口;CreateCustomer 和 GetCustomers 两个方法定义在 customer.pb.go 文件的 CustomerClient 接口中。

CreateCustomer 是一个 simple rpc 类型的 RPC 方法,在这里它接受两个参数,分别是 context objct 和客户端的请求信息,返回值为 proto 文件定义好的 CustomerResponse 对象;GetCustomers 是一个 server-side streaming 类型的 RPC 方法,接受两个参数:CustomerRequest 对象、以及用来作为服务端对客户端响应 stream 的 对象 Customer_GetCustomersServer 。
看看 customer.pb.go 中对 CustomerServer 接口的定义:

// Server API for Customer service

type CustomerServer interface {
    // Get all Customers with filter - A server-to-client streaming RPC.
    GetCustomers(*CustomerFilter, Customer_GetCustomersServer) error
    // Create a new Customer - A simple RPC
    CreateCustomer(context.Context, *CustomerRequest) (*CustomerResponse, error)
}

对比理解服务端代码对两个方法的实现,我们就可以理解参数的传递原理。

服务端代码中 GetCustomers 方法内部有一行代码 stream.Send(customer) 这个 Send 方法是 customer.pb.go 给 Customer_GetCustomersServer 接口定义并好的方法,表示给客户端返回 stream

最后看看服务端代码中的 main 方法。
首先 grpc.NewServer 函数新建一个 gRPC 服务端;
然后调用 customer.pb.go 中的 RegisterCustomerServer(s *grpc.Server, srv CustomerServer) 函数注册该服务:pb.RegisterCustomerServer(s, &server{}) ;
最后通过 gRPC 的 Golang API Server.Serve 监听指定的端口号:s.Serve(lis),新建一个 ServerTransport 和 service goroutine处理监听的端口收到的请求。

新建 gRPC 客户端

首先看 customer.pb.go 生成的客户端调用方法接口部分的代码:

// Client API for Customer service

type CustomerClient interface {
    // Get all Customers with filter - A server-to-client streaming RPC.
    GetCustomers(ctx context.Context, in *CustomerFilter, opts ...grpc.CallOption) (Customer_GetCustomersClient, error)
    // Create a new Customer - A simple RPC
    CreateCustomer(ctx context.Context, in *CustomerRequest, opts ...grpc.CallOption) (*CustomerResponse, error)
}

type customerClient struct {
    cc *grpc.ClientConn
}

func NewCustomerClient(cc *grpc.ClientConn) CustomerClient {
    return &customerClient{cc}
}

*grpc.ClientConn 表示连接到 RPC 服务端的客户端,NewCustomerClient 函数返回一个 customerClient 结构体对象。CustomerClient 接口定义了两个能够被客户端服务调用的方法,另外我们可以在 customer.pb.go 看到给 customerClient 类型的结构体实现这两个函数的方法,故客户端对象能够调用 GetCustomers 和 CreateCustomer 方法:

func (c *customerClient) GetCustomers(ctx context.Context, in *CustomerFilter, opts ...grpc.CallOption) (Customer_GetCustomersClient, error) {
  ...
}

...

func (c *customerClient) CreateCustomer(ctx context.Context, in *CustomerRequest, opts ...grpc.CallOption) (*CustomerResponse, error) {
  ...
}

接着回到实现客户端的源码:

// client/main.go
package main

import (
    "io"
    "log"

    "golang.org/x/net/context"
    "google.golang.org/grpc"

    pb "rpc-protobuf/customer"
)

const (
    address = "localhost:50051"
)

// createCustomer calls the RPC method CreateCustomer of CustomerServer
func createCustomer(client pb.CustomerClient, customer *pb.CustomerRequest) {
    resp, err := client.CreateCustomer(context.Background(), customer)
    if err != nil {
        log.Fatalf("Could not create Customer: %v", err)
    }
    if resp.Success {
        log.Printf("A new Customer has been added with id: %d", resp.Id)
    }
}

// GetCustomers calls the RPC method GetCustomers of CustomerServer
func getCustomers(client pb.CustomerClient, filter *pb.CustomerFilter) {
    // calling the streaming API
    stream, err := client.GetCustomers(context.Background(), filter)
    if err != nil {
        log.Fatal("Error on get customers: %v", err)
    }
    for {
        customer, err := stream.Recv()
        if err == io.EOF {
            break
        }

        if err != nil {
            log.Fatal("%v.GetCustomers(_) = _, %v", client, err)
        }
        log.Printf("Customer: %v", customer)
    }
}

func main() {
    // Set up a connection to the RPC server
    conn, err := grpc.Dial(address, grpc.WithInsecure())
    if err != nil {
        log.Fatal("did not connect: %v", err)
    }
    defer conn.Close()
    // creates a new CustomerClient
    client := pb.NewCustomerClient(conn)

    customer := &pb.CustomerRequest{
        Id:    101,
        Name:  "Shiju Varghese",
        Email: "shiju@xyz.com",
        Phone: "732-757-2923",
        Addresses: []*pb.CustomerRequest_Address{
            &pb.CustomerRequest_Address{
                Street:            "1 Mission Street",
                City:              "San Francisco",
                State:             "CA",
                Zip:               "94105",
                IsShippingAddress: false,
            },
            &pb.CustomerRequest_Address{
                Street:            "Greenfield",
                City:              "Kochi",
                State:             "KL",
                Zip:               "68356",
                IsShippingAddress: true,
            },
        },
    }

    // Create a new customer
    createCustomer(client, customer)

    customer = &pb.CustomerRequest{
        Id:    102,
        Name:  "Irene Rose",
        Email: "irene@xyz.com",
        Phone: "732-757-2924",
        Addresses: []*pb.CustomerRequest_Address{
            &pb.CustomerRequest_Address{
                Street:            "1 Mission Street",
                City:              "San Francisco",
                State:             "CA",
                Zip:               "94105",
                IsShippingAddress: true,
            },
        },
    }

    // Create a new customer
    createCustomer(client, customer)
    //Filter with an empty Keyword
    filter := &pb.CustomerFilter{Keyword: ""}
    getCustomers(client, filter)

}

客户端需要建立 gRPC 通道(channel) 才可与服务端建立通信,调用 RPC 方法。grpc.Dial函数表示新建与 RPC 服务端的连接。Dial函数在 gRPC golang 实现的库中声明代码如下:

func Dial(target string, opts ...DialOption) (*ClientConn, error)

除了连接地址作为第一个参数外,还可以传多个可选参数。这些可选参数表示鉴权校验,例如 TLS 或者 JWT 。在这里的 grpc.WithInsecure 表示客户端连接的安全传输被禁用。

调用服务端的 RPC 方法前,首先需要新建客户端 stub :

// creates a new CustomerClient
client := pb.NewCustomerClient(conn)

在例子中,通过调用 RPC CreateCustomer 方法新增了两个 customer 数据 : createCustomer(client, customer) ;调用 RPC GetCustomers 方法获取所有 customers 数据。

至此,我们已经简单地实现了一套 gRPC 客户端和服务端代码。在项目根目录下运行命令:

➜  rpc-protobuf (nohup go run server/main.go &) && go run client/main.go
appending output to nohup.out
2017/10/28 18:08:02 A new Customer has been added with id: 101
2017/10/28 18:08:02 A new Customer has been added with id: 102
2017/10/28 18:08:02 Customer: id:101 name:"Shiju Varghese" email:"shiju@xyz.com" phone:"732-757-2923" addresses:<street:"1 Mission Street" city:"San Francisco" state:"CA" zip:"94105" > addresses:<street:"Greenfield" city:"Kochi" state:"KL" zip:"68356" isShippingAddress:true >
2017/10/28 18:08:02 Customer: id:102 name:"Irene Rose" email:"irene@xyz.com" phone:"732-757-2924" addresses:<street:"1 Mission Street" city:"San Francisco" state:"CA" zip:"94105" isShippingAddress:true > 

https://zhuanlan.zhihu.com/p/30624616
嵌入式Protobuf扫盲入门 嵌入式Protobuf扫盲入门 什么是Protobuf Protocol Buffer (简称Protobuf) 是Google出品的性能优异、跨语言、跨平台的序列化库。 2001年初,Protobuf首先在Google内部创建, 我们把它称之为 proto1,一直以来在Google的内部使用,其中也不断的演化,根据使用者的需求也添加很多新的功能,一些内部库依赖它。几乎每个Google的开发者都会使用到它。 Google开始开源它的内部项目时,因为依赖的关系,所以他们决定首先把Protobuf开源出去。 p 阅读详情

相关推荐

删除结点

对给定的单链表 L ,设计一个算法,删除 L 中值为 x 的结点的直接前驱结点。 1 由键盘输入值,换行表示输入结束,根据输入建立单链表。 2 输入链表中的一个元素值,从链表中删除这个元素的前驱结点。 3 对删除元素后的链表元素在屏幕上显示。 测试用例1 11 22 33 44 55 66 77↵ 44↵ 期待输出1 11 22 44 55 66 77↵ #include <iostre...

SChenlyx 1616

实战搞定gRPC之移植篇

一、交叉编译protobuf 1.配置交叉编译器 export PATH=$PATH:/opt/EC20_crosstool/ql-ol-crosstool/sysroots/x86_64-oesdk-linux/usr/bin:/opt/EC20_crosstool/ql-ol-crosstool/sysroots/x86_64-oesdk-linux/usr/lib source /opt/...

chenwr2018的博客 5247

calibre QRC提取寄生参数

typical将上面的文件QCI_flow里面的文件 在Cp 到当前 ,其实应该是某几个文件,但是我给忘记了 就全cp过来了 ,无影响,不过也希望大佬指正。

2202_75711937的博客 1515

教你提高接口性能的18种方案

本文我们介绍了,优化接口的18种方案。

Java后端技术栈 1926

InSAR_ISCE软件数据处理流程之:利用哨兵数据获取2015年尼泊尔地震同震形变场

ISCE软件数据处理流程之:利用哨兵数据获取2015年尼泊尔地震同震形变场 ISCE软件做干涉只需要将数据准备好,并配置好insarApp.xml文件即可进行。 一、数据准备 1、SAR数据获取 本利中使用的数据为欧空局提供的sentinel-1A/B数据,可从阿拉斯加数据服务器上下载,具体下载方法可参考: 哨兵数据下载方法. 2、哨兵卫星轨道文件下载 哨兵数据处理除了需要SAR影像外,还需提供轨道参数文件,可从https://qc.sentinel1.eo.esa.int/aux_poeorb/下载。下载

weixin_42128764的博客 1万+

ArcGis布局视图中Excel表格无边框插入与图例符号边框定制技巧

本文详细解析了在ArcGis布局视图中实现Excel表格无边框插入与图例符号边框定制的专业技巧。通过源头净化Excel表格、精准粘贴以及分解图例并单独设置边框属性,有效解决了地图报告中常见的视觉粗糙问题,帮助用户提升专题图与报告的精致度与专业感。

weixin_29292535的博客 305

Golang 使用Protocol Buffer 案例

目录 1. 前言 2. Protobuf 简介 2.1 Protobuf 优点 2.2 Protobuf 缺点 2.3 Protobuf Golang 安装使用 3. Protobuf 通讯案例 3.1 创建.proto协议文件 3.2 protobuf编解码 3.3 socket通讯 4. Protobuf 基础知识 4.1 简单模板 4.2 简单语法 4.3 注意事项 1. 前言 工作几年了。ITDragon龙的编程语言从熟悉的Java,到喜欢的.

hellozhxy的博客 1419

AI开发提效10倍:从需求到部署的全流程实战指南(附Prompt模板)

用AI开发不是“偷懒”,而是把时间花在更有价值的地方——原来花80%时间写重复代码,现在用AI搞定后,能把精力放在“优化用户体验”“提升系统性能”这些核心工作上,这才是AI给开发者的真正红利。如果你还没开始用AI做开发,建议从“写一个简单接口”“补一段注释”开始尝试,先用起来再优化方法。记住:AI是工具,不是对手,学会驾驭它,你会发现开发效率提升的同时,还能告别无效加班。

这里只有干货:从 Modbus 通信到 MES 对接,从 YOLO 训练到工控机部署,带你搞定工业软件开发全流程。 1525

从0到1:用Protocol Buffers与gRPC构建高性能微服务架构

你是否还在为JSON序列化的性能瓶颈而烦恼?是否在寻找一种跨语言、易维护的服务通信方案?本文将带你一文掌握Protocol Buffers(协议缓冲区)与gRPC的核心技术,通过实战案例实现微服务架构的性能飞跃。读完本文你将获得: - 掌握Protobuf数据结构定义与代码生成 - 实现基于gRPC的同步/异步服务调用 - 构建支持多语言的微服务通信层 - 性能优化与生产环境部署最佳实践 ##

gitblog_00383的博客 1066

UVM对象实例化及覆盖的几种方法

UVM对象实例化的方法有多种,以下以object为例: 1)第一种:直接利用new函数进行实例化 trans t1; t1 = new("t1");//利用new函数进行实例化 2)第二种:uvm_component_registry中提供的方法type_id::create,此种方法也是最常使用的方法 trans t2; t2 = trans::type_id::create("t2",this); 3)第三种:uvm_factory中提供的方法creat_object_by_type trans t

沧月九流的博客 3375

08-StarRocks性能调优FAQ

StarRocks性能调优摘要 本文提供了StarRocks数据库性能调优的常见问题解答,涵盖查询优化、索引设计、分区策略和系统配置等关键方面。主要内容包括:查询优化技巧(使用EXPLAIN分析、避免全表扫描、优化JOIN和聚合查询);索引选择与维护建议(高频查询列索引、复合索引);分区策略设计(时间分区、范围分区)及其管理方法;以及系统内存配置优化(JVM参数调整、查询内存限制设置)。这些优化措施可显著提升StarRocks的查询性能和系统效率。

欢迎来到我的博客,一起探索代码里的世界! 540

使用 gRPC 构建高性能微服务通信架构

gRPC 是一个语言无关的 RPC 框架,支持多种编程语言,如 Java、Python、Go、C++ 等。协议高效:基于 Protocol Buffers(简称 Protobuf),这是一种高效的序列化格式,用于定义服务接口和数据结构。跨平台支持:支持多种编程语言,使得不同语言编写的服务可以轻松互操作。双向流式传输:通过 HTTP/2 提供双向流支持,适合实时通信场景。负载均衡与安全性:内置对负载均衡和安全性的支持,例如 TLS 加密。

2501_91162107的博客 1820

uniapp - 强力解决H5网页浏览器跨域问题,如何解决uniapp请求接口在浏览器跨域问题,UniApp H5 跨域代理配置详细教程完美解决所有uniapp跨域(调用第三方接口或服务器api跨域)

uni-app 跨域,uniApp解决h5跨域问题,解决uni-app请求接口在浏览器跨域问题,uni.request请求跨域问题解决方案,uni-app-跨域解决方法,彻底解决uniapp发布为H5的跨域问题,uniapp H5页面设置跨域请求_uniapp h5跨域配置,uniapp开发 H5 解决跨域问题,uniApp请求解决跨域问题,uniapp request如何实现前端请求跨域,解决uniapp调试过程中的请求跨域和环境配置,Uniapp调用外部api接口跨域请求,uni-app发布H5页面跨域

🏆 前端领域优质创作者 5352

从零开始构建gRPC的Go服务

第一步首先定义gRPC服务以及方法请求和响应类型。然后在服务定义中定义RPC方法,指定它们的请求和响应类型。一个简单的RPC,其中客户端使用存根向服务端发送请求并等待响应返回,就像正常的函数调用一样。服务端流式RPC,客户端向服务端发送请求并获取流以读回一系列消息。客户端从返回的流中读取,直到没有更多消息为止。客户端流式RPC,其中客户端写入一系列消息并将它们发送到服务端,再次使用提供的流。一旦客户端完成了消息的写入,它会等待服务端读取所有消息并返回其响应。

ldxxxxll的博客 1478

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

Go语言grpc服务开发——Protocol Buffer

作为一个后端开发,我们用的最多的传递信息的协议是http协议。性能需求的提高,要求我们转向性能更高的协议,这就不得不提到一个由Google公司推广的一个小而快的rpc协议——protocol buffer协议。它到底有着什么样的魔力?让大家觉得真香定律。

追求幸福,探索未知的博客 2008

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

在golang中如何使用gRPC和Protocol buffers

定义 gRPC是一种在两个系统间发送和接受消息的传输机制。传统上这些系统是服务器端和客户端。 一个使用golang实现的,用于传输JSON的RPC,我们叫做JSON RPC;类似的,gRPC被设计用来以protocol buf形式来传输数据。 gRPC使创建服务变得容易和优雅。它提供了一组良好的API用于定义和运行服务。 gRPC的主要优势是它可以支持多种语言。Protocol buffers提供...

yangxc的博客 656
上一篇: 一种 Android 应用内全局获取 Context 实例的装置
下一篇: bips
Omni-Space
博客等级 码龄11年 2158粉丝 23原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值