当前位置:   article > 正文

Go微服务(三)——gRPC详细入门_grpc microservices in go

grpc microservices in go

篇幅可能较长,可以先收藏,方便后续观看。

文章名称地址
Go微服务(一)——RPC详细入门前往
Go微服务(二)——Protobuf详细入门前往
Go微服务(三)——gRPC详细入门前往

GRPC入门

这里会联合protobuf语法以及protobuf如何去定义rpc服务,前面我们只生成了结构体,现在我们要让他为我们同时把接口生成,有了响应的接口,我们就再也不用去手写接口了。

1. gRPC技术介绍

1.1 什么是gRPC

gRPC是什么可以用官网的一句话来概括:

A high-performance, open-source universal RPC framework

一个高性能、开源的通用 RPC 框架

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oR068HIV-1657464088026)(images/image-20220702153051734.png)]

1.2 为什么我们要用gRPC

视频里的:

  • 跨语言的RPC技术:

    主流语言都支持。

  • 基于Protobuf 消息格式:

    高效二进制协议。

  • 基于HTTP2协议通讯:

    • 丰富的周边生态(网关支持/SSL)
    • 单一场链接避免了多TCP连接开销。
    • 服务端推动Sever Push
    • 头部压缩和二进制格式。

Protobuf ON HTTP2:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XD2yfSHD-1657464088027)(images/image-20220702152941585.png)]

博客里的:

  • 生态好:背靠Google。还有比如nginx也对grpc提供了支持。
  • 跨语言:跨语言,主流语言都支持,且自动生成sdk
  • 性能高:比如protobuf性能高过json, 比如http2.0性能高过http1.1
  • 强类型:编译器就给你解决了很大一部分问题
  • 流式处理(基于http2.0):支持客户端流式,服务端流式,双向流式

1.3 gRPC有什么好处以及在什么场景下需要用gRPC

既然是 server/client 模型,那么我们直接用 Restful API 不是也可以满足吗,为什么还需要RPC呢?下面我们就来看看RPC到底有哪些优势:

1.3.1 gRPC vs. Restful API

gRPC和restful API都提供了一套通信机制,用于server/client模型通信,而且它们都使用http作为底层的传输协议(严格地说, gRPC使用的http2.0,而restful api则不一定)。不过gRPC还是有些特有的优势,如下:

  1. gRPC可以通过protobuf来定义接口,从而可以有更加严格的接口约束条件

    另外,通过protobuf可以将数据序列化为二进制编码,这会大幅减少需要传输的数据量,从而大幅提高性能。

  2. gRPC可以方便地支持流式通信(理论上通过http2.0就可以使用streaming模式, 但是通常web服务的restful api似乎很少这么用,通常的流式数据应用如视频流,一般都会使用专门的协议如HLS,RTMP等,这些就不是我们通常web服务了,而是有专门的服务器应用。)

1.3.2 使用场景
  1. 需要对接口进行严格约束的情况,比如我们提供了一个公共的服务,很多人,甚至公司外部的人也可以访问这个服务,这时对于接口我们希望有更加严格的约束,我们不希望客户端给我们传递任意的数据,尤其是考虑到安全性的因素,我们通常需要对接口进行更加严格的约束。这时gRPC就可以通过protobuf来提供严格的接口约束。
  2. 对于性能有更高的要求时。有时我们的服务需要传递大量的数据,而又希望不影响我们的性能,这个时候也可以考虑gRPC服务,因为通过protobuf我们可以将数据压缩编码转化为二进制格式,通常传递的数据量要小得多,而且通过http2我们可以实现异步的请求,从而大大提高了通信效率。

但是,通常我们不会去单独使用gRPC,而是将gRPC作为一个部件进行使用,这是因为在生产环境,我们面对大并发的情况下,需要使用分布式系统来去处理,而gRPC并没有提供分布式系统相关的一些必要组件。而且,真正的线上服务还需要提供包括负载均衡,限流熔断,监控报警,服务注册和发现等等必要的组件。

