grpc的使用

  1. 需要保证电脑中安装了:protobuf安装教程
  2. 如果出现报错请看博客:protobuf报错问题解决
  3. 基本使用demo地址:demo
  4. 安全传输、流式传输的demo地址:demo2

简介:

rpc微服务,grpc是一种开源的高性能RPC框架,能够运行在任何环境中,最初由谷歌进行开发,它使用HTTP2作为传输协议。grpc让客户端可以像调用本地方法一样调用其他服务器上的服务应用程序,可以更容易的创建分布式应用程序和服务。能让我们更容易的编写跨语言的分布式代码。本示例使用protocol buffers(简写:protobuf),使用protobuf可以高效的序列化,简单的IDL(接口描述语言)并且容易进行接口更新。

一、安装gRPC

  1. 安装grpc执行命令

    go get google.golang.org/grpc@latest

  2. 安装Protocol Buffers v3

    安装方法就是文章开头1的protobuf链接

  3. 安装插件

    安装go语言插件,这个插件会根据.proto文件生成一个后缀为.pb.go的文件:
    该文件文件中包含定义的类型及其序列化方法

    go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28

    安装grpc插件,这个插件会生成_grpc.pb.go后缀的文件:
    该文件中包含接口类型(或存根),提供给客户端调用的服务方法;服务器需要实现的接口类型

    go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2

  4. 默认将插件安装到$GOPATH/bin路径下,具体配置在文章开头第1条安装方式博客中已经写好

二、基本使用

  1. 按照博客写完demo后的目录结构:

    user@C02FP58GML7H grpc-demo-master % tree
    .
    ├── LICENSE
    ├── README.en.md
    ├── README.md
    ├── client
    │   ├── grpc_client.go
    │   └── pb
    │       ├── product.pb.go
    │       └── product_grpc.pb.go
    ├── go.mod
    ├── go.sum
    ├── grpc_server.go
    ├── pb
    │   ├── product.pb.go
    │   └── product_grpc.pb.go
    ├── pbfile
    │   └── product.proto
    └── service
        └── product.go
    
  2. 创建proto文件

    • 创建一个名为project-demo的go项目

    • project-demo目录下创建文件夹pbfile

    • pbfile目录下创建文/定义文件product.proto

    • 文件内容如下:

      // 声明protobuf版本
      syntax = "proto3";
      // option go_package = "path;name";  path 表示生成的go文件的存放地址,会自动生成目录
      // name表示生成的go文件所属的包名
      option go_package = "../pb";
      package pb;
      // 请求体
      message ProductRequest {
          int32 prod_id = 1;
      }
      // 响应体
      message ProductResponse {
          int32 prod_stock =1;
      }
      // 定义服务体
      service ProductService {
          // 定义方法
          rpc GetProductStock(ProductRequest) returns (ProductResponse) {}
      }
      
  3. 在控制台生成pb文件夹,文件夹中**.pb.go_grpc.pb.go**文件

    • 切换到pbfile目录下:cd pbfile(demo链接在博客头部,此事例按照demo上面显示的进行讲解)
    • 需要执行命令:protoc --go_out=./ --go_grpc=./ product.proto
    • 项目下会自动生成pb文件夹和 product.pb.goproduct_grpc.pb.go 两个文件
    • 注:demo中的示例已经生成好,如有需要可删除后自行操作,重新生成
  4. 创建服务端

    • project-demo下创建service文件夹

    • service下创建product.go文件,produc.go文件实现了product.protoc定义的接口

    • product.go文件代码:

      package service
      
      import (
      	"context"
      	"projectbao/pb"
      )
      
      var ProductService = &productService{}
      
      type productService struct {
      	pb.UnimplementedProductServiceServer
      }
      
      func (p *productService) GetProductStock(context context.Context, request *pb.ProductRequest) (*pb.ProductResponse, error) {
      	stock := p.GetStockById(request.ProdId)
      	return &pb.ProductResponse{ProdStock: stock}, nil
      }
      
      func (p *productService) GetStockById(id int32) int32 {
      	return id
      }
      
    • project-demo下创建grpc_server.go文件,注册/创建grpc服务

    • grpc_server.go文件代码:

      package main
      
      import (
      	"fmt"
      	"log"
      	"net"
      	"projectbao/pb"
      	"projectbao/service"
      
      	"google.golang.org/grpc"
      )
      
      func main() {
      	rpcServer := grpc.NewServer()
      	pb.RegisterProductServiceServer(rpcServer, service.ProductService)
      	listion, err := net.Listen("tcp", ":8002")
      	if err != nil {
      		log.Fatal("启动监听出错", err)
      	}
      	err = rpcServer.Serve(listion)
      	if err != nil {
      		log.Fatal("启动服务器出错", err)
      	}
      	fmt.Println("启动grpc服务端成功")
      }
      
  5. 创建客户端

    • project-demo下创建client文件夹

    • pb文件夹复制到client目录下一份

    • client文件夹下创建客户端grpc_client.go文件

    • client.go文件代码:

      package main
      
      import (
      	"context"
      	"fmt"
      	"log"
      	"projectbao/pb"
      
      	"google.golang.org/grpc"
      	"google.golang.org/grpc/credentials/insecure"
      )
      
      func main() {
      	conn, err := grpc.Dial(":8002", grpc.WithTransportCredentials(insecure.NewCredentials()))
      	if err != nil {
      		log.Fatal("服务端出错", err)
      	}
      	defer conn.Close()
      	prodClient := pb.NewProductServiceClient(conn)
      	request := &pb.ProductRequest{
      		ProdId: 100,
      	}
      	stockReponse, err := prodClient.GetProductStock(context.Background(), request)
      	if err != nil {
      		log.Fatal("查询库存出错", err)
      	}
      	fmt.Println("查询成功", stockReponse)
      }
      
  6. 运行

    • 终端切换到项目目录下执行:go run grpc_server.go 启动服务端

    • 另启终端并切换到项目下的client目录下执行:go run grpc_client.go 启动客户端,结果示例:

      user@C02FP58GML7H client % go run grpc_client.go
      查询成功 prod_stock:100
      
    • 也可以通过go build生成二进制的可执行文件来操作

