Categories
ProgramingLanguage

go net/http timeout

当用 golang 创建一个 HTTP Server 或者 client 的时候, 超时(timeout) 是最容易且细微的发生错误的地方: 有很多的选项并且一个错误长时间也不会有什么影响直到网络出现问题,然后导致进程 hang 住.

HTTP 是一个复杂的多阶段协议, 所以对超时并没有一个适用多种情况的解决方案.

在这篇文章中,我将分解多个你需要去应用超时的阶段,在 client 端和 Server 端探索不同的解决超时方法.

SetDeadline

首先你需要知道 go 为了实现超时所使用的网络原语: Deadlines.

我们可以使用net.Conn 接口的 Set(Read|Write)Deadline方法,来设置 Deadline, Deadline 是一个绝对时间,当到达这个绝对时间的时候会让所有的 I/O 操作都失败, 并返回timeout error.

Deadline 不是超时, 一旦设置之后就会固定住(或者再次调用 SetDeadline), 不管是否如何使用这个网路连接. 为了使用 SetDeadline 创建一个timeout, 你需要在每次发起请求操作的时候都 call SetDeadline.

你可能并不想调用 SetDeadline, 而是让 net/http 包来为你做这件事. 然而请记住所有 timeout 都是使用 Deadline 来实现的. 所以在发送和接收数据的时候, timeout并不会每次都重置。

Server Timeouts

https://blog.cloudflare.com/exposing-go-on-the-internet/ 这篇有更多关于 http server 的超时信息.

对于将要暴露在 网络上的 HTTP Server 对于客户端连接设置超时是非常关键的, 否则很慢的或者消失的客户端可能会泄露文件描述符,最终导致 如下所示的错误:

http: Accept error: accept tcp [::]:80: accept4: too many open files; retrying in 5ms

HTTP Server 可以设置 ReadTimeout 和 WriteTimeout.

import (
	"fmt"
	"net/http"
	"time"
)

func main() {
	s := &http.Server{
		ReadTimeout:  2 * time.Second,
		WriteTimeout: 3 * time.Second,
	}

	if err := s.ListenAndServe(); err != nil {
		fmt.Printf("run server error: %v\n", err)
	}
}

ReadTimeout 的时间范围是从接受连接到完全读取请求 body 之间的时间。 它在net / http中实现,方法是在Accept之后立即调用SetReadDeadline

WriteTimeout 包括从结束读取 http 请求头到写 response 结束之间的时间(aka. ServeHTTP的生命周期), 通过调用 SetWriteDeadlinereadRequest 结束的时候.(go/server.go at 3ba31558d1bca8ae6d2f03209b4cae55381175b3 · golang/go (github.com))

Client timeout

Client 端的超时可以很简单也可以比较复杂。取决于你使用哪一个。

最简单的就是使用 timeout in http.Client

cli := &http.Client{
		Timeout: 5 * time.Second,
	}
	resp, err := cli.Get("https://techwiki.io")
	if err != nil {
		fmt.Println("request error: ", err)
	}

为了更好地控制超时,有几个具体的超时设置

  • net.Dialer.Timeout 限制创建 TCP 连接所需要的时间.
  • http.Transport.TLSHandshakeTimeout 限制花在建立 TLS handshake 上的时间.
  • http.Transport.ResponseHeaderTimeout 限制读取响应头所花费的时间.
  • http.Transport.ExpectContinueTimeout 限制 client 在发送 http request headers(包括 Expect: 100-continue) 收到 go-ahead 发送 body 之间等待的时间.
cli := &http.Client{
		Transport: &http.Transport{
			Dial: (&net.Dialer{
				Timeout:   30 * time.Second,
				KeepAlive: 30 * time.Second,
			}).Dial,
			TLSHandshakeTimeout:   10 * time.Second,
			ResponseHeaderTimeout: 10 * time.Second,
			ExpectContinueTimeout: time.Second,
		},
	}

Cancel and Context

net/http 有两种提供取消 http 请求的方式. Request.Cancel(Deprecated) 和 Context

package main

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"time"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	timer := time.AfterFunc(10*time.Millisecond, func() {
		cancel()
	})

	req, err := http.NewRequest(http.MethodGet, "https://httpbin.org/get", nil)
	if err != nil {
		fmt.Printf("request get error: %v\n", err)
	}
	req = req.WithContext(ctx)

	fmt.Println("sending request...")
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		fmt.Println("sending err: ", err) // sending err:  Get "https://httpbin.org/get": context canceled
		return
	}

	defer resp.Body.Close()

	fmt.Println("reading body")
	for {
		timer.Reset(10 * time.Millisecond)
		_, err := io.CopyN(io.Discard, resp.Body, 256)
		if err == io.EOF {
			break
		} else if err != nil {
			fmt.Println("read body err: ", err)
			return
		}
	}
}

Leave a Reply

Your email address will not be published. Required fields are marked *