1.4 gRPC 的优点是怎么实现的(可跳过)

  1. grpc性能高:protobuf为什么比json性能高?

    • Protobuf是由Google开发的二进制格式,用于在不同服务之间序列化数据。是一种IDL(interface description language)语言;

    • 他比json快六倍;

      • 为什么protobuf比json快?

        protobuf的二进制数据流:

        [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ozPcl0c6-1657464088028)(images/image-20220701180533004.png)]

        json数据流:

        {
            "content":"test",
            "user":"test",
            "user_id":"test"
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5

        对比json数据和protobuf数据格式可以知道:

        1. 体积小-无需分隔符:TLV存储方式不需要分隔符(逗号,双引号等)就能分隔字段,减少了分隔符的使用
        2. 体积小-空字段省略:若字段没有被设置字段值,那么该字段序列化时的数据是完全不存在的,即不需要进行编码,而json会传key和空值的value
        3. 体积小-tag二进制表示:是用字段的数字值然后转换成二进制进行表示的,比json的key用字符串表示更加省空间。
        4. 编解码快:tag的里面存储了字段的类型,可以直接知道value的长度,或者当value是字符串的时候,则用length存储了长度,可以直接从length后取n个字节就是value的值,而如果不知道value的长度,我们就必须要做字符串匹配。
        5. 细化了解protobuf的编码可以去看:varint 和 zigzag编码方式
  2. grpc性能高:http2.0为什么比http1.1性能高?

    1. 多路复用

      http2.0和http 1.* 还有 http1.1pipling的对比:

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eZ51v5BW-1657464088029)(images/image-20220701222830651.png)]

      1. http/1.*:一次请求,一个响应,建立一个连接用完关闭,每一个请求都要建立一个连接;
      2. http1.1 pipeling:Pipeling解决方式为,若干个请求排队串行化单线程处理,后面的请求等待前面请求的返回才能获得执行机会,一旦有某请求超时等,后续请求只能被阻塞,毫无办法,也就是人们常说的线头阻塞 ;
      3. http2:多个请求可同时在一个连接上并行执行。某个请求任务耗时严重,不会影响到其它连接的正常执行;

      grpc 多路复用还有哪些优点

      • 减少了tcp的连接,降低了服务端和客户端对于内存,cpu等的压力
      • 减少了tcp的连接,保证了不频繁触发tcp重新建立,这样就不会频繁有慢启动
      • 减少了tcp的连接,使网络拥塞情况得以改善

      为什么http/1.1不能实现多路复用而http2.0可以?

      因为http/1.1传输是用的文本,而http2.0用的是二进制分帧传输

    2. 头部压缩

      • 固定字段压缩:http可以通过http对body进行gzip压缩,这样可以节省带宽,但是报文中header也有很多字段没有进行压缩,比如cookie, user agent accept,这些有必要进行压缩
      • 避免重复:大量请求和响应的报文里面又很多字段值是重复的,所以有必要避免重复性
      • 编码改进:字段是ascii编码,效率低,改成二进制编码可以提高
      • 以上通过HPACK算法来进行实现,算法主要包含三个部分:
        • 静态字典:将常用的header字段整成字典,比如{“method”:“GET”} 就可以用单个数字 2来表示;
        • 动态字典:没有在静态字典里面的一些头部字段,则用动态字典
        • Huffman 编码: 压缩编码
    3. 二进制分帧

      • 在二进制分帧层上,HTTP 2.0 会将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码 ,其中HTTP1.x的首部信息会被封装到Headers帧,而我们的request body则封装到Data帧里面。
      • 这样分帧以后这些帧就可以乱序发送,然后根据每个帧首部的流标识符号进行组装。
      • 对比http/1.1因为是基于文本以换行符分割每一条key:value则会有以下问题:
        • 一次只能处理一个请求或者响应,因为这种以分隔符分割消息的数据,在完成之前不能停止解析。
        • 解析这种数据无法预知需要多少内存,会给服务端有很大压力。
    4. 服务器主动推送资源

      由于支持服务器主动推送资源,则可以省去一部分请求。

      比如你需要两个文件1.html,1.css,如果是http1.0则需要请求两次,服务端返回两次。但是http2.0则可以客户端请求一次,然后服务端直接回吐两次。

