web-dev-qa-db-fra.com

Comment diffuser un message en utilisant le canal

Je suis nouveau et j'essaie de créer un serveur de discussion simple où les clients peuvent diffuser des messages à tous les clients connectés. 

Sur mon serveur, j'ai un goroutine (boucle infinie pour) qui accepte la connexion et toutes les connexions sont reçues par un canal. 

go func() {
    for {
        conn, _ := listener.Accept()
        ch <- conn
        }
}()

Ensuite, je lance un gestionnaire (goroutine) pour chaque client connecté. À l'intérieur du gestionnaire, j'essaie de diffuser vers toutes les connexions en effectuant une itération sur le canal. 

for c := range ch {
    conn.Write(msg)
}

Cependant, je ne peux pas émettre parce que (je pense à la lecture de la documentation), le canal doit être fermé avant une itération. Je ne sais pas quand je devrais fermer le canal car je veux accepter continuellement de nouvelles connexions et fermer le canal ne me permet pas de le faire. Si quelqu'un pouvait m'aider ou me fournir un meilleur moyen de diffuser des messages à tous les clients connectés, ce serait apprécié. 

15

Ce que vous faites est un modèle de sortie, c'est-à-dire que plusieurs ordinateurs d'extrémité écoutent une seule source d'entrée. Le résultat de ce modèle est qu'un seul de ces écouteurs sera capable de recevoir le message chaque fois qu'il y a un message dans la source d'entrée. La seule exception est une close de canal. Cette close sera reconnue par tous les auditeurs, et donc une "émission".

Mais ce que vous voulez faire, c'est diffuser un message lu à partir d'une connexion afin que nous puissions faire quelque chose comme ceci:

Quand le nombre d'auditeurs est connu

Laissez chaque travailleur écouter le canal de diffusion dédié et envoyer le message du canal principal à chaque canal de diffusion dédié.

type worker struct {
    source chan interface{}
    quit chan struct{}
}

func (w *worker) Start() {
    w.source = make(chan interface{}, 10) // some buffer size to avoid blocking
    go func() {
        for {
            select {
            case msg := <-w.source
                // do something with msg
            case <-quit: // will explain this in the last section
                return
            }
        }
    }()
}

Et puis nous pourrions avoir un groupe de travailleurs:

workers := []*worker{&worker{}, &worker{}}
for _, worker := range workers { worker.Start() }

Ensuite, démarrez notre auditeur:

go func() {
for {
    conn, _ := listener.Accept()
    ch <- conn
    }
}()

Et un répartiteur:

go func() {
    for {
        msg := <- ch
        for _, worker := workers {
            worker.source <- msg
        }
    }
}()

Quand le nombre d'auditeurs n'est pas connu

Dans ce cas, la solution donnée ci-dessus fonctionne toujours. La seule différence est que, chaque fois que vous avez besoin d'un nouveau travailleur, vous devez en créer un, le démarrer, puis le placer dans la tranche workers. Mais cette méthode nécessite une tranche thread-safe, qui nécessite un verrou autour. L'une des implémentations peut ressembler à ceci:

type threadSafeSlice struct {
    sync.Mutex
    workers []*worker
}

func (slice *threadSafeSlice) Push(w *worker) {
    slice.Lock()
    defer slice.Unlock()

    workers = append(workers, w)
}

func (slice *threadSafeSlice) Iter(routine func(*worker)) {
    slice.Lock()
    defer slice.Unlock()

    for _, worker := range workers {
        routine(worker)
    }
}

Chaque fois que vous souhaitez démarrer un travailleur:

w := &worker{}
w.Start()
threadSafeSlice.Push(w)

Et votre répartiteur sera remplacé par:

go func() {
    for {
        msg := <- ch
        threadSafeSlice.Iter(func(w *worker) { w.source <- msg })
    }
}()

Derniers mots: ne jamais laisser un goroutine pendant

Une des bonnes pratiques est de ne jamais laisser un goroutine pendant. Ainsi, lorsque vous avez fini d'écouter, vous devez fermer toutes les goroutines que vous avez congédiées. Cela se fera via le canal quit dans worker:

Nous devons d’abord créer un canal de signalisation quit global:

globalQuit := make(chan struct{})

Et chaque fois que nous créons un ouvrier, nous lui affectons le canal globalQuit comme signal de sortie:

worker.quit = globalQuit

Ensuite, lorsque nous voulons arrêter tous les travailleurs, nous faisons simplement:

