go开发一个简单的消息队列(一)-基础搭建

 前言

看过很多go项目的源码,但是自己没真正写过。自己一直是做web项目,一般的web的项目java已经非常非常够用了并且非常非常好用。go感觉还是比较适合做中间件,但是自己的眼界有限,就不知道能写神马了,所以就一直看别人源码没自己去写过。然后现在试着做一个简单的消息队列,第一次写go,碰到很多问题,也有很多疑问想得到答案,希望巨佬们能帮忙解惑。

代码在这里

https://github.com/woshidajj/woshidajj-mq

需求

消息队列,差不多就是用户能订阅某个topic,服务给某个topic,发布一条消息,这个topic的所有用户都能收到。用户收到一条消息,他自己知道怎么去处理他。用户不想订阅了,就取消订阅。

设计

1.消息队列服务(msgqueue)维护很多个topic,每个topic应该对应一个订阅者列表(topiclist),订阅者列表里面都是订阅者

2.消息队列服务(msgqueue),提供订阅,取消订阅,发布消息的功能

3.每个订阅者列表(topiclist),都有一个publisher,消息队列服务(msgqueue)发布消息时,通过发送消息给publisher,进行发布

4.订阅者列表(topiclist)的publisher,会直接把消息发送给订阅者(subscriber),订阅者异步处理

5.这里的订阅者(subscriber),发布者(publisher),比较像actor模型里的actor,每个actor有自己的mailbox,消息队列服务(msgqueue),往publisher的mailbox发消息,publisher从他的mailbox里取出消息,然后给他的subscriber的mailbox发消息,然后subscribe会从mailbox里取消息自己处理

6.消息队列服务(msgqueue)也可以弄成一个actor,给它发消息,然后订阅,取消订阅吗?我觉得订阅是不行,因为订阅需要马上反馈给客户。然后取消订阅,我觉得可以,我想到一个场景,就是突然断网,会有大量订阅者取消订阅,造成处理压力,取消订阅因为要操作切片,一般都要锁,一个一个取消,如果取消订阅也用actor模型,先发送消息,后续处理,就不会有这方面的处理压力(但是我没实现)

代码

1.actor

(1)每个actor都有一个Mailbox,一个ActorWork,Mailbox是一个chan,发送消息就是给chan塞入消息

(2)每个actor维持着一个协程runActor,他会监听Mailbox的消息,然后取消息调用actorworker的work方法处理

type ActorWorker interface {
	Work(interface{}) error
}

type actor struct {
	id      int
	status  int
	Mailbox chan interface{}
	ExitC   chan struct{}
	once    sync.Once
	worker  ActorWorker
}

func NewActor(mailboxLen int, worker ActorWorker) *actor {

	mailbox := make(chan interface{}, mailboxLen)
	exitC := make(chan struct{})
	a := &actor{Mailbox: mailbox, ExitC: exitC, worker: worker}

	return a
}

func (a *actor) runActor() {
	for {
		select {
		case msg := <-a.Mailbox:
			a.worker.Work(msg)
		case <-a.ExitC:
			fmt.Printf("actor(%p) is closed \n", a)
			return
		}
	}
}

func (a *actor) close() {
	// 把状态设置为停止,不需要并发安全,重复关闭不会有问题
	a.status = 1
	// 只关闭一次
	a.once.Do(func() {
		close(a.ExitC)
	})
}

2.一个简单的ActorWorker,HelloActorWorker

type HelloActorWorker struct {
}

func (w *HelloActorWorker) Work(m interface{}) error {
	v, ok := m.(string)

	if ok {
		fmt.Printf("HELLO - %s \n", v)
	} else {
		fmt.Println("INVALID M")
	}

	return nil
}

3.测试一下给这个actor发送消息

func TestHelloActor(t *testing.T) {

	a := NewActor(10, &HelloActorWorker{})
	go a.runActor()
	time.Sleep(time.Second * 1)

	go func() {

		for i := 0; i < 20; i++ {
			a.Mailbox <- fmt.Sprintf("haha_%d", i)
		}

	}()

	time.Sleep(time.Second * 5)

}

4.PublisherActorWorker

(1)发布者publisher的actorWorker,他接收PublishMsg消息,包含了一个切片subers,是订阅者的列表,msg是订阅者会接收到的消息。

(2)发布消息的时候,他会对每个订阅者单独开启一个协程,给他们的Mailbox发送消息,5秒超时(因为对方的Mailbox可能满了或者其他原因导致发送不了),超时取消操作

