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é.
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:
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
}
}
}()
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 })
}
}()
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 :)
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
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
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:
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.