grpc的使用

2023-05-16

  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)
	}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

grpc的使用 的相关文章

随机推荐

  • win10系统vs2017+1060配置CUDA安装tensorflow-gpu

    笔者最开始安装的是推荐的cuda10后来虽然vs实例能成功运行 xff0c 但是tf的import还是报错 xff0c 后来安装的9 2 xff0c 运行无误 xff0c 各下载链接在文后可自行查看 检查显卡驱动是否为最新版本 非常重要 安
  • python3-端口扫描(TCP connect扫描,SYN扫描,FIN扫描)

    利用python3进行端口扫描 xff0c TCP的方式有connect扫描 xff0c SYN扫描 xff0c FIN扫描 xff0c NULL扫描 xff0c ACK扫描 xff0c Xmas xff0c windows扫描 本次展示前
  • CSDN 编写数学公式参考文档

    CSDN markdown 编辑器使用的是 katex 来完成对 TeX 公式的解析 公式得紧挨两个 符内部 xff0c 而且第一个 后面不能有空格 xff0c 第二个 前面不能有空格 想要换行居中的话 xff0c 得使用两次 来将公式框起
  • 1、AI系统简介及AI系统的分类

    AI系统简介 AI这个缩写在越来越多的出现在公众面前 但是AI到底是什么就和AI这个词一样 大部分都能举一些例子 但是都却很难说清楚AI到底是什么 AI是Artificial Intelligence的简称 中文叫做人工智能 但是要找到一个
  • 六关节机器人工具坐标系位姿计算函数

    span class token keyword void span span class token function clacToolPose span span class token punctuation span RcEuler
  • ORB-SLAM2项目数据集运行(一)

    作为一名学习一年的机器视觉的学生 xff0c 一直以来都没有写过像样点的自己的东西 xff0c 真的是不像话 xff0c 虽然有时候觉得会占用一些时间 xff0c 但是能很大程度上帮助自己理解 从github上可以下载到源码 xff1a h
  • string是否以‘\0’结尾

    今天刷题 xff0c 刷着刷着发现了一个问题 xff0c 我好想对string这个了解不是很深 xff0c 我之前是把它跟C语言中的char类型总是分不清 xff0c 所以今天题也不刷了 xff0c 试着了解它们俩到底有什么区别 在C语言中
  • Matlab中函数fopen、fread、fseek和fwrite的用法

    1 fopen 打开文件或获取关于打开文件的信息 xff0c 下面以使用最多的情况为例 xff1a fileID 61 fopen filename permission xff1a 打开文件 xff0c 成功时 xff0c 返回一个大于或
  • PCB设计-四层板变两层板

    四层板变成两层板原本思路是 xff1a 从原理图导出一份asc文件 xff0c 将原先四层板的PCB封装全部保存到一个库里面去 xff0c 然后在PADS里导入asc文件 xff0c 然后将原先的结构定位从原来的PCB板上复制过来 xff0
  • Pycharm导入Django项目

    Pycharm导入Django项目 添加项目 xff1a file gt open 找到项目所在的位置打开项目 添加django后台项目路径 file gt settings gt Languages amp Frameworks 找到Dj
  • Windows安装多个python解释器

    Windows安装多个python解释器 注 xff1a 此方法仅仅是针对安装多个需要配置环境变量的解释器 xff0c 意思是可以在cmd中可以使用并完美切换的 xff1b 针对一个环境变量的解释器无效 xff0c 只有一个环境变量的主解释
  • Go语言开发环境搭建

    一 Windows下安装 安装Go开发包 官网下载Go语言开发包 xff0c 安装方法就是next xff0c 除了安装位置选择 安装目录选定一个好记的 尽量不要放在C盘中 xff0c 除非你盘空间很大 xff0c 完全够用 此路径需要自己
  • Django Rest Framework的使用整理

    Django Rest Framework 一 Rest Framework的基本介绍 程序的客户端有很多 xff1a 硬件设备 xff0c 游戏 xff0c APP xff0c 软件 xff0c 其他的外部服务端 1 Web应用模式 在开
  • Linux部署Python项目

    项目部署 项目部署的操作通常是由运维人员来进行统一管理装配的 xff0c 但是对于一个开发人员来讲 xff0c 基础的项目部署还是要会的 这里我主要讲解python的项目是如何部署的 xff0c 至于项目哪里来的 xff0c 这就要看观众如
  • 2、机器学习简介及其分类

    简介 机器学习是指让机器从数据中自动学习规律和知识 并利用这些规律和知识进行预测或决策的技术 机器学习包括监督学习 无监督学习 强化学习 其中监督学习也被称作有监督的学习 有监督的意思就是预先知道据有什么样的目标 通过一些已经知道结果的数据
  • VScode启动Vue项目

    VScode启动Vue项目 1 使用VScode打开文件夹 2 找到运行按钮 3 判断有没有默认的配置文件存在 4 若文件夹存在就检查配置文件是否存在 xff08 1 xff09 打开launch json xff0c 把如下代码粘贴到里面
  • Gunicorn+django部署

    部署前提是服务器中要有项目中所需的其他服务 xff0c 例 xff1a mysql数据库 xff0c nginx xff0c python解释器等 xff0c 在其他环境搭好的情况下使用此方式可简单部署一个django项目 至于安装上面提到
  • Protobuf生成文件报错

    Mac下protobuf生成文件报错问题解决办法 xff0c windows下就不会这么麻烦了 xff0c 如果linux下出现类似报错信息按照下面的解决逻辑依然适用 1 由 go out引发的报错 1 报错信息 xff1a user 64
  • Git操作的基本命令

    git命令常用步骤 初始化 xff0c 把当前文件夹作为git本地仓库 git init 把本地仓库与选程仓库关联 git remote add origin http gitee com 把项目区中做了修改的文件添加到暂存区 git ad
  • grpc的使用

    需要保证电脑中安装了 xff1a protobuf安装教程如果出现报错请看博客 xff1a protobuf报错问题解决基本使用demo地址 xff1a demo安全传输 流式传输的demo地址 xff1a demo2 简介 xff1a r