私は初めてで、クライアントが接続されているすべてのクライアントにメッセージをブロードキャストできるシンプルなチャットサーバーを作成しようとしています。
私のサーバーには、接続を受け入れるゴルーチン(無限ループ)があり、すべての接続がチャネルによって受信されます。
go func() {
for {
conn, _ := listener.Accept()
ch <- conn
}
}()
次に、接続されているすべてのクライアントのハンドラー(goroutine)を開始します。ハンドラー内で、チャネルを反復処理することにより、すべての接続にブロードキャストしようとします。
for c := range ch {
conn.Write(msg)
}
ただし、(ドキュメントを読んだことから考えて)反復する前にチャネルを閉じる必要があるため、ブロードキャストできません。新しい接続を継続的に受け入れたいので、いつチャネルを閉じる必要があるかわかりません。チャネルを閉じても、それはできません。誰かが私を助けてくれたり、接続されているすべてのクライアントにメッセージをブロードキャストするより良い方法を提供してくれたら、ありがたいです。
あなたがしているのは、ファンアウトパターンです。つまり、複数のエンドポイントが単一の入力ソースをリッスンしています。このパターンの結果、入力ソースにメッセージがあるときはいつでもこれらのリスナーの1つだけがメッセージを取得できます。唯一の例外は、チャンネルのclose
です。このclose
は、すべてのリスナーによって認識されるため、「ブロードキャスト」になります。
しかし、あなたがやりたいのは、接続から読み取られたメッセージをブロードキャストすることです。したがって、次のようなことができます。
各ワーカーに専用のブロードキャストチャネルを聞いてもらい、メインチャネルから各専用のブロードキャストチャネルにメッセージをディスパッチします。
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
}
}
}()
}
そして、私たちはたくさんの労働者を持つことができます:
workers := []*worker{&worker{}, &worker{}}
for _, worker := range workers { worker.Start() }
次に、リスナーを開始します。
go func() {
for {
conn, _ := listener.Accept()
ch <- conn
}
}()
そしてディスパッチャ:
go func() {
for {
msg := <- ch
for _, worker := workers {
worker.source <- msg
}
}
}()
この場合、上記のソリューションは引き続き機能します。唯一の違いは、新しいワーカーが必要なときはいつでも、新しいワーカーを作成して起動し、workers
スライスにプッシュする必要があることです。ただし、このメソッドにはスレッドセーフスライスが必要であり、その周囲にロックが必要です。実装の1つは次のようになります。
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)
}
}
ワーカーを開始したいときはいつでも:
w := &worker{}
w.Start()
threadSafeSlice.Push(w)
また、ディスパッチャは次のように変更されます。
go func() {
for {
msg := <- ch
threadSafeSlice.Iter(func(w *worker) { w.source <- msg })
}
}()
良い習慣の1つは、ぶら下がりゴルーチンを残さないことです。したがって、リスニングが終了したら、解雇したゴルーチンをすべて閉じる必要があります。これは、quit
のworker
チャネルを介して行われます。
まず、グローバルquit
シグナリングチャネルを作成する必要があります。
globalQuit := make(chan struct{})
そして、ワーカーを作成するたびに、globalQuit
チャネルを終了シグナルとして割り当てます。
worker.quit = globalQuit
その後、すべてのワーカーをシャットダウンする場合は、次のようにします。
close(globalQuit)
close
はすべてのリスニングゴルーチン(これがあなたが理解したポイントです)によって認識されるため、すべてのゴルーチンが返されます。ディスパッチャルーチンも忘れずに閉じてください。
よりエレガントなソリューションは、クライアントがメッセージを購読および購読解除できる「ブローカー」です。
サブスクライブとサブスクライブ解除もエレガントに処理するために、このためにチャネルを利用できます。そのため、メッセージを受信および配信するブローカーのメインループは、単一のselect
ステートメントを使用してこれらすべてを組み込み、同期はソリューションの自然。
もう1つの方法は、メッセージを配信するために使用するチャネルからマッピングして、サブスクライバーをマップに格納することです。そのため、チャネルをマップのキーとして使用すると、クライアントの追加と削除は「簡単」になります。これは、チャネル値が 比較可能 であり、チャネル値がチャネル記述子への単純なポインタであるため、比較が非常に効率的であるため可能になります。
苦労せずに、簡単なブローカーの実装を次に示します。
_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
}
_
それを使用した例:
_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)
}
_
上記の出力は( 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
_
次の改善点を検討できます。これらは、ブローカーをどのように/どのように使用するかに応じて、有用な場合とそうでない場合があります。
Broker.Unsubscribe()
はメッセージチャネルを閉じ、これ以上メッセージが送信されないことを通知します。
_func (b *Broker) Unsubscribe(msgCh chan interface{}) {
b.unsubCh <- msgCh
close(msgCh)
}
_
これにより、クライアントは次のようにメッセージチャネルを介してrange
できるようになります。
_msgCh := b.Subscribe()
for msg := range msgCh {
fmt.Printf("Client %d got message: %v\n", id, msg)
}
_
次に、誰かがこのmsgCh
の購読を解除すると、次のようになります。
_b.Unsubscribe(msgCh)
_
上記の範囲ループは、Unsubscribe()
の呼び出しの前に送信されたすべてのメッセージを処理した後に終了します。
クライアントが閉じられているメッセージチャネルに依存し、ブローカーの有効期間がアプリの有効期間よりも短い場合、ブローカーが停止したときに、Start()
メソッドですべてのサブスクライブされたクライアントを閉じることもできますこの:
_case <-b.stopCh:
for msgCh := range subs {
close(msgCh)
}
return
_
チャンネルのスライスにブロードキャストし、sync.Mutexを使用してチャンネルの追加と削除を管理するのが、あなたの場合の最も簡単な方法かもしれません。
Golangでbroadcast
にできることは次のとおりです。
Goチャネルは、Communicating Sequential Processes(CSP)パターンに従うため、チャネルはポイントツーポイント通信エンティティです。各交換には、常に1人のライターと1人のリーダーが関与します。
ただし、各チャネルendは、複数のゴルーチン間でsharedにできます。これは安全です-危険な競合状態はありません。
したがって、複数の作家が執筆の終わりを共有することができます。また、複数のリーダーが読み取り終了を共有する場合があります。これについては、例を含む 異なる回答 で詳しく説明しました。
本当にブロードキャストが必要な場合、これを直接行うことはできませんが、出力チャネルの各グループに値をコピーする中間ゴルーチンを実装するのは難しくありません。
チャンネル放送をサポートするシンプルなライブラリを作成しました。これを使用して、多くの最新のサービスAPI呼び出しからの非同期結果をブロードキャストできます。 https://github.com/guiguan/caster#broadcast-a-go-channel