type PublishMsg struct {
	msg    interface{} // 消息
	subers []*actor    // 订阅者的列表
}

type PublisherActorWorker struct {
}

func (w *PublisherActorWorker) Work(m interface{}) error {
	v, ok := m.(PublishMsg)

	if ok {
		msgHandleDuration := time.Second * 5
		// 对每个订阅者,开启一个协程往他mailbox发送消息
		for _, suber := range v.subers {

			go func(s *actor, m interface{}) {
				// 超时退出协程
				idleDuration := msgHandleDuration
				idleDelay := time.NewTimer(idleDuration)
				defer idleDelay.Stop()

				select {
				case s.Mailbox <- m:
					fmt.Printf("publish to : %p \n", s)
				case <-idleDelay.C:
					return
				}
			}(suber, v.msg)

		}

	} else {
		fmt.Println("INVALID M")
	}

	return nil
}

5.测试一下用一个publisher处理消息

func TestPublishActor(t *testing.T) {

	a1 := NewActor(10, &HelloActorWorker{})
	go a1.runActor()
	a2 := NewActor(10, &HelloActorWorker{})
	go a2.runActor()
	a3 := NewActor(10, &HelloActorWorker{})
	go a3.runActor()
	a4 := NewActor(10, &HelloActorWorker{})
	go a4.runActor()

	subers := []*actor{a1, a2, a3, a4}

	p := NewActor(10, &PublisherActorWorker{})

	go p.runActor()

	time.Sleep(time.Second * 1)

	go func() {

		for i := 0; i < 20; i++ {
			msg := PublishMsg{msg: fmt.Sprintf("haha_%d", i), subers: subers}
			p.Mailbox <- msg
		}

	}()

	time.Sleep(time.Second * 20)

}

6.toplist

(1)有一个切片subers是他的订阅者的actor

(2)publisher是这个toplist的一个发布消息的actor,上层的消息队列服务如果要向这个topic发布消息,会直接向publisher的Mailbox塞入消息

(3)订阅的时候要注意,如果是第一个订阅者,要开启publisher的协程

(4)取消订阅的时候要注意,如果是最后一个订阅者,要关闭publisher的协程

(5)这里的topiclist不会主动去关闭订阅者的协程,主要是考虑toplist应该负责关闭自己的资源

type topiclist struct {
	topic  string
	subers []*actor // 订阅者的列表
	sync.RWMutex // 订阅和取消订阅的时候应该锁住
	publisher *actor // publisherActor
}

func newTopiclist(topic string) *topiclist {

	subers := make([]*actor, 0)
	tl := &topiclist{topic: topic, subers: subers}

	return tl
}

// 订阅,只负责添加actor
func (tlist *topiclist) subscribe(suber *actor) {

	tlist.Lock()
	defer tlist.Unlock()
	// 第一个订阅者,需要配备publisher
	if len(tlist.subers) == 0 {
		tlist.publisher = NewActor(10, &PublisherActorWorker{})
		go tlist.publisher.runActor()
	}

	tlist.subers = append(tlist.subers, suber)

}

// 取消订阅,只负责从订阅列表移除,当订阅者列表为0,关闭publisher的协程
func (tlist *topiclist) unsubscribe(suber *actor) {

	tlist.Lock()
	defer tlist.Unlock()

	// 先关闭协程
	suber.close()

	tempList := make([]*actor, 0)
	found := false

	for _, suberInList := range tlist.subers {

		if suberInList == suber {
			found = true
			continue
		}
		tempList = append(tempList, suberInList)
	}

	if found {
		tlist.subers = tempList
	}

	// 取消订阅以后列表为空,要关闭publisher的协程,下次再有订阅会重新new publisher然后启动协程
	if len(tlist.subers) == 0 {
		tlist.publisher.close()
	}

}

7.msqueue

(1)订阅列表放在一个sync.Map里,key是topic,value的topiclist

(2)订阅的时候要调用topiclist的subscribe方法把订阅者的actor放到toplist里,然后要负责启动订阅者actor的协程,等待消息发送过来处理

(3)取消订阅二话不说先停止订阅者actor的协程(我很怕泄露,所以有很多关闭协程的地方,其实这么做好不好?还是应该怎么做),然后再从tooiclist移出这个actor

(4)发布消息比较简单,应该要同步反馈给用户发布成功还是不成功,就没用协程,超时5秒

const (
	publishDuration = time.Second * 5
)

type MsgQueue struct {
	exitC  chan bool
	topics sync.Map
}

