GRPC 快速使用

68 阅读11分钟

前言

  • 真聊会天

    • gRPC 是一种现代化开源的高性能 RPC 框架,能够运行于任意环境之中,最初由谷歌进行开发,使用的是 HTTP/2 作为传输协议
    • 本文开袋即食,大致内容目录里面可以体现,主要帮助新手快速学习 GRPC
  • 开发环境

    • Windows x64
    • go version go1.20 windows/amd64
    • code --version 1.76.2
    • libprotoc 22.1
    • OpenSSL 3.1.0 14 Mar 2023 (Library: OpenSSL 3.1.0 14 Mar 2023)
  • 代码地址


v0 准备工作

  1. 根据自己的开发环境下载 protoc ,并将其 bin 目录配置到环境变量中
    • 在命令行中输入 protoc --version 即可查看是否配置成功
    • 如果你懒得搜且开发环境和我类似,你也可以和我下载一样的
  2. 创建项目与 go mod 文件,在命令行中输入 go get google.golang.org/grpc 安装 grpc 的核心库
  3. 在命令行中输入 go install google.golang.org/protobuf/cmd/protoc-gen-go@latestgo install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest 安装对应工具
    • 在命令行中分别输入 protoc-gen-go --versionprotoc-gen-go-grpc --version 即可查看是否配置成功
    • 此时在 %GOPATH%/bin 目录下可以看到这两个工具:

V1 入门示例

  1. 在 server 端创建 pb 文件夹,编写 hello.proto 文件来定义服务

    // 版本声明,使用Protocol Buffers v3版本
    syntax = "proto3";
    
    // 这部分的内容是关于最后生成的 go 文件是处在哪个目录哪个包中
    // . 代表在当前目录生成
    // 以 ; 号间隔
    // service 代表了生成的 go 文件的包名是 service 
    option go_package = ".;pb";
    
    // 然后我们需要定义一个服务,在这个服务中需要有一个方法,这个方法可以接受客户端的参数,再返回服务端的响应
    // 其实很容易可以看出,我们定义了一个 service ,称为 Greeter ,这个服务中有一个 rpc 方法,名为 SayHello 
    // 这个方法会发送一个 HelloRequest ,然后返回一个 HelloResponse 
    service Greeter {
        rpc SayHello (HelloRequest) returns (HelloResponse) {}
    }
    
    // message 关键字,其实你可以理解为 Golang 中的结构体
    // 这里比较特别的是变量后面的“赋值”
    // 这里并不是赋值,而是定义在这个 message 中的位置
    message HelloRequest {
        string name = 1;    // 第一行就标识为 1
        // int64 age = 2;   // 第二行就标识为 2
    }
    
    message HelloResponse {
        string reply = 1;
    }
    
  2. 在 client 端也创建一个 pb 文件夹,并将上面的 proto 文件拷贝到客户端的文件夹中

  3. 分别在服务端和客户端的 pb 目录下打开命令行,并执行下面两条命令:

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

    pb 目录中会自动生成两个文件:

    • hello.pb.go
    • hello_grpc.pb.go
  4. 此时服务端的目录结构:

    以及客户端的目录结构:

  5. 编写服务端 main.go 文件

    package main
    
    import (
            "context"
            "fmt"
            "net"
    
            "github.com/sanyewudezhuzi/gRPC_study/pb"
    
            "google.golang.org/grpc"
    )
    
    // hello_server
    
    type server struct {
            pb.UnimplementedGreeterServer
    }
    
    func (s *server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
            fmt.Println("hello: " + req.Name)
            return &pb.HelloResponse{Reply: "Hello " + req.Name}, nil
    }
    
    func main() {
            // 监听本地的 9090 端口
            listen, err := net.Listen("tcp", "127.0.0.1:9090")
            if err != nil {
                    panic("failed to listen")
            }
            // 创建gRPC服务器
            grpcServer := grpc.NewServer()
            // 在 grpc 客户端注册我们自己编写的服务
            pb.RegisterGreeterServer(grpcServer, &server{})
            // 启动服务
            err = grpcServer.Serve(listen)
            if err != nil {
                    panic("failed to server")
            }
    }
    
  6. 编写服务端 main.go 文件

    package main
    
    import (
            "context"
            "flag"
            "fmt"
            "log"
            "time"
    
            "github.com/sanyewudezhuzi/gRPC_study/pb"
    
            "google.golang.org/grpc"
            "google.golang.org/grpc/credentials/insecure"
    )
    
    // hello_client
    
    const (
            defaultName = "world"
    )
    
    var (
            addr = flag.String("addr", "127.0.0.1:9090", "IP address and port number of the tcp connection")
            name = flag.String("name", defaultName, "Name to greet")
    )
    
    func main() {
            // 将用户传递至命令行的参数解析为对应变量值
            flag.Parse()
            // 连接到server端,此处禁用安全传输
            conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
            if err != nil {
                    log.Fatalf("failed to connect: %v", err)
            }
            defer conn.Close()
            // 建立连接
            client := pb.NewGreeterClient(conn)
            // 执行RPC调用并打印收到的响应数据(这个方法在服务端实现并返回结果)
            ctx, cancel := context.WithTimeout(context.Background(), time.Second)
            defer cancel()
            res, err := client.SayHello(ctx, &pb.HelloRequest{Name: *name})
            if err != nil {
                    log.Fatalf("failed to sayhello: %v", err)
            }
            fmt.Println(res.GetReply())
    }
    
  7. 先后执行服务端和客户端:


