So stoppen Sie http.ListenAndServe ()

87

Ich verwende die Mux-Bibliothek von Gorilla Web Toolkit zusammen mit dem mitgelieferten Go http-Server.

Das Problem ist, dass in meiner Anwendung der HTTP-Server nur eine Komponente ist und nach meinem Ermessen gestoppt und gestartet werden muss.

Wenn ich http.ListenAndServe(fmt.Sprintf(":%d", service.Port()), service.router)es blockiere, kann ich den Server anscheinend nicht stoppen.

Ich bin mir bewusst, dass dies in der Vergangenheit ein Problem war. Ist das immer noch der Fall? Gibt es neue Lösungen?

Jim
quelle

Antworten:

87

In Bezug auf das ordnungsgemäße Herunterfahren (eingeführt in Go 1.8) ein etwas konkreteres Beispiel:

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")
}
joonas.fi
quelle
1
Ja, die Funktion ist Shutdown (), dessen konkrete Verwendung ich hier demonstriere. Danke, ich hätte klarer sein sollen, ich habe die Überschrift jetzt dahingehend geändert: "In Bezug auf das ordnungsgemäße Herunterfahren (eingeführt in Go 1.8), ein etwas konkreteres Beispiel:"
joonas.fi
Wenn ich weitergehe nil, srv.Shutdownbekomme ich panic: runtime error: invalid memory address or nil pointer dereference. Übergeben context.Todo()funktioniert stattdessen.
Hubro
1
@Hubro das ist komisch, ich habe es gerade mit der neuesten Golang-Version (1.10) versucht und es lief gut. context.Background () oder context.TODO () funktioniert natürlich und wenn es für Sie funktioniert, gut. :)
joonas.fi
1
@ newplayer65 Dafür gibt es mehrere Möglichkeiten. Eine Möglichkeit besteht darin, sync.WaitGroup in main () zu erstellen, Add (1) darauf aufzurufen und einen Zeiger darauf an startHttpServer () zu übergeben und defer waitGroup.Done () beim Start der Goroutine aufzurufen, die einen Aufruf an die hat ListenAndServe (). Rufen Sie dann einfach waitGroup.Wait () am Ende von main () auf, um zu warten, bis die Goroutine ihren Job beendet hat.
joonas.fi
1
@ newplayer65 Ich habe mir deinen Code angesehen. Die Verwendung eines Kanals ist eine gute, wahrscheinlich bessere Option als mein Vorschlag. Mein Code diente hauptsächlich dazu, Shutdown () zu demonstrieren - keinen Produktionsqualitätscode zu präsentieren :) Ps, das "Server Gopher" -Logo Ihres Projekts ist adorbiert! : D
joonas.fi
64

Wie in yo.ian.gder Antwort erwähnt. Go 1.8 hat diese Funktionalität in die Standardbibliothek aufgenommen.

Minimales Beispiel für 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.

Ursprüngliche Antwort - Pre Go 1.8:

Aufbauend auf Uvelichitels Antwort.

Sie können eine eigene Version erstellen, ListenAndServedie ein zurückgibt io.Closerund nicht blockiert.

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
}

Vollständiger Code hier verfügbar .

Der HTTP-Server wird mit dem Fehler geschlossen accept tcp [::]:8080: use of closed network connection

John S. Perayil
quelle
Ich habe ein Paket, das die vorformulierten für Sie tut github.com/pseidemann/finish
pseidemann
24

Go 1.8 wird anmutig und kraftvoll shutdown, erhältlich über umfassen Server::Shutdown(context.Context)und Server::Close()jeweils.

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

srv.Shutdown(context)

Das entsprechende Commit finden Sie hier

yo.ian.g
quelle
6
Es tut mir leid, wählerisch zu sein, und ich weiß, dass Ihr Code nur eine Beispielverwendung war, aber in der Regel: go func() { X() }()Anschließend wird Y()dem Leser die falsche Annahme gemacht, dass X()er zuvor ausgeführt wird Y(). Wartegruppen usw. stellen sicher, dass Timing-Fehler wie diese Sie nicht beißen, wenn Sie es am wenigsten erwarten!
colm.anseo
20

Sie können konstruieren net.Listener

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

was du kannst Close()

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

und http.Serve()darauf

http.Serve(l, service.router)
Uvelichitel
quelle
1
Danke, aber das beantwortet meine Frage nicht. Ich frage http.ListenAndServeaus bestimmten Gründen. So benutze ich die GWT MUX Bibliothek, ich bin mir nicht sicher, wie ich net.listen dafür verwenden soll.
Jim
6
Sie verwenden http.Serve () anstelle von http.ListenAndServe () genauso mit derselben Syntax, nur mit einem eigenen Listener. http.Serve (net.Listener, gorilla.mux.Router)
Uvelichitel
Ah toll, danke. Ich habe noch nicht getestet, sollte aber funktionieren.
Jim
1
Etwas spät, aber wir haben das Manierenpaket für diesen Anwendungsfall verwendet. Es ist ein Drop-In-Ersatz für das Standard-http-Paket, das ein ordnungsgemäßes Herunterfahren ermöglicht (dh alle aktiven Anforderungen werden beendet, während neue abgelehnt werden, und dann beendet).
Kaedys
13

