Jak zatrzymać http.ListenAndServe ()


91

Używam biblioteki Mux z Gorilla Web Toolkit wraz z dołączonym serwerem Go http.

Problem w tym, że w mojej aplikacji serwer HTTP jest tylko jednym komponentem i wymaga zatrzymania i uruchomienia według własnego uznania.

Kiedy nazywam http.ListenAndServe(fmt.Sprintf(":%d", service.Port()), service.router)to blokowaniem i nie mogę zatrzymać działania serwera.

Wiem, że był to problem w przeszłości, czy nadal tak jest? Czy są jakieś nowe rozwiązania?

Odpowiedzi:


92

Jeśli chodzi o bezpieczne zamykanie (wprowadzone w Go 1.8), trochę bardziej konkretny przykład:

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
Tak, ta funkcja to Shutdown (), której konkretne użycie pokazuję tutaj. Dzięki, powinienem był być bardziej jasny, zmieniłem teraz nagłówek na ten: „Jeśli chodzi o bezpieczne zamykanie (wprowadzone w Go 1.8), trochę bardziej konkretny przykład:”
joonas.fi

Kiedy mijam nilsię srv.Shutdowndostać panic: runtime error: invalid memory address or nil pointer dereference. Przechodząc context.Todo()zamiast działa.
Hubro

1
@Hubro to dziwne, właśnie wypróbowałem to w najnowszej wersji Golanga (1.10) i działało dobrze. context.Background () lub context.TODO () oczywiście działa i jeśli to działa, to dobrze. :)
joonas.fi

1
@ newplayer65 można to zrobić na wiele sposobów. Jednym ze sposobów mogłoby być utworzenie sync.WaitGroup w main (), wywołanie Add (1) na nim i przekazanie do niego wskaźnika do startHttpServer () oraz wywołanie defer waitGroup.Done () na początku goroutine, która ma wywołanie ListenAndServe (). następnie po prostu wywołaj waitGroup.Wait () na końcu main (), aby poczekać, aż goroutine zakończy swoje zadanie.
joonas.fi

1
@ newplayer65 Spojrzałem na twój kod. Korzystanie z kanału to dobra, prawdopodobnie lepsza opcja niż moja sugestia. Mój kod służył głównie do demonstracji Shutdown () - a nie pokazania kodu jakości produkcji :) Ps logo twojego projektu "server gopher" to adorbs! : D
joonas.fi

70

Jak wspomniano w yo.ian.godpowiedzi. Go 1.8 zawiera tę funkcjonalność w standardowym lib.

Minimalny przykład dla 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.

Oryginalna odpowiedź - Pre Go 1.8:

Opierając się na odpowiedzi Uvelichitel .

Możesz stworzyć własną wersję, ListenAndServektóra zwraca io.Closeri nie blokuje.

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
}

Pełny kod dostępny tutaj .

Serwer HTTP zamknie się z błędem accept tcp [::]:8080: use of closed network connection


Stworzyłem pakiet, który robi dla Ciebie standardowy
szablon

24

Wersja 1.8 będzie zawierała bezpieczne i wymuszone zamykanie, dostępne odpowiednio za pośrednictwem Server::Shutdown(context.Context)i Server::Close().

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

srv.Shutdown(context)

Odpowiednie zatwierdzenie można znaleźć tutaj


7
przepraszam, że jestem wybredny, i wiem, że twój kod był czysto przykładowym użyciem, ale z reguły: go func() { X() }()po nim następuje Y()fałszywe założenie dla czytelnika, które X()zostanie wykonane wcześniej Y(). Grupy oczekujących itp. Zapewniają, że takie błędy czasowe nie gryzą Cię w najmniej oczekiwanym momencie!
colm.anseo

20

Możesz konstruować net.Listener

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

które możesz Close()

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

i http.Serve()na nim

http.Serve(l, service.router)

1
dzięki, ale to nie odpowiada na moje pytanie. Pytam http.ListenAndServez konkretnych powodów. W ten sposób używam biblioteki GWT MUX, nie jestem pewien, jak używać do tego net.listen ..
jim

6
Używasz http.Serve () zamiast http.ListenAndServe () dokładnie w ten sam sposób, z tą samą składnią, tylko z własnym Listener. http.Serve (net.Listener, gorilla.mux.Router)
Uvelichitel

Świetnie, dzięki. Nie testowałem jeszcze, ale powinno działać.
jim

