web-dev-qa-db-ja.com

チャネルを使用してメッセージをブロードキャストする方法

私は初めてで、クライアントが接続されているすべてのクライアントにメッセージをブロードキャストできるシンプルなチャットサーバーを作成しようとしています。

私のサーバーには、接続を受け入れるゴルーチン(無限ループ)があり、すべての接続がチャネルによって受信されます。

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

次に、接続されているすべてのクライアントのハンドラー(goroutine)を開始します。ハンドラー内で、チャネルを反復処理することにより、すべての接続にブロードキャストしようとします。

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

ただし、(ドキュメントを読んだことから考えて)反復する前にチャネルを閉じる必要があるため、ブロードキャストできません。新しい接続を継続的に受け入れたいので、いつチャネルを閉じる必要があるかわかりません。チャネルを閉じても、それはできません。誰かが私を助けてくれたり、接続されているすべてのクライアントにメッセージをブロードキャストするより良い方法を提供してくれたら、ありがたいです。

19

あなたがしているのは、ファンアウトパターンです。つまり、複数のエンドポイントが単一の入力ソースをリッスンしています。このパターンの結果、入力ソースにメッセージがあるときはいつでもこれらのリスナーの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つは、ぶら下がりゴルーチンを残さないことです。したがって、リスニングが終了したら、解雇したゴルーチンをすべて閉じる必要があります。これは、quitworkerチャネルを介して行われます。

まず、グローバルquitシグナリングチャネルを作成する必要があります。

globalQuit := make(chan struct{})

そして、ワーカーを作成するたびに、globalQuitチャネルを終了シグナルとして割り当てます。

worker.quit = globalQuit

その後、すべてのワーカーをシャットダウンする場合は、次のようにします。

close(globalQuit)

closeはすべてのリスニングゴルーチン(これがあなたが理解したポイントです)によって認識されるため、すべてのゴルーチンが返されます。ディスパッチャルーチンも忘れずに閉じてください。

35
nevets

よりエレガントなソリューションは、クライアントがメッセージを購読および購読解除できる「ブローカー」です。

サブスクライブとサブスクライブ解除もエレガントに処理するために、このためにチャネルを利用できます。そのため、メッセージを受信および配信するブローカーのメインループは、単一の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
_
12
icza

チャンネルのスライスにブロードキャストし、sync.Mutexを使用してチャンネルの追加と削除を管理するのが、あなたの場合の最も簡単な方法かもしれません。

Golangでbroadcastにできることは次のとおりです。

  • Sync.Condを使用して、共有ステータスの変更をブロードキャストできます。この方法では、一度セットアップされたallocはありませんが、タイムアウト機能を追加したり、別のチャネルで作業したりすることはできません。
  • 古いチャネルを閉じて共有ステータスの変更をブロードキャストし、新しいチャネルとsync.Mutexを作成できます。この方法では、ステータスの変更ごとに1つのallocがありますが、タイムアウト機能を追加して、別のチャネルを操作できます。
  • 関数コールバックのスライスにブロードキャストし、sync.Mutexを使用してそれらを管理できます。発信者はチャンネルのことをすることができます。この方法では、呼び出し元ごとに複数のallocがあり、別のチャネルで機能します。
  • チャンネルのスライスにブロードキャストし、sync.Mutexを使用してそれらを管理できます。この方法では、呼び出し元ごとに複数のallocがあり、別のチャネルで機能します。
  • Sync.WaitGroupのスライスにブロードキャストし、sync.Mutexを使用してそれらを管理できます。
3
bronze man

Goチャネルは、Communicating Sequential Processes(CSP)パターンに従うため、チャネルはポイントツーポイント通信エンティティです。各交換には、常に1人のライターと1人のリーダーが関与します。

ただし、各チャネルendは、複数のゴルーチン間でsharedにできます。これは安全です-危険な競合状態はありません。

したがって、複数の作家が執筆の終わりを共有することができます。また、複数のリーダーが読み取り終了を共有する場合があります。これについては、例を含む 異なる回答 で詳しく説明しました。

本当にブロードキャストが必要な場合、これを直接行うことはできませんが、出力チャネルの各グループに値をコピーする中間ゴルーチンを実装するのは難しくありません。

0
Rick-777

チャンネル放送をサポートするシンプルなライブラリを作成しました。これを使用して、多くの最新のサービスAPI呼び出しからの非同期結果をブロードキャストできます。 https://github.com/guiguan/caster#broadcast-a-go-channel

0
Guan Gui