GRPC框架学习 | 青训营笔记

198 阅读15分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 6 天

1.1 安装protobuf

Protocol buffers,通常称为Protobuf,是Google开发的一种协议,用于允许对结构化数据进行序列化和反序列化。它在开发程序以通过网络相互通信或存储数据时很有用。谷歌开发它的目的是提供一种比XML更好的方式来通信。

【安装步骤】

  1. 先安装protoc的编译器(直接linux下sudo apt install protobuf-compiler)
  2. 对应项目安装gRPC核心库go get google.golang. org/grpc
  3. 安装配合编译器的各种语言的代码生成工具。
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文件编写

  1. 首先,我们模拟建立两个文件夹--服务端和客户端,随后我们要创建好关于接口的文件夹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;
}
  1. 我们要去将写的接口文件编译一下:
//生成go(当然也可以生成其他的语言代码 )的相关文件---第一个参数是在哪里生成,第二个参数是编译什么文件
protoc --go_out=. he11o.proto
//生成grpc的相关文件
protoc --go-grpc_out=. he1lo. proto
  1. 这个时候,我们去同文件夹下的hello_grpc.pb.go下面去实现自动生成的go中的func里面的SayHello()函数即可
  2. 同理去处理客户端的接口文件即可。

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生成证书和私钥:

  1. 首先点击此处下载exe安装包
  2. 我们使用便捷版安装包,一直下一步即可
  3. 配置环境变量D:\EnvironmentlOpenSSL-Win64lbin
  4. 命令行测试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)复制一份你安装的opensslbin目录里面的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设计的巧妙之处