原文链接: 测试小姐姐问我 gRPC 怎么用,我直接把这篇文章甩给了她

上篇文章 gRPC,爆赞 直接爆了,内容主要包括:简单的 gRPC 服务,流处理模式,验证器,Token 认证和证书认证。

在多个平台的阅读量都创了新高,在 oschina 更是获得了首页推荐,阅读量到了 1w+,这已经是我单篇阅读的高峰了。

看来只要用心写还是有收获的。

这篇咱们还是从实战出发,主要介绍 gRPC 的发布订阅模式,REST 接口和超时控制。

相关代码我会都上传到 GitHub,感兴趣的小伙伴可以去查看或下载。

发布和订阅模式

发布订阅是一个常见的设计模式,开源社区中已经存在很多该模式的实现。其中 docker 项目中提供了一个 pubsub 的极简实现,下面是基于 pubsub 包实现的本地发布订阅代码:

  1. package main
  2. import (
  3. "fmt"
  4. "strings"
  5. "time"
  6. "github.com/moby/moby/pkg/pubsub"
  7. )
  8. func main() {
  9. p := pubsub.NewPublisher(100*time.Millisecond, 10)
  10. golang := p.SubscribeTopic(func(v interface{}) bool {
  11. if key, ok := v.(string); ok {
  12. if strings.HasPrefix(key, "golang:") {
  13. return true
  14. }
  15. }
  16. return false
  17. })
  18. docker := p.SubscribeTopic(func(v interface{}) bool {
  19. if key, ok := v.(string); ok {
  20. if strings.HasPrefix(key, "docker:") {
  21. return true
  22. }
  23. }
  24. return false
  25. })
  26. go p.Publish("hi")
  27. go p.Publish("golang: https://golang.org")
  28. go p.Publish("docker: https://www.docker.com/")
  29. time.Sleep(1)
  30. go func() {
  31. fmt.Println("golang topic:", <-golang)
  32. }()
  33. go func() {
  34. fmt.Println("docker topic:", <-docker)
  35. }()
  36. <-make(chan bool)
  37. }

这段代码首先通过 pubsub.NewPublisher 创建了一个对象,然后通过 p.SubscribeTopic 实现订阅,p.Publish 来发布消息。

执行效果如下:

  1. docker topic: docker: https://www.docker.com/
  2. golang topic: golang: https://golang.org
  3. fatal error: all goroutines are asleep - deadlock!
  4. goroutine 1 [chan receive]:
  5. main.main()
  6. /Users/zhangyongxin/src/go-example/grpc-example/pubsub/server/pubsub.go:43 +0x1e7
  7. exit status 2

订阅消息可以正常打印。

但有一个死锁报错,是因为这条语句 <-make(chan bool) 引起的。但是如果没有这条语句就不能正常打印订阅消息。

这里就不是很懂了,有没有大佬知道,欢迎留言,求指导。

接下来就用 gRPC 和 pubsub 包实现发布订阅模式。

需要实现四个部分:

  1. proto 文件;
  2. 服务端: 用于接收订阅请求,同时也接收发布请求,并将发布请求转发给订阅者;
  3. 订阅客户端: 用于从服务端订阅消息,处理消息;
  4. 发布客户端: 用于向服务端发送消息。

proto 文件

首先定义 proto 文件:

  1. syntax = "proto3";
  2. package proto;
  3. message String {
  4. string value = 1;
  5. }
  6. service PubsubService {
  7. rpc Publish (String) returns (String);
  8. rpc SubscribeTopic (String) returns (stream String);
  9. rpc Subscribe (String) returns (stream String);
  10. }

定义三个方法,分别是一个发布 Publish 和两个订阅 SubscribeSubscribeTopic

Subscribe 方法接收全部消息,而 SubscribeTopic 根据特定的 Topic 接收消息。