v2 加密认证

先唠嗑几个知识点:

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

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

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

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

在 v1 的基础上,于服务端中新增 key 目录

  1. 下载 openssl
    • 在命令行中输入 openssl version 即可查看是否配置成功
    • 如果你懒得搜且开发环境和我类似,你也可以和我下载一样的
  2. key 目录下打开命令行,输入 openssl genrsa -out server.key 2048 生成私钥
    • 跟我一起念~ 私(sī)钥(yuè)~
  3. 在命令行输入 openssl req -new -x509 -key server.key -out server.crt -days 36500 生成证书,有关选项可以不填,全部回车即可
  4. 在命令行输入 openssl req -new -key server.key -out server.csr 生成 csr ,有关选项可以不填,全部回车即可
  5. 更改 openssl.cfg
    • 在 openssl 安装路径的 bin 目录下找到 openssl.cfg 文件,将其拷贝到项目的 key 目录下
    • 搜索 copy_extensions ,将 copy_extensions = copy 开启
    • 搜索 req_extensions ,将 req_extensions = v3_req # The extensions to add to a certificate request 开启
    • 搜索 v3_req ,添加字段 **subjectAltName** = @alt_names
    • 添加标签 [ alt_names ] 和字段 DNS.1 = *.sanyewu.com
      其中 sanyewu 可以替换为你所想要的域名
  6. 在命令行输入 openssl genpkey -algorithm RSA -out test.key 生成证书私钥
  7. 在命令行输入 openssl req -new -nodes -key test.key -out test.csr -days 3650 -subj "/C=cn/OU=myorg/O=mycomp/CN=myname" -config ./openssl.cfg -extensions v3_req 通过私钥 test.key 生成证书请求文件 test.csr
  8. 在命令行输入 openssl x509 -req -days 365 -in test.csr -out test.pem -CA server.crt -CAkey server.key -CAcreateserial -extfile ./openssl.cfg -extensions v3_req 生成 SAN 证书 pem
  9. 完成后你的 key 目录下应该有这 8 个文件:

更新服务端的 main.go

func main() {
	// 生成证书
        // cretFile: 证书文件的绝对路径
        // keyFile: 私钥文件的绝对路径
	creds, err := credentials.NewServerTLSFromFile(cretFile, keyFile)
	if err != nil {
		panic("failed to creds")
	}
	// 监听本地的 9090 端口
	listen, err := net.Listen("tcp", "127.0.0.1:9090")
	if err != nil {
		panic("failed to listen")
	}
	// 创建gRPC服务器
	// 配置证书
	grpcServer := grpc.NewServer(grpc.Creds(creds))
	// 在 grpc 客户端注册我们自己编写的服务
	pb.RegisterGreeterServer(grpcServer, &server{})
	// 启动服务
	err = grpcServer.Serve(listen)
	if err != nil {
		panic("failed to server")
	}
}

test.pem 拷贝到客户端的 key 目录下,并更新客户端的 main.go