三、安全传输

gRPC 内置支持 SSL/TLS,可以通过 SSL/TLS 证书建立安全连接,对传输的数据进行加密处理。这里介绍使用自签名证书进行server端加密,客户端认证。

SSL

SSL(Secure Socket Layer,安全套接字层)SSL是Netscape开发的位于可靠的面向连接的网络层协议(如TCP/IP)和应用层协议之间的一种协议。SSL通过互相认证、使用数字签名确保完整性、使用加密确保私密性,以实现客户端和服务器之间的安全通讯。现在有1,2,3 ,总共3个版本,现在基本使用3.0。

SSL协议的作用:

  • 认证用户和服务器,确保数据发送到正确的客户机和服务器,互联网连接安全;

  • 加密数据以防止数据中途被窃取;

  • 维护数据的完整性,确保数据在传输过程中不被改变。

TLS

TLS(Transport Layer Security,安全传输层),TLS是建立在传输层TCP协议之上的协议,服务于应用层,前身是SSL(Secure Socket Layer,安全套接字层),它实现了将应用层的报文进行加密后再交由TCP进行传输的功能。TLS的产生是为了让SSL更安全,使协议更加精确和完善。TLS在SSL3.0基础上增强了其他内容。它们的最主要的差别是所支持的加密算法不同,TLS和SSL3.0不能互相操作,TLS相当于SSL 3.1。

TLS协议主要解决三个网络安全问题:

  • 保密(message privacy),保密通过加密encryption实现,所有信息都加密传输,第三方无法嗅探,防窃听;
  • 完整性(message integrity),通过MAC校验机制,一旦被篡改,通信双方会立刻发现,防篡改;
  • 认证(mutual authentication),双方认证,双方都可以配备证书,防止身份被冒充;

1. 生成自签证书

Windows自行下载OpenSSL,Mac自带OpenSSL,Mac OS X自 10.11 El Capitan 起因为OpenSSL的"心脏出血",已将OpenSSL替换成LibreSSL 。注:Windows需要配置环境变量

心脏出血:也简称为:心血漏洞,此漏洞不仅仅影响https类型网站,此漏洞可被利用获取电脑上的内存数据。

1.1-RSA非对称加密

生成私钥文件(需要先创建cert目录,cd到cert目录中)

生成RSA命令:openssl genrsa -des3 -out server_rsa.key

执行结果示例:

# 如下显示时输入密码
user@C02FP58GML7H cert % openssl genrsa -des3 -out server_rsa.key
Generating RSA private key, 2048 bit long modulus
...................................................................................................................................................................................+++
........+++
e is 65537 (0x10001)
# 输入密码:1234
Enter pass phrase for server_rsa.key:
# 确认密码:1234
Verifying - Enter pass phrase for server_rsa.key:
# 这里使用1234作为密码

创建证书请求

生成证书命令:openssl req -new -key server_rsa.key -out server_rsa.csr

执行结果示例:

user@C02FP58GML7H cert % openssl req -new -key server_rsa.key -out server_rsa.csr
# 输入 server_rsa.key 的密码:1234
Enter pass phrase for server_rsa.key:
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
# 国家
Country Name (2 letter code) []:cn
# 省
State or Province Name (full name) []:beijing
# 市
Locality Name (eg, city) []:beijing
# 组织名称
Organization Name (eg, company) []:org
# 组织单位名称
Organizational Unit Name (eg, section) []:org
# 公用名,一般填写主机域名
Common Name (eg, fully qualified host name) []:test.com
# 邮箱地址
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
# 注意:上述一些非必要选项可以不填写

生成.crt文件

生成server_rsa.crt文件命令:openssl x509 -req -sha256 -days 365 -in server_rsa.csr -signkey server_rsa.key -out server_rsa.crt

执行结果示例:

user@C02FP58GML7H cert % openssl x509 -req -days 365 -in server_rsa.csr -signkey server_rsa.key -out server_rsa.crt
Signature ok
subject=/C=cn/ST=beijing/L=beijing/O=org/OU=org/CN=org
Getting Private key
# 输入密码:1234
Enter pass phrase for server_rsa.key:

SAN证书

