【问题标题】:Shutting down HTTP server after returning response返回响应后关闭 HTTP 服务器
【发布时间】:2021-07-10 00:41:37
【问题描述】:

我正在构建一个基于命令行的小 Go 机器人,它与 Instagram API 交互。

Instagram API 基于 OAuth,因此不太适合基于命令行的应用程序。

为了解决这个问题,我在浏览器中打开了适当的授权 URL,并使用我为重定向 URI 启动的本地服务器 - 这样我可以捕获并优雅地显示访问令牌,而不是用户需要获取手动从 URL 获取。

到目前为止一切顺利,应用程序可以成功打开浏览器到授权 URL,您对其进行授权,它会将您重定向到本地 HTTP 服务器。

现在,在向用户显示访问令牌后,我不再需要 HTTP 服务器,因此我想在执行此操作后手动关闭服务器。

为此,我从answer 中汲取了灵感,并鼓吹了以下内容:

package main

import (
    "fmt"
    "io"
    "log"
    "net/http"
    "os/exec"
    "runtime"
    "time"
)

var client_id = "my_client_id"
var client_secret = "my_client_secret"
var redirect_url = "http://localhost:8000/instagram/callback"

func main() {

    srv := startHttpServer()

    openbrowser(fmt.Sprintf("https://api.instagram.com/oauth/authorize/?client_id=%v&redirect_uri=%v&response_type=code", client_id, redirect_url))

    // Backup to gracefully shutdown the server
    time.Sleep(20 * time.Second)
    if err := srv.Shutdown(nil); err != nil {
        panic(err) // failure/timeout shutting down the server gracefully
    }
}

func showTokenToUser(w http.ResponseWriter, r *http.Request, srv *http.Server) {
    io.WriteString(w, fmt.Sprintf("Your access token is: %v", r.URL.Query().Get("code")))
    if err := srv.Shutdown(nil); err != nil {
        log.Fatal(err) // failure/timeout shutting down the server gracefully
    }
}

func startHttpServer() *http.Server {
    srv := &http.Server{Addr: ":8000"}

    http.HandleFunc("/instagram/callback", func(w http.ResponseWriter, r *http.Request) {
        showTokenToUser(w, r, srv)
    })

    go func() {
        if err := srv.ListenAndServe(); err != nil {
            // cannot panic, because this probably is an intentional close
            log.Printf("Httpserver: ListenAndServe() error: %s", err)
        }
    }()

    // returning reference so caller can call Shutdown()
    return srv
}

func openbrowser(url string) {
    var err error

    switch runtime.GOOS {
    case "linux":
        err = exec.Command("xdg-open", url).Start()
    case "windows":
        err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
    case "darwin":
        err = exec.Command("open", url).Start()
    default:
        err = fmt.Errorf("unsupported platform")
    }
    if err != nil {
        log.Fatal(err)
    }
}

但是,上面的导致这个错误:

2017/11/23 16:02:03 Httpserver:ListenAndServe() 错误:http:服务器关闭

2017/11/23 16:02:03 http:panic serving [::1]:61793:运行时错误:无效的内存地址或 nil 指针取消引用

如果我在处理程序中注释掉这些行,那么它可以完美运行,尽管在我点击回调路由时没有关闭服务器:

if err := srv.Shutdown(nil); err != nil {
    log.Fatal(err) // failure/timeout shutting down the server gracefully
}

我哪里出错了?在向用户显示文本后,我需要更改什么以便在点击回调路由时关闭服务器。

【问题讨论】:

    标签: go httpserver


    【解决方案1】:
    1. 您可以使用context.WithCancel:
    package main
    
    import (
        "context"
        "io"
        "log"
        "net/http"
    )
    
    func main() {
        ctx, cancel := context.WithCancel(context.Background())
    
        http.HandleFunc("/quit", func(w http.ResponseWriter, r *http.Request) {
            io.WriteString(w, "Bye\n")
            cancel()
        })
        http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
            io.WriteString(w, "Hi\n")
        })
    
        srv := &http.Server{Addr: ":8080"}
        go func() {
            err := srv.ListenAndServe()
            if err != http.ErrServerClosed {
                log.Println(err)
            }
        }()
    
        <-ctx.Done() // wait for the signal to gracefully shutdown the server
    
        // gracefully shutdown the server:
        // waiting indefinitely for connections to return to idle and then shut down.
        err := srv.Shutdown(context.Background())
        if err != nil {
            log.Println(err)
        }
    
        log.Println("done.")
    }
    

    1. The same Context 可以传递给在不同的 goroutine 中运行的函数:

    “多个 goroutine 同时使用上下文是安全的。”

    您可以使用相同的context - 如果您不想再等待:

    package main
    
    import (
        "context"
        "io"
        "log"
        "net/http"
    )
    
    func main() {
        ctx, cancel := context.WithCancel(context.Background())
        http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
            io.WriteString(w, "Hi\n")
        })
        http.HandleFunc("/quit", func(w http.ResponseWriter, r *http.Request) {
            io.WriteString(w, "Bye\n")
            cancel()
        })
        srv := &http.Server{Addr: ":8080"}
        go func() {
            if err := srv.ListenAndServe(); err != nil {
                log.Printf("Httpserver: ListenAndServe() error: %s", err)
            }
        }()
        <-ctx.Done()
        // if err := srv.Shutdown(ctx); err != nil && err != context.Canceled {
        //  log.Println(err)
        // }
        log.Println("done.")
    }
    

    Server.Shutdown:
    Shutdown 优雅地关闭服务器而不中断任何活动连接。关闭首先关闭所有打开的侦听器,然后关闭所有空闲连接,然后无限期等待连接返回空闲状态,然后关闭。如果提供的上下文在关闭完成之前过期,则 Shutdown 返回上下文的错误,否则返回关闭服务器的底层侦听器返回的任何错误。

    当调用 Shutdown 时,Serve、ListenAndServe 和 ListenAndServeTLS 立即返回 ErrServerClosed。确保程序不会退出,而是等待 Shutdown 返回。

    Shutdown 不会尝试关闭或等待被劫持的连接,例如 WebSockets。 Shutdown 的调用者应单独通知此类长期连接的关闭并等待它们关闭(如果需要)。有关注册关闭通知功能的方法,请参阅 RegisterOnShutdown。

    一旦在服务器上调用了 Shutdown,它就不能被重用;未来对 Serve 等方法的调用将返回 ErrServerClosed。

    【讨论】:

    • 完美运行。
    • &lt;-ctx.Done 在取消(完成)后返回。您不应将相同的上下文传递给 Shutdown 方法,因为 ctx 将始终处于取消状态。
    • 感谢您的评论,我认为这里有一些选择:无限期地等待连接返回空闲然后关闭 - 或者不关闭。此外,我们可能想也可能不想使用上下文 - 或使用其他一些同步方法。可能性仅限于您的想象力。
    【解决方案2】:

    Shutdown 函数接受参数ctx context.Context。尝试传递一个空的上下文。

    ctx := context.Background()
    

    还有:

    当调用 Shutdown 时,Serve、ListenAndServe 和 ListenAndServeTLS 立即返回 ErrServerClosed。确保程序不会退出,而是等待 Shutdown 返回。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-03-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-09-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多