func main() {
	// 将用户传递至命令行的参数解析为对应变量值
	flag.Parse()
	// 获取证书
        // cretFile: 证书文件的绝对路径
        // serverNameOverride: 服务器的名称替代
	creds, err := credentials.NewClientTLSFromFile(cretFile, *serverNameOverride)
	if err != nil {
		panic("failed to creds")
	}
	// 连接到server端
	conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(creds))
	if err != nil {
		log.Fatalf("failed to connect: %v", err)
	}
	defer conn.Close()
	// 建立连接
	client := pb.NewGreeterClient(conn)
	// 执行RPC调用并打印收到的响应数据(这个方法在服务端实现并返回结果)
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	res, err := client.SayHello(ctx, &pb.HelloRequest{Name: *name})
	if err != nil {
		log.Fatalf("failed to sayhello: %v", err)
	}
	fmt.Println(res.GetReply())
}

分别运行服务端和客户端


v3 流式传输

v3.0 服务端流式 rpc

服务端流 RPC 下,客户端发出一个请求,但不会立即得到一个响应,而是在服务端与客户端之间建立一个单向的流,服务端可以随时向流中写入多个响应消息,最后主动关闭流,而客户端需要监听这个流,不断获取响应直到流关闭

  1. 定义服务

    rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse) {}
    
  2. 服务端实现

    func (s *server) LotsOfReplies(req *pb.HelloRequest, stream pb.Greeter_LotsOfRepliesServer) error {
            fmt.Println(req.GetName())
            words := []string{
                    "你好 ",
                    "hello ",
                    "こんにちは ",
                    "안녕하세요 ",
                    "สวัสดี ",
            }
            for _, word := range words {
                    data := &pb.HelloResponse{
                            Reply: word + req.GetName(),
                    }
                    // 使用 send 方法返回多个数据
                    if err := stream.Send(data); err != nil {
                            return err
                    }
            }
            return nil
    }
    
  3. 客户端实现

    // LotsOfReplies 返回使用多种语言打招呼
    func runLotsOfReplies(c pb.GreeterClient) {
            // server端流式RPC
            ctx, cancel := context.WithTimeout(context.Background(), time.Second)
            defer cancel()
            stream, err := c.LotsOfReplies(ctx, &pb.HelloRequest{Name: *name})
            if err != nil {
                    log.Fatalln("failed to lotsofreplies:", err)
            }
            for {
                    // 接收服务端返回的流式数据,当收到io.EOF或错误时退出
                    res, err := stream.Recv()
                    if err == io.EOF {
                            break
                    }
                    if err != nil {
                            log.Fatalln("failed to recv:", err)
                    }
                    fmt.Println(res.GetReply())
            }
    }
    
  4. 运行

v3.1 客户端流式 rpc

客户端传入多个请求对象,服务端返回一个响应结果

  1. 定义服务

    rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {}
    
  2. 服务端实现

    // LotsOfGreetings 接收流式数据
    func (s *server) LotsOfGreetings(stream pb.Greeter_LotsOfGreetingsServer) error {
            reply := "你好: "
            for {
                    // 接收客户端发来的流式数据
                    res, err := stream.Recv()
                    if err == io.EOF {
                            // 最终统一回复
                            return stream.SendAndClose(&pb.HelloResponse{
                                    Reply: reply,
                            })
                    }
                    if err != nil {
                            return err
                    }
                    reply += res.GetName()
                    fmt.Println("res: ", res.GetName())
            }
    }
    
  3. 客户端实现

    func runLotsOfGreeting(c pb.GreeterClient) {
            // 客户端流式RPC
            ctx, cancel := context.WithTimeout(context.Background(), time.Second)
            defer cancel()
            stream, err := c.LotsOfGreetings(ctx)
            if err != nil {
                    log.Fatalln("failed to lotsofgreetings:", err)
            }
            namelist := strings.Split(*names, ",")
            for _, name := range namelist {
                    // 发送流式数据
                    err := stream.Send(&pb.HelloRequest{Name: name})
                    if err != nil {
                            log.Fatalln("failed to send:", err)
                    }
            }
            res, err := stream.CloseAndRecv()
            if err != nil {
                    log.Fatalln("failed to closeandrecv:", err)
            }
            fmt.Println(res.GetReply())
    }
    
  4. 运行

v3.2 双向流式 rpc