在Go1.15版本后需要添加SAN证书才能正常使用,如果使用的Go版本低这步请自动忽略。

  • 找到openssl.cnf文件(**注意:**是在openssl下的openssl.cnf配置文件,注意不是go/pkg文件下的,pkg下的是命令生成的那一个,不是我们需要的)

  • 复制openssl.cnf到cert文件夹下,可以找官网上的对应版本的tar/zip压缩包解压后进入到apps目录下复制出openssl.cnf

  • 搜索ctrl+F:copy_extensions = copy,定位删掉#打开copy_extensions=copy的注释

  • 搜索ctrl+F:req_extensions = v3_req,定位删掉#打开req_extensions = v3_req的注释

  • 搜索ctrl+F:[ v3_req ],在下面添加:subjectAltName = @alt_names

  • 添加新的标签:[ alt_names ]

    标签下面添加字段:DNS.1 = *.test.com

  • 文章头部的项目中cert文件目录下有一份openssl.cnf配置文件,可以直接拿来使用,不过还是推荐按章上面的方法复制操作修改一份。

  • 生成证书私钥server_rsa_san.key

    执行命令:openssl genpkey -algorithm RSA -out server_rsa_san.key

    执行结果示例:

    user@C02FP58GML7H cert % openssl genpkey -algorithm RSA -out server_rsa_san.key
    .................+++
    ............................+++
    
  • 通过server_rsa_san.key生成证书请求文件server_rsa_san.csr

    执行命令:openssl req -new -nodes -key server_rsa_san.key -out server_rsa_san.csr -days 3650 -config ./openssl.cnf -extensions v3_req

    执行结果示例:

    user@C02FP58GML7H cert % openssl req -new -nodes -key server_rsa_san.key -out server_rsa_san.csr -days 3650 -config ./openssl.cnf -extensions v3_req
    You are about to be asked to enter information that will be incorporated
    into your certificate request.
    What you are about to enter is what is called a Distinguished Name or a DN.
    There are quite a few fields but you can leave some blank
    For some fields there will be a default value,
    If you enter '.', the field will be left blank.
    -----
    # 国家
    Country Name (2 letter code) []:cn
    # 省
    State or Province Name (full name) []:beijing
    # 市
    Locality Name (eg, city) []:beijing
    # 组织名称
    Organization Name (eg, company) []:org
    # 组织单位名称
    Organizational Unit Name (eg, section) []:org
    # 公用名,一般填写主机域名
    Common Name (eg, fully qualified host name) []:org
    # 邮箱地址
    Email Address []:
    
    Please enter the following 'extra' attributes
    to be sent with your certificate request
    A challenge password []:
    An optional company name []:
    # 注意:上述一些非必要选项可以不填写
    
  • 生成SAN证书

    SAN(Subject Alternative Name)是SSL标准x509中定义的一个扩展。使用了SAN字段的SSL证书,可以扩展此证书支持的域名,使得一个证书可以支持多个不同域名的解析。

    执行命令:openssl x509 -req -sha256 -days 365 -in server_rsa_san.csr -out server_rsa_san.pem -CA server_rsa.crt -CAkey server_rsa.key -CAcreateserial -extfile ./openssl.cnf -extensions v3_req

    执行结果示例:

    user@C02FP58GML7H cert % openssl x509 -req -days 365 -in server_rsa_san.csr -out server_rsa_san.pem -CA server_rsa.crt -CAkey server_rsa.key -CAcreateserial -extfile ./openssl.cnf -extensions v3_req
    Signature ok
    subject=/C=cn/ST=beijing/L=beijing/O=org/OU=org/CN=org
    Getting CA Private Key
    # 输入密码(和上面设置的密码一致):1234
    Enter pass phrase for server_rsa.key:
    
    • key:服务器上的私钥文件,用于发送给客户端数据的加密,以及对从客户端接收到数据的解密。
    • csr:证书签名请求文件,用于提交证书颁发机构(CA)对证书签名。
    • crt:由证书颁发机构(CA)签名后的证书,或者是开发者自签的证书,包含证书持有人信息,持有人公钥,以及签署者的签名等信息。
    • pem:是基于Base64编码的证书格式,扩展名包括PEM、CRT和CER。

1.2-ECC椭圆曲线加密

生成私钥文件(需要先创建cert目录,cd到cert目录中)

生成ECC命令:openssl ecparam -genkey -name secp384r1 -out server_ecc.key

SAN证书

在cert目录下创建文件server_ecc.cnf,文件内容

[ req ]
default_bits       = 4096
default_md		= sha256
distinguished_name = req_distinguished_name
req_extensions     = req_ext

[ req_distinguished_name ]
countryName                 = Country Name (2 letter code)
countryName_default         = CN
stateOrProvinceName         = State or Province Name (full name)
stateOrProvinceName_default = BEIJING
localityName                = Locality Name (eg, city)
localityName_default        = BEIJING
organizationName            = Organization Name (eg, company)
organizationName_default    = DEV
commonName                  = Common Name (e.g. server FQDN or YOUR name)
commonName_max              = 64
commonName_default          = text.com

[ req_ext ]
subjectAltName = @alt_names

[alt_names]
DNS.1   = localhost
IP      = 127.0.0.1

生成证书命令:openssl req -nodes -new -x509 -sha256 -days 3650 -config server_ecc.cnf -extensions ‘req_ext’ -key server_ecc.key -out server_ecc.crt

执行结果示例:

user@C02FP58GML7H cert % openssl req -nodes -new -x509 -sha256 -days 3650 -config server_ecc.cnf -extensions 'req_ext' -key server_ecc.key -out server_ecc.crt
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
# 国家
Country Name (2 letter code) [CN]:cn
# 省
State or Province Name (full name) [BEIJING]:beijing
# 市
Locality Name (eg, city) [BEIJING]:beijing
# 组织
Organization Name (eg, company) [DEV]:org
# 公用名
Common Name (e.g. server FQDN or YOUR name) [text.com]:org

**注:**文件夹下面的openssl.cnf和server_ecc.cnf是一样的配置文件(openssl配置),rsa操作中复制修改openssl配置文件,ecc操作中是重写openssl配置,是一样的效果。

2. 服务端/客户端应用证书(单向认证)

弊端:单向认证会被一种中叫间人攻击的方式进行抓包截获数据

服务端代码示例:

package main

import (
	"fmt"
	"log"
	"net"
	"projectbao/pb"
	"projectbao/service"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
)

func main() {
	// 添加证书
	creds, err := credentials.NewServerTLSFromFile("cert/server_rsa_san.pem", "cert/server_rsa_san.key")
	if err != nil {
		log.Fatal("证书生成错误!", err)
	}
	rpcServer := grpc.NewServer(grpc.Creds(creds))
	pb.RegisterProductServiceServer(rpcServer, service.ProductService)
	listion, err := net.Listen("tcp", ":8002")
	if err != nil {
		log.Fatal("启动监听出错", err)
	}
	err = rpcServer.Serve(listion)
	if err != nil {
		log.Fatal("启动服务器出错", err)
	}
	fmt.Println("启动grpc服务端成功")
}

客户端代码示例:

package main

import (
	"context"
	"fmt"
	"log"
	"projectbao/pb"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
)

func main() {
	creds, err := credentials.NewClientTLSFromFile("../cert/server_rsa_san.pem", "*.test.com")
	if err != nil {
		log.Fatal("证书生成错误!", err)
	}
	conn, err := grpc.Dial(":8002", grpc.WithTransportCredentials(creds))
	if err != nil {
		log.Fatal("服务端出错", err)
	}
	defer conn.Close()
	prodClient := pb.NewProductServiceClient(conn)
	request := &pb.ProductRequest{
		ProdId: 100,
	}
	stockReponse, err := prodClient.GetProductStock(context.Background(), request)
	if err != nil {
		log.Fatal("查询出错", err)
	}
	fmt.Println("查询成功", stockReponse)
}

