Cómo detener http.ListenAndServe ()

91

Estoy usando la biblioteca Mux de Gorilla Web Toolkit junto con el servidor http de Go incluido.

El problema es que en mi aplicación, el servidor HTTP es solo un componente y debe detenerse y comenzar a mi discreción.

Cuando lo llamo http.ListenAndServe(fmt.Sprintf(":%d", service.Port()), service.router)bloquea y parece que no puedo detener la ejecución del servidor.

Soy consciente de que esto ha sido un problema en el pasado, ¿sigue siendo así? ¿Hay nuevas soluciones?

Jim
fuente

Respuestas:

92

Con respecto al cierre elegante (introducido en Go 1.8), un ejemplo un poco más concreto:

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
fuente
1
Sí, la función es Shutdown (), cuyo uso concreto estoy demostrando aquí. Gracias, debería haber sido más claro, cambié el título a este ahora: "Con respecto al cierre elegante (introducido en Go 1.8), un ejemplo un poco más concreto:"
joonas.fi
Cuando paso nila srv.Shutdownconseguir panic: runtime error: invalid memory address or nil pointer dereference. En context.Todo()cambio, pasar funciona.
Hubro
1
@Hubro eso es extraño, acabo de probar esto en la última versión de Golang (1.10) y funcionó bien. context.Background () o context.TODO () por supuesto funciona y si funciona para usted, bien. :)
joonas.fi
1
@ newplayer65 hay varias formas de hacerlo. Una forma podría ser crear sync.WaitGroup en main (), llamar a Add (1) y pasarle un puntero a startHttpServer () y llamar a defer waitGroup.Done () al inicio de la goroutine que tiene una llamada al ListenAndServe (). luego simplemente llame a waitGroup.Wait () al final de main () para esperar a que la goroutine haya terminado su trabajo.
joonas.fi
1
@ newplayer65 Miré tu código. Usar un canal es una buena opción, probablemente mejor que mi sugerencia. Mi código fue principalmente para demostrar Shutdown () - no mostrar código de calidad de producción :) Ps, ¡el logo de "servidor gopher" de su proyecto es adorbs! : D
joonas.fi
70

Como se menciona en yo.ian.gla respuesta de. Go 1.8 ha incluido esta funcionalidad en la biblioteca estándar.

Ejemplo mínimo para para 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.

Respuesta original - Pre Go 1.8:

Sobre la base de la respuesta de Uvelichitel .

Puede crear su propia versión de la ListenAndServeque devuelve un io.Closery no bloquea.

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
}

Código completo disponible aquí .

El servidor HTTP se cerrará con el error accept tcp [::]:8080: use of closed network connection

John S Perayil
fuente
Creé
pseidemann
24

Go 1.8 incluirá un cierre elegante y contundente, disponible a través de Server::Shutdown(context.Context)y Server::Close()respectivamente.

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

srv.Shutdown(context)

El compromiso relevante se puede encontrar aquí

yo.ian.g
fuente
7
Lamento ser exigente, y sé que su código fue puramente de uso de ejemplo, pero como regla general: go func() { X() }()seguido de Y()hace la suposición falsa para el lector de que X()se ejecutará antes Y(). Los grupos de espera, etc., garantizan que los errores de sincronización como este no te muerdan cuando menos lo esperas.
colm.anseo
20

Puedes construir net.Listener

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

que puedes Close()

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

y http.Serve()en eso

http.Serve(l, service.router)
Uvelichitel
fuente
1
gracias pero eso no responde a mi pregunta. Estoy preguntando http.ListenAndServepor razones específicas. Así es como uso la biblioteca GWT MUX, no estoy seguro de cómo usar net.listen para eso ..
jim
6
Utiliza http.Serve () en lugar de http.ListenAndServe () exactamente de la misma manera con la misma sintaxis solo con su propio Listener. http.Serve (net.Listener, gorilla.mux.Router)
Uvelichitel
Ah genial, gracias. No lo he probado todavía, pero debería funcionar.
jim
1
Un poco tarde, pero hemos estado usando el paquete de modales para este caso de uso. Es un reemplazo directo para el paquete http estándar que permite un cierre ordenado (es decir, finaliza todas las solicitudes activas mientras rechaza las nuevas y luego sale).
Kaedys
13

