这是我参与「第五届青训营 」伴学笔记创作活动的第 6 天
1.1 安装protobuf
Protocol buffers,通常称为Protobuf,是Google开发的一种协议,用于允许对结构化数据进行序列化和反序列化。它在开发程序以通过网络相互通信或存储数据时很有用。谷歌开发它的目的是提供一种比XML更好的方式来通信。
【安装步骤】
- 先安装protoc的编译器(直接linux下
sudo apt install protobuf-compiler) - 对应项目安装gRPC核心库
go get google.golang. org/grpc - 安装配合编译器的各种语言的代码生成工具。
go insta1l google.golang.org/protobuf/cmd/protoc-gen-go@latest2
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
//两个都没报错就是引入成功了
因为这些文件在安装grpc的时候,已经下载下来了,因此使用instal1命令就可以了,而不需要使用get命令。
1.2 Proto文件编写
- 首先,我们模拟建立两个文件夹--服务端和客户端,随后我们要创建好关于接口的文件夹proto来存放我们以
.proto结尾的接口文件(这个文件相当于一个接口,在客户端和服务端都要求一样的---届时服务端实现这个文件的接口,客户端调用这个文件的接口,因此本身是统一的),我们先看一下服务端的端口文件怎么写如下:
//语法声明--约定使用的是proto3语法
syntax = "proto3";
//因为这个是proto语法,肯定没法被调用,因此最终是要生成某个语言版本的,以go为例:
//这部分的内容是关于最后生成的go文件是处在哪个目录哪个包中,
//.代表在当前目录生成,service代表了生成的go文件的包名是service(分号隔开)
option go_package = ".;service" ;
//然后我们需要定义一个服务,在这个服务中需要有一个方法,这个方法可以接受客户端的参数,再返回服务端的响应。
//其实很容易可以看出,我们定义了一个service,称为SayHello,这个服务中有一个rpc方法,名为SayHello。
//这个方法会发送一个HelloRequest,然后返回一个HelloResponse。
service SayHello {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
//此处生成的接口代码作为客户端与服务端的约定,服务端必须实现定义的所有接口方法,客户端直接调用同名方法向服务端发起请求。
//上面三行是方法,下面六行是结构体(调用参数的)
// HelloRequest 请求数据格式,message对应golang中的struct
//这里比较特别的是变量后面的"“赋值"。注意,这里并不是赋值,而是在定义这个变量在这个message中的位置。
message HelloRequest {
string name = 1;
//int64 age = 2;这代表第二个参数为age
}
// HelloReply 响应数据格式
message HelloReply {
string message = 1;
}
- 我们要去将写的接口文件编译一下:
//生成go(当然也可以生成其他的语言代码 )的相关文件---第一个参数是在哪里生成,第二个参数是编译什么文件
protoc --go_out=. he11o.proto
//生成grpc的相关文件
protoc --go-grpc_out=. he1lo. proto
- 这个时候,我们去同文件夹下的
hello_grpc.pb.go下面去实现自动生成的go中的func里面的SayHello()函数即可 - 同理去处理客户端的接口文件即可。
1.3 Proto文件的具体介绍
message
message : protobuf 中定义一个消息类型式是通过关键字message字段指定的。消息就是需要传输的数据格式的定义,常用于传入传出参数定义。
message 关键字类似于C++中的class,JAVA中的class,go中的struct
在消息中承载的数据分别对应于每一个字段,其中每个字段都有一个名字和一种类型
一个proto文件中可以定义多个消息类型
字段规则
required:消息体中必填字段,不设置会导致编码异常。在protobuf2中使用,在protobuf3中被删去
optional:消息体中可选字段。protobuf3没有了required,optional等说明关键字,都默认为optional(这个就是message里面的int64、string等)
repeate:消息体中可重复字段,重复的值的顺序会被保留在go中,从而被定义为切片。例如repeated会被定义为name []string
信息号
在消息体的定义中,每个字段都必须要有一个唯一的标识号,标识号是[1,2^{29-1}]范围内的一个整数。--这个就是name = 1里面的1
嵌套消息(就是嵌套结构体)
可以在其他消息类型中定义、使用消息类型,在下面的例子中,person消息就定义在PersonInfo消息内如
message PersonInfo{
message Person{
string name = 1;
int32 height = 2;
repeated int32 weight = 3;
}
repeated Person info = 1;
}
如果要在它的父消息类型的外部重用这个消息类型,需要PersonInfo.Person的形式使用它,如:
message PersonMessage{
PersonInfo.Person info = 1;
}
服务定义(就是一个个函数接口)
如果想要将消息类型用在RPC系统中,可以在.proto文件中定义一个RPC服务接口,protocol buffer编译器将会根据所选择的不同语言生成服务接口代码及存根。
service 服务名{
# rpc服务函数名(参数)返回(返回参数)
rpc 函数名(SearchRequest) returns (SearchResponse)
rpc 函数名(SearchRequest) returns (SearchResponse)//可以搞好多
}
上述代表表示,定义了一个RPC服务,该方法接受SearchRequest返回SearchResponse
1.4 服务端和客户端具体实现
服务端编写:
- 创建gRPC Server对象,你可以理解为它是Server端的抽象对象
- 将server (其包含需要被调用的服务端接口)注册到gRPC Server的内部注册中心。这样可以在接受到请求时,通过内部的服务发现,发现该服务端接口并转接进行逻辑处理
- 创建Listen,监听TCP端口
- gRPC Server开始lis.Accept,直到Stop
客户端编写:
- 创建与给定目标(服务端)的连接交互
- 创建server的客户端对象
- 发送RPC请求,等待同步响应,得到回调后返回响应结果
- 输出响应结果
服务端如何写实现接口
直接重写protobuf实现的grpc的go文件里面的方法即可,例如:在hello_grpc.pb.go中生成了一个对象成员函数SayHello(我们定义的rpc服务函数),我们找到这个对象的名字,例如自动生成的名字为UnimplementedSayHelloServer:
-
在服务端文件夹下的
main.go文件中重写方法,首先import引入那个存放protobuf文件的文件夹,随后我们取到那个对象类型(可以取个别名,如图中server),例如import ( "context" pb "xuexiangban_go/xxb-grpc-study/hello-server/proto"//此处pb为别名 ) // hello server 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() { // 开启端口 listen,_ := net.Listen( network: "tcp" , address: ":9090") // 创建grpc服务 grpcServer := grpc.NewServer() // 在grpc服务端中去注册我们自己编写的服务(已经注册好了,我们直接调用即可) //第一个参数不多说了,第二个参数一定要引号加起来,这样通过server就能找到一个具体的实现了 pb.RegisterSayHelloServer(grpcServer ,&server{}) //启动服务 err := grpcServer.Serve(listen) if err != nil { fmt.Printf( format: "failed to serve: %v", err) return } }
客户端如何写实现接口?
这里直接去写业务代码进行连接即可,我们下面以客户端文件夹下的main.go为例。
package main
import (
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"log"
//下面这个切记,是接口文件,所以说两个端都需要有的
pb "xuexiangban_go/xxb-grpc-study/hello-server/proto"
)
func main() {
//第一个参数是服务器的地址和端口(这里先默认无加密方式,后面再讲)
//step1.连接到server端,此处禁用安全传输,没有加密和验证
conn, err := grpc.Dial("127.0.0.1:9090",grpc.WithTransportCredentials(insecure.NewCredentials()))
//下面是defer两件套
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
//step2.创建客户端与服务端建立连接--具体调用的函数名和对象都参考服务器端的前两步--先编译好protobuf,随后找自动生成的名字
client := pb.NewSayHelloclient(conn)
//step3.随后按自动生成的格式,执行rpc调用(这个方法在服务端实现并返回结果)
//里面就一个参数,直接指定即可
resp, _ := client.SayHello(context.Background(), &pb.HelloRequest{RequestName : "world"})
//step4.获得回应的信息
fmt.Println(resp.GetResponseMsg())
}
1.5 认证-安全传输
gRPC是一个典型的C/S模型,需要开发客户端和服务端,客户端与服务端需要达成协议,使用某一个确认的传输协议来传输数据,gRPC通常默认是使用protobuf来作为传输协议,当然也是可以使用其他自定义的。
此处的认证,不是用户的身份认证而是指多个server和多个client之间,如何识别对方是谁,并且可以安全的进行数据传输
- SSL/TLS认证方式(采用http2协议)
- 基于Token的认证方式(基于安全连接)
- 不采用任何措施的连接,这是不安全的连接(默认采用http1)
- 自定义的身份认证
TLS (Transport Layer Security,安全传输层),TLS是建立在传输层TCP协议之上的协议,服务于应用层,它的前身是SSL(Secure Socket Layer,安全套接字层),它实现了将应用层的报文进行加密后再交由TCP进行传输的功能。
TLS协议主要解决如下三个网络安全问题:
- 保密(message privacy),保密通过加密encryption实现,所有信息都加密传输,第三方无法嗅探;
- 完整性(message integrity),通过MAC校验机制,一旦被篡改,通信双方会立刻发现;
- 认证(mutual authentication),双方认证,双方都可以配备证书,防止身份被冒充;
生产环境可以购买证书或者使用一些平台发放的免费证书
key:服务器上的私钥文件,用于对发送给客户端数据的加密,以及对从客户端接收到数据的解密。
csr:证书签名请求文件,用于提交给证书颁发机构(CA)对证书签名。
crt由证书颁发机构(CA)签名后的证书,或者是开发者自签名的证书,包含证书持有人的信息,持有人的公钥,以及签署者的签名等信息。
pem:是基于Base64编码的证书格式,扩展名包括PEM、CRT和CER。
SSL/TLS认证方式
首先通过openssl生成证书和私钥:
- 首先点击此处下载exe安装包
- 我们使用便捷版安装包,一直下一步即可
- 配置环境变量D:\EnvironmentlOpenSSL-Win64lbin
- 命令行测试openssl
step1 生成证书
专门建立一个文件夹为key,进入该文件夹下进行下面命令行输入,此时会生成一个.crt、.key文件以及.csr文件
# 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 (fu11 name)[Some-state] : GuangDong
#城市名称
Locality Name (eg, city) [] :Meizhou1
#公司组织名称
Organization Name (eg,company)[Internet widgits Pty Ltd] : Xuexiangban
#部门名称
Organizational unit Name (eg,section) []:go
#服务器or网站名称
Common Name (e.g. server FQDN or YOUR name)[]:kuangstudy
# 邮件
Email Address []:24736743qq.com
#3、生成csr
22openss1 req -new -key server.key -out server.csr
step2 更改openssl.cnf (Linux是openssl.cfg),设置只有特定域名才能访问该服务器,保持服务端的健壮性
#1)复制一份你安装的openssl的bin目录里面的openss1.cnf 文件到你项目所在的key目录(存有刚刚生成的三个文件)
# ---下面的查找动作都是打开复制好的.cfg文件,用ctrl+F来查找语句
#2)找到[ CA_default ],打开 copy_extensions = copy(就是把前面的#去掉)
#3〉找到[ req ],打开req_extensions = v3_req # The extensions to add to a certificate request
#4)找到[ v3_req ],在下面行中添加subjectAltName = @alt_names
#5)添加新的标签[ alt_names ](就是在[]和[]之间加进去),和标签字段DNS.1 = *.angryhei.com
#这些标签字段很重要,可以添加多个。这个代表以后只有通过这个域名去访问你的代码,你才会同意--客户端请求时,必须携带上这个域名才可以(如果只有一个*就是谁都可以访问到)
step3 生成自己的私钥
#生成证书私钥test.key
openssl genpkey -algorithm RSA -out test.key
#通过私钥test.key生成证书请求文件test.csr(注意cfg和cnf)
openssl req -new -nodes -key test.key -out test.csr -days 3650 -subj "/C=cn/OU=myorg/0=mycomp/CN=myname" -config ./openssl.cnf -extensions v3_req
#test.csr是上面生成的证书请求文件。ca.crt/server.key是CA证书文件和key,用来对test.csr进行签名认证。这两个文件在第一部分生成。
#生成SAN证书pem
openssl x509 -req -days 365 -in test.csr -out test.pem -CA server.crt -CAkey server.key -CAcreateserial -extfile ./openssl.cnf -extensions v3_req
最后生成的7个文件中,我们仅需要.key和.pem,其他的只是认证用的。
服务器的main.go文件进行加密监听
func main() {
//grpc提供给我们的现成函数,我们这里选择用文件(另一种在Token认证提到)
//这里的第一个参数是生成.pem的文件的绝对路径
//这里的第二个参数是生成.key的文件的绝对路径
creds, _ := credentials.NewServerTLSFromFile("xxx", "xxx")//step1
// 开启端口
listen,_ := net.Listen( network: "tcp" , address: ":9090")
// 创建grpc服务--此处加入证书即可
grpcServer := grpc.NewServer(grpc.Creds(creds))//step2在这里加入
pb.RegisterSayHelloServer(grpcServer ,&server{})
err := grpcServer.Serve(listen)
if err != nil {
fmt.Printf( format: "failed to serve: %v", err)
return
}
}
客户端的main.go进行加密调用
func main() {
//客户端只需要有证书.pem即可,是不需要带有私钥的,私钥只放在服务端
//这里的第一个参数是与服务端一样的.pem的文件的绝对路径
//这里的第二个参数是浏览器中获得的域名(要想访问就应该是第二步中加入的域名,这里为了图方便,直接就填写上了,但如果实际上线,这个地方应该是个变量--是由浏览器url中提取的)--有个域的概念,不是随便网址都可以通过该客户端去使用函数
creds,_ := credentials.NewClientTLSFromFile("", "*.angryhei.com")//step1
//连接到server端
conn, err := grpc.Dial("127.0.0.1:9090",grpc.WithTransportCredentials(creds))//step2
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
//建立连接
client := pb.NewSayHelloclient(conn)
//执行rpc调用
resp, _ := client.SayHello(context.Background(), &pb.HelloRequest{RequestName : "world"})
fmt.Println(resp.GetResponseMsg())
}
Token认证
我们先看一个gRPC提供我们的一个接口,这个接口中有两个方法,接口位于credentials包下,这个接口需要客户端来实现
type PerRPCCredentials interface {
GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
RequireTransportsecurity() bool
}
第一个方法作用是获取元数据信息,也就是客户端提供的key,value对,context用于控制超时和取消,uri是请求入口处uri
第二个方法的作用是否需要基于TLS认证进行安全传输,如果返回值是true,则必须加上TLS验证,返回值是false则不用。---也就是说两种认证方法结合的话,只需要把第二个地方改成True即可,这里只将如何使用第一种。
step1先看客户端
import (
"..."
"google.golang.org/grpc/credentials" //先引入
"..."
)
//首先我们定义一个结构体,这是客户端自己定义的Token认证
//随后定义一下开始提到的接口中的两个方法,去实现就可以了
//step1
type ClientTokenAuth struct {
}
//此处就是定义的Token,就是kv嘛,随便自己设置
func (c ClientTokenAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
//直接要return什么,就return即可
return map[string]string {
"appId" : "angryhei",
"appKey": "123123",
}, nil
}
func (c ClientTokenAuth) RequireTransportsecurity() bool {
return false//代表只是用Token认证
}
func main() {
//因为我们要传递多个东西给grpc.WithTransportCredentials,因此我们搞个切片慢慢放
var opts []grpc. DialOption
//1. 这里是先填入了是否使用TLS,因为这里我用第二种方法返回false,因此这里直接填这个,否则填的东西是跟前面的那个认证一样的
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
//2. 这里将写好接口的结构体传入进去
opts = append(opts, grpc.withPerRPCCredentials(new(ClientTokenAuth)))
//连接到server端
conn, err := grpc.Dial("127.0.0.1:9090",grpc.WithTransportCredentials(opts))//step2
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
//建立连接
client := pb.NewSayHelloclient(conn)
//执行rpc调用
resp, _ := client.SayHello(context.Background(), &pb.HelloRequest{RequestName : "world"})
fmt.Println(resp.GetResponseMsg())
}
2再看服务端
因为Token是类似一个数据发过去的,因此我们可以在当时在重写的那个接口业务代码中取出元数据来:
func (s *server) SayHello(ctx context.Context , req *pb.HelloRequest) (*pb.HelloResponse,error) {
//step1 获取元数据--服务器捕捉到的都会存到这个context中
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, errors.New("未传输token")
}
//step2 处理元数据
var appId string
var appKey string
if v ,ok := md ["appid"]; ok {
appId = v[0]
}
if v ,ok := md ["appkey"]; ok {
appKey = v[0]
}
//step3 进行校验
if appId ! = "kuangshen" ll appKey != "123123" {
return nil,errors.New( text: "token不正确")
}
return &pb.HelloResponse{ResponseMsg: "hello" + req.RequestName},nil
}
【总结】
- 客户端定义Token接口的方法,随后丢到启动类中即可
- 服务器端就直接在接口函数上现场判断即可
gRPC将各种认证方式浓缩统一到一个凭证(credentials)上,可以单独使用一种凭证,比如只使用TLS凭证或者只使用自定义凭证,也可以多种凭证组合,gRPC提供统一的AP险证机制,使研发人员使用方便,这也是gRPC设计的巧妙之处