运行

博主本人使用的是go run的方式:

第一个终端切cd换到项目project-demo目录下执行:go run grpc_server.go

第二个终端切cd换到项目project-demo/client目录下执行:go run grpc_client.go

注意:client客户端如有报错找不到cert/server_rsa_san.pem文件是因为路径问题,自行修改就好

如果运行客户端后如下错误,解决办法请参照下面的双向认证的解决方式,一般情况下是不会在单向认证出现如下报错的

user@C02FP58GML7H client % go run grpc_client.go           

2022/10/12 11:02:27 rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: x509: certificate signed by unknown authority (possibly because of \"x509: cannot verify signature: insecure algorithm SHA1-RSA (temporarily override with GODEBUG=x509sha1=1)\" while trying to verify candidate authority certificate \"test.com\")"

exit status 1

路径问题报错示例:

user@C02FP58GML7H client % go run grpc_client.go
2022/10/10 17:38:04 证书生成错误!open cert/server_rsa_san.pem: no such file or directory
exit status 1

正确结果示例:

user@C02FP58GML7H client % go run grpc_client.go
查询成功 prod_stock:100

3. 双向认证

客户端同样需要生成一份证书密钥,客户端SAN证书的生成需要和服务端使用相同的crt去注册证书

此处以RSA为例,ECC同理,上面已经赘述了一遍方法,这里就不在赘述。

生成证书客户端私钥

生成RSA命令:openssl genpkey -algorithm RSA -out client_rsa.key

执行结果示例:

user@C02FP58GML7H cert % openssl genpkey -algorithm RSA -out client_rsa.key
......................+++
...............................+++

生成证书请求文件

生成证书命令:openssl req -new -nodes -key client_rsa.key -out client_rsa.csr -days 3650 -config ./openssl.cnf -extensions v3_req

执行结果示例:

user@C02FP58GML7H cert % openssl req -new -nodes -key client_rsa.key -out client_rsa.csr -days 3650 -config ./openssl.cnf -extensions v3_req
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
# 国家
Country Name (2 letter code) []:cn
# 省
State or Province Name (full name) []:beijing
# 市
Locality Name (eg, city) []:beijing
# 组织名称
Organization Name (eg, company) []:org
# 组织单位名称
Organizational Unit Name (eg, section) []:org
# 公用名,一般填写主机域名
Common Name (eg, fully qualified host name) []:org
# 邮箱地址
Email Address []:

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
# 注意:上述一些非必要选项可以不填写

生成SAN证书

生成SAN证书命令:openssl x509 -req -sha256 -days 365 -in client_rsa.csr -out client_rsa.pem -CA server_rsa.crt -CAkey server_rsa.key -CAcreateserial -extfile ./openssl.cnf -extensions v3_req

执行结果示例:

user@C02FP58GML7H cert % openssl x509 -req -days 365 -in client_rsa.csr -out client_rsa.pem -CA server_rsa.crt -CAkey server_rsa.key -CAcreateserial -extfile ./openssl.cnf -extensions v3_req
Signature ok
subject=/C=cn/ST=beijing/L=beijing/O=org/OU=org/CN=org
Getting CA Private Key
# 输入密码:1234
Enter pass phrase for server_rsa.key:

服务端代码示例:

package main

import (
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"io/ioutil"
	"log"
	"net"
	"projectbao/pb"
	"projectbao/service"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
)

func main() {
	// 单向认证
	// 添加证书
	// creds, err := credentials.NewServerTLSFromFile("cert/server_rsa_san.pem", "cert/server_rsa_san.key")
	// if err != nil {
	// 	log.Fatal("证书生成错误!", err)
	// }

	// 双向认证
	// 证书认证-双向认证
	// 从证书相关文件中读取和解析信息,得到证书公钥、密钥对
	cert, err := tls.LoadX509KeyPair("cert/server_rsa_san.pem", "cert/server_rsa_san.key")
	if err != nil {
		log.Fatal("证书读取错误!", err)
	}
	// 创建一个新的空 CertPool
	certPool := x509.NewCertPool()
	ca, err := ioutil.ReadFile("cert/server_rsa.crt")
	if err != nil {
		log.Fatal("ca证书读取错误!", err)
	}
	// 解析传入的PEM编码的证书。如果解析成功会将其加到CertPool中,便于后面的使用
	certPool.AppendCertsFromPEM(ca)
	// 构建基于TLS的TransportCredentials选项
	creds := credentials.NewTLS(&tls.Config{
		// 设置证书链,允许包含一个或多个
		Certificates: []tls.Certificate{cert},
		// 要求必须交验客户端的证书。可以根据实际情况先用以下参数
		ClientAuth: tls.RequireAndVerifyClientCert,
		// 设置根证书的集合,校验方式使用ClientAuth中设定的模式
		ClientCAs: certPool,
	})

	rpcServer := grpc.NewServer(grpc.Creds(creds))
	pb.RegisterProductServiceServer(rpcServer, service.ProductService)
	listion, err := net.Listen("tcp", ":8002")
	if err != nil {
		log.Fatal("启动监听出错", err)
	}
	err = rpcServer.Serve(listion)
	if err != nil {
		log.Fatal("启动服务器出错", err)
	}
	fmt.Println("启动grpc服务端成功")
}

客户端代码示例:

package main

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"io/ioutil"
	"log"
	"projectbao/pb"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
)

