Grpc

GRPC #

介绍 #

gRPC 是一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计。

gRPC基于 HTTP/2标准设计,带来诸如双向流、流控、头部压缩、单 TCP连接上的多复用请求等特。这些特性使得 其在移动设备上表现更好,更省电和节省空间占用。

在 gRPC里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,使得您能够更容 易地创建分布式应用和服务。与许多 RPC系统类似, gRPC也是基于以下理念:

定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。

在服务端实现这个接口,并运行一个 gRPC服务器来处理客户端调用。

在客户端拥有一个存根能够像服务端一样的方法。 gRPC客户端和服务端可以在多种环境中运行和交互 -从 google 内部的服务器到你自己的笔记本,并且可以用任何 gRPC支持的语言 来编写。

所以,你可以很容易地用 Java创建一个 gRPC服务端,用 Go、 Python、Ruby来创建客户端。此外, Google最新 API将有 gRPC版本的接口,使你很容易地将 Google的功能集成到你的应用里。

gRPC 内置了以下 encryption 机制:

  • SSL / TLS:通过证书进行数据加密;
  • ALTS:Google开发的一种双向身份验证和传输加密系统。
    • 只有运行在 Google Cloud Platform 才可用,一般不用考虑。

gRPC 中的连接类型一共有以下3种

  • insecure connection:不使用TLS加密
  • server-side TLS:仅服务端TLS加密
  • mutual TLS:客户端、服务端都使用TLS加密

gRPC 与 RESTful API比较 #

特性 gRPC RESTful API
规范 必须.proto 可选 OpenAPI
协议 HTTP/2 任意版本的 HTTP 协议
有效载荷 Protobuf(小、二进制) JSON(大、易读)
浏览器支持 否(需要 grpc-web)
流传输 客户端、服务端、双向 客户端、服务端
代码生成 OpenAPI + 第三方工具

使用场景 #

  1. 低延时、高可用的分布式系统;
  2. 移动端与云服务端的通讯;
  3. 使用protobuf,独立于语言的协议,支持多语言之间的通讯;
  4. 可以分层扩展,如:身份验证,负载均衡,日志记录,监控等;

RPC #

RPC(Remote Procedure Call Protocol)——远程过程调用协议,它是一种通过网络从远程计算机程序上请求 服务,而不需要了解底层网络技术的协议。

简单来说,就是跟远程访问或者web请求差不多,都是一个client向远端服务器请求服务返回结果,但是web请求 使用的网络协议是http高层协议,而rpc所使用的协议多为TCP,是网络层协议,减少了信息的包装,加快了处理速 度。

golang本身有rpc包,可以方便的使用,来构建自己的rpc服务,下边是一个简单是实例,可以加深我们的理解

1.调用客户端句柄;执行传送参数

2.调用本地系统内核发送网络消息

3.消息传送到远程主机

4.服务器句柄得到消息并取得参数

5.执行远程过程

6.执行的过程将结果返回服务器句柄

7.服务器句柄返回结果,调用远程系统内核

8.消息传回本地主机

9.客户句柄由内核接收消息

10.客户接收句柄返回的数据

protocol buffers #

gRPC默认使用protoBuf,这是 Google开源的一套成熟的结构数据序列化机制(当然也可以使用其他数据格式如 JSON)。正如你将在下方例子里所看到的,你用 proto files创建 gRPC服务,用 protoBuf消息类型来定义方法参 数和返回类型。你可以在 Protocol Buffers文档找到更多关于 protoBuf的资料。 虽然你可以使用 proto2 (当前默 认的 protocol buffers版本 ),我们通常建议你在 gRPC里使用 proto3,因为这样你可以使用 gRPC支持全部范围的 的语言,并且能避免 proto2客户端与 proto3服务端交互时出现的兼容性问题,反之亦然。

安装protoc #

//https://github.com/protocolbuffers/protobuf/releases  下载安装

并将bin文件夹下的protoc应用程序复制到../go/bin

安装协议编译器的插件 #

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

更新你的PATH,以便protoc编译器可以找到插件:

