Cách dừng http.ListenAndServe ()


90

Tôi đang sử dụng thư viện Mux từ Bộ công cụ Web Gorilla cùng với máy chủ Go http đi kèm.

Vấn đề là trong ứng dụng của tôi, máy chủ HTTP chỉ là một thành phần và nó được yêu cầu dừng và bắt đầu theo quyết định của tôi.

Khi tôi gọi http.ListenAndServe(fmt.Sprintf(":%d", service.Port()), service.router)nó là khối và tôi dường như không thể ngăn máy chủ chạy.

Tôi biết điều này đã từng là một vấn đề trong quá khứ, vẫn còn đó chứ? Có giải pháp mới nào không?

Câu trả lời:


92

Về khả năng tắt nhanh (được giới thiệu trong Go 1.8), một ví dụ cụ thể hơn một chút:

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
Có, tính năng này là Shutdown (), cách sử dụng cụ thể mà tôi đang trình bày ở đây. Cảm ơn, tôi lẽ ra phải nói rõ hơn, tôi đã thay đổi tiêu đề thành thế này ngay bây giờ: "Về việc tắt máy theo cách duyên dáng (được giới thiệu trong Go 1.8), một ví dụ cụ thể hơn một chút:"
joonas.fi

Khi tôi vượt qua nilđể srv.Shutdowntôi nhận được panic: runtime error: invalid memory address or nil pointer dereference. context.Todo()Thay vào đó, vượt qua hoạt động.
Hubro

1
@Hubro thật kỳ lạ, tôi vừa thử điều này trên phiên bản Golang mới nhất (1.10) và nó chạy tốt. context.Background () hoặc context.TODO () tất nhiên hoạt động và nếu nó phù hợp với bạn thì tốt. :)
joonas.fi

1
@ newplayer65 có nhiều cách để làm điều đó. Một cách có thể là tạo sync.WaitGroup trong main (), gọi Add (1) trên nó và chuyển một con trỏ tới nó để startHttpServer () và gọi defer waitGroup.Done () ở đầu goroutine có lệnh gọi đến ListenAndServe (). sau đó chỉ cần gọi waitGroup.Wait () ở cuối main () để đợi goroutine hoàn thành công việc của nó.
joonas.fi 27/12/18

1
@ newplayer65 Tôi đã xem mã của bạn. Sử dụng kênh là một lựa chọn tốt, có lẽ tốt hơn đề xuất của tôi. Mã của tôi chủ yếu là để chứng minh Shutdown () - không phải giới thiệu mã chất lượng sản xuất :) Ps, logo "máy chủ" của dự án của bạn là tô điểm! : D
joonas.fi

70

Như đã đề cập trong yo.ian.gcâu trả lời của. Go 1.8 đã bao gồm chức năng này trong lib tiêu chuẩn.

Ví dụ tối thiểu cho 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.

Câu trả lời gốc - Pre Go 1.8:

Dựa trên câu trả lời của Uvelichitel .

Bạn có thể tạo phiên bản của riêng mình, trong ListenAndServeđó trả về một io.Closervà không chặn.

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
}

Mã đầy đủ có sẵn ở đây .

Máy chủ HTTP sẽ đóng với lỗi accept tcp [::]:8080: use of closed network connection


Tôi tạo ra một gói phần mềm mà làm bản mẫu cho bạn github.com/pseidemann/finish
pseidemann

24

Go 1.8 sẽ bao gồm tính năng tắt nhanh và mạnh mẽ, có sẵn thông qua Server::Shutdown(context.Context)Server::Close()tương ứng.

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

srv.Shutdown(context)

Cam kết liên quan có thể được tìm thấy ở đây


7
xin lỗi vì đã kén chọn, và tôi biết mã của bạn hoàn toàn là cách sử dụng ví dụ, nhưng theo quy tắc chung: go func() { X() }()tiếp theo là Y()đưa ra giả định sai cho người đọc X()sẽ thực thi trước đó Y(). Các nhóm chờ, v.v. đảm bảo rằng các lỗi thời gian như thế này không cắn bạn khi ít mong đợi nhất!
colm.anseo

20

Bạn có thể xây dựng net.Listener

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

mà bạn có thể Close()

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

http.Serve()trên đó

http.Serve(l, service.router)

1
cảm ơn nhưng điều đó không trả lời câu hỏi của tôi. Tôi đang hỏi về http.ListenAndServenhững lý do cụ thể. Đó là cách tôi sử dụng thư viện GWT MUX, tôi không chắc làm thế nào để sử dụng net.listen cho điều đó ..
jim

6
Bạn sử dụng http.Serve () thay vì http.ListenAndServe () theo cùng một cách với cùng một cú pháp chỉ với Trình nghe riêng. http.Serve (net.Listener, gorilla.mux.Router)
Uvelichitel

Ah tuyệt, cảm ơn. Tôi chưa thử nghiệm nhưng nên làm việc.
jim

