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

Posted

技术标签:

【中文标题】Gorilla Websocket 示例在处理另一个通道时尝试将数据发送到通道时挂起?【英文标题】:Gorilla Websocket example hangs when trying to send data to a channel whilst handling another channel? 【发布时间】:2021-05-12 05:04:39 【问题描述】:

我正在关注 gorilla websocket 库的聊天客户端/服务器示例。

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 案例中的逻辑,但它只是挂起。

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

【问题讨论】:

【参考方案1】:

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

h.broadcast &lt;- []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 例行公事:

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 例程永远不会填满缓冲区。

【讨论】:

是的,一次可以连接多个客户端。您仍然可以通过确保始终在注册前检查广播并确保只有 1 个寄存器可以触发 1 个广播来保持此 1 go 例程。我会更新我的答案以表明我的意思【参考方案2】:

集线器的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)
        
    

【讨论】:

【参考方案3】:

您的通道是无缓冲的,这意味着每次读/写都会阻塞,直到另一个 goroutine 在同一通道上执行相反的操作。

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

【讨论】:

以上是关于Gorilla Websocket 示例在处理另一个通道时尝试将数据发送到通道时挂起?的主要内容,如果未能解决你的问题,请参考以下文章

golang gorilla websocket例子

WebSocket - 关闭握手 Gorilla

go-webSocket——gorilla

golang websocket压测示例

golang websocket压测示例

带有多余通道的大猩猩 websocket 示例? [关闭]