Як зупинити http.ListenAndServe ()


90

Я використовую бібліотеку Mux від Gorilla Web Toolkit разом із серверним сервером Go http.

Проблема полягає в тому, що в моїй програмі сервер HTTP є лише одним компонентом, і його потрібно зупиняти та запускати на мій розсуд.

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

Я усвідомлюю, що це була проблема в минулому, чи все ще так? Чи є якісь нові рішення?

Відповіді:


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

Коли я переходжу nilдо srv.Shutdownя отримую panic: runtime error: invalid memory address or nil pointer dereference. Проходження context.Todo()замість цього працює.
Hubro

1
@Hubro це дивно, я щойно спробував це в останній версії Golang (1.10), і воно працювало нормально. context.Background () або context.TODO (), звичайно, працює, і якщо це працює для вас, добре. :)
joonas.fi

1
@ newplayer65 існує кілька способів зробити це. Одним із способів може бути створення sync.WaitGroup в main (), виклик Add (1) на ньому і передача на нього вказівника на startHttpServer () та виклик defer waitGroup.Done () на початку програми, яка має виклик до ListenAndServe (). тоді просто зателефонуйте waitGroup.Wait () в кінці main (), щоб дочекатися, поки програма завершить свою роботу.
joonas.fi

1
@ newplayer65 Я подивився ваш код. Використання каналу - хороший, мабуть, кращий варіант, ніж моя пропозиція. Мій код був головним чином для демонстрації вимкнення () - не демонструвати код якості виробництва :) Ps. Логотип "серверного сусліка" вашого проекту - adorbs! : 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:

Спираючись на відповідь Увеличителя .

Ви можете створити власну версію, 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


Я створив пакет, який робить шаблон
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

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 для цього ..
Джим,

6
Ви використовуєте http.Serve () замість http.ListenAndServe () точно так само, з однаковим синтаксисом, лише з власним слухачем. http.Serve (net.Listener, gorilla.mux.Router)
Uvelichitel

Ах чудово, дякую. Я ще не тестував, але повинен працювати.
Джим,

1
Трохи пізно, але ми використовуємо пакет манер для цього випадку використання. Це заміна стандартного пакета http, що дозволяє витончено завершувати роботу (тобто завершує всі активні запити, одночасно відхиляючи нові, а потім виходить).
Kaedys

13

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

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 ..." :)
Пол Нопф,

Варто зазначити, що для витонченого вимкнення перед виходом потрібно дочекатися повернення вимкнення, що, здається, тут не відбувається.
Marcin

Ваше використання ctxto server.Shutdownнеправильно. Контекст уже скасовано, тому він не буде чисто вимкнено. Можливо, ви цілком закликали server.Closeдо нечистого відключення. (Для чистого вимкнення цей код потрібно було б широко переробити.
Дейв С

0

Це можна вирішити за context.Contextдопомогою a net.ListenConfig. У моєму випадку, я не хочу використовувати sync.WaitGroupабо http.Server«s 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
}

-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. Але якщо доступний 1.8, то os.Exit(0), на мою думку , це рішення може бути вбудовано сюди замість виклику, якщо це бажано.

Код для виконання всієї цієї роботи з очищення залишається вправою для читача.

Додатковий кредит, якщо ви можете сказати, де цей код очищення може бути найбільш розумно розміщений, оскільки я б не рекомендував робити це тут, і як це звернення до кінцевої точки повинно викликати цей код.

Більше додаткового кредиту, якщо ви можете сказати, де цей os.exit(0)дзвінок (або будь-який вихід із процесу, який ви вирішите використовувати), наведений тут лише для ілюстративних цілей, буде найбільш розумно розміщений.

І все-таки ще більше додаткового кредиту, якщо ви можете пояснити, чому цей механізм сигналізації процесу HTTP-сервера слід розглядати перш за все інші такі механізми, які вважаються працездатними у цьому випадку.


Звичайно, я відповів на поставлене запитання без будь-яких подальших припущень про суть проблеми і, зокрема, без припущень щодо будь-якого даного виробничого середовища. Але для мого власного побудови, @MarcinBilski, які саме вимоги можуть зробити це рішення непридатним для будь-якого середовища, виробництва чи іншого?
greg.carter

2
Мав на увазі це більше, ніж будь-що, тому що зрозуміло, що у вас не буде обробника / вимкнення у виробничому додатку. :) Все, що стосується внутрішнього оснащення, я думаю. Окрім цього, є способи витончено вимкнути сервер, щоб він раптово не припиняв з’єднання або не
Марцін

Звичайно, не може бути так, що виборці, які не голосують, не уявляють. Має бути, я припускаю занадто багато фантазії. Я оновив відповідь (включаючи приклад), щоб виправити помилку.
greg.carter
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.