func main() {
	// 单向认证
	// creds, err := credentials.NewClientTLSFromFile("../cert/server_rsa_san.pem", "*.test.com")
	// if err != nil {
	// 	log.Fatal("证书生成错误!", err)
	// }

	// 双向认证
	// 证书认证-双向认证
	// 从证书相关文件中读取和解析信息,得到证书公钥、密钥对
	cert, err := tls.LoadX509KeyPair("../cert/client_rsa.pem", "../cert/client_rsa.key")
	if err != nil {
		log.Fatal("证书读取错误!", err)
	}
	// 创建一个新的空 CertPool
	certPool := x509.NewCertPool()
	ca, err := ioutil.ReadFile("../cert/server_rsa.crt")
	if err != nil {
		log.Fatal("ca证书读取错误!", err)
	}
	// 解析传入的PEM编码的证书。如果解析成功会将其加到CertPool中,便于后面的使用
	certPool.AppendCertsFromPEM(ca)
	// 构建基于TLS的TransportCredentials选项
	creds := credentials.NewTLS(&tls.Config{
		// 设置证书链,允许包含一个或多个
		Certificates: []tls.Certificate{cert},
		ServerName:   "*.test.com",
		// 设置根证书的集合,校验方式使用ClientAuth中设定的模式
		RootCAs: certPool,
	})

	conn, err := grpc.Dial(":8002", grpc.WithTransportCredentials(creds))
	if err != nil {
		log.Fatal("服务端出错", err)
	}
	defer conn.Close()
	prodClient := pb.NewProductServiceClient(conn)
	request := &pb.ProductRequest{
		ProdId: 100,
	}
	stockReponse, err := prodClient.GetProductStock(context.Background(), request)
	if err != nil {
		log.Fatal("查询出错", err)
	}
	fmt.Println("查询成功", stockReponse)
}

运行

博主本人使用的是go run的方式:

第一个终端切cd换到项目project-demo目录下执行:go run grpc_server.go

第二个终端切cd换到项目project-demo/client目录下执行:go run grpc_client.go

如果运行客户端后未报如下错误请自行忽略下面操作

user@C02FP58GML7H client % go run grpc_client.go           

2022/10/12 11:02:27 rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: x509: certificate signed by unknown authority (possibly because of \"x509: cannot verify signature: insecure algorithm SHA1-RSA (temporarily override with GODEBUG=x509sha1=1)\" while trying to verify candidate authority certificate \"test.com\")"

exit status 1

解决办法1:

当出现这个问题别慌,执行的命令前添加这个即可:GODEBUG=x509sha1=1

go run server.go就改成:GODEBUG=x509sha1=1 go run server.go

go run client.go就改成:GODEBUG=x509sha1=1 go run client.go

注意这里的客户端和服务端要同时修改,单独修改客户端会报如下错:

2022/10/12 11:03:39 rpc error: code = Unavailable desc = connection closed before server preface received
exit status 1

**注意:**同理go build打包也是和go run一样的方式

执行结果示例:

user@C02FP58GML7H client % GODEBUG=x509sha1=1 go run grpc_client.go
查询成功 prod_stock:100

解决办法2:

使用非sha1加密方式生成密钥,证书。博主本人使用的是sha256加密方式,解决办法:

  • 删除cert目录下除以.cnf后缀文件外的所有文件

  • 重新操作文件生成命令,这里为了方便博主就直接粘贴到下面了:

    # 1
    openssl genrsa -des3 -out server_rsa.key
    # 2
    openssl req -new -key server_rsa.key -out server_rsa.csr
    # 3 这里添加了 -sha256
    openssl x509 -req -sha256 -days 365 -in server_rsa.csr -signkey server_rsa.key -out server_rsa.crt
    # 服务端SAN
    # 4
    openssl genpkey -algorithm RSA -out server_rsa_san.key
    # 5
    openssl req -new -nodes -key server_rsa_san.key -out server_rsa_san.csr -days 3650 -config ./openssl.cnf -extensions v3_req
    # 6 这里添加了 -sha256
    openssl x509 -req -sha256 -days 365 -in server_rsa_san.csr -out server_rsa_san.pem -CA server_rsa.crt -CAkey server_rsa.key -CAcreateserial -extfile ./openssl.cnf -extensions v3_req
    # 客户端SAN
    # 7
    openssl genpkey -algorithm RSA -out client_rsa.key
    # 8
    openssl req -new -nodes -key client_rsa.key -out client_rsa.csr -days 3650 -config ./openssl.cnf -extensions v3_req
    # 9 这里添加了 -sha256
    openssl x509 -req -sha256 -days 365 -in client_rsa.csr -out client_rsa.pem -CA server_rsa.crt -CAkey server_rsa.key -CAcreateserial -extfile ./openssl.cnf -extensions v3_req
    
  • 创建过程中可以查看文件的加密方式:

    • 查看crt/pem文件加密方式命令:openssl x509 -in server_rsa.pem -text
    • 查看csr文件加密方式的命令:openssl req -in server_rsa.csr -text
    • Signature Algorithm: sha256WithRSAEncryption表示sha256加密
    • Signature Algorithm: sha1WithRSAEncryption表示sha1加密
    • 注:这里rt/pem/csr文件显示sha256或者只要是非sha1的就没问题,否则就需要检查修改命令

执行结果示例:

user@C02FP58GML7H client % go run grpc_client.go
查询成功 prod_stock:100

4. Token认证

修改server端添加拦截器加入token校验机制,代码示例:

package main

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"io/ioutil"
	"log"
	"net"
	"projectbao/pb"
	"projectbao/service"

	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
)