服务端

  1. package main
  2. import (
  3. "context"
  4. "fmt"
  5. "log"
  6. "net"
  7. "server/proto"
  8. "strings"
  9. "time"
  10. "github.com/moby/moby/pkg/pubsub"
  11. "google.golang.org/grpc"
  12. "google.golang.org/grpc/reflection"
  13. )
  14. type PubsubService struct {
  15. pub *pubsub.Publisher
  16. }
  17. func (p *PubsubService) Publish(ctx context.Context, arg *proto.String) (*proto.String, error) {
  18. p.pub.Publish(arg.GetValue())
  19. return &proto.String{}, nil
  20. }
  21. func (p *PubsubService) SubscribeTopic(arg *proto.String, stream proto.PubsubService_SubscribeTopicServer) error {
  22. ch := p.pub.SubscribeTopic(func(v interface{}) bool {
  23. if key, ok := v.(string); ok {
  24. if strings.HasPrefix(key, arg.GetValue()) {
  25. return true
  26. }
  27. }
  28. return false
  29. })
  30. for v := range ch {
  31. if err := stream.Send(&proto.String{Value: v.(string)}); nil != err {
  32. return err
  33. }
  34. }
  35. return nil
  36. }
  37. func (p *PubsubService) Subscribe(arg *proto.String, stream proto.PubsubService_SubscribeServer) error {
  38. ch := p.pub.Subscribe()
  39. for v := range ch {
  40. if err := stream.Send(&proto.String{Value: v.(string)}); nil != err {
  41. return err
  42. }
  43. }
  44. return nil
  45. }
  46. func NewPubsubService() *PubsubService {
  47. return &PubsubService{pub: pubsub.NewPublisher(100*time.Millisecond, 10)}
  48. }
  49. func main() {
  50. lis, err := net.Listen("tcp", ":50051")
  51. if err != nil {
  52. log.Fatalf("failed to listen: %v", err)
  53. }
  54. // 简单调用
  55. server := grpc.NewServer()
  56. // 注册 grpcurl 所需的 reflection 服务
  57. reflection.Register(server)
  58. // 注册业务服务
  59. proto.RegisterPubsubServiceServer(server, NewPubsubService())
  60. fmt.Println("grpc server start ...")
  61. if err := server.Serve(lis); err != nil {
  62. log.Fatalf("failed to serve: %v", err)
  63. }
  64. }

对比之前的发布订阅程序,其实这里是将 *pubsub.Publisher 作为了 gRPC 的结构体 PubsubService 的一个成员。

然后还是按照 gRPC 的开发流程,实现结构体对应的三个方法。

最后,在注册服务时,将 NewPubsubService() 服务注入,实现本地发布订阅功能。

订阅客户端

  1. package main
  2. import (
  3. "client/proto"
  4. "context"
  5. "fmt"
  6. "io"
  7. "log"
  8. "google.golang.org/grpc"
  9. )
  10. func main() {
  11. conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
  12. if err != nil {
  13. log.Fatal(err)
  14. }
  15. defer conn.Close()
  16. client := proto.NewPubsubServiceClient(conn)
  17. stream, err := client.Subscribe(
  18. context.Background(), &proto.String{Value: "golang:"},
  19. )
  20. if nil != err {
  21. log.Fatal(err)
  22. }
  23. go func() {
  24. for {
  25. reply, err := stream.Recv()
  26. if nil != err {
  27. if io.EOF == err {
  28. break
  29. }
  30. log.Fatal(err)
  31. }
  32. fmt.Println("sub1: ", reply.GetValue())
  33. }
  34. }()
  35. streamTopic, err := client.SubscribeTopic(
  36. context.Background(), &proto.String{Value: "golang:"},
  37. )
  38. if nil != err {
  39. log.Fatal(err)
  40. }
  41. go func() {
  42. for {
  43. reply, err := streamTopic.Recv()
  44. if nil != err {
  45. if io.EOF == err {
  46. break
  47. }
  48. log.Fatal(err)
  49. }
  50. fmt.Println("subTopic: ", reply.GetValue())
  51. }
  52. }()
  53. <-make(chan bool)
  54. }

新建一个 NewPubsubServiceClient 对象,然后分别实现 client.Subscribeclient.SubscribeTopic 方法,再通过 goroutine 不停接收消息。