$ export PATH="$PATH:$(go env GOPATH)/bin"

生成grpc代码 #

protoc --go_out=. --go-grpc_out=. demo.proto
//下面是原始示例
protoc user.proto --go_out=./protobuf --go-grpc_out=./protobuf --go-grpc_opt=require_unimplemented_servers=false
//require_unimplemented_servers=false 这个配置不加,会存在兼容问题,有个接口需要实现

demo #

demo

server-side TLS #

1. 流程 #

服务端 TLS 具体包含以下几个步骤:

  • 1)制作证书,包含服务端证书和 CA 证书;
  • 2)服务端启动时加载证书;
  • 3)客户端连接时使用CA 证书校验服务端证书有效性。

也可以不使用 CA证书,即服务端证书自签名。

2. 制作证书 #

具体证书相关,点击查看证书制作章节,实在不行可以直接使用本教程 Github 仓库中提供的证书文件。

CA 证书 #
# 生成.key  私钥文件
$ openssl genrsa -out ca.key 2048

# 生成.csr 证书签名请求文件
$ openssl req -new -key ca.key -out ca.csr  -subj "/C=GB/L=China/O=lixd/CN=www.lixueduan.com"

# 自签名生成.crt 证书文件
$ openssl req -new -x509 -days 3650 -key ca.key -out ca.crt  -subj "/C=GB/L=China/O=lixd/CN=www.lixueduan.com"
服务端证书 #

和生成 CA证书类似,不过最后一步由 CA 证书进行签名,而不是自签名。

然后openssl 配置文件可能位置不同,需要自己修改一下。

$ find / -name "openssl.cnf"
# 生成.key  私钥文件
$ openssl genrsa -out server.key 2048

# 生成.csr 证书签名请求文件
$ openssl req -new -key server.key -out server.csr \
	-subj "/C=GB/L=China/O=lixd/CN=www.lixueduan.com" \
	-reqexts SAN \
	-config <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.lixueduan.com,DNS:*.refersmoon.com"))

# 签名生成.crt 证书文件
$ openssl x509 -req -days 3650 \
   -in server.csr -out server.crt \
   -CA ca.crt -CAkey ca.key -CAcreateserial \
   -extensions SAN \
   -extfile <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.lixueduan.com,DNS:*.refersmoon.com"))

到此会生成以下 6 个文件:

ca.crt  
ca.csr  
ca.key   
server.crt  
server.csr  
server.key

会用到的有下面这3个

  • 1)ca.crt
  • 2)server.key
  • 3)server.crt

3. 服务端 #

服务端代码修改点如下:

  • 1)NewServerTLSFromFile 加载证书
  • 2)NewServer 时指定 Creds。
func main() {
	flag.Parse()

	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	// 指定使用服务端证书创建一个 TLS credentials。
	creds, err := credentials.NewServerTLSFromFile(data.Path("x509/server.crt"), data.Path("x509/server.key"))
	if err != nil {
		log.Fatalf("failed to create credentials: %v", err)
	}
	// 指定使用 TLS credentials。
	s := grpc.NewServer(grpc.Creds(creds))
	ecpb.RegisterEchoServer(s, &ecServer{})

	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

4. 客户端 #

客户端代码主要修改点:

  • 1)NewClientTLSFromFile 指定使用 CA 证书来校验服务端的证书有效性。
    • 注意:第二个参数域名就是前面生成服务端证书时指定的CN参数。
  • 2)建立连接时 指定建立安全连接 WithTransportCredentials。
func main() {
	flag.Parse()

	// 客户端通过ca证书来验证服务的提供的证书
	creds, err := credentials.NewClientTLSFromFile(data.Path("x509/ca.crt"), "www.lixueduan.com")
	if err != nil {
		log.Fatalf("failed to load credentials: %v", err)
	}
	// 建立连接时指定使用 TLS
	conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(creds))
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()


	rgc := ecpb.NewEchoClient(conn)
	callUnaryEcho(rgc, "hello world")
}

5. Test #

