Как остановить http.ListenAndServe()

Я использую библиотеку Mux из Gorilla Web Toolkit вместе со встроенным http-сервером Go.

Проблема в том, что в моем приложении HTTP-сервер является только одним компонентом, и он должен останавливаться и запускаться по моему усмотрению.

Когда я звоню http.ListenAndServe(fmt.Sprintf(":%d", service.Port()), service.router) это блокирует, и я не могу остановить работу сервера.

Я знаю, что это было проблемой в прошлом, это все еще так? Есть ли новые решения, пожалуйста? Благодарю.

11 ответов

Что касается постепенного выключения (введено в Go 1.8), приведу более конкретный пример:

package main

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

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

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

    go func() {
        if err := srv.ListenAndServe(); err != nil {
            // cannot panic, because this probably is an intentional close
            log.Printf("Httpserver: ListenAndServe() error: %s", err)
        }
    }()

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

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

    srv := startHttpServer()

    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 instead of nil as a https://golang.org/pkg/context/
    if err := srv.Shutdown(nil); err != nil {
        panic(err) // failure/timeout shutting down the server gracefully
    }

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

Как уже упоминалось в 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, _ := context.WithTimeout(context.Background(), 5*time.Second)
if err := server.Shutdown(ctx); err != nil {
    // handle err
}

Оригинальный ответ - 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

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)

Соответствующий коммит можно найти здесь

Вы можете построить 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)

Поскольку ни один из предыдущих ответов не говорит, почему вы не можете сделать это, если используете http.ListenAndServe(), я вошел в исходный код http v1.8 и вот что он говорит:

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

Как видите, функция http.ListenAndServe не возвращает серверную переменную. Это означает, что вы не можете попасть на "сервер", чтобы использовать команду "Завершение работы". Следовательно, вам нужно создать свой собственный "серверный" экземпляр, а не использовать эту функцию для постепенного завершения работы.

Вы можете закрыть сервер, закрыв его контекст.

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

Я придумал тот же вопрос, поэтому решил записать все это в учебник по Github. Ознакомьтесь с полным исходным кодом, интеграционным тестом и способами реализации уровня SSL для защиты!

Если кто-то хотел бы внести свой вклад в это, сделать его еще лучше, написать больше тестов, не стесняйтесь представить PR!

Вклад и обмен знаниями приветствуются!

Воспроизводимый пример, когда вы не хотите, чтобы ваш главный сервер запускался в отдельной горутине:

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.

Это можно решить, используя context.Context используя net.ListenConfig. В моем случае я не хотел использоватьsync.WaitGroup или http.Serverс Shutdown() звоните, а вместо этого полагайтесь на 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
}

ребята, как насчет этого

 package gracefull_shutdown_server

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



    func startHttpServer() *http.Server{
        mux:=http.NewServeMux()
        mux.HandleFunc("/",defaultRoute)
        srv:=&http.Server{
            Addr:":8080",
            Handler:mux,
        }
        go func() {
            if err:=srv.ListenAndServe();err != http.ErrServerClosed{
                log.Fatalf("ListenAndServe(): %s",err)
            }

        }()
        return srv
    }

    func defaultRoute(w http.ResponseWriter, r *http.Request){
        time.Sleep(time.Second*30)
        w.Write([]byte("it's working"))
    }
    func MainStartHttpServer()  {
        srv:=startHttpServer()
        stop:= make(chan os.Signal)
        signal.Notify(stop,os.Interrupt)
        select {
        case <-stop:
            fmt.Println("server going to shut down")
            ctx,cancel:=context.WithTimeout(context.Background(),time.Second*5)
            defer cancel()
            err:=srv.Shutdown(ctx)
            if err!=nil{
                fmt.Println(err)
            }
        }
    }

Существует модуль, который реализует (изящную) остановку HTTP-серверов Go:https://github.com/pseidemann/finish .

Это устраняет необходимость в шаблоне, представленном в других ответах.

То, что я сделал для таких случаев, когда приложение является просто сервером и не выполняет никакой другой функции, это установил http.HandleFunc для картины как /shutdown, Что-то вроде

http.HandleFunc("/shutdown", func(w http.ResponseWriter, r *http.Request) {
    if <credentials check passes> {
        fmt.Fprint(w, "Goodbye!\n")
        os.Exit(0)
    }
})

Не требует 1.8. Но если доступен 1.8, тогда это решение может быть встроено вместо os.Exit(0) позвоните, если желательно, я считаю.

Другие вопросы по тегам