发布客户端

  1. package main
  2. import (
  3. "client/proto"
  4. "context"
  5. "log"
  6. "google.golang.org/grpc"
  7. )
  8. func main() {
  9. conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
  10. if err != nil {
  11. log.Fatal(err)
  12. }
  13. defer conn.Close()
  14. client := proto.NewPubsubServiceClient(conn)
  15. _, err = client.Publish(
  16. context.Background(), &proto.String{Value: "golang: hello Go"},
  17. )
  18. if err != nil {
  19. log.Fatal(err)
  20. }
  21. _, err = client.Publish(
  22. context.Background(), &proto.String{Value: "docker: hello Docker"},
  23. )
  24. if nil != err {
  25. log.Fatal(err)
  26. }
  27. }

新建一个 NewPubsubServiceClient 对象,然后通过 client.Publish 方法发布消息。

当代码全部写好之后,我们开三个终端来测试一下:

终端1 上启动服务端:

  1. go run main.go

终端2 上启动订阅客户端:

  1. go run sub_client.go

终端3 上执行发布客户端:

  1. go run pub_client.go

这样,在 终端2 上就有对应的输出了:

  1. subTopic: golang: hello Go
  2. sub1: golang: hello Go
  3. sub1: docker: hello Docker

也可以再多开几个订阅终端,那么每一个订阅终端上都会有相同的内容输出。

源码地址: GitHub

REST 接口

gRPC 一般用于集群内部通信,如果需要对外提供服务,大部分都是通过 REST 接口的方式。开源项目 grpc-gateway 提供了将 gRPC 服务转换成 REST 服务的能力,通过这种方式,就可以直接访问 gRPC API 了。

但我觉得,实际上这么用的应该还是比较少的。如果提供 REST 接口的话,直接写一个 HTTP 服务会方便很多。

proto 文件

第一步还是创建一个 proto 文件:

  1. syntax = "proto3";
  2. package proto;
  3. import "google/api/annotations.proto";
  4. message StringMessage {
  5. string value = 1;
  6. }
  7. service RestService {
  8. rpc Get(StringMessage) returns (StringMessage) {
  9. option (google.api.http) = {
  10. get: "/get/{value}"
  11. };
  12. }
  13. rpc Post(StringMessage) returns (StringMessage) {
  14. option (google.api.http) = {
  15. post: "/post"
  16. body: "*"
  17. };
  18. }
  19. }

定义一个 REST 服务 RestService,分别实现 GETPOST 方法。

安装插件:

  1. go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway

生成对应代码:

  1. protoc -I/usr/local/include -I. \
  2. -I$GOPATH/pkg/mod \
  3. -I$GOPATH/pkg/mod/github.com/grpc-ecosystem/grpc-gateway@v1.16.0/third_party/googleapis \
  4. --grpc-gateway_out=. --go_out=plugins=grpc:.\
  5. --swagger_out=. \
  6. helloworld.proto

--grpc-gateway_out 参数可生成对应的 gw 文件,--swagger_out 参数可生成对应的 API 文档。

在我这里生成的两个文件如下:

  1. helloworld.pb.gw.go
  2. helloworld.swagger.json

REST 服务

  1. package main
  2. import (
  3. "context"
  4. "log"
  5. "net/http"
  6. "rest/proto"
  7. "github.com/grpc-ecosystem/grpc-gateway/runtime"
  8. "google.golang.org/grpc"
  9. )
  10. func main() {
  11. ctx := context.Background()
  12. ctx, cancel := context.WithCancel(ctx)
  13. defer cancel()
  14. mux := runtime.NewServeMux()
  15. err := proto.RegisterRestServiceHandlerFromEndpoint(
  16. ctx, mux, "localhost:50051",
  17. []grpc.DialOption{grpc.WithInsecure()},
  18. )
  19. if err != nil {
  20. log.Fatal(err)
  21. }
  22. http.ListenAndServe(":8080", mux)
  23. }

这里主要是通过实现 gw 文件中的 RegisterRestServiceHandlerFromEndpoint 方法来连接 gRPC 服务。