2. gRPC Hello World

2.1 插件安装:

# protoc-gen-go 插件之前在protobuf教程中已经安装
# 
# go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

# 安装protoc-gen-go-grpc插件
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qVw2iMTn-1657464088031)(images/image-20220702153507003.png)]

查看当前grpc插件的版本:

protoc-gen-go-grpc --version

protoc-gen-go-grpc 1.2.0
  • 1
  • 2
  • 3

2.2 编写.proto文件

./service/service.proto

syntax = "proto3";
package hello;
option go_package = "MicroServiceStudy01/08-grpc/service";

message Request{
  string value = 1;
}
message Response{
  string value = 1;
}

// The HelloService service definition
// service 关键字
// HelloService 服务名称 对应接口的名称
// service服务会对应.pb.go文件里interface,里面的rpc对应接口中的函数
service HelloService{
  rpc Hello (Request) returns (Response){}
  rpc Channel(stream Request) returns (stream Response) {}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

生成代码:

# D:\code\GoLandProjects\src\MicroServiceStudy01\
protoc -I . \
--go_out=./08-grpc/service \
--go_opt=module="MicroServiceStudy01/08-grpc/service" \
-go-grpc_out=./08-grpc/service \
--go-grpc_opt=module="MicroServiceStudy01/08-grpc/service" \
./08-grpc/service/service.proto
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在这里插入图片描述

--go_out=......:proto-gen-go 插件编译产物的存放目录

--go_opt=......:protoc—gen-go 插件的opt参数,采用go moudle模式.

--go-grpc_out=.....:proto-gen-go-grpc 插件编译产物的存放目录

--go-grpc_opt=......:proto-gen-go-grpc 插件的opt参数,采用go moudle模式.

可能大家会感觉生成代码的命令内容比较多,后面会有有一些小技巧,真正去做代码生成的时候,很少会写这么多,这是现在的临时性工作。

我们可以看到,除了之前我们见过的.pb.go文件,还多了个_grpc.pb.go文件,里面是我们proto文件里定义的service对应生成的代码。

生成的客户端代码:

...
// HelloServiceClient是HelloService服务的客户端API。
type HelloServiceClient interface {
	Hello(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error)
	Channel(ctx context.Context, opts ...grpc.CallOption) (HelloService_ChannelClient, error)
}
...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

生成的服务端代码:

...
// HelloServiceServer 是 HelloService 服务的服务端 API。
// 所有实现都必须嵌入 UnimplementedHelloServiceServer
// 为了向前兼容
type HelloServiceServer interface {
	Hello(context.Context, *Request) (*Response, error)
	Channel(HelloService_ChannelServer) error
	mustEmbedUnimplementedHelloServiceServer()
}
...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

我们在生成的service_grpc.pb.go文件中要注意一个部分:

// UnimplementedHelloServiceServer 必须嵌入到向前兼容的实现中。
type UnimplementedHelloServiceServer struct {
}

func (UnimplementedHelloServiceServer) Hello(context.Context, *Request) (*Response, error) {
	return nil, status.Errorf(codes.Unimplemented, "method Hello not implemented")
}
func (UnimplementedHelloServiceServer) Channel(HelloService_ChannelServer) error {
	return status.Errorf(codes.Unimplemented, "method Channel not implemented")
}
func (UnimplementedHelloServiceServer) mustEmbedUnimplementedHelloServiceServer() {}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

生成的这个文件里有一个叫做UnimplementedHelloServiceServer的对象,这个对象它是实现了我们的HelloSerivce接口,但是它实现了过后给你反的是一个错误信息,如果你没有任何对象实现这个接口,而你又调用了这个服务的话,它会给你返回一个return status.Errorf(codes.Unimplemented, "method Channel not implemented")这样的错误信息。

2.3 server服务端

./server/server.go

type HelloService struct {
    // UnimplementedHelloServiceServer这个结构体是必须要内嵌进来的
    // 也就是说我们定义的这个结构体对象必须继承UnimplementedHelloServiceServer。
    // 嵌入之后,我们就已经实现了GRPC这个服务的接口,但是实现之后我们什么都没做,没有写自己的业务逻辑,
    // 我们要重写实现的这个接口里的函数,这样才能提供一个真正的rpc的能力。
	service.UnimplementedHelloServiceServer
}

var _ service.HelloServiceServer = (*HelloService)(nil)

// Hello 重写实现的接口里的Hello函数
func (p *HelloService) Hello(ctx context.Context, req *service.Request) (*service.Response, error){
	resp := &service.Response{}
	resp.Value = "hello:" + req.Value
	return resp, nil
}

func main(){
	// 首先通过grpc.NewServer() 构造一个grpc服务对象
	grpcServer:=grpc.NewServer()
	// 然后通过grpc插件生成的RegisterHelloServiceServer函数注册我们实现的HelloService服务。
	service.RegisterHelloServiceServer(grpcServer,new(HelloService))

	listen,err:=net.Listen("tcp",":1234")
	if err!=nil{
		log.Fatal("Listen TCP err:", err)
	}
	//最后通过grpcServer.Serve(listen) 在一个监听端口上提供gRPC服务
	grpcServer.Serve(listen)
}
  • 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

注意两点:

  1. 内嵌:

    Unimplementedxxx这个结构体是必须要内嵌进来的,也就是说我们定义的这个结构体对象必须继承Unimplementedxxx

    嵌入之后,我们就已经实现了GRPC这个服务的接口,但是实现之后我们什么都没做,没有写自己的业务逻辑,我们要重写实现的这个接口里的函数,这样才能提供一个真正的rpc的能力。

  2. 重写

2.4 client客户端

client/client.go

func main() {
	// grpc.Dial负责和gRPC服务建立连接
	conn, err := grpc.Dial("localhost:1234", grpc.WithInsecure())
	// 这里会提示,WithInsecure已被弃用,
    // 如果你不想继续使用WithInsecure,可以使用
    // 函数insecure.NewCredentials()返回credentials.TransportCredentials的一个实现。
	// 您可以将其作为DialOption与grpc.WithTransportCredentials一起使用:
	// 但是,API标记为实验性的,因此即使他们已经添加了弃用警告,您也不必立即切换。
	//conn, err := grpc.Dial("localhost:1234",grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatal("Dial err: ", err)
	}
	defer conn.Close()

	// NewHelloServiceClient函数是xxx_grpc.pb.go中自动生成的函数,
	// 基于已经建立的连接构造HelloServiceClient对象,
	// 返回的client其实是一个HelloServiceClient接口对象
	//
	client := service.NewHelloServiceClient(conn)

	// 通过接口定义的方法就可以调用服务端对应gRPC服务提供的方法
	req := &service.Request{Value: "小亮"}
	reply, err := client.Hello(context.Background(), req)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(reply.GetValue())
}
  • 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
  1. client := service.NewHelloServiceClient(conn)

    func NewHelloServiceClient(cc grpc.ClientConnInterface) HelloServiceClient {
    	return &helloServiceClient{cc}
    }
    
    • 1
    • 2
    • 3

    NewHelloServiceClient函数是xxx_grpc.pb.go中自动生成的函数,基于已经建立的连接构造HelloServiceClient对象,返回的client其实是一个HelloServiceClient接口对象。

    需要的参数是grpc拨号后放回的连接对象。

  2. context.Background()

    返回一个非nil的空Context。它永远不会被取消,没有值,并且没有截止日期。它通常由主函数使用,初始化和测试,并作为传入的顶级上下文请求。

