How to stop http.ListenAndServe()

我正在使用来自 Gorilla Web Toolkit 的 Mux 库以及捆绑的 Go http 服务器。

The problem is that in my application the HTTP server is only one component and it is required to stop and start at my discretion.

当我调用 http.ListenAndServe(fmt.Sprintf(":%d", service.Port()), service.router)它阻塞,我似乎不能停止服务器运行。

I am aware this has been a problem in the past, is that still the case? Are there any new solutions?

105808 次浏览

You can construct net.Listener

l, err := net.Listen("tcp", fmt.Sprintf(":%d", service.Port()))
if err != nil {
log.Fatal(err)
}

你可以 Close()

go func(){
//...
l.Close()
}()

还有 http.Serve()

http.Serve(l, service.router)

正如在 yo.ian.g的回答中所提到的,Go 1.8在标准库中包含了这个功能。

Go 1.8+的最小示例:

    server := &http.Server{Addr: ":8080", Handler: handler}


go func() {
if err := server.ListenAndServe(); err != nil {
// handle err
}
}()


// Setting up signal capturing
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt)


// Waiting for SIGINT (kill -2)
<-stop


ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
// handle err
}


// Wait for ListenAndServe goroutine to close.

您可以使用 kill -2 <pid>优雅地终止服务器


Original Answer - Pre Go 1.8 :

基于 Uvelichitel 的的答案。

您可以创建自己的 ListenAndServe版本,它返回 io.Closer并且不阻塞。

func ListenAndServeWithClose(addr string, handler http.Handler) (io.Closer,error) {


var (
listener  net.Listener
srvCloser io.Closer
err       error
)


srv := &http.Server{Addr: addr, Handler: handler}


if addr == "" {
addr = ":http"
}


listener, err = net.Listen("tcp", addr)
if err != nil {
return nil, err
}


go func() {
err := srv.Serve(tcpKeepAliveListener{listener.(*net.TCPListener)})
if err != nil {
log.Println("HTTP Server Error - ", err)
}
}()
    

srvCloser = listener
return srvCloser, nil
}

完整代码可用 给你

HTTP 服务器将以错误关闭 accept tcp [::]:8080: use of closed network connection

去1.8将包括优雅和强有力的关闭,可通过 Server::Shutdown(context.Context)Server::Close()分别。

go func() {
httpError := srv.ListenAndServe(address, handler)
if httpError != nil {
log.Println("While serving HTTP: ", httpError)
}
}()


srv.Shutdown(context)

可以找到相关的提交 给你

关于优雅的关闭(在 Go 1.8中引入) ,一个更具体的例子:

package main


import (
"context"
"io"
"log"
"net/http"
"sync"
"time"
)


func startHttpServer(wg *sync.WaitGroup) *http.Server {
srv := &http.Server{Addr: ":8080"}


http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "hello world\n")
})


go func() {
defer wg.Done() // let main know we are done cleaning up


// always returns error. ErrServerClosed on graceful close
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
// unexpected error. port in use?
log.Fatalf("ListenAndServe(): %v", err)
}
}()


// returning reference so caller can call Shutdown()
return srv
}


func main() {
log.Printf("main: starting HTTP server")


httpServerExitDone := &sync.WaitGroup{}


httpServerExitDone.Add(1)
srv := startHttpServer(httpServerExitDone)


log.Printf("main: serving for 10 seconds")


time.Sleep(10 * time.Second)


log.Printf("main: stopping HTTP server")


// now close the server gracefully ("shutdown")
// timeout could be given with a proper context
// (in real world you shouldn't use TODO()).
if err := srv.Shutdown(context.TODO()); err != nil {
panic(err) // failure/timeout shutting down the server gracefully
}


// wait for goroutine started in startHttpServer() to stop
httpServerExitDone.Wait()


log.Printf("main: done. exiting")
}

对于应用程序只是服务器而不执行其他功能的情况,我所做的就是为类似于 /shutdown的模式安装 http.HandleFunc。差不多

http.HandleFunc("/shutdown", func(w http.ResponseWriter, r *http.Request) {
if <credentials check passes> {
// - Turn on mechanism to reject incoming requests.
// - Block until "in-flight" requests complete.
// - Release resources, both internal and external.
// - Perform all other cleanup procedures thought necessary
//   for this to be called a "graceful shutdown".
fmt.Fprint(w, "Goodbye!\n")
os.Exit(0)
}
})

不需要1.8。但是如果1.8是可用的,那么我相信那个解决方案可以嵌入在这里,而不是需要的 os.Exit(0)调用。

执行所有这些清理工作的代码留给读者作为练习。

如果您能够说明清理代码可能放在最合理的位置,那么就更加值得称赞了,因为我不建议在这里这样做,以及这个端点命中应该如何导致该代码的调用。