gRPC 服务

  1. package main
  2. import (
  3. "context"
  4. "net"
  5. "rest/proto"
  6. "google.golang.org/grpc"
  7. )
  8. type RestServiceImpl struct{}
  9. func (r *RestServiceImpl) Get(ctx context.Context, message *proto.StringMessage) (*proto.StringMessage, error) {
  10. return &proto.StringMessage{Value: "Get hi:" + message.Value + "#"}, nil
  11. }
  12. func (r *RestServiceImpl) Post(ctx context.Context, message *proto.StringMessage) (*proto.StringMessage, error) {
  13. return &proto.StringMessage{Value: "Post hi:" + message.Value + "@"}, nil
  14. }
  15. func main() {
  16. grpcServer := grpc.NewServer()
  17. proto.RegisterRestServiceServer(grpcServer, new(RestServiceImpl))
  18. lis, _ := net.Listen("tcp", ":50051")
  19. grpcServer.Serve(lis)
  20. }

gRPC 服务的实现方式还是和以前一样。

以上就是全部代码,现在来测试一下:

启动三个终端:

终端1 启动 gRPC 服务:

  1. go run grpc_service.go

终端2 启动 REST 服务:

  1. go run rest_service.go

终端3 来请求 REST 服务:

  1. $ curl localhost:8080/get/gopher
  2. {"value":"Get hi:gopher"}
  3. $ curl localhost:8080/post -X POST --data '{"value":"grpc"}'
  4. {"value":"Post hi:grpc"}

源码地址: GitHub

超时控制

最后一部分介绍一下超时控制,这部分内容是非常重要的。

一般的 WEB 服务 API,或者是 Nginx 都会设置一个超时时间,超过这个时间,如果还没有数据返回,服务端可能直接返回一个超时错误,或者客户端也可能结束这个连接。

如果没有这个超时时间,那是相当危险的。所有请求都阻塞在服务端,会消耗大量资源,比如内存。如果资源耗尽的话,甚至可能会导致整个服务崩溃。

那么,在 gRPC 中怎么设置超时时间呢?主要是通过上下文 context.Context 参数,具体来说就是 context.WithDeadline 函数。

proto 文件

创建最简单的 proto 文件,这个不多说。

  1. syntax = "proto3";
  2. package proto;
  3. // The greeting service definition.
  4. service Greeter {
  5. // Sends a greeting
  6. rpc SayHello (HelloRequest) returns (HelloReply) {}
  7. }
  8. // The request message containing the user's name.
  9. message HelloRequest {
  10. string name = 1;
  11. }
  12. // The response message containing the greetings
  13. message HelloReply {
  14. string message = 1;
  15. }

客户端

  1. package main
  2. import (
  3. "client/proto"
  4. "context"
  5. "fmt"
  6. "log"
  7. "time"
  8. "google.golang.org/grpc"
  9. "google.golang.org/grpc/codes"
  10. "google.golang.org/grpc/status"
  11. )
  12. func main() {
  13. // 简单调用
  14. conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
  15. defer conn.Close()
  16. ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Duration(3*time.Second)))
  17. defer cancel()
  18. client := proto.NewGreeterClient(conn)
  19. // 简单调用
  20. reply, err := client.SayHello(ctx, &proto.HelloRequest{Name: "zzz"})
  21. if err != nil {
  22. statusErr, ok := status.FromError(err)
  23. if ok {
  24. if statusErr.Code() == codes.DeadlineExceeded {
  25. log.Fatalln("client.SayHello err: deadline")
  26. }
  27. }
  28. log.Fatalf("client.SayHello err: %v", err)
  29. }
  30. fmt.Println(reply.Message)
  31. }

通过下面的函数设置一个 3s 的超时时间:

  1. ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Duration(3*time.Second)))
  2. defer cancel()

然后在响应错误中对超时错误进行检测。

