Gorilla Websocket 示例在处理另一个通道的同时尝试将数据发送到通道时挂起?

Gorilla Websocket example hangs when trying to send data to a channel whilst handling another channel?

我正在关注 gorilla websocket 库的聊天 client/server 示例。

https://github.com/gorilla/websocket/blob/master/examples/chat/hub.go#L36

我尝试修改代码以在新客户端连接时通知其他客户端,如下所示:

    for {
        select {
        case client := <-h.register:
            h.clients[client] = true

            // My addition. Hangs after this (no further register/unregister events are processed):
            h.broadcast <- []byte("Another client connected!")
        case client := <-h.unregister:
            if _, ok := h.clients[client]; ok {
                delete(h.clients, client)
                close(client.send)
            }
        case message := <-h.broadcast:
            for client := range h.clients {
                select {
                case client.send <- message:
                default:
                    close(client.send)
                    delete(h.clients, client)
                }
            }
        }
    }
}

我的理解是外for循环的下一次迭代,广播频道应该接收数据并遵循message情况下的逻辑,但它只是挂起。

为什么?我找不到任何理由。没有进一步的通道事件被处理(register/unregister 或广播上没有任何事件),这让我认为它是某种无缓冲的通道机制,但我不明白如何?

你的通道是无缓冲的,这意味着每个 read/write 阻塞,直到另一个 goroutine 在同一个通道上执行相反的操作。

当您尝试写入 h.broadcast 时,goroutine 停止,等待 reader。但是同一个 goroutine 应该充当这个通道的 reader,这永远不会发生,因为 goroutine 被写入阻塞了。于是程序死锁。

是的,这行不通。您不能 send/receive 在同一个 go 例程中的同一个无缓冲通道上。

h.broadcast <- []byte("Another client connected!") 阻塞,直到另一个 go 例程从队列中弹出。一个简单的解决方案是使 broadcast 通道具有长度为 1 的缓冲区。 broadcast := make(chan []byte, 1)

你可以在这个playground例子中看到它

    // c := make(chan int) <- This will hang
    c := make(chan int, 1)
    c <- 1
    fmt.Println(<-c)

解除长度为1的缓冲区和整个系统的死锁。您可以 运行 解决的一个问题是,如果 2 个客户端同时注册,那么您可能会遇到 2 个项目试图被塞入 broadcast 频道的情况,我们又回到了无缓冲通道也有同样的问题。你可以避免这种情况并像这样保持 1 go 例行程序:

for {
    select { 
        case message := <-h.broadcast:
            // ...
        default:
    }

    select { // This select statement can only add 1 item to broadcast at most
        case client := <-h.register:
            // ...
            h.broadcast <- []byte("Another client connected!")
        }
    }
}

但是,如果另一个 go 例程也添加到 broadcast 频道,这仍然会中断。所以我会选择 Cerise Limon 的解决方案,或者对通道进行足够的缓冲,以使其他 go 例程永远不会填充缓冲区。

集线器的 broadcast 通道是无缓冲的。无缓冲通道上的通信等待就绪的发送方和就绪的接收方。 hub goroutine阻塞是因为goroutine不能同时准备好发送和接收。

将频道从无缓冲频道更改为有缓冲频道并不能解决问题。考虑缓冲容量为1的情况:

return &Hub{
    broadcast:  make(chan []byte, 1),
    ...
}

这个时间轴:

1 clientA: client.hub.register <- client 
2 clientB: c.hub.broadcast <- message
3 hub:     case client := <-h.register:
4 hub:     h.broadcast <- []byte("Another client connected!")

集线器在 #4 阻塞,因为通道在 #2 已满。将频道容量增加到两个或更多并不能解决问题,因为任何数量的客户端都可以在另一个客户端注册时广播消息。

要解决此问题,请将广播代码移至一个函数并从 select 中的两种情况调用该函数:

// sendAll sends message to all registered clients.
// This method must only be called by Hub.run.
func (h *Hub) sendAll(message []byte) {
    for client := range h.clients {
        select {
        case client.send <- message:
        default:
            close(client.send)
            delete(h.clients, client)
        }
    }
}

func (h *Hub) run() {
    for {
        select {
        case client := <-h.register:
            h.clients[client] = true
            h.sendAll([]byte("Another client connected!"))
        case client := <-h.unregister:
            if _, ok := h.clients[client]; ok {
                delete(h.clients, client)
                close(client.send)
            }
        case message := <-h.broadcast:
            h.sendAll(message)
        }
    }
}