官方中文文档

视频教学

什么是gRPC

概念:

序列化:将数据结构或对象转换成二进制串的过程
反序列化:将在序列化过程中所产生的二进制串转换成数据结构或者对象的过程

env

grpc和protobuf

1
go get google.golang.org/grpc
1
brew install protobuf

protobuf配套go语言的代码生成工具,因为这些文件在安装grpc的时候已经下载下来了,所以使用install即可。

1
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
1
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest


1. proto文件编写

模版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
syntax = "proto3";

option go_package = ".;service";

service SayHello {
rpc SayHello(HelloRequest) returns (HelloResponse) {}
}

message HelloRequest {
string requestName = 1;
}

message HelloResponse {
string responseMsg = 1;
}

说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 这是在说明我们使用的是proto3语法
syntax = "proto3";

// 这部分的内容是关于最后生成的go文件是处在哪个目录哪个包中,
// .代表当前目录生成
// service代表生成go文件的包名是service
option go_package = ".;service";

// 然后我们需要定义一个服务,在这个服务中需要有一个方法,这个方法可接受客户端的参数,再返回服务端的响应
// 其实很容易可以看出,我们定义了一个service,称为SayHello,这个服务中有一个rpc方法,名为SayHello
// 这个方法会发送一个HelloRequest,然后再返回一个HelloResponse
service SayHello {
rpc SayHello(HelloRequest) returns (HelloResponse) {}
}

// message 关键字,其实你可以理解为Golang中的结构体
// 这里比较特别的是变量后面的“赋值”。注意:这里并不是赋值,而是在定义这个变量在这个message中的位置。
message HelloRequest {
string requestName = 1;
// int64 age = 2;
}

message HelloResponse {
string responseMsg = 1;
}

当前文件结构:

1
2
3
4
5
6
7
8
9
.
├── hello_client
│   ├── main.go
│   └── proto
│   └── hello.proto
└── hello_server
├── main.go
└── proto
└── hello.proto

在编写完上面的内容后,在helloworld/proto目录下执行如下命令:

1
protoc --go_out=. hello.proto
1
protoc --go-grpc_out=. hello.proto

执行命令后文件结构:

1
2
3
4
5
6
7
8
9
10
11
.
├── hello_client
│   ├── main.go
│   └── proto
│   └── hello.proto
└── hello_server
├── main.go
└── proto
├── hello.pb.go
├── hello.proto
└── hello_grpc.pb.go




2. proto文件介绍

message

message:protobuf中定义一个消息类型是通过关键字message字段指定的,消息就是需要传输的数据格式的定义

message关键字类似于C++中class,Java中的class,go中的struct

在消息体中承载的数据分别对应一个字段,其中每个字段都有一个名字和一种类型

一个proto文件中可以定义多个消息类型。

eg:

1
2
3
4
message HelloRequest {
string requestName = 1;
int64 age = 2;
}

字段规则

require:消息体中必填的字段,不设置会导致编码异常。在protobuf2中使用,在protobuf中被删去。

optional:消息体中的可选字段。protobuf3没有了required,optional等说明关键字,都默认optional

eg:

1
2
3
4
message HelloRequest {
string requestName = 1;
int64 age = 2;
}

repeat:消息体中可重复字段,重复的值的顺序会被保留,在go中重复的会被定义为切片。

eg:

1
2
3
4
5
message HelloRequest {
string requestName = 1;
int64 age = 2;
repeated int32 weight = 3
}

消息号

在消息体中,每个字段都必须要有一个唯一的标识号,标识号是[1, 2^29-1]范围内的一个整数。

eg:

1
2
3
4
message HelloRequest {
string requestName = 1;
int64 age = 2;
}

嵌套消息

可以在其他消息类型中定义、使用消息类型,在下面的例子中,person消息就定义在PersonInfo消息内:

1
2
3
4
5
6
7
8
message PersonInfo {
message Person {
string name = 1;
int32 height = 2;
repeated int32 weight = 3
}
repeated Person info = 1;
}

如果要在它的父消息类型的外部重用这个消息类型,需要PersonInfo.Person的形式使用它。


服务定义

如果想要将消息类型用在RPC系统中,可以在 .proto 文件中定义一个RPC服务接口,protocal buffer编译器将会根据所选择的不同语言生成服务接口代码及存根。

1
2
3
4
service SearchService {
// rpc 服务函数名(参数) returns(返回参数)
rpc Search(SearchRequest) returns (SearchResponse)
}

上述代表表示,定义了一个RPC服务,该方法接受SearchRequest返回SearchResponse。