服务端

  1. package main
  2. import (
  3. "context"
  4. "fmt"
  5. "log"
  6. "net"
  7. "runtime"
  8. "server/proto"
  9. "time"
  10. "google.golang.org/grpc"
  11. "google.golang.org/grpc/codes"
  12. "google.golang.org/grpc/reflection"
  13. "google.golang.org/grpc/status"
  14. )
  15. type greeter struct {
  16. }
  17. func (*greeter) SayHello(ctx context.Context, req *proto.HelloRequest) (*proto.HelloReply, error) {
  18. data := make(chan *proto.HelloReply, 1)
  19. go handle(ctx, req, data)
  20. select {
  21. case res := <-data:
  22. return res, nil
  23. case <-ctx.Done():
  24. return nil, status.Errorf(codes.Canceled, "Client cancelled, abandoning.")
  25. }
  26. }
  27. func handle(ctx context.Context, req *proto.HelloRequest, data chan<- *proto.HelloReply) {
  28. select {
  29. case <-ctx.Done():
  30. log.Println(ctx.Err())
  31. runtime.Goexit() //超时后退出该Go协程
  32. case <-time.After(4 * time.Second): // 模拟耗时操作
  33. res := proto.HelloReply{
  34. Message: "hello " + req.Name,
  35. }
  36. // //修改数据库前进行超时判断
  37. // if ctx.Err() == context.Canceled{
  38. // ...
  39. // //如果已经超时,则退出
  40. // }
  41. data <- &res
  42. }
  43. }
  44. func main() {
  45. lis, err := net.Listen("tcp", ":50051")
  46. if err != nil {
  47. log.Fatalf("failed to listen: %v", err)
  48. }
  49. // 简单调用
  50. server := grpc.NewServer()
  51. // 注册 grpcurl 所需的 reflection 服务
  52. reflection.Register(server)
  53. // 注册业务服务
  54. proto.RegisterGreeterServer(server, &greeter{})
  55. fmt.Println("grpc server start ...")
  56. if err := server.Serve(lis); err != nil {
  57. log.Fatalf("failed to serve: %v", err)
  58. }
  59. }

服务端增加一个 handle 函数,其中 case <-time.After(4 * time.Second) 表示 4s 之后才会执行其对应代码,用来模拟超时请求。

如果客户端超时时间超过 4s 的话,就会产生超时报错。

下面来模拟一下:

服务端:

  1. $ go run main.go
  2. grpc server start ...
  3. 2021/10/24 22:57:40 context deadline exceeded

客户端:

  1. $ go run main.go
  2. 2021/10/24 22:57:40 client.SayHello err: deadline
  3. exit status 1

源码地址: GitHub

总结

本文主要介绍了 gRPC 的三部分实战内容,分别是:

  1. 发布订阅模式
  2. REST 接口
  3. 超时控制

个人感觉,超时控制还是最重要的,在平时的开发过程中需要多多注意。

结合上篇文章,gRPC 的实战内容就写完了,代码全部可以执行,也都上传到了 GitHub

大家如果有任何疑问,欢迎给我留言,如果感觉不错的话,也欢迎关注和转发。


源码地址:

推荐阅读:

参考:

