如何停止http.ListenAndServe()


90

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

问题在于,在我的应用程序中,HTTP服务器只是一个组件,需要自行决定停止和启动。

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

我知道过去这一直是个问题,还是这样吗?有没有新的解决方案?

Answers:


92

关于正常关机(在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")
}

1
是的,功能是Shutdown(),我在这里演示其具体用法。谢谢,我应该更清楚一些,我现在将标题更改为:“关于正常关机(在Go 1.8中引入),更具体的示例:”
joonas.fi

当我通过nilsrv.Shutdown我得到了panic: runtime error: invalid memory address or nil pointer dereferencecontext.Todo()而是通过传递。
Hubro

1
@Hubro太奇怪了,我只是在最新的Golang版本(1.10)上尝试了一下,它运行良好。当然可以使用context.Background()或context.TODO(),如果对您有用,那就很好。:)
joonas.fi

1
@ newplayer65有多种方法可以做到这一点。一种方法是在main()中创建sync.WaitGroup,在其上调用Add(1)并将指向它的指针传递给startHttpServer(),并在goroutine的开始处调用defer waitGroup.Done(),该例程对它的调用ListenAndServe()。然后只需在main()的末尾调用waitGroup.Wait()即可等待goroutine完成其工作。
joonas.fi

1
@ newplayer65我看了你的代码。使用渠道是比我的建议更好的选择,可能是更好的选择。我的代码主要是为了演示Shutdown()-而不是展示生产质量代码:)请注意,您项目的“服务器地鼠”徽标是漂亮的!:D
joonas.fi

70

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 (pkill -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.

原始答案-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


我创建了一个为您做样板的程序包github.com/pseidemann/finish
pseidemann

24

Go 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)

相关的提交可以在这里找到


7
抱歉,我很挑剔,我知道您的代码纯粹是示例用法,但作为一般规则:go func() { X() }()随后Y()X()之前执行的读者做出错误的假设Y()。等待组等确保计时错误不会在最不期望的时候咬你!
colm.anseo'3

20

你可以构造 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)

1
谢谢,但是没有回答我的问题。我http.ListenAndServe出于特定原因询问。那就是我使用GWT MUX库的方式,我不确定如何使用net.listen。.–
jim

6
您可以使用http.Serve()而不是http.ListenAndServe(),以完全相同的语法和相同的语法使用自己的侦听器。http.Serve(net.Listener,gorilla.mux.Router)
Uvelichitel

很好,谢谢。我尚未测试,但应该可以。
2016年

1
有点晚了,但是在这个用例中,我们一直在使用行为方式包。它是标准http程序包的直接替代,该程序包允许正常关闭(即在拒绝所有新请求的同时完成所有活动请求,然后退出)。
Kaedys

13

由于先前的答案都没有说明为什么使用http.ListenAndServe()无法执行此操作,因此我进入了v1.8 http源代码,它的含义如下:

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

如您所见,http.ListenAndServe函数不会返回服务器变量。这意味着您无法进入“服务器”以使用“关机”命令。因此,您需要创建自己的“服务器”实例,而不是使用此功能来实现正常关机。


2

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

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()

“关闭服务器不是一件坏事,请ffs Go ...” :)
Paul Knopf

值得注意的是,要正常关机,在退出之前,您需要等待关机返回,这似乎在这里没有发生。
Marcin Bilski

您对ctxto的使用server.Shutdown不正确。该上下文已被取消,因此不会被彻底关闭。您可能已经要求server.Close进行不干净的关机。(干净关闭该代码将需要广泛的再加工。
Dave C制作

0

这有可能使用来解决context.Context使用net.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
}

-6

对于这种情况,在这种情况下,应用程序仅是服务器,而没有执行其他功能,我http.HandleFunc为此安装了像这样的模式/shutdown。就像是

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。但是os.Exit(0),我认为,如果有1.8版本可用,那么可以在此处嵌入该解决方案,而不是调用该解决方案。

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

如果您能说出清理代码可能最合理的放置位置,那么这是额外的功劳,因为我不建议您在此处进行清理,以及此端点命中应如何导致该代码的调用。

如果您能说出此os.exit(0)调用(或选择使用的任何进程出口)在何处(仅出于说明目的),则将获得更多的额外信用,这是最合理的放置方式。

如果您能解释为什么在这种情况下认为可行的所有其他此类机制之上,应该首先考虑这种HTTP服务器进程信令机制,则还有更多的功劳。


当然,我按要求回答了问题,没有对问题的性质做任何进一步的假设,尤其是对任何给定的生产环境都没有任何假设。但是对于我自己的@MarcinBilski而言,究竟有什么要求会使该解决方案不适用于任何环境,生产环境或其他环境?
greg.carter

2
这意味着它比任何事情都更容易被别人嘲笑,因为很明显,您在生产应用程序中不会有/ shutdown处理程序。:)我想任何东西都可以用于内部工具。那一边,有一些方法可以正常关闭服务器,因此它不会突然通过一个数据库事务或删除连接或崩溃的中途,更糟的是,虽然写入磁盘等等
马辛Bilski案

当然,倒下的选民不是没有想象力的情况。一定是我承担了过多的想象力。我已经更新了响应(包括示例)以更正我的错误。
greg.carter,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.