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的测试信息")
}

浙公网安备 33010602011771号