【问题标题】:how to close/abort a Golang http.Client POST prematurely如何过早关闭/中止 Golang http.Client POST
【发布时间】:2015-03-22 17:43:22
【问题描述】:

我正在使用http.Client 进行长轮询的客户端实现:

resp, err := client.Post(url, "application/json", bytes.NewBuffer(jsonPostBytes))
if err != nil {
    panic(err)
}
defer resp.Body.Close()

var results []*ResponseMessage
err = json.NewDecoder(resp.Body).Decode(&results)  // code blocks here on long-poll

是否有一种标准的方式来抢占/取消来自客户端的请求?

我想调用 resp.Body.Close() 可以做到这一点,但我必须从另一个 goroutine 调用它,因为客户端通常在读取长轮询的响应时已经被阻塞了。

我知道有一种方法可以通过 http.Transport 设置超时,但我的应用逻辑需要根据用户操作进行取消,而不仅仅是超时。

【问题讨论】:

    标签: go long-polling


    【解决方案1】:

    现在不推荐使用CancelRequest。

    当前的策略是使用http.Request.WithContext 传递带有截止日期的上下文,否则将被取消。 之后就像正常请求一样使用它。

    req, err := http.NewRequest("GET", "http://example.com", nil)
    // ...
    req.Header.Add("If-None-Match", `W/"wyzzy"`)
    req = req.WithContext(ctx)
    resp, err := client.Do(req)
    // ...
    

    【讨论】:

      【解决方案2】:

      标准方法是使用context.Context 类型的上下文,并将其传递给所有需要知道何时取消请求的函数。

      func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error {
          // Run the HTTP request in a goroutine and pass the response to f.
          tr := &http.Transport{}
          client := &http.Client{Transport: tr}
          c := make(chan error, 1)
          go func() { c <- f(client.Do(req)) }()
          select {
          case <-ctx.Done():
              tr.CancelRequest(req)
              <-c // Wait for f to return.
              return ctx.Err()
          case err := <-c:
              return err
          }
      }
      

      golang.org/x/net/context

      // A Context carries a deadline, cancelation signal, and request-scoped values
      // across API boundaries. Its methods are safe for simultaneous use by multiple
      // goroutines.
      type Context interface {
          // Done returns a channel that is closed when this Context is canceled
          // or times out.
          Done() <-chan struct{}
      
          // Err indicates why this context was canceled, after the Done channel
          // is closed.
          Err() error
      
          // Deadline returns the time when this Context will be canceled, if any.
          Deadline() (deadline time.Time, ok bool)
      
          // Value returns the value associated with key or nil if none.
          Value(key interface{}) interface{}
      }
      

      来源和更多信息https://blog.golang.org/context

      更新

      正如 Paulo mentioned,Request.Cancel 现在已弃用,作者应将上下文传递给请求本身(使用 *Request.WithContext)并使用上下文的取消通道(取消请求)。

      package main
      
      import (
          "context"
          "net/http"
          "time"
      )
      
      func main() {
          cx, cancel := context.WithCancel(context.Background())
          req, _ := http.NewRequest("GET", "http://google.com", nil)
          req = req.WithContext(cx)
          ch := make(chan error)
      
          go func() {
              _, err := http.DefaultClient.Do(req)
              select {
              case <-cx.Done():
                  // Already timedout
              default:
                  ch <- err
              }
          }()
      
          // Simulating user cancel request
          go func() {
              time.Sleep(100 * time.Millisecond)
              cancel()
          }()
          select {
          case err := <-ch:
              if err != nil {
                  // HTTP error
                  panic(err)
              }
              print("no error")
          case <-cx.Done():
              panic(cx.Err())
          }
      
      }
      

      【讨论】:

      • 你甚至不需要额外的 goroutine 或特殊的客户端。只需设置req.Cancel = ctx.Done(),它就会为您处理一切。
      • 自从我发布答案以来,http 库经历了几次更新。 @Paulo 现在提供了最惯用的解决方案。
      • @captncraig 我已经更新了我的答案。如果方法再次改变(尽管我对此表示怀疑),请随意写新的答案,并希望它会得到应有的投票。我认为 SO 作者不负责维护。投票系统应该在这方面有所帮助。
      • 这是关于代码的更新部分。我是新手,但这看起来不对。想法是取消请求,这里第一个 go 例程一直运行,直到请求实际完成。你不取消它,你只是根据谁先完成在主线程中前进。
      • @shadow,我已经更新了答案以包含实际的取消操作(WithCancel)
      【解决方案3】:

      不,client.Post 是 90% 不需要取消请求的用例的便捷包装器。

      可能只需重新实现您的客户端以访问具有 CancelRequest() 函数的底层 Transport 对象就足够了。

      只是一个简单的例子:

      package main
      
      import (
          "log"
          "net/http"
          "time"
      )
      
      func main() {
          req, _ := http.NewRequest("GET", "http://google.com", nil)
          tr := &http.Transport{} // TODO: copy defaults from http.DefaultTransport
          client := &http.Client{Transport: tr}
          c := make(chan error, 1)
          go func() {
              resp, err := client.Do(req)
              // handle response ...
              _ = resp
              c <- err
          }()
      
          // Simulating user cancel request channel
          user := make(chan struct{}, 0)
          go func() {
              time.Sleep(100 * time.Millisecond)
              user <- struct{}{}
          }()
      
          for {
              select {
              case <-user:
                  log.Println("Cancelling request")
                  tr.CancelRequest(req)
              case err := <-c:
                  log.Println("Client finished:", err)
                  return
              }
          }
      }
      

      【讨论】:

      • 并不是说它与这个答案有关,但通常最好使用http.DefaultTransport,它有合理的超时(和ProxyFromEnvironment)而不是空的Transport。
      • 当然,不要完全按照上面写的那样使用。这是一个精简的示例,它丢弃了从client.Do 返回的*http.Response,而不调用resp.Body.Close()。永远不要那样做。
      • 啊,tr.CancelRequest()(以及相关的示例代码)就是我要找的。​​span>
      • @CaffeineComa 如果您不接受这一点并在下面接受保罗或 themihai 的回答,那很好。现在这是一个更好的答案。
      【解决方案4】:

      要添加到将 context.Context 附加到 http 请求的其他答案,因为 1.13 我们有:

      添加了一个新函数NewRequestWithContext,它接受一个Context,它控制所创建的传出Request 的整个生命周期,适用于Client.Do 和Transport.RoundTrip。

      https://golang.org/doc/go1.13#net/http

      可以使用这个函数来代替NewRequest然后Request.WithContext。

      req, err := http.NewRequest(...)
      if err != nil {...}
      req.WithContext(ctx)
      

      变成

      req, err := http.NewRequestWithContext(ctx, ...)
      if err != nil {...}
      

      【讨论】:

        【解决方案5】:

        @Paulo Casaretto 的答案是正确的,应该使用 http.Request.WithContext.

        这是一个完整的演示(请注意时间数字:5、10、30 秒)。

        HTTP 服务器:

        package main
        
        import (
            "fmt"
            "log"
            "net/http"
            "time"
        )
        
        func handler(w http.ResponseWriter, r *http.Request) {
            fmt.Println("before sleep")
            time.Sleep(10 * time.Second)
            fmt.Println("after sleep")
        
            fmt.Fprintf(w, "Hi")
        }
        
        func main() {
            http.HandleFunc("/", handler)
            log.Fatal(http.ListenAndServe(":9191", nil))
        }
        

        HTTP 服务器控制台打印:

        before sleep
        after sleep 
        

        HTTP 客户端:

        package main
        
        import (
            "context"
            "fmt"
            "net/http"
            "time"
        )
        
        func main() {
            ctx, cancel := context.WithCancel(context.Background())
        
            go func() {
                fmt.Println("before request")
                client := &http.Client{Timeout: 30 * time.Second}
                req, err := http.NewRequest("GET", "http://127.0.0.1:9191", nil)
                if err != nil {
                    panic(err)
                }
                req = req.WithContext(ctx)
                _, err = client.Do(req)
                if err != nil {
                    panic(err)
                }
                fmt.Println("will not reach here")
            }()
        
            time.Sleep(5 * time.Second)
            cancel()
            fmt.Println("finished")
        }
        

        HTTP 客户端控制台打印:

        before request
        finished
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2015-06-25
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-11-30
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多