Dado que ninguna de las respuestas anteriores dice por qué no puede hacerlo si usa http.ListenAndServe (), entré en el código fuente http v1.8 y esto es lo que dice:

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

Como puede ver, la función http.ListenAndServe no devuelve la variable del servidor. Esto significa que no puede acceder al 'servidor' para usar el comando de apagado. Por lo tanto, debe crear su propia instancia de 'servidor' en lugar de usar esta función para que se implemente el cierre ordenado.

juz
fuente
2

Puede cerrar el servidor cerrando su contexto.

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
}

Y cuando esté listo para cerrarlo, llame a:

ctx, closeServer := context.WithCancel(context.Background())
err := ServeReqs(ctx, etc)
closeServer()
Lukas Lukac
fuente
"Cerrar el servidor no es algo malo ffs Go ..." :)
Paul Knopf
Una cosa que vale la pena señalar es que, para un cierre elegante, antes de salir, debe esperar a que regrese el cierre, lo que no parece estar sucediendo aquí.
Marcin Bilski
Su uso de ctxto server.Shutdownes incorrecto. El contexto ya está cancelado, por lo que no se cerrará limpiamente. Bien podría haber pedido server.Closeun cierre impuro. (Para un apagado limpio, este código debería ser reelaborado extensamente.
Dave C
0

Es posible resolver esto context.Contextusando a net.ListenConfig. En mi caso, yo no quería utilizar una sync.WaitGroupo http.Server's Shutdown()llamada, y en lugar de confiar en una context.Context(que estaba cerrada con una señal).

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
fuente
-6

Lo que he hecho para esos casos en los que la aplicación es solo el servidor y no realiza ninguna otra función es instalar y http.HandleFuncpara un patrón como /shutdown. Algo como

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

No requiere 1.8. Pero si 1.8 está disponible, entonces esa solución puede integrarse aquí en lugar de la os.Exit(0)llamada si es deseable, creo.

El código para realizar todo ese trabajo de limpieza se deja como ejercicio para el lector.

Crédito adicional si puede decir dónde podría colocarse el código de limpieza de manera más razonable, ya que no recomendaría hacerlo aquí, y cómo este golpe de punto final debería causar la invocación de ese código.

Más crédito adicional si puede decir dónde se os.exit(0)ubicaría de manera más razonable esa llamada (o cualquier salida de proceso que elija usar), que se proporciona aquí solo con fines ilustrativos.

Aún más crédito adicional si puede explicar por qué este mecanismo de señalización del proceso del servidor HTTP debe considerarse por encima de todos los demás mecanismos que se consideran viables en este caso.

greg.carter
fuente
Por supuesto, respondí la pregunta tal como se formuló sin más suposiciones sobre la naturaleza del problema y, en particular, sin suposiciones sobre un entorno de producción dado. Pero para mi propia edificación, @MarcinBilski, ¿qué requisitos exactamente harían que esta solución no sea adecuada para ningún entorno, producción o de otro tipo?
greg.carter
2
Lo quise decir más irónicamente que cualquier otra cosa porque está claro que no tendría un controlador / shutdown en una aplicación de producción. :) Todo vale para las herramientas internas, supongo. Aparte de eso, hay maneras de cerrar correctamente el servidor para que no se baje repentinamente conexiones o accidente a mitad de camino a través de una transacción de base de datos o, peor aún, al escribir en el disco, etc
Marcin Bilski
Ciertamente, no puede ser el caso de que los votantes en contra no tengan imaginación. Debe ser que asumo demasiada imaginación. Actualicé la respuesta (incluido el ejemplo) para corregir mi error.
greg.carter