func main() {
	// 单向认证
	// 添加证书
	// creds, err := credentials.NewServerTLSFromFile("cert/server_rsa_san.pem", "cert/server_rsa_san.key")
	// if err != nil {
	// 	log.Fatal("证书生成错误!", err)
	// }

	// 双向认证
	// 证书认证-双向认证
	// 从证书相关文件中读取和解析信息,得到证书公钥、密钥对
	cert, err := tls.LoadX509KeyPair("cert/server_rsa_san.pem", "cert/server_rsa_san.key")
	if err != nil {
		log.Fatal("证书读取错误!", err)
	}
	// 创建一个新的空 CertPool
	certPool := x509.NewCertPool()
	ca, err := ioutil.ReadFile("cert/server_rsa.crt")
	if err != nil {
		log.Fatal("ca证书读取错误!", err)
	}
	// 解析传入的PEM编码的证书。如果解析成功会将其加到CertPool中,便于后面的使用
	certPool.AppendCertsFromPEM(ca)
	// 构建基于TLS的TransportCredentials选项
	creds := credentials.NewTLS(&tls.Config{
		// 设置证书链,允许包含一个或多个
		Certificates: []tls.Certificate{cert},
		// 要求必须交验客户端的证书。可以根据实际情况先用以下参数
		ClientAuth: tls.RequireAndVerifyClientCert,
		// 设置根证书的集合,校验方式使用ClientAuth中设定的模式
		ClientCAs: certPool,
	})

	// 实现Tocken认证,拦截器
	// 可以简写authInterceptor := func()(){}
	var authInterceptor grpc.UnaryServerInterceptor
	authInterceptor = func(
		ctx context.Context,
		req interface{},
		info *grpc.UnaryServerInfo,
		handler grpc.UnaryHandler,
	) (resp interface{}, err error) {
		// 拦截普通方法请求,验证Token
		err = Auth(ctx)
		if err != nil {
			return
		}
		// 向下执行继续处理请求
		return handler(ctx, req)
	}

	// 证书认证
	// rpcServer := grpc.NewServer(grpc.Creds(creds))

	// Token认证
	// var opts []grpc.ServerOption //grpc为使用的第三方的grpc包
	// opts = append(opts, grpc.UnaryInterceptor(interceptor))
	rpcServer := grpc.NewServer(grpc.Creds(creds), grpc.UnaryInterceptor(authInterceptor))

	pb.RegisterProductServiceServer(rpcServer, service.ProductService)
	listion, err := net.Listen("tcp", ":8002")
	if err != nil {
		log.Fatal("启动监听出错", err)
	}
	err = rpcServer.Serve(listion)
	if err != nil {
		log.Fatal("启动服务器出错", err)
	}
	fmt.Println("启动grpc服务端成功")
}

func Auth(ctx context.Context) error {
	// 获取传输的用户名和密码
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return fmt.Errorf("参数获取失败!")
	}
	var user string
	var password string
	if val, ok := md["user"]; ok {
		user = val[0]
	}
	if val, ok := md["password"]; ok {
		password = val[0]
	}
	if user != "admin" || password != "admin" {
		return status.Errorf(codes.Unauthenticated, "Token 不合法")
	}
	return nil
}

在原有demo下面的client文件夹下创建auth目录,auth目录下创建auth.go文件,实现用户类,示例:

package auth

import "context"

type Authentication struct {
	User     string
	Password string
}

func (a *Authentication) GetRequestMetadata(context.Context, ...string) (map[string]string, error) {
	return map[string]string{"user": a.User, "password": a.Password}, nil
}

func (a *Authentication) RequireTransportSecurity() bool {
	return false
}

修改客户端加入用户信息修改client目录下的grpc_client.go文件,代码示例:

package main

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"io/ioutil"
	"log"
	"projectbao/client/auth"
	"projectbao/pb"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
)

func main() {
	// 单向认证
	// creds, err := credentials.NewClientTLSFromFile("../cert/server_rsa_san.pem", "*.test.com")
	// if err != nil {
	// 	log.Fatal("证书生成错误!", err)
	// }

	// 双向认证
	// 证书认证-双向认证
	// 从证书相关文件中读取和解析信息,得到证书公钥、密钥对
	cert, err := tls.LoadX509KeyPair("../cert/client_rsa.pem", "../cert/client_rsa.key")
	if err != nil {
		log.Fatal("证书读取错误!", err)
	}
	// 创建一个新的空 CertPool
	certPool := x509.NewCertPool()
	ca, err := ioutil.ReadFile("../cert/server_rsa.crt")
	if err != nil {
		log.Fatal("ca证书读取错误!", err)
	}
	// 解析传入的PEM编码的证书。如果解析成功会将其加到CertPool中,便于后面的使用
	certPool.AppendCertsFromPEM(ca)
	// 构建基于TLS的TransportCredentials选项
	creds := credentials.NewTLS(&tls.Config{
		// 设置证书链,允许包含一个或多个
		Certificates: []tls.Certificate{cert},
		ServerName:   "*.test.com",
		// 设置根证书的集合,校验方式使用ClientAuth中设定的模式
		RootCAs: certPool,
	})

	// 证书认证
	// conn, err := grpc.Dial(":8002", grpc.WithTransportCredentials(creds))

	// Tocken认证
	token := &auth.Authentication{
		User:     "admin",
		Password: "admin",
	}
	conn, err := grpc.Dial(":8002", grpc.WithTransportCredentials(creds), grpc.WithPerRPCCredentials(token))
	if err != nil {
		log.Fatal("服务端出错", err)
	}
	defer conn.Close()
	prodClient := pb.NewProductServiceClient(conn)
	request := &pb.ProductRequest{
		ProdId: 100,
	}
	stockReponse, err := prodClient.GetProductStock(context.Background(), request)
	if err != nil {
		log.Fatal("查询出错", err)
	}
	fmt.Println("查询成功", stockReponse)
}

四、流式传输

// 普通RPC
rpc SimplePing(PingRequest) return (PingReply) {}
// 客户端流式RPC
rpc ClientStreamPing(stream PingRequest) return (PingReplay) {}
// 服务端流式RPC
rpc ServerStreamPing(PingRequest) return (stream PingReplay) {}
//双向流式RPC
rpc BothStreamPing(stream PingRequest) return (stream PingReply) {}

stream关键字,当该关键字在参数前面时,表示这是一个客户端流式的gRPC接口;当该关键字在返回值前面时,表示这是一个服务端流式的gRPC接口;当该关键字同时都有时,表示这是一个双向流式的gRPC接口。

1. 客户端流传输

打开pbfile目录下的product.proto文件,定义客户端流传输的接口

// 在service ProductService中添加
// 客户端流传输
rpc UpdateProductStockClientStream(stream ProductRequest) returns(ProductResponse) {}

执行命令生成对应的go语言的代码:protoc --go_out=./ --go-grpc_out=./ product.proto

复制项目目录下的pb目录中的两个文件覆盖client/pb目录下的两个文件

打开service目录下的product.go文件,实现接口方法,demo示例中直接使用即可