如果您能够说出 os.exit(0)调用(或者您选择使用的任何进程退出)的位置(这里只是为了说明目的而给出) ,那么将会获得更多的额外学分。

Yet even more extra credit if you can explain why 这个 mechanism of HTTP server process signaling should be considered above all other such mechanisms thought workable in this case.

因为之前的答案都没有说明为什么如果使用 http 就不能这样做。ListenAndServe () ,我查看了1.8版本的 http 源代码,它是这样说的:

func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}

正如您可以看到的 http。函数不返回服务器变量。这意味着您无法到达“服务器”使用“关闭”命令。因此,您需要创建自己的“服务器”实例,而不是使用这个函数来实现优雅的关机。

您可以通过关闭服务器的上下文来关闭它。

type ServeReqs func(ctx context.Context, cfg Config, deps ReqHandlersDependencies) error


var ServeReqsImpl = func(ctx context.Context, cfg Config, deps ReqHandlersDependencies) error {
http.Handle(pingRoute, decorateHttpRes(pingHandlerImpl(deps.pingRouteResponseMessage), addJsonHeader()))


server := &http.Server{Addr: fmt.Sprintf(":%d", cfg.port), Handler: nil}


go func() {
<-ctx.Done()
fmt.Println("Shutting down the HTTP server...")
server.Shutdown(ctx)
}()


err := server.ListenAndServeTLS(
cfg.certificatePemFilePath,
cfg.certificatePemPrivKeyFilePath,
)


// Shutting down the server is not something bad ffs Go...
if err == http.ErrServerClosed {
return nil
}


return err
}

无论何时你准备关闭它,打电话:

ctx, closeServer := context.WithCancel(context.Background())
err := ServeReqs(ctx, etc)
closeServer()

可以使用 context.Contextnet.ListenConfig来解决这个问题。在我的例子中,我不想使用 sync.WaitGrouphttp.ServerShutdown()调用,而是依赖于 context.Context(它是通过信号关闭的)。

import (
"context"
"http"
"net"
"net/http/pprof"
)


func myListen(ctx context.Context, cancel context.CancelFunc) error {
lc := net.ListenConfig{}
ln, err := lc.Listen(ctx, "tcp4", "127.0.0.1:6060")
if err != nil {
// wrap the err or log why the listen failed
return err
}


mux := http.NewServeMux()
mux.Handle("/debug/pprof/", pprof.Index)
mux.Handle("/debug/pprof/cmdline", pprof.CmdLine)
mux.Handle("/debug/pprof/profile", pprof.Profile)
mux.Handle("/debug/pprof/symbol", pprof.Symbol)
mux.Handle("/debug/pprof/trace", pprof.Trace)


go func() {
if err := http.Serve(l, mux); err != nil {
cancel()
// log why we shut down the context
return err
}
}()


// If you want something semi-synchronous, sleep here for a fraction of a second


return nil
}

当您不希望您的主服务器在单独的 goroutine 中运行时,可重现的示例:

main.go:

package main


import (
"context"
"log"
"net/http"
"os"
"os/signal"
"sync"
"time"
)


func main() {
http.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
// wait for 10 seconds before sending OK
time.Sleep(10 * time.Second)
_, _ = w.Write([]byte("OK\n"))
})
server := &http.Server{Addr: ":3333", Handler: nil}


// Creating a waiting group that waits until the graceful shutdown procedure is done
var wg sync.WaitGroup
wg.Add(1)


// This goroutine is running in parallels to the main one
go func() {
// creating a channel to listen for signals, like SIGINT
stop := make(chan os.Signal, 1)
// subscribing to interruption signals
signal.Notify(stop, os.Interrupt)
// this blocks until the signal is received
<-stop
// initiating the shutdown
err := server.Shutdown(context.Background())
// can't do much here except for logging any errors
if err != nil {
log.Printf("error during shutdown: %v\n", err)
}
// notifying the main goroutine that we are done
wg.Done()
}()


log.Println("listening on port 3333...")
err := server.ListenAndServe()
if err == http.ErrServerClosed { // graceful shutdown
log.Println("commencing server shutdown...")
wg.Wait()
log.Println("server was gracefully shut down.")
} else if err != nil {
log.Printf("server error: %v\n", err)
}
}

打开两个终端。在第一次运行应用程序,在第二次运行 curl localhost:3333,然后 快点切换到第一个,并尝试停止与 CTRL+C的应用程序

产出应为:

2021/03/12 13:39:49 listening on port 3333...
2021/03/12 13:39:50 user initiated a request
2021/03/12 13:39:54 commencing server shutdown...
2021/03/12 13:40:00 user request is fulfilled
2021/03/12 13:40:01 server was gracefully shut down.

有一个模块实现(优雅地)停止 Go HTTP 服务器: Https://github.com/pseidemann/finish

这样就不需要在其他答案中提供样板了。