1
Trochę późno, ale w tym przypadku używaliśmy pakietu manier . Jest to zamiennik standardowego pakietu http, który umożliwia bezpieczne zamykanie (tj. Kończy wszystkie aktywne żądania, odrzucając nowe, a następnie kończy działanie).
Kaedys

13

Ponieważ żadna z poprzednich odpowiedzi nie mówi, dlaczego nie możesz tego zrobić, jeśli używasz http.ListenAndServe (), przeszedłem do kodu źródłowego http v1.8 i oto, co mówi:

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

Jak widać, funkcja http.ListenAndServe nie zwraca zmiennej serwera. Oznacza to, że nie możesz dostać się do „serwera”, aby użyć polecenia Shutdown. Dlatego zamiast używać tej funkcji, aby zaimplementować wdzięczne zamknięcie systemu, musisz utworzyć własną instancję „serwera”.


2

Możesz zamknąć serwer, zamykając jego kontekst.

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
}

A kiedy będziesz gotowy, aby go zamknąć, zadzwoń:

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

„Zamknięcie serwera nie jest czymś złym, ffs Go ...” :)
Paul Knopf

Warto zauważyć, że w celu płynnego zamknięcia systemu przed zakończeniem należy poczekać, aż funkcja Shutdown powróci, co nie wydaje się mieć miejsca tutaj.
Marcin Bilski

Twoje użycie ctxdo server.Shutdownjest nieprawidłowe. Kontekst jest już anulowany, więc nie zostanie zamknięty w czysty sposób. Mogłeś wezwać server.Closedo nieczystego zamknięcia. (Aby całkowicie zamknąć system, kod ten musiałby zostać gruntownie przerobiony.
Dave C

0

Można to rozwiązać za context.Contextpomocą pliku net.ListenConfig. W moim przypadku, nie chcę użyć sync.WaitGrouplub http.Server„s Shutdown()połączenia, a zamiast polegać na context.Context(który został zamknięty z sygnałem).

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

To, co zrobiłem w takich przypadkach, gdy aplikacja jest tylko serwerem i nie wykonuje żadnej innej funkcji, to zainstalowanie http.HandleFuncwzorca takiego jak /shutdown. Coś jak

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

Nie wymaga 1.8. Ale jeśli jest dostępna wersja 1.8, to os.Exit(0)moim zdaniem to rozwiązanie można osadzić tutaj zamiast wywołania, jeśli jest to pożądane.

Kod do wykonania wszystkich prac porządkowych pozostawiamy jako ćwiczenie dla czytelnika.

Dodatkowy kredyt, jeśli możesz powiedzieć, gdzie ten kod czyszczący może być najbardziej racjonalnie umieszczony, ponieważ nie polecałbym tego robić w tym miejscu i jak to trafienie w punkt końcowy powinno spowodować wywołanie tego kodu.

Więcej dodatkowych punktów, jeśli możesz powiedzieć, gdzie to os.exit(0)wywołanie (lub jakiekolwiek wyjście z procesu, którego zdecydujesz się użyć), podane tutaj tylko w celach ilustracyjnych, byłoby najbardziej uzasadnione.

Jeszcze większy zasługa, jeśli potrafisz wyjaśnić, dlaczego ten mechanizm sygnalizacji procesu serwera HTTP powinien być wzięty pod uwagę ponad wszystkie inne tego typu mechanizmy, które są w tym przypadku możliwe.


Oczywiście odpowiedziałem na zadane pytanie bez dalszych przypuszczeń co do natury problemu, aw szczególności bez założeń dotyczących dowolnego środowiska produkcyjnego. Ale dla mojej własnej edukacji, @MarcinBilski, jakie dokładnie wymagania sprawiłyby, że to rozwiązanie nie pasowałoby do żadnego środowiska, produkcji lub innego?
greg.carter

2
Oznaczało to, że jest to bardziej żartobliwe niż cokolwiek innego, ponieważ jasne jest, że nie miałbyś programu obsługi / shutdown w aplikacji produkcyjnej. :) Chyba wszystko idzie o wewnętrzne narzędzia. Poza tym istnieją sposoby na łagodne zamknięcie serwera, aby nie przerywał nagle połączeń lub nie zawieszał się w trakcie transakcji w bazie danych lub, co gorsza, podczas zapisywania na dysk itp.
Marcin Bilski

Z pewnością nie może być tak, że słabsi wyborcy są pozbawieni wyobraźni. Musi być tak, że zakładam zbyt dużą wyobraźnię. Zaktualizowałem odpowiedź (w tym przykład), aby poprawić mój błąd.
greg.carter
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.