
在Go语言中构建网络服务器时,实现优雅关闭是一个常见的需求。这意味着服务器在收到停止信号后,应该停止接受新的连接,并尽可能地处理完现有连接,然后安全退出,避免遗留资源或不必要的错误日志。对于基于net.Listener的TCP服务器,核心的挑战在于listener.Accept()方法会阻塞,直到有新的连接到来或listener被关闭。当listener.Close()被调用时,Accept()会立即返回一个错误,通常是“use of closed network connection”。
问题在于,这个错误信息本身并不直接暴露为可导出的错误类型(如net.ErrClosed),因此我们无法通过类型断言或特定的错误值来判断这是否是预期的关闭错误。如果简单地记录所有Accept()返回的错误,那么在正常关闭服务器时,日志中就会出现一条不必要的“Accept failed: use of closed network connection”信息,这会干扰对真正异常的监控。
考虑以下一个简单的Echo服务器实现,它在关闭时会打印出预期的错误:
package main
import (
"io"
"log"
"net"
"time"
)
// EchoServer 结构体定义了一个简单的Echo服务器
type EchoServer struct {
listen net.Listener
done chan bool
}
// respond 处理单个客户端连接,将接收到的数据原样写回
func (es *EchoServer) respond(remote *net.TCPConn) {
defer remote.Close()
_, err := io.Copy(remote, remote)
if err != nil {
log.Printf("Error handling connection: %s", err)
}
}
// serve 循环监听传入连接
func (es *EchoServer) serve() {
for {
conn, err := es.listen.Accept()
// FIXME: 期望在此处区分“use of closed network connection”错误
// 但该错误不是net包导出的类型
if err != nil {
log.Printf("Accept failed: %v", err) // 正常关闭时会打印此日志
break
}
go es.respond(conn.(*net.TCPConn))
}
es.done <- true // 通知stop方法serve协程已退出
}
// stop 通过关闭监听器来停止服务器
func (es *EchoServer) stop() {
es.listen.Close() // 关闭监听器,导致Accept()返回错误
<-es.done // 等待serve协程退出
}
// NewEchoServer 创建并启动一个新的Echo服务器
func NewEchoServer(address string) *EchoServer {
listen, err := net.Listen("tcp", address)
if err != nil {
log.Fatalf("Failed to open listening socket: %s", err)
}
es := &EchoServer{
listen: listen,
done: make(chan bool), // 无缓冲通道
}
go es.serve()
return es
}
func main() {
log.Println("Starting echo server")
es := NewEchoServer("127.0.0.1:18081")
time.Sleep(1 * time.Second) // 运行服务器1秒
log.Println("Stopping echo server")
es.stop()
log.Println("Server stopped")
}运行上述代码,会得到类似如下的输出:
立即学习“go语言免费学习笔记(深入)”;
2023/10/27 10:00:00 Starting echo server 2023/10/27 10:00:01 Stopping echo server 2023/10/27 10:00:01 Accept failed: accept tcp 127.0.0.1:18081: use of closed network connection 2023/10/27 10:00:01 Server stopped
我们希望在服务器正常关闭时,避免打印“Accept failed”这条日志,因为它并非真正的错误。
为了解决上述问题,我们可以引入一个带缓冲的通道来作为服务器停止的信号。这个通道在stop()方法中被写入,用于预先通知serve()方法,服务器即将关闭。这样,当Accept()返回错误时,serve()可以通过检查这个通道来判断错误是否是由于主动关闭引起的。
核心思路:
下面是修改后的EchoServer实现:
package main
import (
"io"
"log"
"net"
"time"
)
// EchoServer 结构体定义了一个简单的Echo服务器
type EchoServer struct {
listen net.Listener
done chan bool // 修改为带缓冲通道
}
// respond 处理单个客户端连接,将接收到的数据原样写回
func (es *EchoServer) respond(remote *net.TCPConn) {
defer remote.Close()
_, err := io.Copy(remote, remote)
if err != nil {
log.Printf("Error handling connection: %s", err)
}
}
// serve 循环监听传入连接
func (es *EchoServer) serve() {
for {
conn, err := es.listen.Accept()
if err != nil {
select {
case <-es.done:
// 如果能从es.done读取到值,说明stop()已发送关闭信号,
// 此时的Accept错误是预期的“use of closed network connection”,
// 无需打印日志,直接退出。
log.Println("Server listener closed gracefully.")
default:
// 否则,是其他非预期的Accept错误,需要打印日志。
log.Printf("Accept failed unexpectedly: %v", err)
}
return // 退出serve循环
}
go es.respond(conn.(*net.TCPConn))
}
}
// stop 通过关闭监听器来停止服务器
func (es *EchoServer) stop() {
es.done <- true // 1. 先向es.done发送信号,由于是缓冲通道,此处不会阻塞
es.listen.Close() // 2. 关闭监听器,导致Accept()返回错误
// 注意:此处不再需要等待es.done,因为serve协程会在收到信号并处理完Accept错误后自行退出
}
// NewEchoServer 创建并启动一个新的Echo服务器
func NewEchoServer(address string) *EchoServer {
listen, err := net.Listen("tcp", address)
if err != nil {
log.Fatalf("Failed to open listening socket: %s", err)
}
es := &EchoServer{
listen: listen,
done: make(chan bool, 1), // 创建一个容量为1的缓冲通道
}
go es.serve()
return es
}
func main() {
log.Println("Starting echo server")
es := NewEchoServer("127.0.0.1:18081")
time.Sleep(1 * time.Second) // 运行服务器1秒
log.Println("Stopping echo server")
es.stop()
// 在main goroutine中等待一段时间,确保serve goroutine有时间退出
// 实际应用中可能需要更健壮的等待机制,例如使用sync.WaitGroup
time.Sleep(100 * time.Millisecond)
log.Println("Server stopped")
}运行修改后的代码,输出将变为:
2023/10/27 10:00:00 Starting echo server 2023/10/27 10:00:01 Stopping echo server 2023/10/27 10:00:01 Server listener closed gracefully. 2023/10/27 10:00:01 Server stopped
可以看到,预期的“Accept failed: use of closed network connection”错误日志不再出现,取而代之的是我们自定义的优雅关闭提示。
通过巧妙地利用Go语言的缓冲通道和select语句,我们可以实现net.Listener服务器的优雅关闭,避免在正常停止时产生不必要的错误日志。这种模式不仅提升了日志的清晰度,也使得服务器的关闭逻辑更加健壮和易于维护。在构建Go语言网络服务时,理解并应用这种模式对于创建高质量、生产就绪的应用程序至关重要。
以上就是Go语言网络服务器优雅关闭:处理net.Listener.Accept错误的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号