双向流式rpc 结合客户端流式rpc和服务端流式rpc,可以传入多个对象,返回多个响应对象

  1. 定义服务

    rpc BidiHello(stream HelloRequest) returns (stream HelloResponse) {}
    
  2. 服务端实现

    // 从隔壁大佬偷的价值连城的 AI 模型
    func aimodel(s string) string {
            s = strings.ReplaceAll(s, "吗", "")
            s = strings.ReplaceAll(s, "吧", "")
            s = strings.ReplaceAll(s, "你", "我")
            s = strings.ReplaceAll(s, "?", "!")
            s = strings.ReplaceAll(s, "?", "!")
            return s
    }
    
    // BidiHello 双向流式打招呼
    func (s *server) BidiHello(stream pb.Greeter_BidiHelloServer) error {
            for {
                    // 接收流式请求
                    res, err := stream.Recv()
                    if res == nil {
                            return err
                    }
                    if err != nil {
                            log.Fatalln("failed to recv:", err)
                            return err
                    }
                    // 对收到的数据做些处理
                    fmt.Println(res.GetName())
                    reply := aimodel(res.GetName())
                    // 返回流式响应
                    if err := stream.Send(&pb.HelloResponse{Reply: reply}); err != nil {
                            return err
                    }
            }
    }
    
  3. 客户端实现

    func runBidiHello(c pb.GreeterClient) {
            // 双向流模式
            ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
            defer cancel()
            stream, err := c.BidiHello(ctx)
            // 双向流要手动关闭来标记消息的结束
            defer stream.CloseSend()
            if err != nil {
                    log.Fatalln("failed to bidihello:", err)
            }
            wg := sync.WaitGroup{}
            wg.Add(1)
            go func() {
                    defer wg.Done()
                    for {
                            // 接收服务端返回的响应
                            res, err := stream.Recv()
                            if res == nil {
                                    return
                            }
                            if err != nil {
                                    log.Fatalln("failed to recv:", err)
                            }
                            fmt.Println("AI: ", res.GetReply())
                    }
            }()
            // 从标准输入获取用户输入
            reader := bufio.NewReader(os.Stdin)
            for {
                    cmd, _ := reader.ReadString('\n')
                    cmd = strings.TrimSpace(cmd)
                    if len(cmd) == 0 {
                            continue
                    }
                    if strings.ToUpper(cmd) == "EXIT" {
                            stream.Send(nil)
                            break
                    }
                    // 将获取到的数据发送至服务端
                    if err := stream.Send(&pb.HelloRequest{Name: cmd}); err != nil {
                            log.Fatalln("failed to send:", err)
                    }
            }
            wg.Wait()
    }
    
  4. 运行


v4 metadata

在多个微服务的调用当中,信息交换常常是使用方法之间的参数传递的方式,但是在有些场景下,一些信息可能和 RPC 方法的业务参数没有直接的关联,所以不能作为参数的一部分,在 gRPC 中,可以使用元数据来存储这类信息。元数据最主要的作用:

  1. 提供RPC调用的元数据信息,例如用于链路追踪的traceId、调用时间、应用版本等等。
  2. 控制gRPC消息的格式,例如是否压缩或是否加密。

实际开发中,我们一般使用第三方库 google.golang.org/grpc/metada… 来操作元数据。

创建元数据

元数据的类型定义:

type md map[string][]string

metadata中的键是大小写不敏感的,由字母、数字和特殊字符 -_. 组成并且不能以 grpc- 开头(gRPC保留自用),二进制值的键名必须以 -bin 结尾

  1. 使用 metadata 库的 New() 函数来创建元数据

    md := metadata.New(map[string]string{"key1": "val1", "key2": "val2"})
    
  2. 使用 metadata 库的 Pairs() 函数来创建元数据

    md := metadata.Pairs(
        "key1", "val1",
        "key1", "val1-2", // "key1"的值将会是 []string{"val1", "val1-2"}
        "key2", "val2",
        "key-bin", string([]byte{96, 102}), // 二进制数据在发送前会进行(base64) 编码
    )
    
  3. 使用 FromIncomingContext 从 RPC 请求的上下文中获取

    func (s *server) SomeRPC(ctx context.Context, in *pb.SomeRequest) (*pb.SomeResponse, err) {
        md, ok := metadata.FromIncomingContext(ctx)
        // do something with metadata
    }
    

客户端发送接收元数据

var database = map[string]string{
	"zhuzi": "abc123",
}