3. gRPC Stream

这一部分我们本次不作为重点内容,暂时简单过一遍。

3.1 为什么需要Stream RPC

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dbF2AbZL-1657464088032)(images/image-20220710171022803.png)]

  1. 文件上传下载:比如你上传的文件有500个G,你不可能全部存在内存当中,你只有通过流的方式,服务端发一点,客户端接收一点,以这样的方式进行,才不会把服务端的资源给吃满;
  2. web端可能有一些WebSockets方面的需求,那么对应webSockets的后端就是stream的一套rpc来进行支撑;
  3. heartbeat心跳:就比如你的客户端想要上报自己的心跳,那么你可以直接用stream rpc来建立一套,流式接口类似于tcp的一套接口,然后客户端不断的在接口里发送心跳的数据,而不是发一次request,接收一次response这样的方式;你连上之后可以不停的发,服务端也可以不响应你,当你什么时候合适了,服务端会响应你一下数据,因此我们把这种叫做stream模式;

3.2 gRPC Stream rpc定义

09-grpc-stream/service/service.proto

syntax = "proto3";
package hello;
option go_package = "MicroServiceStudy01/09-grpc-stream/service";

message Request{
  string value = 1;
}

message Response{
  string value = 1;
}

// The HelloService service definition
// service 关键字
// HelloService 服务名称 对应接口的名称
// service服务会对应.pb.go文件里interface,里面的rpc对应接口中的函数
service HelloService{
  rpc Hello (Request) returns (Response){}
  
  rpc Channel(stream Request) returns (stream Response) {}

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

所以定义streaming RPC的语法如下:

rpc <function_name> (stream <type>) returns (stream <type>) {}
  • 1

关键点:

  1. stream关键字:

    这个关键字可以出现在请求上面(stream Request),也可以出现在响应上(stream Response)

    • 如果出现在请求上,就代表是client客户端的一个stream的模式,你的客户端就可以进行一些流模式的上传,比如你有个500G的文件,你的客户端此时就可以把文件读1MB出来发送,然后再读1MB发送到server端;
    • 如果出现在服务端,就代表server服务端的一个stream的模式,你的服务端就可以进行一些流模式的下载,你的服务端可以1MB、1MB的在这个数据流里发送;

3.3 生成 Stream RPC代码

cd ./09-grpc-stream/service

protoc -I . --go_out=. --go_opt=module="MicroServiceStudy01/09-grpc-stream/service" --go-grpc_out=. --go-grpc_opt=module="MicroServiceStudy01/09-grpc-stream/service" ./service.proto
  • 1
  • 2
  • 3

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h804CMzT-1657464088034)(images/image-20220710182022423.png)]

