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  (0x)
# 输入密码: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)
	}

新闻标题:grpc的使用
URL标题:http://myzitong.com/article/dsoicci.html