如何在Go中停止一个监听服务器

23

我一直在尝试寻找一种优雅地停止Go中的监听服务器的方法。由于listen.Accept会阻塞,因此关闭侦听套接字以发出结束信号是必要的,但我无法将该相关错误与其他错误区分开来,因为该错误未导出。

我能做得比这更好吗?请参见下面代码中serve()中的FIXME

package main

import (
    "io"
    "log"
    "net"
    "time"
)

// Echo server struct
type EchoServer struct {
    listen net.Listener
    done   chan bool
}

// Respond to incoming connection
//
// Write the address connected to then echo
func (es *EchoServer) respond(remote *net.TCPConn) {
    defer remote.Close()
    _, err := io.Copy(remote, remote)
    if err != nil {
        log.Printf("Error: %s", err)
    }
}

// Listen for incoming connections
func (es *EchoServer) serve() {
    for {
        conn, err := es.listen.Accept()
        // FIXME I'd like to detect "use of closed network connection" here
        // FIXME but it isn't exported from net
        if err != nil {
            log.Printf("Accept failed: %v", err)
            break
        }
        go es.respond(conn.(*net.TCPConn))
    }
    es.done <- true
}

// Stop the server by closing the listening listen
func (es *EchoServer) stop() {
    es.listen.Close()
    <-es.done
}

// Make a new echo server
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
}

// Main
func main() {
    log.Println("Starting echo server")
    es := NewEchoServer("127.0.0.1:18081")
    // Run the server for 1 second
    time.Sleep(1 * time.Second)
    // Close the server
    log.Println("Stopping echo server")
    es.stop()
}

这将打印

2012/11/16 12:53:35 Starting echo server
2012/11/16 12:53:36 Stopping echo server
2012/11/16 12:53:36 Accept failed: accept tcp 127.0.0.1:18081: use of closed network connection

我想隐藏“接受失败”的消息,但显然我不想掩盖其他可能会被报告的错误。当然,我可以在错误检查中查找“使用关闭的网络连接”,但那样很丑陋。如果可能的话,我可以设置一个标志,表示我即将关闭并忽略错误——是否有更好的方法?

我认为这是一个很好的解释。https://forum.golangbridge.org/t/correct-shutdown-of-net-listener/8705 - Karimai
4个回答

17

我会使用 es.done 在关闭连接之前发送一个信号来处理这个问题。除了以下代码,您需要使用 make(chan bool, 1) 来创建 es.done,以便我们可以在其中放置一个单一的值而不会阻塞。

// Listen for incoming connections
func (es *EchoServer) serve() {
  for {
    conn, err := es.listen.Accept()
    if err != nil {
      select {
      case <-es.done:
        // If we called stop() then there will be a value in es.done, so
        // we'll get here and we can exit without showing the error.
      default:
        log.Printf("Accept failed: %v", err)
      }
      return
    }
    go es.respond(conn.(*net.TCPConn))
  }
}

// Stop the server by closing the listening listen
func (es *EchoServer) stop() {
  es.done <- true   // We can advance past this because we gave it buffer of 1
  es.listen.Close() // Now it the Accept will have an error above
}

嗯,好主意。我认为使用bool跟使用通道差不多,这基本上就是我想出来的解决方案。不过你仍然需要通道来将stopserve同步,以便你知道它何时停止。 - Nick Craig-Wood
6
不必缓冲通道或向通道发送消息,只需关闭它即可。 - Dustin

8

accept()调用后立即检查一些“是否停止”的标志,然后从您的main中切换它,然后连接到侦听端口以使服务器套接字“解除阻塞”。这与旧的“self-pipe trick”非常相似。


1
这是一个不错的想法!但是,由于真实连接的竞态条件存在,您需要解决这个问题。 - Nick Craig-Wood
可以尝试反过来 - 从一开始就始终保持单个内部连接,并将其用作控制通道。是的,没错。 - Nikolai Fetissov
太棒了,又简单! - Prisacari Dmitrii

2

希望以下内容对您有所帮助:

在这种情况下,以下内容可能会有所作用。
// Listen for incoming connections
func (es *EchoServer) serve() {
        for {
                conn, err := es.listen.Accept()
                if err != nil {
                    if x, ok := err.(*net.OpError); ok && x.Op == "accept" { // We're done
                            log.Print("Stoping")
                            break
                    }

                    log.Printf("Accept failed: %v", err)
                    continue
                }
                go es.respond(conn.(*net.TCPConn))
        }
        es.done <- true
}

好主意,谢谢!但我不确定它是否能区分正式停止和 Syscall.Accept() 返回错误(我最近见过的一个例子是进程用完了套接字)。 - Nick Craig-Wood
不知道,我得试一下/实验一下。 - zzzz
很好的例子,但是x.Op不应该与close进行比较吗,例如&& x.Op == "close"。如果我们使用listener.Close()停止监听器,似乎会将close放入错误操作中。https://godoc.org/net#TCPListener.Close - luben

-7

这是一个简单的方法,对于本地开发来说已经足够好了。

http://www.sergiotapia.me/how-to-stop-your-go-http-server/


package main

import (  
    "net/http"
    "os"

    "github.com/bmizerany/pat"
)

var mux = pat.New()

func main() {  
    mux.Get("/kill", http.HandlerFunc(kill))
    http.Handle("/", mux)
    http.ListenAndServe(":8080", nil)
}

func kill(w http.ResponseWriter, r *http.Request) {  
    os.Exit(0)
}

3
退出整个进程并不是关闭服务器的有效方式! - Setomidor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接