我正在编写一个简单的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
通道将被关闭(使用同步。请确保仅关闭关闭一次):
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
处理紧急情况?但这也看起来很丑。
那么,有没有一种简单直接的方法来完成我想做的事情?
(我的英语不好,所以如果有歧义,请指出,谢谢。)
答案 0 :(得分:4)
您的示例看起来不错,我认为您已经满足了90%的需求。
我认为您看到的问题是发送,实际上您可能已经“完成”了。
您可以使用“完成”频道将已完成的go例程通知所有。您将始终能够从封闭通道中读取一个值(该值为零)。这意味着您可以更新Send(msg)
方法来考虑已完成的频道。
func (connHandler *connHandler) Send(msg string) {
select {
case connHandler.writeChan <- msg:
case <- connHandler.done:
log.Debug("connHandler is done, exiting Send without sending.")
case <-time.After(10 * time.Second):
log.Warning(connHandler.Addr() + " send msg timeout:" + msg)
}
}
此选择现在将发生的事情之一:
writeChan
close(done)
在其他地方已被调用,完成的操作已关闭。您将能够从完成中读取内容,从而打破选择。