close(globalQuit)

Puisque close sera reconnu par toutes les goroutines à l’écoute (c’est le point que vous avez compris), toutes les goroutines seront renvoyées. N'oubliez pas de fermer votre routine de répartiteur également, mais je vous le laisserai :)

30
nevets

Une solution plus élégante est un "courtier", dans lequel les clients peuvent s'abonner et se désinscrire des messages.

Pour gérer également les abonnements et les désabonnements avec élégance, nous pouvons utiliser des canaux à cet effet. Ainsi, la boucle principale du courtier qui reçoit et distribue les messages peut incorporer tout cela à l'aide d'une seule instruction select et la synchronisation est donnée à partir de la nature de la solution.

Une autre astuce consiste à stocker les abonnés dans une carte, à partir du canal que nous utilisons pour leur distribuer des messages. Utilisez donc le canal comme clé dans la carte, puis ajouter et supprimer des clients est une opération "morte". Cela est rendu possible par le fait que les valeurs de canal sont comparables , et que leur comparaison est très efficace car les valeurs de canal sont de simples pointeurs sur des descripteurs de canal.

Sans plus tarder, voici une implémentation simple de courtier:

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
}

Exemple d'utilisation:

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)
}

La sortie de ce qui précède sera (essayez sur le 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

Améliorations

Vous pouvez envisager les améliorations suivantes. Ceux-ci peuvent ou peuvent ne pas être utiles selon comment/à quoi vous utilisez le courtier.

Broker.Unsubscribe() peut fermer le canal de message en signalant qu'aucun autre message ne sera envoyé dessus:

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

Cela permettrait aux clients de range sur le canal de message, comme ceci:

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

Ensuite, si quelqu'un se désabonne de cette msgCh comme ceci:

b.Unsubscribe(msgCh)

La boucle de plage ci-dessus se terminera après le traitement de tous les messages envoyés avant l'appel à Unsubscribe().

Si vous souhaitez que vos clients comptent sur la fermeture du canal de message et que la durée de vie du courtier est plus étroite que celle de votre application, vous pouvez également fermer tous les clients abonnés lorsque le courtier est arrêté, selon la méthode Start() suivante:

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

Diffuser sur une tranche de canal et utiliser sync.Mutex pour gérer l’ajout et la suppression de canaux peut être le moyen le plus simple dans votre cas.

Voici ce que vous pouvez faire pour broadcast in golang:

  • Vous pouvez diffuser un changement de statut de partage avec sync.Cond. De cette façon, ne pas allouer une fois l'installation, mais vous ne pouvez pas ajouter de délai d'attente fonctionnel ou travailler avec un autre canal.
  • Vous pouvez diffuser un changement de statut de partage avec une ancienne chaîne proche et créer une nouvelle chaîne et sync.Mutex. De cette façon, vous affectez un changement par statut, mais vous pouvez ajouter un délai d’attente fonctionnel et utiliser un autre canal.
  • Vous pouvez diffuser sur une tranche de rappel de fonction et utiliser sync.Mutex pour les gérer. L'appelant peut faire des choses sur le canal. De cette façon, vous disposez de plus d’une allocation par appelant et travaillez avec un autre canal.
  • Vous pouvez diffuser sur une tranche de chaîne et utiliser sync.Mutex pour les gérer. De cette façon, vous disposez de plus d’une allocation par appelant et travaillez avec un autre canal.
  • Vous pouvez diffuser sur une tranche de sync.WaitGroup et utiliser sync.Mutex pour les gérer.
2
bronze man

Comme les canaux Go suivent le modèle de processus de communication séquentielle (CSP), les canaux constituent une entité de communication point à point. Il y a toujours un écrivain et un lecteur impliqués dans chaque échange.

Cependant, chaque canal end peut être shared entre plusieurs goroutines. C'est sûr à faire - il n'y a pas de condition de course dangereuse.

Il peut donc y avoir plusieurs auteurs partageant la fin de l'écriture. Et/ou il peut y avoir plusieurs lecteurs partageant la fin de la lecture. J'ai écrit plus à ce sujet dans un réponse différente , qui inclut des exemples.

Si vous avez vraiment besoin d'une diffusion, vous ne pouvez pas le faire directement, mais il n'est pas difficile d'implémenter un goroutine intermédiaire qui copie une valeur sur chacun des groupes de canaux de sortie.

0
Rick-777