// 客户端流gRPC
func (p *productService) UpdateProductStockClientStream(stream pb.ProductService_UpdateProductStockClientStreamServer) error {
	count := 0
	for {
		// 接收客户端发来的信息
		recv, err := stream.Recv()
		if err != nil {
			if err == io.EOF {
				return nil
			}
			return err
		}
		fmt.Println("服务端接收到的流", recv.ProdId, count)
		count++
		if count > 10 {
			rsp := &pb.ProductResponse{ProdStock: recv.ProdId}
			err := stream.SendAndClose(rsp)
			if err != nil {
				return err
			}
		}
	}
}

打开client目录下的grpc_client文件夹实现,demo示例中直接切换解开注视即可

// main函数下添加
// 客户端流gRPC
	stream, err := prodClient.UpdateProductStockClientStream(context.Background())
	if err != nil {
		log.Fatal("获取流出错", err)
	}
	rsp := make(chan struct{}, 1)
	go prodRequest(stream, rsp)
	select {
	case <-rsp:
		recv, err := stream.CloseAndRecv()
		if err != nil {
			log.Fatal(err)
		}
		stock := recv.ProdStock
		fmt.Println("客户端收到响应:", stock)
	}

// prodRequest
func prodRequest(stream pb.ProductService_UpdateProductStockClientStreamClient, rsp chan struct{}) {
	count := 0
	for {
		request := &pb.ProductRequest{
			ProdId: 100,
		}
		err := stream.Send(request)
		if err != nil {
			log.Fatal(err)
		}
		count++
		if count > 10 {
			rsp <- struct{}{}
			break
		}
	}
}

2. 服务端流传输

打开pbfile目录下的product.proto文件,定义服务端流传输的接口

// 在service ProductService中添加
// 服务端流传输
rpc GetProductStockServerStream(ProductRequest) returns(stream ProductResponse) {}

执行命令生成对应的go语言的代码:protoc --go_out=./ --go-grpc_out=./ product.proto

复制项目目录下的pb目录中的两个文件覆盖client/pb目录下的两个文件

打开service目录下的product.go文件,实现接口方法,demo示例中直接使用即可

// 服务端流gRPC
func (*productService) GetProductStockServerStream(request *pb.ProductRequest, stream pb.ProductService_GetProductStockServerStreamServer) error {
	count := 0
	for {
		rsp := &pb.ProductResponse{ProdStock: request.ProdId}
		err := stream.Send(rsp)
		if err != nil {
			return err
		}
		count++
		if count > 10 {
			return nil
		}
	}
}

打开client目录下的grpc_client文件夹实现,demo示例中直接切换解开注视即可

// main函数下添加
// 服务端流式gRPC
	request := &pb.ProductRequest{
		ProdId: 100,
	}
	stream, err := prodClient.GetProductStockServerStream(context.Background(), request)
	if err != nil {
		log.Fatal("获取流出错", err)
	}
	count := 0
	for {
		recv, err := stream.Recv()
		if err != nil {
			if err == io.EOF {
				fmt.Println("客户端数据接收完成")
				err := stream.CloseSend()
				if err != nil {
					log.Fatal(err)
				}
			}
			log.Fatal(err)
		}
		fmt.Println("客户端收到的流", recv.ProdStock, count)
		count++
	}

3. 双向流

打开pbfile目录下的product.proto文件,定义双向流传输的接口

// 在service ProductService中添加
// 双向流传输
rpc ModeServerStream(stream ProductRequest) returns(stream ProductResponse) {}

执行命令生成对应的go语言的代码:protoc --go_out=./ --go-grpc_out=./ product.proto

复制项目目录下的pb目录中的两个文件覆盖client/pb目录下的两个文件

打开service目录下的product.go文件,实现接口方法,demo示例中直接使用即可

// main函数下添加
// 双向流传输
func (p *productService) ModeServerStream(stream pb.ProductService_ModeServerStreamServer) error {
	for {
		recv, err := stream.Recv()
		if err != nil {
			return nil
		}
		fmt.Println("服务端接收到客户端的消息", recv.ProdId)
		rsp := &pb.ProductResponse{ProdStock: recv.ProdId}
		err = stream.Send(rsp)
		if err != nil {
			return nil
		}
	}
}

打开client目录下的grpc_client文件夹实现,demo示例中直接切换解开注视即可

// 双向流式gRPC(多用于心跳检测)
	stream, err := prodClient.ModeServerStream(context.Background())
	if err != nil {
		log.Fatal("获取流出错", err)
	}
	for {
		request := &pb.ProductRequest{
			ProdId: 100,
		}
		err = stream.Send(request)
		if err != nil {
			log.Fatal(err)
		}
		recv, err := stream.Recv()
		if err != nil {
			log.Fatal(err)
		}
		fmt.Println("客户端收到的流信息", recv.ProdStock)
	}