Da in keiner der vorherigen Antworten angegeben ist, warum Sie dies nicht tun können, wenn Sie http.ListenAndServe () verwenden, habe ich den http-Quellcode v1.8 verwendet.

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

Wie Sie sehen können, gibt die Funktion http.ListenAndServe die Servervariable nicht zurück. Dies bedeutet, dass Sie nicht zum 'Server' gelangen können, um den Befehl Herunterfahren zu verwenden. Daher müssen Sie Ihre eigene 'Server'-Instanz erstellen, anstatt diese Funktion zu verwenden, damit das ordnungsgemäße Herunterfahren implementiert werden kann.

juz
quelle
2

Sie können den Server schließen, indem Sie seinen Kontext schließen.

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
}

Und wann immer Sie bereit sind, es zu schließen, rufen Sie an:

ctx, closeServer := context.WithCancel(context.Background())
err := ServeReqs(ctx, etc)
closeServer()
Lukas Lukac
quelle
"Das Herunterfahren des Servers ist nichts Schlechtes ffs Go ..." :)
Paul Knopf
Eine erwähnenswerte Sache ist, dass Sie für ein ordnungsgemäßes Herunterfahren vor dem Beenden warten müssen, bis das Herunterfahren zurückkehrt, was hier anscheinend nicht geschieht.
Marcin Bilski
Ihre Verwendung von ctxto server.Shutdownist falsch. Der Kontext ist bereits abgebrochen, sodass er nicht sauber heruntergefahren werden kann. Möglicherweise haben Sie server.Closeein unreines Herunterfahren gefordert . (Für ein sauberes Herunterfahren müsste dieser Code umfassend überarbeitet werden.
Dave C
0

Es ist möglich , dies mit einem lösen context.Contextmit ein net.ListenConfig. In meinem Fall wollte ich nicht verwenden sync.WaitGroupoder http.Server‚s Shutdown()Ruf, sondern stützen sich auf eine context.Context(die mit einem Signal geschlossen wurde).

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
}
Sean
quelle
-5

Was ich für solche Fälle getan habe, in denen die Anwendung nur der Server ist und keine andere Funktion ausführt, ist die Installation http.HandleFunceines Musters wie /shutdown. Etwas wie

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

Es erfordert nicht 1.8. Aber wenn 1.8 verfügbar ist, kann diese Lösung hier eingebettet werden, anstatt den os.Exit(0)Aufruf, falls gewünscht, glaube ich.

Der Code zur Durchführung all dieser Aufräumarbeiten bleibt dem Leser als Übung überlassen.

Zusätzliches Guthaben, wenn Sie sagen können, wo dieser Bereinigungscode am vernünftigsten platziert werden könnte, da ich dies hier nicht empfehlen würde und wie dieser Endpunktschlag den Aufruf dieses Codes verursachen sollte.

Mehr zusätzliche Gutschrift, wenn Sie sagen können, wo dieser os.exit(0)Anruf (oder welcher Prozess-Exit, den Sie verwenden möchten), der hier nur zur Veranschaulichung angegeben wird, am vernünftigsten platziert wäre.

Noch mehr zusätzliche Anerkennung, wenn Sie erklären können, warum dieser Mechanismus der HTTP-Server-Prozesssignalisierung vor allen anderen Mechanismen in Betracht gezogen werden sollte, die in diesem Fall für praktikabel gehalten werden.

greg.carter
quelle
Natürlich beantwortete ich die gestellte Frage ohne weitere Annahmen über die Art des Problems und insbesondere ohne Annahmen über eine bestimmte Produktionsumgebung. Aber für meine eigene Erbauung, @MarcinBilski, genau welche Anforderungen würden diese Lösung nicht für eine Umgebung, Produktion oder auf andere Weise geeignet machen?
Greg.Carter
2
Bedeutete es mehr als alles andere, denn es ist klar, dass Sie in einer Produktions-App keinen / shutdown-Handler haben würden. :) Alles für interne Werkzeuge, denke ich. Abgesehen davon gibt es Möglichkeiten, den Server ordnungsgemäß herunterzufahren, damit er nicht plötzlich Verbindungen trennt oder mitten in einer Datenbanktransaktion abstürzt oder, schlimmer noch, beim Schreiben auf die Festplatte usw.
Marcin Bilski
Sicherlich kann es nicht sein, dass die nach unten gerichteten Wähler einfallslos sind. Es muss sein, dass ich zu viel Vorstellungskraft annehme. Ich habe die Antwort (einschließlich des Beispiels) aktualisiert, um meinen Fehler zu korrigieren.
Greg.Carter