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 <- []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 示例在处理另一个通道时尝试将数据发送到通道时挂起?的主要内容,如果未能解决你的问题,请参考以下文章