<think>嗯,我现在要写一篇关于gRPC使用攻略的介绍。用户需要结构清晰,分步骤解决问题,同时内容真实可靠。首先,我应该先回忆一下自己对gRPC的了解,然后整理出主要的点,确保不遗漏关键内容。 首先,gRPC是什么?它是一个高性能、开源的RPC框架,由Google开发,基于HTTP/2和Protocol Buffers。那RPC又是什么呢?远程过程调用,允许像调用本地函数一样调用远程服务。这点需要解释清楚,因为可能有些读者不熟悉这个概念。 接下来,gRPC的核心特性。比如基于HTTP/2,支持双向流、多路复用;使用Protocol Buffers作为IDL,高效序列化;支持多种编程语言;还有四种通信模式,这个要详细说明。可能用户会关心这些特性带来的好处,比如性能提升,跨语言支持等。 然后是使用步骤。第一步应该是定义服务接口,也就是编写.proto文件。这部分需要详细说明语法,比如如何定义service、rpc方法、消息类型。比如一个简单的例子,定义一个Hello服务,里面有个SayHello的rpc方法,接收HelloRequest,返回HelloResponse。这里要注意语法正确,比如message的结构,字段类型和编号。 第二步是生成代码,使用protoc编译器配合gRPC插件生成对应语言的代码。需要提到常用的语言,比如Go、Java、Python等,以及如何安装protoc工具,生成命令的例子,比如protoc --go_out=. --go-grpc_out=. *.proto。 第三步是实现服务端。需要举一个例子,比如用Go语言实现服务接口,创建gRPC服务器,注册服务,监听端口,启动服务器。代码示例可能对用户有帮助,但要注意不要出错,同时用中文解释每一步的作用。 第四步是实现客户端。同样用例子说明如何创建连接,创建客户端实例,调用远程方法,处理响应。这里要强调客户端如何像调用本地方法一样使用远程服务。 第五步是运行和测试,确保服务端和客户端能正常通信,可以补充测试方法,比如使用grpcurl工具或者编写单元测试。 接下来要讲通信模式,四种类型:简单RPC、服务端流式、客户端流式、双向流式。每种模式的应用场景和示例,比如简单RPC适合一次性请求响应,流式适合大数据传输或实时通信。这部分可能需要用不同的.proto定义来展示,比如在rpc方法中使用stream关键字。 然后是关键技术点:Protocol Buffers的高效序列化,HTTP/2的优势,拦截器的作用,比如认证、日志、监控,还有超时和错误处理的重要性。需要详细说明每个技术点如何应用,比如设置超时时间,处理状态码。 最后是注意事项,比如版本兼容性,.proto文件的维护,性能优化方法,比如连接复用、压缩,以及安全方面的TLS加密和身份验证。 在组织内容时,需要确保结构清晰,分步骤讲解,每个部分有必要的示例和解释。同时,注意使用中文,避免专业术语过于晦涩,必要时进行解释。可能还需要检查是否有遗漏的部分,比如负载均衡、服务发现等高级话题,但根据用户的要求,可能基础攻略不需要深入这些,但可以简要提及扩展方向。 还要注意LaTeX格式的问题,用户提到行内公式用$...$,独立公式用$$...$$,但在这个主题中可能数学公式不多,但如果有涉及性能数据或公式,需要正确应用格式。不过gRPC攻略可能较少涉及数学表达式,所以这部分可能用不上,但需要留意用户可能的隐藏要求。 最后总结一下,确保内容覆盖了gRPC的核心概念、使用步骤、关键技术和注意事项,结构层次分明,帮助用户从零开始使用gRPC,并解决可能遇到的问题。</think>以下是一份结构清晰的gRPC使用攻略,分步骤解析核心用法及注意事项: --- ### 一、gRPC基础概念 1. **定义** gRPC是Google开源的高性能RPC框架,基于**HTTP/2协议**和**Protocol Buffers**(Protobuf)实现,支持跨语言调用。 2. **核心特性** - 多语言支持(如Go、Java、Python等) - 双向流式通信 - 高效的序列化(Protobuf) - 内置负载均衡、重试、超时机制 --- ### 二、使用步骤详解 #### 步骤1:定义服务接口(.proto文件) ```protobuf syntax = "proto3"; service Greeter { rpc SayHello (HelloRequest) returns (HelloResponse) {} } message HelloRequest { string name = 1; } message HelloResponse { string message = 1; } ``` - **关键语法** - `service`定义服务,`rpc`定义方法 - `message`定义请求/响应结构 - 字段编号(如`=1`)需唯一且不可重复 #### 步骤2:生成代码 安装编译器`protoc`和对应语言插件(以Go为例): ```bash protoc --go_out=. --go-grpc_out=. *.proto ``` 生成代码包含: - 客户端存根(Stub) - 服务端接口 #### 步骤3:实现服务端 ```go type server struct { pb.UnimplementedGreeterServer } func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) { return &pb.HelloResponse{Message: "Hello " + req.Name}, nil } // 启动服务 lis, _ := net.Listen("tcp", ":50051") s := grpc.NewServer() pb.RegisterGreeterServer(s, &server{}) s.Serve(lis) ``` #### 步骤4:实现客户端 ```go conn, _ := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials())) client := pb.NewGreeterClient(conn) resp, _ := client.SayHello(context.Background(), &pb.HelloRequest{Name: "World"}) fmt.Println(resp.Message) // 输出:Hello World ``` #### 步骤5:运行与测试 - 启动服务端:`go run server.go` - 运行客户端验证结果 - 推荐工具:`grpcurl`(命令行测试)、BloomRPC(GUI工具) --- ### 三、4种通信模式 1. **简单RPC(Unary)** 一请求一响应,示例见上文。 2. **服务端流式(Server Streaming)** ```protobuf rpc StreamData (Request) returns (stream Response) {} ``` 3. **客户端流式(Client Streaming)** ```protobuf rpc UploadData (stream Request) returns (Response) {} ``` 4. **双向流式(Bidirectional Streaming)** ```protobuf rpc Chat (stream Message) returns (stream Message) {} ``` --- ### 四、关键技术点 1. **Protocol Buffers优化** - 使用`repeated`定义数组 - 通过`oneof`实现联合类型 2. **HTTP/2优势** - 多路复用降低延迟 - 头部压缩减少带宽占用 3. **拦截器(Interceptor)** - 客户端拦截器:实现认证、日志 - 服务端拦截器:限流、监控 4. **错误处理** - 使用`status`包返回错误码: ```go return nil, status.Error(codes.NotFound, "data not found") ``` --- ### 五、注意事项 1. **版本兼容性** 确保`protoc`编译器与gRPC库版本匹配。 2. **性能优化** - 复用gRPC连接(避免频繁创建) - 启用压缩:`grpc.UseCompressor("gzip")` 3. **安全配置** - 服务端启用TLS: ```go creds := credentials.NewServerTLSFromCert(cert) s := grpc.NewServer(grpc.Creds(creds)) ``` --- ### 六、扩展场景 - **服务发现**:集成Consul/Etcd - **负载均衡**:客户端权重轮询 - **网关代理**:搭配gRPC-Gateway暴露HTTP接口 --- 通过以上步骤和技巧,可快速掌握gRPC的核心用法。建议从简单RPC入手,逐步尝试流式通信和高级特性。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值