// normalCall 普通调用
func (s *server) normalCall(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
	// 设置 trailer
	defer func() {
		trailer := metadata.Pairs("timestamp", strconv.Itoa(int(time.Now().Unix())))
		grpc.SetTrailer(ctx, trailer)
	}()
	// 获取 metadata
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return nil, status.Errorf(codes.DataLoss, "failed to get metadata")
	}
	name := md["name"][0]
	pwd := md["pwd"][0]
	if p, ok := database[name]; ok && p == pwd {
		fmt.Println("name:", name)
	} else {
		return nil, status.Error(codes.Unauthenticated, "invalid info")
	}
	// 发送 header
	header := metadata.New(map[string]string{"greeting": name + " say: hello " + req.Name})
	grpc.SendHeader(ctx, header)
	return &pb.HelloResponse{Reply: req.Name}, nil
}

// streamCalls 流式调用
func (s server) streamCalls(stream pb.Greeter_BidiHelloServer) error {
	// 设置 trailer
	defer func() {
		trailer := metadata.Pairs("timestamp", strconv.Itoa(int(time.Now().Unix())))
		stream.SetTrailer(trailer)
	}()
	// 获取 metadata
	md, ok := metadata.FromIncomingContext(stream.Context())
	if !ok {
		return status.Errorf(codes.DataLoss, "failed to get metadata")
	}
	name := md["name"][0]
	pwd := md["pwd"][0]
	if p, ok := database[name]; ok && p == pwd {
		fmt.Println("name:", name)
	} else {
		return status.Error(codes.Unauthenticated, "invalid info")
	}
	// 发送数据
	for {
		res, err := stream.Recv()
		if err == io.EOF {
			return nil
		}
		if err != nil {
			log.Fatalln("failed to recv:", err)
			return err
		}
		fmt.Println("get name: ", res.GetName())
		if err := stream.Send(&pb.HelloResponse{Reply: name + "say: hello " + res.Name}); err != nil {
			return err
		}
	}
}

服务端发送接收元数据

func runSayHello(c pb.GreeterClient, name string) {
	// 创建 metadata
	md := metadata.Pairs(
		"name", "zhuzi",
		"pwd", "abc123",
	)
	// 基于 metadata 创建 context
	ctx := metadata.NewOutgoingContext(context.Background(), md)
	// RPC 调用
	var header, trailer metadata.MD
	res, err := c.SayHello(
		ctx,
		&pb.HelloRequest{Name: name},
		grpc.Header(&header),   // 接收服务端发来的 header
		grpc.Trailer(&trailer), // 接收服务端发来的 trailer
	)
	if err != nil {
		log.Println("failed to call sayhello:", err)
		return
	}
	// 从 header 中获取 greeting
	if g, ok := header["greeting"]; ok {
		fmt.Println(g[0])
	} else {
		log.Println("failed to get greeting")
		return
	}
	// 获取响应
	fmt.Println("res: ", res.GetReply())
	// 从trailer中取timestamp
	if t, ok := trailer["timestamp"]; ok {
		fmt.Println("timestamp: ", t[0])
	} else {
		log.Println("failed to get timestamp")
	}
}

func runBidiHello(c pb.GreeterClient) {
	// 创建 metadata
	md := metadata.Pairs(
		"name", "zhuzi",
		"pwd", "abc123",
	)
	// 基于 metadata 创建 context
	ctx := metadata.NewOutgoingContext(context.Background(), md)
	// RPC 调用
	stream, err := c.BidiHello(ctx)
	defer stream.CloseSend()
	if err != nil {
		log.Println("failed to call bidhello:", err)
		return
	}
	wg := sync.WaitGroup{}
	wg.Add(1)
	go func() {
		defer wg.Done()
		for {
			// 接收服务端返回的响应
			res, err := stream.Recv()
			if err == io.EOF {
				return
			}
			if err != nil {
				log.Fatalln("failed to recv:", err)
			}
			fmt.Println(res.GetReply())
		}
	}()
	// 从标准输入获取用户输入
	reader := bufio.NewReader(os.Stdin)
	for {
		cmd, _ := reader.ReadString('\n')
		cmd = strings.TrimSpace(cmd)
		if len(cmd) == 0 {
			continue
		}
		if strings.ToUpper(cmd) == "EXIT" {
			stream.Send(nil)
			break
		}
		// 将获取到的数据发送至服务端
		if err := stream.Send(&pb.HelloRequest{Name: cmd}); err != nil {
			log.Fatalln("failed to send:", err)
		}
	}
	wg.Wait()
	// 结束时读取 trailer
	trailer := stream.Trailer()
	if t, ok := trailer["timestamp"]; ok {
		fmt.Println("timestamp: ", t[0])
	} else {
		log.Println("failed to get timestamp")
	}
}

参考链接