How to broadcast message using channel

后端 未结 6 1799
粉色の甜心
粉色の甜心 2020-11-27 14:04

I am new to go and I am trying to create a simple chat server where clients can broadcast messages to all connected clients.

In my server, I have a goroutine (infin

6条回答
  •  孤独总比滥情好
    2020-11-27 14:26

    A more elegant solution is a "broker", where clients may subscribe and unsubscibe to messages.

    To also handle subscribing and unsubscribing elegantly, we may utilize channels for this, so the main loop of the broker which receives and distributes the messages can incorporate all these using a single select statement, and synchronization is given from the solution's nature.

    Another trick is to store the subscribers in a map, mapping from the channel we use to distribute messages to them. So use the channel as the key in the map, and then adding and removing the clients is "dead" simple. This is made possible because channel values are comparable, and their comparison is very efficient as channel values are simple pointers to channel descriptors.

    Without further ado, here's a simple broker implementation:

    type Broker struct {
        stopCh    chan struct{}
        publishCh chan interface{}
        subCh     chan chan interface{}
        unsubCh   chan chan interface{}
    }
    
    func NewBroker() *Broker {
        return &Broker{
            stopCh:    make(chan struct{}),
            publishCh: make(chan interface{}, 1),
            subCh:     make(chan chan interface{}, 1),
            unsubCh:   make(chan chan interface{}, 1),
        }
    }
    
    func (b *Broker) Start() {
        subs := map[chan interface{}]struct{}{}
        for {
            select {
            case <-b.stopCh:
                return
            case msgCh := <-b.subCh:
                subs[msgCh] = struct{}{}
            case msgCh := <-b.unsubCh:
                delete(subs, msgCh)
            case msg := <-b.publishCh:
                for msgCh := range subs {
                    // msgCh is buffered, use non-blocking send to protect the broker:
                    select {
                    case msgCh <- msg:
                    default:
                    }
                }
            }
        }
    }
    
    func (b *Broker) Stop() {
        close(b.stopCh)
    }
    
    func (b *Broker) Subscribe() chan interface{} {
        msgCh := make(chan interface{}, 5)
        b.subCh <- msgCh
        return msgCh
    }
    
    func (b *Broker) Unsubscribe(msgCh chan interface{}) {
        b.unsubCh <- msgCh
    }
    
    func (b *Broker) Publish(msg interface{}) {
        b.publishCh <- msg
    }
    

    Example using it:

    func main() {
        // Create and start a broker:
        b := NewBroker()
        go b.Start()
    
        // Create and subscribe 3 clients:
        clientFunc := func(id int) {
            msgCh := b.Subscribe()
            for {
                fmt.Printf("Client %d got message: %v\n", id, <-msgCh)
            }
        }
        for i := 0; i < 3; i++ {
            go clientFunc(i)
        }
    
        // Start publishing messages:
        go func() {
            for msgId := 0; ; msgId++ {
                b.Publish(fmt.Sprintf("msg#%d", msgId))
                time.Sleep(300 * time.Millisecond)
            }
        }()
    
        time.Sleep(time.Second)
    }
    

    Output of the above will be (try it on the Go Playground):

    Client 2 got message: msg#0
    Client 0 got message: msg#0
    Client 1 got message: msg#0
    Client 2 got message: msg#1
    Client 0 got message: msg#1
    Client 1 got message: msg#1
    Client 1 got message: msg#2
    Client 2 got message: msg#2
    Client 0 got message: msg#2
    Client 2 got message: msg#3
    Client 0 got message: msg#3
    Client 1 got message: msg#3
    

    Improvements

    You may consider the following improvements. These may or may not be useful depending on how / to what you use the broker.

    Broker.Unsubscribe() may close the message channel, signalling that no more messages will be sent on it:

    func (b *Broker) Unsubscribe(msgCh chan interface{}) {
        b.unsubCh <- msgCh
        close(msgCh)
    }
    

    This would allow clients to range over the message channel, like this:

    msgCh := b.Subscribe()
    for msg := range msgCh {
        fmt.Printf("Client %d got message: %v\n", id, msg)
    }
    

    Then if someone unsubscribes this msgCh like this:

    b.Unsubscribe(msgCh)
    

    The above range loop will terminate after processing all messages that were sent before the call to Unsubscribe().

    If you want your clients to rely on the message channel being closed, and the broker's lifetime is narrower than your app's lifetime, then you could also close all subscribed clients when the broker is stopped, in the Start() method like this:

    case <-b.stopCh:
        for msgCh := range subs {
            close(msgCh)
        }
        return
    

提交回复
热议问题