1

我正在使用 context.Context 来取消一个 http 请求

我发现虽然我得到了“上下文取消”,但底层的套接字连接仍然可用,几秒钟后我就能得到响应。一旦提出请求,它是否以这种方式设计来读取响应?

这是代码

func SendRequest(ctx context.Context, url string) {
    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        fmt.Println(err)
    }
    req = req.WithContext(ctx)

    res, err := client.Do(req)
    select {
    case <-ctx.Done():
        fmt.Printf("%s Canceled\n", url)
        //client.Transport.(*http.Transport).CancelRequest(req)
        //client.Transport.(*http.Transport).CloseIdleConnections()
    }
    if res != nil {
        defer res.Body.Close()
    }
    if err != nil {
        fmt.Printf("Failed: %v\n", err)
    } else {
        io.Copy(ioutil.Discard, res.Body)
        fmt.Printf("return status: %d\n", url, res.StatusCode)
    }
}

我请求的 URL 将在几秒钟后返回,因此我仍然可以读取响应正文,并且在进程退出后连接已关闭。

这是重现问题的简单代码

func client() {
    ctx, cancel := context.WithCancel(context.Background())
    client := http.DefaultClient
    request, _ := http.NewRequest("GET", "http://127.0.0.1:9090", nil)
    req := request.WithContext(ctx)

    go func() {
        client.Do(req)
    }()

    time.Sleep(time.Duration(1) * time.Second)
    cancel()

    <-time.After(time.Duration(10) * time.Second)

}

func sayhelloName(w http.ResponseWriter, r *http.Request) {
    time.Sleep(time.Duration(10) * time.Second)
    fmt.Fprintf(w, "Hello world!")
}

func server() {
    http.HandleFunc("/", sayhelloName)
    err := http.ListenAndServe(":9090", nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

连接在 10 秒后关闭
连接在 10 秒后关闭

4

1 回答 1

6

当上下文被取消时,您不需要自己做任何事情来取消请求。这已由标准 http 包处理,如文档所述。

您只需要:

func SendRequest(ctx context.Context, url string) {
    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        panic(err)
    }
    req = req.WithContext(ctx)

    res, err := client.Do(req)
    if err != nil {
        panic(err)
    }
    defer res.Body.Close()
    io.Copy(ioutil.Discard, res.Body)
    fmt.Printf("return status: %d\n", url, res.StatusCode)
}

编辑

您的 TCP 转储确认一切正常,并且正如我上面所描述的那样。

分解它:

您的出站请求确实在一秒钟后被取消,因为您的 TCP 转储确认:

6 2017-12-04 06:10:21.864955 0.0993000      40 TCP       17503 → 9090 [FIN, ACK] Seq=96 Ack=1 Win=8192 Len=0
7 2017-12-04 06:10:21.864955 0.0000000      40 TCP       9090 → 17503 [ACK] Seq=1 Ack=97 Win-7936 Len=0

FIN6 行的意思是 HTTP 客户端告诉服务器“我和你谈完了”。第ACK7 行表示服务器响应了请求,并关闭了连接。

但是,您的 HTTP 处理程序会忽略取消的请求,并尝试响应,生成第 8 行:

8 2017-12-04 06:10:30.868955 9.004000     169 HTTP      HTTP/1.1 200 OK  (text/plain)

但随后服务器收到连接无效的消息:

9 2017-12-04 06:10:30.868955  0.000000      40 TCP       17503 → 8080 [RST, ACK] Seq=97 Ack=130 Win=0 Len=0

RST表示连接已重置,当前无效。这是因为连接提前 9 秒终止。

所以你看,你的请求被立即取消了,就像它应该的那样。

您可以添加到代码中的唯一改进是让服务器中的 HTTP 处理程序实际检测并遵守此类取消,方法是在传入请求被取消时提前退出。

于 2017-12-01T11:23:53.100 回答