可以看到,流式上传,他的客户端是没有参数的,只有一些选项和我们的上下文信息,会返回一个HelloService_ChannelClient类型的返回值(就是管道),可以用于和服务端进行双向通信。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-he0FvSZT-1657464088035)(images/image-20220710182101154.png)]

可以看到在服务端的Channel方法参数是一个新的HelloService_ChannelServer类型 的参数(就是管道),可以用于和客户端双向通信;

3.4 Stream RPC 接口解读

HelloService_ChannelClientHelloService_ChannelServer接口定义:

// Request	----->
// Response	<-----
type HelloService_ChannelClient interface {
	Send(*Request) error
	Recv() (*Response, error)
	grpc.ClientStream
}

// Request	<-----
// Response	----->
type HelloService_ChannelServer interface {
	Send(*Response) error
	Recv() (*Request, error)
	grpc.ServerStream
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

可以发现服务端和客户端的流辅助接口 均定义了Send和Recv方法 用于流数据的双向通信。

3.5 Stream 服务端

09-grpc-stream/server/server.proto

server端的核心逻辑:

  • 接收一个Request
  • 发送一个Response
type HelloService struct {
	service.UnimplementedHelloServiceServer
}

var _ service.HelloServiceServer = (*HelloService)(nil)

func (p *HelloService) Channel(stream service.HelloService_ChannelServer) error {
	// 服务端在循环中接收客户端发来的数据
	for {
		args, err := stream.Recv()
		if err != nil {
			// 如果遇到io.EOF表示客户端流关闭
			if err == io.EOF {
				return nil
			}
			return err
		}

		// 响应一个请求
		// 生成返回的数据通过流发送给客户端
		resp := &service.Response{
			Value: "hello," + args.GetValue(),
		}
		err = stream.Send(resp)
		if err != nil {
			// 服务端发送异常,函数退出,服务端流关闭
			return err
		}
	}
}



func main() {
	// 1. 构造一个gRPC服务对象
	grpcServer:=grpc.NewServer()
	// 2. 通过gRPC插件生成的RegisterHelloServiceServer 函数注册我们实现的HelloService服务。
	service.RegisterHelloServiceServer(grpcServer,new(HelloService))
	// 3. 监听:1234端口
	listen,err:=net.Listen("tcp",":1234")
	if err!=nil{
		log.Fatal("Listen TCP err:", err)
	}
	// 4. 通过grpcServer.Serve(listen) 在一个监听端口上提供gRPC服务
	grpcServer.Serve(listen)
}

  • 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
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

3.6 Stream 客户端

09-grpc-stream/client/client.proto

func main() {
	conn, err := grpc.Dial("localhost:1234", grpc.WithInsecure())
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	client := service.NewHelloServiceClient(conn)

	// 客户端先调用Channel方法,获取返回的流对象

	stream, err01 := client.Channel(context.Background())
	if err01 != nil {
		log.Fatal(err01)
	}

	// 在客户端我们将发送和接收放到两个独立的Goroutine

	// 首先向服务端发送数据:
	go func() {
		for {
			req := &service.Request{
				Value: "小明",
			}
			if err := stream.Send(req); err != nil {
				log.Fatal(err)
			}
			time.Sleep(time.Second)
		}
	}()

	// 然后再循环中接收服务端返回的数据
	for {
		reply, err := stream.Recv()
		if err != nil {
			if err == io.EOF {
				break
			}
			log.Fatal(err)
		}
		fmt.Println(reply.GetValue())
	}
}
  • 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
  • 41
  • 42
  • 43

4. gRPC 中间件(认证)

前面我们的rpc都存在一些缺陷,我们的rpc在调用的时候是裸着的,任何一个有这个客户端的人都可以调用,就相当于大门常打开,所以我们需要为他加一把锁。

但是加锁的话有很多方式,比如颁发证书,我为你这个客户端颁发了证书,你才可以调用,这也是最常用的方式,不过这里我们没有这样处理,而是写的一个认证中间件:

4.1 中间件RPC接口解读

4.1.1 正常模式
// UnaryServerInterceptor 提供了一个钩子来拦截服务器上一元RPC的执行。
// info 包含拦截器可以操作的RPC的所有信息。
// handler 是服务方法实现的包装器.
// 拦截器负责调用 handler 来完成RPC。
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)
  • 1
  • 2
  • 3
  • 4
  • 5
  • ctx:请求上下文
  • req:rpc请求数据
  • info:服务端相关数据,不用理解这个
  • handler:处理请求的handler,相对于next()
  • resp:rpc相应数据
  • err:rpc错误

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gWbOcIoo-1657464088036)(images/image-20220710210456739.png)]

