• gRPC的发布订阅模式


      之前我们通过Watch机制实现了简化版本的监视服务,这里我们基于这种机制实现一个发布订阅模式,但是因为RPC缺乏流机制导致每次只能返回一个结果,在发布订阅模式中,由调用者主动发起的发布行为类似于一个普通函数调用,而被动的订阅者则类似gRPC客户端单向流中的接收者。现在我们可以尝试基于gRPC的流特性构造一个发布订阅系统。

      首先我们需要使用一个第三方模块:go get  github.com/docker/docker, 下面我们写一个简单的订阅模式demo:

    package main
    import (
    	"fmt"
    	"github.com/docker/docker/pkg/pubsub"
    	"strings"
    	"time"
    )
    func main(){
    	p := pubsub.NewPublisher(100*time.Microsecond, 10)
    	golang := p.SubscribeTopic(func(v interface{}) bool {
    		if key, ok := v.(string); ok {
    			if strings.HasPrefix(key, "golang:") {
    				return true
    			}
    		}
    		return false
    	})
    
    	docker := p.SubscribeTopic(func(v interface{}) bool {
    		if key, ok := v.(string); ok {
    			if strings.HasPrefix(key, "docker:") {
    				return true
    			}
    		}
    		return false
    	})
    
    	go p.Publish("wang")
    	go p.Publish("golang: https://golang.org")
    	go p.Publish("docker: https://www.docker.com")
    
    	time.Sleep(time.Second*2)
    	go func() {
    		fmt.Println("golang topic:", <-golang)
    	}()
    
    	go func() {
    		fmt.Println("docker topic:",<-docker)
    	}()
    	time.Sleep(time.Second*3)
    	fmt.Println("end")
    }

    以上代码运行后,会通过我们的订阅过滤函数:p.SubscribeTopic过滤调我们不是订阅的信息,最终打印出相关的结果。

    gRPC发布订阅实例

    我们分别需要有一个proto文件定义服务端和客户端的接口实现,里面定义了传输数据类型和实现方法,紧接着我们需要一个服务端,它用来支撑起整个服务给所有的客户端访问,再然后我们需要两个客户端,一个发布一个订阅(先订阅)。

    proto/publish.proto文件:

    syntax="proto3";
    
    package proto;
    message StringPub{
      string value =1;
    }
    
    service PubsubService {
      // 发布是rpc的普通方法
      rpc Publish (StringPub) returns (StringPub);
      // 订阅则是一个单向的流服务,服务端返回的数据可能很大
      rpc Subscribe (StringPub) returns (stream StringPub);
    }

    我们使用:protoc --go_out=plugins=grpc:. publish.proto 生成相应的go文件,重点分析:

    // 客户端接受体
    type pubsubServiceClient struct {
    	cc *grpc.ClientConn
    }
    
    // 客户端调用它生成接受体
    func NewPubsubServiceClient(cc *grpc.ClientConn) PubsubServiceClient {
    	return &pubsubServiceClient{cc}
    }
    
    // 客户端的方法实现
    func (c *pubsubServiceClient) Publish(ctx context.Context, in *StringPub, opts ...grpc.CallOption) (*StringPub, error) {
    	out := new(StringPub)
    	err := c.cc.Invoke(ctx, "/proto.PubsubService/Publish", in, out, opts...)
    	if err != nil {
    		return nil, err
    	}
    	return out, nil
    }
    
    func (c *pubsubServiceClient) Subscribe(ctx context.Context, in *StringPub, opts ...grpc.CallOption) (PubsubService_SubscribeClient, error) {
    	stream, err := c.cc.NewStream(ctx, &_PubsubService_serviceDesc.Streams[0], "/proto.PubsubService/Subscribe", opts...)
    	if err != nil {
    		return nil, err
    	}
    	x := &pubsubServiceSubscribeClient{stream}
    	if err := x.ClientStream.SendMsg(in); err != nil {
    		return nil, err
    	}
    	if err := x.ClientStream.CloseSend(); err != nil {
    		return nil, err
    	}
    	return x, nil
    }
    
    // 返回用于发送接受的对象,类似socket
    type PubsubService_SubscribeClient interface {
    	Recv() (*StringPub, error)
    	grpc.ClientStream
    }
    
    type pubsubServiceSubscribeClient struct {
    	grpc.ClientStream
    }
    
    ...
    func (x *pubsubServiceSubscribeClient) Recv() (*StringPub, error) {
    	m := new(StringPub)
    	if err := x.ClientStream.RecvMsg(m); err != nil {
    		return nil, err
    	}
    	return m, nil
    }
    
    // PubsubServiceServer is the server API for PubsubService service.
    type PubsubServiceServer interface {
    	// 发布是rpc的普通方法
    	Publish(context.Context, *StringPub) (*StringPub, error)
    	// 订阅则是一个单向的流服务,服务端返回的数据可能很大
    	Subscribe(*StringPub, PubsubService_SubscribeServer) error
    }
    
    func RegisterPubsubServiceServer(s *grpc.Server, srv PubsubServiceServer) {
    	s.RegisterService(&_PubsubService_serviceDesc, srv)
    }
    
    type PubsubService_SubscribeServer interface {
    	Send(*StringPub) error
    	grpc.ServerStream
    }
    

      

    接着我们可以实现一个发布的客户端:pub_service.go:

    package main
    
    import (
    	"context"
    	"gRPC_demo/proto"
    	"google.golang.org/grpc"
    	"log"
    )
    
    func main() {
    	conn, err := grpc.Dial("localhost:1234", grpc.WithInsecure())
    	if err != nil {
    		log.Fatal(err)
    	}
    	defer conn.Close()
    	client := proto.NewPubsubServiceClient(conn)
    
    	_, err = client.Publish(context.Background(), &proto.StringPub{Value: "golang: hello Wang"})
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	_, err = client.Publish(context.Background(), &proto.StringPub{Value: "docker: hello Wang"})
    	if err != nil {
    		log.Fatal(err)
    	}
    }
    

      

    最后我们实现订阅客户端:sub_client.go:

    package main
    
    import (
    	"context"
    	"fmt"
    	"gRPC_demo/proto"
    	"google.golang.org/grpc"
    	"io"
    	"log"
    )
    
    func main() {
    	conn, err := grpc.Dial("localhost:1234", grpc.WithInsecure())
    	if err != nil {
    		log.Fatal(err)
    	}
    	defer conn.Close()
    	client := proto.NewPubsubServiceClient(conn)
    
    	stream, err := client.Subscribe(context.Background(),&proto.StringPub{Value:"golang: "})
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	for {
    		reply, err := stream.Recv()
    		if err != nil {
    			if err == io.EOF{
    				break
    			}
    			log.Fatal(err)
    		}
    
    		fmt.Println(reply.GetValue())
    	}
    }
    

     到此发布订阅功能就基本实现了,依次启动服务端,订阅客户端,发布客户端就可以查看结果了。

    手写发布订阅

    分别由服务端、客户端代码,以及一个调用文件。(此实例是摘抄于:http://www.itkeyword.com/doc/602627406483745x914/golang-pubsub)感觉写的不错,所以摘抄。

    服务端代码:service.go

    package pubsub
    
    import (
    	"errors"
    	"sync"
    )
    
    type Client struct {
    	Id int
    	Ip string
    }
    
    type Server struct {
    	Dict map[string]*Channel //map[Channel.Name]*Channel
    	sync.RWMutex
    }
    
    func NewServer() *Server {
    	s := &Server{}
    	s.Dict = make(map[string]*Channel) //所有channel
    	return s
    }
    
    //订阅
    func (srv *Server) Subscribe(client *Client, channelName string) {
    
    	// 客户是否在Channel的客户列表中
    	srv.RLock()
    	ch, found := srv.Dict[channelName]
    	srv.RUnlock()
    
    	if !found {
    		ch = NewChannel(channelName)
    		ch.AddClient(client)
    		srv.Lock()
    		srv.Dict[channelName] = ch
    		srv.Unlock()
    	} else {
    		ch.AddClient(client)
    	}
    
    }
    
    //取消订阅
    func (srv *Server) Unsubscribe(client *Client, channelName string) {
    	srv.RLock()
    	ch, found := srv.Dict[channelName]
    	srv.RUnlock()
    	if found {
    		if ch.DeleteClient(client) == 0 {
    			ch.Exit()
    			srv.Lock()
    			delete(srv.Dict, channelName)
    			srv.Unlock()
    		}
    	}
    }
    
    //发布消息
    func (srv *Server) PublishMessage(channelName, message string) (bool, error) {
    	srv.RLock()
    	ch, found := srv.Dict[channelName]
    	if !found {
    		srv.RUnlock()
    		return false, errors.New("channelName不存在!")
    	}
    	srv.RUnlock()
    
    	ch.Notify(message)
    	ch.Wait()
    	return true, nil
    }
    

      

    客户端代码:client.go

    import (
    	"fmt"
    	"sync"
    	"sync/atomic"
    )
    
    type Channel struct {
    	Name    string
    	clients map[int]*Client
    	//  exitChan   chan int
    	sync.RWMutex
    	waitGroup    WaitGroupWrapper
    	messageCount uint64
    	exitFlag     int32
    }
    
    func NewChannel(channelName string) *Channel {
    	return &Channel{
    		Name: channelName,
    		//  exitChan:       make(chan int),
    		clients: make(map[int]*Client),
    	}
    }
    
    func (ch *Channel) AddClient(client *Client) bool {
    	ch.RLock()
    	_, found := ch.clients[client.Id]
    	ch.RUnlock()
    
    	ch.Lock()
    	if !found {
    		ch.clients[client.Id] = client
    	}
    	ch.Unlock()
    	return found
    }
    
    func (ch *Channel) DeleteClient(client *Client) int {
    	var ret int
    	ch.ReplyMsg(
    		fmt.Sprintf("从channel:%s 中删除client:%d ", ch.Name, client.Id))
    
    	ch.Lock()
    	delete(ch.clients, client.Id)
    	ch.Unlock()
    
    	ch.RLock()
    	ret = len(ch.clients)
    	ch.RUnlock()
    
    	return ret
    }
    
    func (ch *Channel) Notify(message string) bool {
    
    	ch.RLock()
    	defer ch.RUnlock()
    
    	for cid, _ := range ch.clients {
    		ch.ReplyMsg(
    			fmt.Sprintf("channel:%s client:%d message:%s", ch.Name, cid, message))
    	}
    	return true
    }
    
    func (ch *Channel) ReplyMsg(message string) {
    	ch.waitGroup.Wrap(func() { fmt.Println(message) })
    }
    
    func (ch *Channel) Wait() {
    	ch.waitGroup.Wait()
    }
    
    func (ch *Channel) Exiting() bool {
    	return atomic.LoadInt32(&ch.exitFlag) == 1
    }
    
    func (ch *Channel) Exit() {
    	if !atomic.CompareAndSwapInt32(&ch.exitFlag, 0, 1) {
    		return
    	}
    	//close(ch.exitChan)
    	ch.Wait()
    }
    
    func (ch *Channel) PutMessage(clientID int, message string) {
    	ch.RLock()
    	defer ch.RUnlock()
    
    	if ch.Exiting() {
    		return
    	}
    
    	//select {
    	// case <-t.exitChan:
    	// return
    	//}
    	fmt.Println(ch.Name, ":", message)
    
    	atomic.AddUint64(&ch.messageCount, 1)
    	return
    }
    

      

    最后是主函数文件:

    package main
    
    import (
      . "pubsub"
    )
    
    func main(){
      c1 := &Client{Id:100,Ip:"172.18.1.1"}
      c3:=  &Client{Id:300,Ip:"172.18.1.3"}
    
       srv := NewServer()
       srv.Subscribe(c1,"Topic")
       srv.Subscribe(c3,"Topic")
    
       srv.PublishMessage("Topic","测试信息1")
    
       srv.Unsubscribe(c3,"Topic")
       srv.PublishMessage("Topic","测试信息2222")
    
        srv.Subscribe(c1,"Topic2")
        srv.Subscribe(c3,"Topic2")
        srv.PublishMessage("Topic2"," Topic2的测试信息")   
    }
    

      

  • 相关阅读:
    k8s云集群混搭模式落地分享
    开发人员需要掌握的日常Linux命令集
    k8s云集群混搭模式,可能帮你节省50%以上的服务成本
    PyQt5Day27--展示控件QLCDNumber(LCD屏幕显示)
    PyQt5Day26--展示控件QLabel
    PyQt5Day25--输入控件QClendarWidget(日期控件)
    python学习Day40--复习+初始协程
    python学习Day39--复习(前期+进程和线程相关)
    PyQt5Day24--输入控件QDialog(对话框控件)
    python学习Day38--进程与线程
  • 原文地址:https://www.cnblogs.com/double-W/p/12765701.html
Copyright © 2020-2023  润新知