Go 语言实现 gRPC 的发布订阅模式,REST 接口和超时控制的更多相关文章

  1. 《JavaScript设计模式与开发实践》-- 发布-订阅模式

    详情个人博客:https://shengchangwei.github.io/js-shejimoshi-fabudingyue/ 发布-订阅模式 1.定义 发布-订阅模式:发布-订阅模式又叫观察者模 ...

  2. javascript设计模式——发布订阅模式

    前面的话 发布—订阅模式又叫观察者模式,它定义对象间的一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知.在javascript开发中,一般用事件模型来替代传统的发布—订阅模 ...

  3. javascript中的发布订阅模式与观察者模式

    这里了解一下JavaScript中的发布订阅模式和观察者模式,观察者模式是24种基础设计模式之一. 设计模式的背景 设计模式并非是软件开发的专业术语,实际上设计模式最早诞生于建筑学. 设计模式的定义是 ...

  4. JavaScript设计模式(发布订阅模式)

    发布—订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知.在JavaScript开发中,我们一般用事件模型来替代传统的发布—订阅模式 ...

  5. [转]js设计模式—发布订阅模式

    发布—订阅模式又叫观察者模式,它定义对象间的一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知.在javascript开发中,一般用事件模型来替代传统的发布—订阅模式.本文将 ...

  6. js里的发布订阅模式及vue里的事件订阅实现

    发布订阅模式(观察者模式) 发布订阅模式的定义:它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知. 发布订阅模式在JS中最常见的就是DOM的事件绑定与触发 ...

  7. 15天玩转redis —— 第九篇 发布/订阅模式

    本系列已经过半了,这一篇我们来看看redis好玩的发布订阅模式,其实在很多的MQ产品中都存在这样的一个模式,我们常听到的一个例子 就是邮件订阅的场景,什么意思呢,也就是说100个人订阅了你的博客,如果 ...

  8. 第五章 --- 关于Javascript 设计模式 之 发布-订阅模式

    先来个最简单的 发布订阅模式 document.body.addEventListener('click',function(){ alert(123); }); document.body.clic ...

  9. NetMQ(三): 发布订阅模式 Publisher-Subscriber

    ZeroMQ系列 之NetMQ 一:zeromq简介 二:NetMQ 请求响应模式 Request-Reply 三:NetMQ 发布订阅模式 Publisher-Subscriber 四:NetMQ ...

随机推荐

  1. Django中使用MySQL数据库的连接配置

    1. 安装pymysql pip install pymysql 2. 导入 # 在与 settings.py 同级目录下的 __init__.py 中引入模块和进行配置 import pymysql ...

  2. 【linux】 linux超实用命令整理

    linux实用命令整理 由于开发过程中经常接触linux系统进行各种情况下的操作,故开此博客整理生产环境下操作命令集,温故而知新. 系统命令 快捷键操作命令 1.tab //命令或路径等的补全键,li ...

  3. RMI源码调试

    看RMI漏洞时候,对其漏洞原理并不是很理解,所以简单调试了下源码加强下漏洞理解 由于要调试到RegistryImpl_Stub这种动态类,刚开始用的源码版本是JDK8u141,后来发现源码有些地方进行 ...

  4. SQL注入与burPsuit工具介绍

    sql注入原理 原理:用户输入不可控,用户输入导致了sql语义发生了改变 用户输入不可控:网站不能控制普通用户的输入 sql语义发生变化: 动态网页介绍: 网站数据请求 脚本语言:解释类语言,如,后端 ...

  5. 机器学习——主成分分析(PCA)

    1 前言 PCA(Principal Component Analysis)是一种常用的无监督学习方法,是一种常用的数据分析方法. PCA 通过利用 正交变换 把由 线性相关变量 表示的观测数据转换为 ...

  6. 洛谷P1208——P1208 [USACO1.3]Mixing Milk(贪心)

    题目描述 由于乳制品产业利润很低,所以降低原材料(牛奶)价格就变得十分重要.帮助Marry乳业找到最优的牛奶采购方案. Marry乳业从一些奶农手中采购牛奶,并且每一位奶农为乳制品加工企业提供的价格是 ...

  7. mysql操作中卡死 解决方法

    1.使用指令查询当前进程 show full processlist; -- 查询全部当前进程; show processlist;-- 只列出前100条 2.找出卡死的进程id 3.删除卡死进程 k ...

  8. Java基础系列(7)- 标识符和关键字

    关键字 标识符 Java所有的组成部分都需要名字.类名.变量名.方法名都称为标识符 首字母以字母(A-Z或者a-z),美元符号($),或者下划线(_)开头 首字母之后可以用字母.美元符号.下划线.数字 ...

  9. Linux系列(20) - shutdown

    作用 用于关机或重启 例子 [shutdown -h 05:30]:设定凌晨05:30关机 [shutdown -h +30]:30分钟后关机 [shutdown -h now] 立即关机 [shut ...

  10. php stream 流封装协议

    http://php.net/manual/wrappers.php 过程: 1. 开始通信 2. 读取数据 3. 写入数据 4. 结束通信 usage: * 读写文件系统 <?php $han ...