1
Hơi muộn, nhưng chúng tôi đã sử dụng gói cách cư xử cho trường hợp sử dụng này. Đó là một phần thay thế cho gói http tiêu chuẩn cho phép tắt nhanh (tức là hoàn tất tất cả các yêu cầu đang hoạt động trong khi từ chối các yêu cầu mới, sau đó thoát).
Kaedys

13

Vì không có câu trả lời nào trước đây cho biết tại sao bạn không thể làm điều đó nếu bạn sử dụng http.ListenAndServe (), tôi đã đi vào mã nguồn http v1.8 và đây là nội dung của nó:

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

Như bạn có thể thấy, hàm http.ListenAndServe không trả về biến máy chủ. Điều này có nghĩa là bạn không thể truy cập 'máy chủ' để sử dụng lệnh Tắt máy. Do đó, bạn cần tạo phiên bản 'máy chủ' của riêng mình thay vì sử dụng chức năng này để thực hiện quá trình tắt duyên dáng.


2

Bạn có thể đóng máy chủ bằng cách đóng ngữ cảnh của nó.

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
}

Và bất cứ khi nào bạn sẵn sàng đóng nó, hãy gọi:

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

"Tắt máy chủ không phải là một cái gì đó xấu ffs Go ..." :)
Paul Knopf

Một điều đáng chú ý là, để tắt máy có duyên, trước khi thoát ra, bạn cần đợi Shutdown trở lại, điều này dường như không xảy ra ở đây.
Marcin Bilski

Việc bạn sử dụng ctxto server.Shutdownlà không chính xác. Ngữ cảnh đã bị hủy nên nó sẽ không bị tắt hoàn toàn. Bạn có thể đã kêu gọi server.Closetắt máy ô uế. (Để tắt hoàn toàn, mã này sẽ cần được làm lại toàn bộ.
Dave C

0

Có thể giải quyết vấn đề này bằng context.Contextcách sử dụng a net.ListenConfig. Trong trường hợp của tôi, tôi không muốn sử dụng một cuộc gọi sync.WaitGrouphoặc http.Server's Shutdown(), và thay vào đó dựa vào một context.Context(được đóng bằng một tín hiệu).

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

Những gì tôi đã làm cho những trường hợp như vậy trong đó ứng dụng chỉ là máy chủ và không thực hiện chức năng nào khác là cài đặt http.HandleFuncmột mẫu như thế /shutdown. Cái gì đó như

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

Nó không yêu cầu 1.8. Nhưng nếu 1.8 có sẵn, thì giải pháp đó có thể được nhúng ở đây thay vì os.Exit(0)cuộc gọi nếu muốn, tôi tin.

Mã để thực hiện tất cả công việc dọn dẹp đó được để lại như một bài tập cho người đọc.

Thêm tín dụng nếu bạn có thể nói nơi mã dọn dẹp đó có thể được đặt hợp lý nhất, vì tôi không khuyên bạn nên làm điều đó ở đây và cách lần truy cập điểm cuối này sẽ gây ra lệnh gọi mã đó.

Thêm tín dụng nếu bạn có thể nói nơi os.exit(0)cuộc gọi đó (hoặc bất kỳ lối ra quy trình nào bạn chọn sử dụng), được cung cấp ở đây chỉ cho mục đích minh họa, sẽ được đặt hợp lý nhất.

Tuy nhiên, thậm chí nhiều hơn thêm tín dụng nếu bạn có thể giải thích tại sao này cơ chế HTTP quá trình máy chủ phát tín hiệu cần được xem xét trên tất cả các cơ chế khác như nghĩ hoàn toàn khả thi trong trường hợp này.


Tất nhiên, tôi đã trả lời câu hỏi như đã hỏi mà không có thêm bất kỳ giả định nào về bản chất của vấn đề, và đặc biệt, không có giả định nào về bất kỳ môi trường sản xuất nhất định nào. Nhưng đối với bản chỉnh sửa của riêng tôi, @MarcinBilski, chính xác thì những yêu cầu nào sẽ khiến giải pháp này không phù hợp tốt cho bất kỳ môi trường, sản xuất hoặc cách nào khác?
greg.carter

2
Có nghĩa là nó giống như bất cứ điều gì vì rõ ràng là bạn sẽ không có trình xử lý / tắt máy trong một ứng dụng sản xuất. :) Bất cứ điều gì đi cho công cụ nội bộ, tôi đoán. Đó sang một bên, có nhiều cách để một cách duyên dáng tắt máy chủ vì vậy nó không đột nhiên thả kết nối hoặc tai nạn giữa đường thông qua một giao dịch cơ sở dữ liệu hoặc, tồi tệ hơn, khi viết vào đĩa, vv
Marcin Bilski

Chắc chắn không thể xảy ra trường hợp cử tri xuống cấp ngoài sức tưởng tượng. Chắc là tôi tưởng tượng quá nhiều. Tôi đã cập nhật phản hồi (bao gồm cả ví dụ) để sửa lỗi của mình.
greg.carter
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.