Server

lixd@17x:~/17x/projects/grpc-go-example/features/encryption/server-side-TLS/server$ go run main.go 
2021/01/24 18:00:25 Server gRPC on 0.0.0.0:50051

Client

lixd@17x:~/17x/projects/grpc-go-example/features/encryption/server-side-TLS/client$ go run main.go 
UnaryEcho:  hello world

可以看到成功开启了 TLS。

3. mutual TLS #

server-side TLS 中虽然服务端使用了证书,但是客户端却没有使用证书,本章节会给客户端也生成一个证书,并完成 mutual TLS。

1. 制作证书 #

# 生成.key  私钥文件
$ openssl genrsa -out server.key 2048

# 生成.csr 证书签名请求文件
$ openssl req -new -key server.key -out server.csr \
	-subj "/C=GB/L=China/O=lixd/CN=www.lixueduan.com" \
	-reqexts SAN \
	-config <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.lixueduan.com,DNS:*.refersmoon.com"))

# 签名生成.crt 证书文件
$ openssl x509 -req -days 3650 \
   -in server.csr -out server.crt \
   -CA ca.crt -CAkey ca.key -CAcreateserial \
   -extensions SAN \
   -extfile <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.lixueduan.com,DNS:*.refersmoon.com"))

这里又会生成3个文件,需要的是下面这两个:

  • client.crt
  • client.key

到此为止,我们已经有了如下5个文件:

  • ca.crt
  • client.crt
  • client.key
  • server.crt
  • server.key

2. 服务端 #

mutual TLS 中服务端、客户端改动都比较多。

具体步骤如下:

  • 1)加载服务端证书
  • 2)构建用于校验客户端证书的 CertPool
  • 3)使用上面的参数构建一个 TransportCredentials
  • 4)newServer 是指定使用前面创建的 creds。

具体改动如下:

看似改动很大,其实如果你仔细查看了前面 NewServerTLSFromFile 方法做的事的话,就会发现是差不多的,只有极个别参数不同。

修改点如下:

  • 1)tls.Config的参数ClientAuth,这里改成了tls.RequireAndVerifyClientCert,即服务端也必须校验客户端的证书,之前使用的默认值(即不校验)
  • 2)tls.Config的参数ClientCAs,由于之前都不校验客户端证书,所以也没有指定用什么证书来校验。
func main() {
	// 从证书相关文件中读取和解析信息,得到证书公钥、密钥对
	certificate, err := tls.LoadX509KeyPair(data.Path("x509/server.crt"), data.Path("x509/server.key"))
	if err != nil {
		log.Fatal(err)
	}
	// 创建CertPool,后续就用池里的证书来校验客户端证书有效性
	// 所以如果有多个客户端 可以给每个客户端使用不同的 CA 证书,来实现分别校验的目的
	certPool := x509.NewCertPool()
	ca, err := ioutil.ReadFile(data.Path("x509/ca.crt"))
	if err != nil {
		log.Fatal(err)
	}
	if ok := certPool.AppendCertsFromPEM(ca); !ok {
		log.Fatal("failed to append certs")
	}

	// 构建基于 TLS 的 TransportCredentials
	creds := credentials.NewTLS(&tls.Config{
		// 设置证书链,允许包含一个或多个
		Certificates: []tls.Certificate{certificate},
		// 要求必须校验客户端的证书 可以根据实际情况选用其他参数
		ClientAuth: tls.RequireAndVerifyClientCert, // NOTE: this is optional!
		// 设置根证书的集合,校验方式使用 ClientAuth 中设定的模式
		ClientCAs: certPool,
	})
	s := grpc.NewServer(grpc.Creds(creds))
	ecpb.RegisterEchoServer(s, &ecServer{})

	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	log.Println("Serving gRPC on 0.0.0.0" + fmt.Sprintf(":%d", *port))
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

3. 客户端 #

客户端改动和前面服务端差不多,具体步骤都一样,除了不能指定校验策略之外基本一样。

大概是因为客户端必校验服务端证书,所以没有提供可选项。

func main() {
	// 加载客户端证书
	certificate, err := tls.LoadX509KeyPair(data.Path("x509/client.crt"), data.Path("x509/client.key"))
	if err != nil {
		log.Fatal(err)
	}
	// 构建CertPool以校验服务端证书有效性
	certPool := x509.NewCertPool()
	ca, err := ioutil.ReadFile(data.Path("x509/ca.crt"))
	if err != nil {
		log.Fatal(err)
	}
	if ok := certPool.AppendCertsFromPEM(ca); !ok {
		log.Fatal("failed to append ca certs")
	}

	creds := credentials.NewTLS(&tls.Config{
		Certificates: []tls.Certificate{certificate},
		ServerName:   "www.lixueduan.com", // NOTE: this is required!
		RootCAs:      certPool,
	})
	conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(creds))
	if err != nil {
		log.Fatalf("DialContext error:%v", err)
	}
	defer conn.Close()
	client := ecpb.NewEchoClient(conn)
	callUnaryEcho(client, "hello world")
}