4.1.2 Stream 流模式
// StreamServerInterceptor 提供了一个钩子来拦截服务器上 流RPC 的执行。
// info 包含拦截器可以操作的RPC的所有信息。
// handler是服务方法实现的包装器.
// 拦截器负责调用处理程序来完成RPC。
type StreamServerInterceptor func(srv interface{}, ss ServerStream, info *StreamServerInfo, handler StreamHandler) error
  • 1
  • 2
  • 3
  • 4
  • 5
  • srv:service信息
  • ss:Server数据流
  • info:服务端相关数据,不用理解这个
  • handler:处理请求的handler,相对于next()
  • err:rpc错误

4.2 代码:

4.2.1 protobuf文件
syntax = "proto3";
package hello;
option go_package = "MicroServiceStudy01/10-grpc-auth/service";

message Request{
  string value = 1;
}

message Response{
  string value = 1;
}

// The HelloService service definition
// service 关键字
// HelloService 服务名称 对应接口的名称
// service服务会对应.pb.go文件里interface,里面的rpc对应接口中的函数
service HelloService{
  rpc Hello (Request) returns (Response){}
  rpc Channel(stream Request) returns (stream Response) {}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

生成代码:

cd ./10-grpc-auth/service
protoc -I . --go_out=. --go_opt=module="MicroServiceStudy01/10-grpc-auth/service" --go-grpc_out=. --go-grpc_opt=module="Mic
roServiceStudy01/10-grpc-auth/service" ./service.proto 
  • 1
  • 2
  • 3
4.2.2 认证中间件代码
1. 客户端

10-grpc-auth/auther/client.go

客户端的核心逻辑就是从meta中取clientId,clientSecret;

package author

import "context"

type Authentication struct {
	clientId     string
	clientSecret string
}

// NewClientAuthentication 构造凭证
func NewClientAuthentication(clientId, clientSecret string) *Authentication {
	return &Authentication{
		clientId:     clientId,
		clientSecret: clientSecret,
	}
}

// WithClientCredentials 通过客户端初始化凭证
func (a *Authentication) WithClientCredentials(clientId, clientSecret string) {
	a.clientId = clientId
	a.clientSecret = clientSecret
}

// GetRequestMetadata 从meta中取凭证信息:clientId,clientSecret
func (a *Authentication) GetRequestMetadata(context.Context, ...string) (
	map[string]string, error) {

	return map[string]string{
		"client_id":     a.clientId,
		"client_secret": a.clientSecret,
	}, nil
}

// RequireTransportSecurity 指示凭据是否需要传输安全性。
func (a *Authentication) RequireTransportSecurity() bool {
	return false
}
  • 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
2. 服务端

10-grpc-auth/auther/server.go

package author

import (
	"context"
	"fmt"
	"github.com/infraboard/mcube/logger"
	"github.com/infraboard/mcube/logger/zap"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
)

const (
	ClientHeaderKey = "client_id"
	ClientSecretKey = "client_secret"
)

type grpcAuthor struct {
	log logger.Logger
}

// 构造并初始化 grpc author
func newGrpcAuthor() *grpcAuthor {
	return &grpcAuthor{
		// zap.L(): 返回一个未命名的全局 logger。
		// .Named(): 添加一个新的路径段 到 logger 的 名称 。段由句点连接。
		log: zap.L().Named("Grpc Author"),
	}
}

// GetClientCredentialsFromMeta 从客户端发来的请求中获取凭证信息
func (a *grpcAuthor) GetClientCredentialsFromMeta(md metadata.MD) (
	clientId, clientSecret string) {
	cids := md.Get(ClientHeaderKey)
	sids := md.Get(ClientSecretKey)
	if len(cids) > 0 {
		clientId = cids[0]
	}
	if len(sids) > 0 {
		clientSecret = sids[0]
	}
	return
}

// 验证凭证信息
func (a *grpcAuthor) validateServiceCredential(clientId, clientSecret string) error {
	if clientId == "" && clientSecret == "" {
		return status.Errorf(codes.Unauthenticated, "client_id or client_secret is \"\"")
	}
	if !(clientId == "admin" && clientSecret == "123456") {
		return status.Errorf(codes.Unauthenticated, "client_id or client_secret invalidate")
	}
	return nil
}

// Auth 普通模式的拦截器
func (a *grpcAuthor) Auth(
	ctx context.Context,
	req interface{},
	info *grpc.UnaryServerInfo,
	handler grpc.UnaryHandler,
) (resp interface{}, err error) {
	// 从上下文中获取认证信息
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return nil, fmt.Errorf("ctx is not an grpc incoming context!")
	}

	fmt.Println("grpc header info :", md)

	// 获取客户端凭证信息
	clientId, clientSecret := a.GetClientCredentialsFromMeta(md)

	// 校验调用的客户端携带的凭证是否有效
	if err := a.validateServiceCredential(clientId, clientSecret); err != nil {
		return nil, err
	}

	resp, err = handler(ctx, req)
	return resp, err
}

