【发布时间】:2018-12-10 09:22:12
【问题描述】:
我正在写一个简单的 tcp 服务器,goroutine 模型非常简单:
一个goroutine负责接受新的连接;对于每个新连接,都会启动三个 goroutine:
- 供阅读
- 一个用于处理和处理应用程序逻辑
- 一个写
目前一台服务器服务的用户不超过 1000 个,所以我不尝试限制 goroutine 数量。
for {
conn, err := listener.Accept()
// ....
connHandler := connHandler{
conn: conn,
done: make(chan struct{}),
readChan: make(chan string, 100),
writeChan: make(chan string, 100),
}
// ....
go connHandler.readAll()
go connHandler.processAll()
go connHandler.writeAll()
}
我使用done通道通知所有三个通道完成,当用户注销或发生永久性网络错误时,done通道将被关闭(使用sync.Once确保关闭只发生一次):
func (connHandler *connHandler) Close() {
connHandler.doOnce.Do(func() {
connHandler.isClosed = true
close(connHandler.done)
})
}
下面是writeAll()方法的代码:
func (connHandler *connHandler) writeAll() {
writer := bufio.NewWriter(connHandler.conn)
for {
select {
case <-connHandler.done:
connHandler.conn.Close()
return
case msg := <-connHandler.writeChan:
connHandler.writeOne(msg, writer)
}
}
}
有一个Send 方法可以通过向写入通道发送字符串来向用户发送消息:
func (connHandler *connHandler) Send(msg string) {
case connHandler.writeChan <- msg:
}
Send 方法将主要在processAll() goroutine 中调用,但也会在许多其他 goroutine 中调用,因为不同的用户需要相互通信。
现在的问题是:如果用户 A 注销或网络失败,用户 B 向用户 A 发送消息,用户 B 的 goroutine 可能会被永久阻塞,因为没有人会收到来自频道的消息。
我的解决方案:
我的第一个想法是使用布尔值来确保 connHanler 在发送给它时没有关闭:
func (connHandler *connHandler) Send(msg string) {
if !connHandler.isClosed {
connHandler.writeChan <- msg
}
}
但是我觉得connHandler.writeChan <- msg和close(done)还是可以同时发生的,阻塞的可能性还是存在的。所以我必须添加一个超时:
func (connHandler *connHandler) Send(msg string) {
if !connHandler.isClosed {
timer := time.NewTimer(10 * time.Second)
defer timer.Stop()
select {
case connHandler.writeChan <- msg:
case <-timer.C:
log.Warning(connHandler.Addr() + " send msg timeout:" + msg)
}
}
}
现在我觉得代码很安全,但也很丑,每次发送消息时都启动一个计时器,感觉像是不必要的开销。
然后我看了这篇文章:https://go101.org/article/channel-closing.html,我的问题看起来像文章中的第二个例子:
一个接收者,N个发送者,接收者说“请停止发送更多” 通过关闭额外的信号通道
但我认为这种解决方案并不能消除我这种情况下阻塞的可能性。
也许最简单的解决方案是关闭写入通道并让Send 方法恐慌,然后使用recover 处理恐慌?但这看起来也很丑陋。
那么有没有一种简单直接的方法来完成我想做的事情?
(本人英文不好,如有歧义,请指出,谢谢。)
【问题讨论】:
-
在您的
Send()中,也许您可以在接收用户B 的connHandler.done上select。