func NewMsgQueue() (*MsgQueue, error) {

	exitC := make(chan bool)
	var topics sync.Map
	mq := &MsgQueue{exitC: exitC, topics: topics}

	return mq, nil
}

func (mq *MsgQueue) Subscribe(topic string, suber *actor) (*actor, error) {

	select {
	// wait for exit signal
	case <-mq.exitC:
		return nil, errors.New("MQ EXIT")
	default:
	}

	fmt.Printf("(%p) subscirbe %s \n", suber, topic)

	llist, _ := mq.topics.LoadOrStore(topic, newTopiclist(topic))
	tlist := llist.(*topiclist)
	tlist.subscribe(suber)

	go suber.runActor()

	return suber, nil

}

func (mq *MsgQueue) Unsubscribe(topic string, suber *actor) error {

	if suber == nil {
		return nil
	}

	// 先关闭订阅者,退出协程
	suber.close()

	select {
	// wait for exit signal
	case <-mq.exitC:
		return errors.New("MQ EXIT")
	default:
	}

	llist, ok := mq.topics.Load(topic)
	if !ok {
		return nil
	}
	tlist := llist.(*topiclist)
	tlist.unsubscribe(suber)
	return nil
}

func (mq *MsgQueue) Publish(topic string, msg interface{}) error {

	select {
	// wait for exit signal
	case <-mq.exitC:
		return errors.New("MQ EXIT")
	default:
	}

	llist, ok := mq.topics.Load(topic)
	if !ok {
		return nil
	}

	tlist := llist.(*topiclist)

	// 超时退出协程
	idleDuration := publishDuration
	idleDelay := time.NewTimer(idleDuration)
	defer idleDelay.Stop()

	msgP := PublishMsg{msg: msg, subers: tlist.subers}

	select {
	case tlist.publisher.Mailbox <- msgP:

	case <-idleDelay.C:
		return errors.New("MQ PUBLISH TIMEOUT")
	}

	return nil
}

func (mq *MsgQueue) Close() {
	close(mq.exitC)

	// 发布者和订阅者的协程
	mq.topics.Range(func(key, value interface{}) bool {

		tlist := value.(*topiclist)

		tlist.publisher.close()
		for _, suber := range tlist.subers {
			suber.close()
		}

		return true
	})

}

一些问题

1.我没go开发的经验,很多规范不是很熟悉,我很希望有人能分享是不是有不规范的问题

2.我比较在意的一个事情是,会不会协程泄露,我的方式是每个协程监听一个exitC的chan,但是我总是很怕关闭的时候panic或者其他原因,导致没成功关闭(例如,topiclist在取消订阅的时候,如果没订阅者了要关闭publisher的协程,如果没关成功,有新的订阅者订阅,会再开启协程,这样就会有残留一个。)。不知道我的代码有没有这方面的问题,或者平时生产是怎么解决这个问题,希望有人能提供解决的办法合思路

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
FreeRTOS是一个开源的实时操作系统内核,被广泛应用于嵌入式系统中。ESP32是一款具有双核处理器和Wi-Fi功能的芯片,通过使用ESP-IDF开发框架可以进行软件开发。在ESP32-IDF开发中,使用FreeRTOS的消息队列可以实现不同任务之间的通信。 在ESP32开发中,可以通过中断服务程序(Interrupt Service Routine,ISR)来发送消息消息队列,并在任务中通过接收方法响应。 首先,我们需要创建一个全局的消息队列句柄,可以使用xQueueCreate函数来创建一个消息队列。例如,可以使用以下代码创建一个大小为10的消息队列: xQueueHandle messageQueue = xQueueCreate(10, sizeof(int)); 然后,在中断服务程序中,可以使用xQueueSendFromISR方法将消息发送到消息队列中。例如,可以使用以下代码将一个整数值发送到消息队列中: int value = 100; xQueueSendFromISR(messageQueue, &value, NULL); 在任务中,可以使用xQueueReceive方法从消息队列中接收消息并进行响应。例如,可以使用以下代码从消息队列中接收一个整数值并打印出来: int receivedValue; xQueueReceive(messageQueue, &receivedValue, portMAX_DELAY); printf("Received value: %d\n", receivedValue); 需要注意的是,在接收消息时,可以通过指定第三个参数来设置等待时间。例如,使用portMAX_DELAY表示无限等待,即直到接收到消息为止。 通过以上步骤,我们可以实现在ESP32开发中使用FreeRTOS消息队列进行中断服务消息发送与响应。这种方式可以实现不同任务之间的通信和同步,提高系统的并发性和实时性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值