// StreamAuth 流模式的拦截器
func (a *grpcAuthor) StreamAuth(
	srv interface{},
	ss grpc.ServerStream,
	info *grpc.StreamServerInfo,
	handler grpc.StreamHandler,
) (err error) {
	fmt.Println(srv, info)
	// 从上下文中获取认证信息
	// https://www.bilibili.com/video/BV1mi4y1d7SL?p=3&t=5032.0
	md, ok := metadata.FromIncomingContext(ss.Context())
	if !ok {
		return fmt.Errorf("ctx is not an grpc incoming context!")
	}

	fmt.Println("grpc header info :", md)

	// 获取客户端凭证
	clientId, clientSecret := a.GetClientCredentialsFromMeta(md)

	// 校验调用的客户端凭证是否有效
	if err := a.validateServiceCredential(clientId, clientSecret); err != nil {
		return err
	}

	return handler(srv, ss)
}

func GrpcAuthUnaryServerInterceptor() grpc.UnaryServerInterceptor {
	return newGrpcAuthor().Auth
}

func GrpcAuthStreamServerInterceptor() grpc.StreamServerInterceptor {
	return newGrpcAuthor().StreamAuth
}

  • 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
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
4.2.1 客户端代码

10-grpc-auth/client/client.go

package main

import (
	"MicroServiceStudy01/10-grpc-auth/author"
	"MicroServiceStudy01/10-grpc-auth/service"
	"context"
	"fmt"
	"google.golang.org/grpc"
	"log"
)

