起因
grpc这个大名早就有所耳闻,之前学python时,也同样学过一段时间的grpc。现在学go,想学下原汁原味的grpc框架,搞搞清楚其中的原理。为之后的k8s源码提供基础
概念
所谓RPC(remote procedure call 远程过程调用)框架实际是提供了一套机制,使得应用程序之间可以进行通信,而且也遵从server/client模型。使用的时候客户端调用server端提供的接口就像是调用本地的函数一样。
gRPC vs. Restful APIServer端:
gRPC和restful API都提供了一套通信机制,用于server/client模型通信,而且它们都使用http作为底层的传输协议(严格地说, gRPC使用的http2.0,而restful api则不一定)。不过gRPC还是有些特有的优势,如下:
- gRPC可以通过protobuf来定义接口,从而可以有更加严格的接口约束条件。
- 另外,通过protobuf可以将数据序列化为二进制编码,这会大幅减少需要传输的数据量,从而大幅提高性能。
- gRPC可以方便地支持流式通信(理论上通过http2.0就可以使用streaming模式, 但是通常web服务的restful api似乎很少这么用,通常的流式数据应用如视频流,一般都会使用专门的协议如HLS,RTMP等,这些就不是我们通常web服务了,而是有专门的服务器应用。)
使用场景
- 需要对接口进行严格约束的情况,比如我们提供了一个公共的服务,很多人,甚至公司外部的人也可以访问这个服务,这时对于接口我们希望有更加严格的约束,我们不希望客户端给我们传递任意的数据,尤其是考虑到安全性的因素,我们通常需要对接口进行更加严格的约束。这时gRPC就可以通过protobuf来提供严格的接口约束。
- 对于性能有更高的要求时。有时我们的服务需要传递大量的数据,而又希望不影响我们的性能,这个时候也可以考虑gRPC服务,因为通过protobuf我们可以将数据压缩编码转化为二进制格式,通常传递的数据量要小得多,而且通过http2我们可以实现异步的请求,从而大大提高了通信效率。
gRPC实例详解
单向认证
服务端:
项目目录:
Prod.proto
// 指定协议版本
syntax="proto3";
package service;
// 数据类型,相当于结构体
message ProdRequest{
int32 prod_id=1;
}
message ProdResponse{
int32 prod_stock=1;
}
// 服务
service ProdService{
rpc GetProdStock(ProdRequest)returns(ProdResponse);
}
-
运行
protoc --go_out=plugins=grpc:../service Prod.proto
,其中go语言使用--go_out
,plugins=grpc
,../service
为生成文件的目录,Prod.proto
为生成文件的名称 -
会自动在service下生成
Prod.pb.go
文件
其中自动生成的Prod.pb.go
文件中会生成两个接口类型,而之后服务端就是要实现这接口
ProdService.go
定义一个结构体来实现上述的接口
package service
import "context"
type ProdService struct {
}
func (this *ProdService) GetProdStock(ctx context.Context,request *ProdRequest) (*ProdResponse, error){
return &ProdResponse{ProdStock:20},nil
}
keys
keys 文件夹中有两个文件分别为server.crt
和 server.key
是使用openssl 工具生成的自签名SSL证书
-
生成私钥
openssl genrsa -des3 -out server.pass.key 2048
-
去除私钥中的密码
openssl rsa -in server.pass.key -out server.key
-
生成CSR(证书签名请求)这里的 /CN(Common Name) 后面还会见到
openssl req -new -key server.key -out server.csr -subj "/C=CN/ST=Guangdong/L=Guangzhou/O=xdevops/OU=xdevops/CN=gitlab.xdevops.cn"
-
req 生成证书签名请求
-
-new 新生成
-
-key 私钥文件
-
-out 生成的CSR文件
-
-subj 生成CSR证书的参数
-
subj参数说明如下:
字段 | 字段含义 | 示例 |
---|---|---|
/C= | Country 国家 | CN |
/ST= | State or Province 省 | Guangdong |
/L= | Location or City 城市 | Guangzhou |
/O= | Organization 组织或企业 | xdevops |
/OU= | Organization Unit 部门 | xdevops |
/CN= | Common Name 域名或IP | gitlab.xdevops.cn |
- 生成自签名SSL证书
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
X.509证书包含三个文件:key,csr,crt。
- key是服务器上的私钥文件,用于对发送给客户端数据的加密,以及对从客户端接收到数据的解密
- csr是证书签名请求文件,用于提交给证书颁发机构(CA)对证书签名
- crt是由证书颁发机构(CA)签名后的证书,或者是开发者自签名的证书,包含证书持有人的信息,持有人的公钥,以及签署者的签名等信息
备注:在密码学中,X.509是一个标准,规范了公开秘钥认证、证书吊销列表、授权凭证、凭证路径验证算法等。
main.go
package main
import (
"awesomeProject5/service"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc"
"log"
"net"
)
func main(){
creds,err := credentials.NewServerTLSFromFile("keys/server.crt","keys/server.key")
if err != nil{
log.Fatal(err)
return
}
rpcServer := grpc.NewServer(grpc.Creds(creds))
service.RegisterProdServiceServer(rpcServer,&service.ProdService{})
lis,_ := net.Listen("tcp",":8081")
rpcServer.Serve(lis)
}
客户端:
项目目录:
service
只需要将服务端定义好的 Prod.pb.go
拷贝过来即可
keys
现在为单向认证,只需要拷贝.crt 文件
main.go
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"grpcclient/service"
"log"
)
func main(){
// 这里Common Name 为刚刚生成自签证书所填写的
creds, err := credentials.NewClientTLSFromFile("keys/server.crt",
"gitlab.xdevops.cn")
if err != nil{
log.Fatal(err)
return
}
// 如果服务端没有证书认证,这里 conn,err := grpc.Dial(":8081",grpc.WithInsecure())
conn,err := grpc.Dial(":8081",grpc.WithTransportCredentials(creds))
if err != nil{
log.Fatal(err)
return
}
defer conn.Close()
prodClient := service.NewProdServiceClient(conn)
prdRes,err :=prodClient.GetProdStock(context.Background(),
&service.ProdRequest{ProdId:1})
if err != nil{
log.Fatal(err)
}
fmt.Println(prdRes.ProdStock)
}
这里可能会出现
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"
如果出现上述报错,是因为 go 1.15 版本开始废弃 CommonName,因此推荐使用 SAN 证书。 如果想兼容之前的方式,需要设置环境变量 GODEBUG 为 x509ignoreCN=0
。
export GODEBUG="x509ignoreCN=0"
双向认证:
生成证书
生成ca证书
# 会生成 ca.pem 和 ca.key
openssl genrsa -out ca.key 2048
openssl req -new -x509 -days 3650 -key ca.key -out ca.pem
生成服务端证书
# 会生成 ca.srl server.pem server.csr server.key
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr
openssl x509 -req -sha256 -CA ca.pem -CAkey ca.key -CAcreateserial -days 3650 -in server.csr -out server.pem
生成客户端证书
# 会生成 client.pem client.csr client.key
openssl ecparam -genkey -name secp384r1 -out client.key
openssl req -new -key client.key -out client.csr
openssl x509 -req -sha256 -CA ca.pem -CAkey ca.key -CAcreateserial -days 3650 -in client.csr -out client.pem
服务端
将 server.pem
、server.key
、ca.pem
放入cert 文件夹中,这里我的certPool中AppendCertsFromPEM方法老是爆红,但运行时正常的,发现是goland 运行插件的时候报错了,而导致不能很好的提示。关闭插件后正常
package main
import (
"awesomeProject5/service"
"crypto/tls"
"crypto/x509"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"io/ioutil"
"net"
)
func main(){
//从证书相关文件中读取和解析信息,得到证书公钥、密钥对
cert,_ := tls.LoadX509KeyPair("cert/server.pem","cert/server.key")
// 创建一个新的、空的 CertPool
certPool := x509.NewCertPool()
ca,_ := ioutil.ReadFile("cert/ca.pem")
//尝试解析所传入的 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))
service.RegisterProdServiceServer(rpcServer,&service.ProdService{})
lis,_ := net.Listen("tcp",":8081")
rpcServer.Serve(lis)
}
客户端
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"grpcclient/service"
"io/ioutil"
"log"
"crypto/tls"
"crypto/x509"
)
func main(){
//从证书相关文件中读取和解析信息,得到证书公钥、密钥对
cert,_ := tls.LoadX509KeyPair("cert/client.pem","cert/client.key")
// 创建一个新的、空的 CertPool
certPool := x509.NewCertPool()
ca,_ := ioutil.ReadFile("cert/ca.pem")
//尝试解析所传入的 PEM 编码的证书。如果解析成功会将其加到 CertPool 中,便于后面的使用
certPool.AppendCertsFromPEM(ca)
//构建基于 TLS 的 TransportCredentials 选项
creds := credentials.NewTLS(&tls.Config{
//设置证书链,允许包含一个或多个
Certificates: []tls.Certificate{cert},
//要求必须校验客户端的证书。可以根据实际情况选用以下参数
ServerName: "localhost",
RootCAs:certPool,
})
conn,err := grpc.Dial(":8081",grpc.WithTransportCredentials(creds))
if err != nil{
log.Fatal(err)
return
}
defer conn.Close()
prodClient := service.NewProdServiceClient(conn)
prdRes,err :=prodClient.GetProdStock(context.Background(),
&service.ProdRequest{ProdId:1})
if err != nil{
log.Fatal(err)
}
fmt.Println(prdRes.ProdStock)
}