4. Test #

Server

lixd@17x:~/17x/projects/grpc-go-example/features/encryption/mutual-TLS/server$ go run main.go 
2021/01/24 18:02:01 Serving gRPC on 0.0.0.0:50051

Client

lixd@17x:~/17x/projects/grpc-go-example/features/encryption/mutual-TLS/client$ go run main.go 
UnaryEcho:  hello world

一切正常,大功告成。

4. FAQ #

问题

error:rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: x509: certificate relies on legacy Common Name field, use SANs or temporarily enable Common Name matching with GODEBUG=x509ignoreCN=0"

由于之前使用的不是 SAN 证书,在Go版本升级到1.15后出现了该问题。

原因

Go 1.15 版本开始废弃 CommonName 并且推荐使用 SAN 证书,导致依赖 CommonName 的证书都无法使用了。

解决方案

  • 1)开启兼容:设置环境变量 GODEBUG 为 x509ignoreCN=0
  • 2)使用 SAN 证书

本教程已经修改成了 SAN 证书,所以不会遇到该问题了。

特殊函数用法 #

grpc.MaxCallRecvMsgSize #

grpc.MaxCallRecvMsgSize(maxCallRecvMsgSize) 是用于设置 gRPC 调用接收消息的最大大小限制的函数。这个函数允许你限制 gRPC 调用接收消息的最大大小,以防止潜在的内存溢出或拒绝服务攻击。

这个函数的作用是设置 gRPC 调用接收消息的最大大小限制,以确保接收到的消息大小不超过指定的值 maxCallRecvMsgSize。如果接收到的消息超过了这个限制,gRPC 调用可能会失败并返回相应的错误。

ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Second*time.Duration(timeoutSec)))
defer cancel()
conn, err := grpc.DialContext(ctx, port, grpc.WithInsecure(), grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxCallRecvMsgSize)), grpc.WithBlock())
if err != nil {
	log.Fatalf("fail to dial: %v", err)
	return err
}

设置重连次数 #

// 定义grpc服务端口
func Init(port string) (err error) {
	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Second*time.Duration(timeoutSec)))
	defer cancel()
	retryOpts := []grpc_retry.CallOption{
		grpc_retry.WithMax(2), // 最大重试次数
		grpc_retry.WithBackoff(grpc_retry.BackoffLinear(100 * time.Millisecond)), // 重试间隔
	}
	conn, err := grpc.DialContext(ctx,
		port,
		grpc.WithInsecure(),
		grpc.WithUnaryInterceptor(grpc_retry.UnaryClientInterceptor(retryOpts...)), //设置重连次数
		grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxCallRecvMsgSize)),
		grpc.WithBlock(),
	)

	if err != nil {
		return err
	}
	//defer conn.Close()
	gRPCclient.conn = conn
	gRPCclient.cl = data.NewAnalyzerClient(conn)
	return
}

https://blog.51cto.com/u_93011/10599853

拦截器 #

http://123.56.139.157:8082/article/23/6223422/detail.html