func main() {
	conn, err := grpc.Dial("localhost:1234",
		grpc.WithInsecure(),
		grpc.WithPerRPCCredentials(author.NewClientAuthentication("admin", "123456")),
	)
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	// NewHelloServiceClient函数是xxx_grpc.pb.go中自动生成的函数,
	// 基于已经建立的连接构造HelloServiceClient对象,
	// 返回的client其实是一个HelloServiceClient接口对象
	//
	client := service.NewHelloServiceClient(conn)

	// 通过接口定义的方法就可以调用服务端对应gRPC服务提供的方法
	req := &service.Request{Value: "小亮"}
	reply, err01 := client.Hello(context.Background(), req)
	if err01 != nil {
		log.Fatal(err01)
	}
	fmt.Println(reply.GetValue())
}
  • 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
4.2.3 服务端代码

10-grpc-auth/server/server.go

package main

import (
	"MicroServiceStudy01/10-grpc-auth/author"
	"MicroServiceStudy01/10-grpc-auth/service"
	"context"
	"google.golang.org/grpc"
	"log"
	"net"
)

type HelloService struct {
	// UnimplementedHelloServiceServer这个结构体是必须要内嵌进来的
	// 也就是说我们定义的这个结构体对象必须继承UnimplementedHelloServiceServer。
	// 嵌入之后,我们就已经实现了GRPC这个服务的接口,但是实现之后我们什么都没做,没有写自己的业务逻辑,
	// 我们要重写实现的这个接口里的函数,这样才能提供一个真正的rpc的能力。
	service.UnimplementedHelloServiceServer
}

var _ service.HelloServiceServer = (*HelloService)(nil)

// Hello 重写实现的接口里的Hello函数
func (p *HelloService) Hello(ctx context.Context, req *service.Request) (*service.Response, error){
	resp := &service.Response{}
	resp.Value = "hello:" + req.Value
	return resp, nil
}

func main() {
	// 1. 构造一个gRPC服务对象
	grpcServer:=grpc.NewServer(
		// 添加认证中间件,如果有多个中间件需要添加,使用ChainUnaryInterceptor
		grpc.UnaryInterceptor(author.GrpcAuthUnaryServerInterceptor()),
		// 添加stream API拦截器
		grpc.StreamInterceptor(author.GrpcAuthStreamServerInterceptor()),
	)
	// 2.通过gRPC插件生成的RegisterHelloServiceServer 函数注册我们实现的HelloService服务。
	service.RegisterHelloServiceServer(grpcServer,new(HelloService))

	// 3. 监听:1234端口
	listen,err:=net.Listen("tcp",":1234")
	if err!=nil{
		log.Fatal("Listen TCP err:", err)
	}
	// 4. 通过grpcServer.Serve(listen) 在一个监听端口上提供gRPC服务
	grpcServer.Serve(listen)
}

  • 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
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/人工智能uu/article/detail/886304
推荐阅读
相关标签
  

闽ICP备14008679号