3. 服务端代码编写

  1. 创建gRPC Server对象,你可以理解为它是Server端的抽象对象
  2. 将server(其包含需要被调用的服务端口)注册到gRPC Server的内部注册中心。
    这样可以在接受请求时,通过内部服务发现,发现该服务端接口并转接进行逻辑处理。
  3. 创建listen,监听TCP端口
  4. gRPC Server 开始 lis.Accept,直到Stop.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
"context"
"log"
"net"

pb "learning/grpc/hello_server/proto"

"google.golang.org/grpc"
)

type server struct {
pb.UnimplementedSayHelloServer
}

func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
return &pb.HelloResponse{ResponseMsg: "hello " + req.RequestName}, nil
}

func main() {

// 创建grpc服务
grpcServer := grpc.NewServer()
// 在grpc服务中注册我们自己编写的服务
pb.RegisterSayHelloServer(grpcServer, &server{})
// 开启端口
listen, err := net.Listen("tcp", ":9090")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// 启动服务
err = grpcServer.Serve(listen)
if err != nil {
log.Fatalf("failed to serve: %v", err)
}
}




4. 客户端代码编写

  1. 创建与给定目标(服务端)的连接交互
  2. 创建server的客户端对象
  3. 发送RPC请求,等待同步响应,得到回调后返回响应结果
  4. 输出响应结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"context"
"fmt"
"log"

pb "learning/grpc/hello_server/proto"

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

func main() {
// 连接到server端,此处禁用安全传输,没有加密和验证
conn, err := grpc.Dial("localhost:9090", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()

// 建立连接
client := pb.NewSayHelloClient(conn)
// 执行rpc调用(这个方法在服务端来实现并返回结果)
resp, err := client.SayHello(context.Background(), &pb.HelloRequest{RequestName: "xxx"})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
fmt.Println(resp.GetResponseMsg())
}




5. 认证-安全传输

gRPC是一个典型的C/S模型,需要开发客户端和服务器,客户端个服务器需要达成协议,使用某一个确认的传输协议来传输数据,gRPC通常默认使用protobuf来作为传输协议,当然也可以使用其他自定义的。

那么,客户端与服务端要通信之前,客户端如何知道自己的数据是发给哪一个明确的服务端呢?反过来,服务端是不是也需要有一种方式来弄清楚自己的数据要返回给谁呢?


那么就不得不提gRPC的认证

此处说到的认证,不是用户的身份认证,而是指多个server和多个client之间,如何识别对方是谁,并且可以安全的进行数据传输

  • SSL/TLS认证方式(采用http2协议)
  • 基于Token的认证方式(基于安全连接)
  • 不采用任何措施的连接,这是不安全的连接(默认采用http1)
  • 自定义的身份认证

客户端和服务端之间调用,我们可以通过加入证书的方式,实现调用的安全性。

TLS(Transport Layer Security,安全传输层),TLS是建立在传输层TCP协议之上的协议,服务于应用层,它的前身是SSL(Secure Socket Layer,安全套接字层),它实现了将应用层的报文进行加密后再交由TCP进行传输的功能。

TLS协议主要解决如下三个网络安全问题。

1)保密(message privacy),保密通过加密encryption实现,所有信息都加密传输,第三方无法嗅探;

2)完整性(message integrity),通过MAC校验机制,一旦被篡改,通信双方会立刻发现;

3)认证(mutual authentication),双方认证,双方都可以配备证书,防止身份被冒充;


生产环境可以购买证书或者使用一些平台发放的免费证书

key:服务器上的私钥文件,用于对发送给客户端数据的加密,以及对从客户端接收到数据的解密。

csr:证书签名请求文件,用于提交给证书颁发机构(CA)对证书签名。

crt:由证书颁发机构(CA)签名后的证书,或者是开发者自签名的证书,包含证书持有人的信息,持有人的公钥,以及签署者的签名等信息。

pem:是基于Base64编码的证书格式,扩展名包括PEM、CRT和CER.


聊聊HTTPS和SSL/TLS协议





6. SSL/TSL认证方式

首先通过openssl生成证书和私钥(mac自带,或者homebrew安装)

官网:https://www.openssl.org/source


生成证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#1、生成私钥
openssl genrsa -out server.key 2048

#2、生成证书 全部回车即可,可以不填
openssl req -new -x509 -key server.key -out server.crt -days 36500
# 国家名称
Country Name (2 letter code)[AU]:CN
#省名称
State or Province Name (full name) [Some-State]:GuangDong
# 城市名称
Locality Name (eg, city)[]:Meizhou
# 公司组织名称
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Xuexiangban
# 部门名称
Organizational Unit Name (eg, section) []:go
# 服务器or网站名称
Common Name (e.g. server FQDNor YOUR name) []:kuangstudy
#邮件
Email Address[]:24736743@qq.com

#3、生成 csr
openssl req -new -key server.key -out server.csr
  1. 生成私钥,可以在项目根目录下新建一个key目录,用来存放安全文件,在这个目录下执行第一条命令。

    目录结构变为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    .
    ├── hello_client
    │   ├── main.go
    │   └── proto
    │   └── hello.proto
    ├── hello_server
    │   ├── main.go
    │   └── proto
    │   ├── hello.pb.go
    │   ├── hello.proto
    │   └── hello_grpc.pb.go
    └── key
    └── server.key
  2. 在这个目录下执行第二条命令,全部回车。

    1
    2
    3
    .
    ├── server.crt
    └── server.key
  3. 在这个目录下执行第二条命令,全部回车。

    1
    2
    3
    4
    .
    ├── server.crt
    ├── server.csr
    └── server.key

在终端中运行以下命令,可以帮助查找 OpenSSL 配置文件的默认路径:

1
openssl version -d

复制配置文件到key目录下

1
2
3
4
5
.
├── openssl.cnf
├── server.crt
├── server.csr
└── server.key

修改openssl.cnf:

1
2
3
4
5
6
#1)找到[CA_default],打开copy_extensions=copy(就是把前面的#去掉)
#2)找到[req],打开 req_extensions =v3_req #The extensions to add to a certificate request
#3)找到[v3_req],添加subjectAltName = @alt_names
#4)添加新的标签[alt_names],和标签字段:
[alt_names]
DNS.1 = *.kuangstudy.com

  1. 生成证书私钥test.key
1
openssl genpkey -algorithm RSA -out test.key
1
2
3
4
5
6
.
├── openssl.cnf
├── server.crt
├── server.csr
├── server.key
└── test.key
  1. 通过私钥test.key生成证书请求文件test.csr(注意cfg和cnf)
1
openssl req -new -nodes -key test.key -out test.csr -days 3650 -subj "/C=cn/OU=myorg/O=mycomp/CN=myname" -config ./openssl.cnf -extensions v3_req
1
2
3
4
5
6
7
.
├── openssl.cnf
├── server.crt
├── server.csr
├── server.key
├── test.csr
└── test.key

test.csr是上面生成的证书请求文件。ca.crt/server.key是CA证书文件和key,用来对test.csr进行签名认证。这两个文件在第一部分生.

  1. 生成SAN证书 pem
1
openssl x509 -req -days 365 -in test.csr -out test.pem -CA server.crt -CAkey server.key -CAcreateserial -extfile ./openssl.cnf -extensions v3_req
1
2
3
4
5
6
7
8
9
.
├── openssl.cnf
├── server.crt
├── server.csr
├── server.key
├── server.srl
├── test.csr
├── test.key
└── test.pem

修改hello_server/main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import (
"context"
"google.golang.org/grpc/credentials"
"log"
"net"

pb "learning/grpc/hello_server/proto"

"google.golang.org/grpc"
)

type server struct {
pb.UnimplementedSayHelloServer
}

func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
return &pb.HelloResponse{ResponseMsg: "hello " + req.RequestName}, nil
}

func main() {
// 1. 新增
creds, _ := credentials.NewServerTLSFromFile("../key/test.pem", "../key/test.key")
// 创建grpc服务
// grpcServer := grpc.NewServer()
grpcServer := grpc.NewServer(grpc.Creds(creds)) // 修改
// 在grpc服务中注册我们自己编写的服务
pb.RegisterSayHelloServer(grpcServer, &server{})
// 开启端口
listen, err := net.Listen("tcp", ":9090")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// 启动服务
err = grpcServer.Serve(listen)
if err != nil {
log.Fatalf("failed to serve: %v", err)
}
}

修改hello_client/main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
"context"
"fmt"
"google.golang.org/grpc/credentials"
"log"

pb "learning/grpc/hello_server/proto"

"google.golang.org/grpc"
)

func main() {
// 新增
creds, _ := credentials.NewClientTLSFromFile("../key/test.pem", "*.kuangstudy.com")

// 连接到server端,此处禁用安全传输,没有加密和验证
// conn, err := grpc.Dial("localhost:9090", grpc.WithTransportCredentials(insecure.NewCredentials()))
conn, err := grpc.Dial("localhost:9090", grpc.WithTransportCredentials(creds)) // 修改
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()

// 建立连接
client := pb.NewSayHelloClient(conn)
// 执行rpc调用(这个方法在服务端来实现并返回结果)
resp, err := client.SayHello(context.Background(), &pb.HelloRequest{RequestName: "xxx"})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
fmt.Println(resp.GetResponseMsg())
}




7. 自定义token认证(pending)