Programação Go HTTP (2): anatomia da implementação subjacente de http.Client

A estrutura de dados de http.Client

Nós já introduziram, http.Get(), http.Post(), http.PostForm() e  http.Head() métodos são realmente  http.DefaultClient realizado com base da chamada.

http.DefaultClient É a   implementação padrão do cliente HTTP fornecido pelo pacote net / http :

// DefaultClient is the default Client and is used by Get, Head, and Post.
var DefaultClient = &Client{}

Na verdade, também podemos implementá-lo com base em  http.Client um cliente HTTP personalizado. Antes disso, vamos dar uma olhada na  Client estrutura de dados do tipo:

type Client struct {
    // Transport 用于指定单次 HTTP 请求响应的完整流程
    // 默认值是 DefaultTransport 
    Transport RoundTripper

    // CheckRedirect 用于定义重定向处理策略
    // 它是一个函数类型,接收 req 和 via 两个参数,分别表示即将发起的请求和已经发起的所有请求,最早的已发起请求在最前面
    // 如果不为空,客户端将在跟踪 HTTP 重定向前调用该函数
    // 如果返回错误,客户端将直接返回错误,不会再发起该请求
    // 如果为空,Client 将采用一种确认策略,会在 10 个连续请求后终止 
    CheckRedirect func(req *Request, via []*Request) error

    // Jar 用于指定请求和响应头中的 Cookie 
    // 如果该字段为空,则只有在请求中显式设置的 Cookie 才会被发送
    Jar CookieJar

    // 指定单次 HTTP 请求响应事务的超时时间
    // 未设置的话使用 Transport 的默认设置,为零的话表示不设置超时时间
    Timeout time.Duration
}

Transport campo deve implementar a  http.RoundTripper interface, Transport especificando o fluxo completo de uma transação HTTP (resposta da solicitação). Se não for especificado  Transport, http.DefaultTransport a implementação padrão será usada por  padrão. Por exemplo  http.DefaultClient , este é o caso, e discutiremos http.DefaultTransport a implementação subjacente em profundidade mais tarde  .

CheckRedirect As funções são usadas para definir estratégias para lidar com o redirecionamento. Ao  enviar uma solicitação HTTP usando o método Get() ou  fornecido pelo cliente HTTP padrão  Head(), se o código de status de resposta for  30x (como  301, 302 etc.), o cliente HTTP chamará esta CheckRedirect função antes de seguir a regra de redirecionamento  .

Jar Pode ser usado para definir cookies no cliente HTTP.O Jar tipo deve implementar uma  http.CookieJar interface, que tem predefinidos  SetCookies() e  Cookies() dois métodos. Se não houver configuração no cliente HTTP  Jar, o cookie será ignorado e não será enviado ao cliente. Na verdade, geralmente usamos  http.SetCookie() métodos para definir cookies.

Timeout O campo é usado para especificar  Transport o período de tempo limite, se não for especificado, Transport uma configuração personalizada é usada  .

A implementação subjacente de http.Transport

A seguir, usaremos  http.DefaultTransport a implementação para focar   , ela será usada quando http.Transportnão houver uma configuração explícita do  Transportcampo  DefaultTransport:

func (c *Client) transport() RoundTripper {
    if c.Transport != nil {
        return c.Transport
    }
    return DefaultTransport
}

DefaultTransport Sim  Transport , a implementação padrão, o código de inicialização correspondente é o seguinte:

var DefaultTransport RoundTripper = &Transport{
    Proxy: ProxyFromEnvironment,
    DialContext: (&net.Dialer{
        Timeout:   30 * time.Second,
        KeepAlive: 30 * time.Second,
        DualStack: true,
    }).DialContext,
    MaxIdleConns:          100,
    IdleConnTimeout:       90 * time.Second,
    TLSHandshakeTimeout:   10 * time.Second,
    ExpectContinueTimeout: 1 * time.Second,
}

Aqui, apenas Transport parte dos atributos são definidos  e Transport a estrutura de dados completa do tipo é a seguinte:

type Transport struct {
    ...
    
    // 定义 HTTP 代理策略
    Proxy func(*Request) (*url.URL, error)
    
    // 用于指定创建未加密 TCP 连接的上下文参数(通过 net.Dial()创建连接时使用)
    DialContext func(ctx context.Context, network, addr string) (net.Conn, error)
    // 已废弃,使用 DialContext 替代
    Dial func(network, addr string) (net.Conn, error)
    // 创建加密 TCP 连接
    DialTLS func(network, addr string) (net.Conn, error)
    // 指定 tls.Client 所使用的 TLS 配置
    TLSClientConfig *tls.Config
    // TLS 握手超时时间
    TLSHandshakeTimeout time.Duration
    
    // 是否禁用 HTTP 长连接
    DisableKeepAlives bool
    // 是否对 HTTP 报文进行压缩传输(gzip)
    DisableCompression bool
    
    // 最大空闲连接数(支持长连接时有效)
    MaxIdleConns int
    // 单个服务(域名)最大空闲连接数
    MaxIdleConnsPerHost int
    // 单个服务(域名)最大连接数
    MaxConnsPerHost int
    // 空闲连接超时时间
    IdleConnTimeout time.Duration

    // 从客户端把请求完全提交给服务器到从服务器接收到响应报文头的超时时间
    ResponseHeaderTimeout time.Duration
    // 包含 "Expect: 100-continue" 请求头的情况下从客户端把请求完全提交给服务器到从服务器接收到响应报文头的超时时间
    ExpectContinueTimeout time.Duration
    
    ...        
}

Transport Observe DefaultTransport as configurações em conjunto com a  estrutura de dados  :

  • Ao  net.Dialer inicializar a configuração do contexto de discagem, o período de tempo limite padrão é definido para 30 segundos;
  • Via  MaxIdleConns conexão 100 para especificar o número máximo de ociosos, não explicitamente definido  MaxIdleConnsPerHost e  MaxConnsPerHost, MaxIdleConnsPerHost com um valor padrão,  http.DefaultMaxIdleConnsPerHost definindo o valor padrão correspondente é 2;
  • Ao  IdleConnTimeout especificar o tempo máximo de conexão ociosa de 90 segundos, ou seja, quando uma conexão ociosa não é reutilizado por mais de 90 segundos, ele será destruído. As necessidades de conexão ociosa  DisableKeepAlives para  false estar disponível, ou seja, é válido no HTTP longa estado da conexão (HTTP / 1.1 e superior) Suporte a conexão longa, correspondendo ao cabeçalho da solicitação  Connection:keep-alive);
  • Ao  TLSHandshakeTimeout especificar que uma conexão TCP segura com base no protocolo TLS é estabelecida, o período de tempo limite da fase de handshake é de 10 segundos;
  • Ao  ExpectContinueTimeout especificar que o cliente deseja usar uma solicitação POST para enviar um corpo de mensagem grande ao servidor, ele primeiro Expect: 100-continue pergunta ao servidor se deseja receber o período de tempo limite correspondente a esse corpo de mensagem grande enviando um cabeçalho de solicitação incluído  . configuração aqui é de 1 segundo.

Além disso, Transport a RoundTrip implementação do método está incluída  , de forma que a RoundTripper interface é implementada  . Vejamos  Transport a  RoundTrip implementação do método.

Implementação do método Transport.RoundTrip ()

Primeiro, vamos dar uma olhada na  http.RoundTripper definição específica da interface:

type RoundTripper interface {
    RoundTrip(*Request) (*Response, error)
}

Como você pode ver no código acima, a http.RoundTripper interface é muito simples e define apenas um RoundTrip método chamado  . RoundTrip() O método é usado para realizar uma transação HTTP independente, aceitar o *Request valor da solicitação de entrada  como um parâmetro e retornar o *Response valor de resposta correspondente  , bem como um  error valor.

Ao implementar um RoundTrip() método específico  , você não deve tentar analisar as informações de resposta HTTP nesta função. Se a resposta for bem-sucedida, error o valor deve ser  nil, independentemente do código de status HTTP retornado. Se a resposta do servidor não puder ser obtida com sucesso, ela error deve ser um valor diferente de zero. Da mesma forma, você não deve tentar  RoundTrip() lidar com detalhes no nível do protocolo, como redirecionamento, autenticação ou cookies.

Se não for necessário, o RoundTrip() objeto de solicitação recebido ( *Request) não deve ser reescrito no  método. O conteúdo da solicitação (como URL e cabeçalho, etc.) deve ser RoundTrip() organizado e inicializado antes de ser passado  .

Qualquer RoundTrip() tipo que implementa um  método implementa uma  http.RoundTripper interface. http.Transport É o RoundTrip() método que implementa o  método e, em seguida, implementa a interface. Na camada inferior, Go  implementa uma única transação de resposta de solicitação HTTP por meio da  API de busca WHATWG :

func (t *Transport) RoundTrip(req *Request) (*Response, error) {
	if useFakeNetwork() {
		return t.roundTrip(req)
	}

	ac := js.Global().Get("AbortController")
	if ac != js.Undefined() {
		// Some browsers that support WASM don't necessarily support
		// the AbortController. See
		// https://developer.mozilla.org/en-US/docs/Web/API/AbortController#Browser_compatibility.
		ac = ac.New()
	}

	opt := js.Global().Get("Object").New()
	// See https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
	// for options available.
	opt.Set("method", req.Method)
	opt.Set("credentials", "same-origin")
	if h := req.Header.Get(jsFetchCreds); h != "" {
		opt.Set("credentials", h)
		req.Header.Del(jsFetchCreds)
	}
	if h := req.Header.Get(jsFetchMode); h != "" {
		opt.Set("mode", h)
		req.Header.Del(jsFetchMode)
	}
	if ac != js.Undefined() {
		opt.Set("signal", ac.Get("signal"))
	}
	headers := js.Global().Get("Headers").New()
	for key, values := range req.Header {
		for _, value := range values {
			headers.Call("append", key, value)
		}
	}
	opt.Set("headers", headers)

	if req.Body != nil {
		// TODO(johanbrandhorst): Stream request body when possible.
		// See https://bugs.chromium.org/p/chromium/issues/detail?id=688906 for Blink issue.
		// See https://bugzilla.mozilla.org/show_bug.cgi?id=1387483 for Firefox issue.
		// See https://github.com/web-platform-tests/wpt/issues/7693 for WHATWG tests issue.
		// See https://developer.mozilla.org/en-US/docs/Web/API/Streams_API for more details on the Streams API
		// and browser support.
		body, err := ioutil.ReadAll(req.Body)
		if err != nil {
			req.Body.Close() // RoundTrip must always close the body, including on errors.
			return nil, err
		}
		req.Body.Close()
		a := js.TypedArrayOf(body)
		defer a.Release()
		opt.Set("body", a)
	}
	respPromise := js.Global().Call("fetch", req.URL.String(), opt)
	var (
		respCh = make(chan *Response, 1)
		errCh  = make(chan error, 1)
	)
	success := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		result := args[0]
		header := Header{}
		// https://developer.mozilla.org/en-US/docs/Web/API/Headers/entries
		headersIt := result.Get("headers").Call("entries")
		for {
			n := headersIt.Call("next")
			if n.Get("done").Bool() {
				break
			}
			pair := n.Get("value")
			key, value := pair.Index(0).String(), pair.Index(1).String()
			ck := CanonicalHeaderKey(key)
			header[ck] = append(header[ck], value)
		}

		contentLength := int64(0)
		if cl, err := strconv.ParseInt(header.Get("Content-Length"), 10, 64); err == nil {
			contentLength = cl
		}

		b := result.Get("body")
		var body io.ReadCloser
		// The body is undefined when the browser does not support streaming response bodies (Firefox),
		// and null in certain error cases, i.e. when the request is blocked because of CORS settings.
		if b != js.Undefined() && b != js.Null() {
			body = &streamReader{stream: b.Call("getReader")}
		} else {
			// Fall back to using ArrayBuffer
			// https://developer.mozilla.org/en-US/docs/Web/API/Body/arrayBuffer
			body = &arrayReader{arrayPromise: result.Call("arrayBuffer")}
		}

		select {
		case respCh <- &Response{
			Status:        result.Get("status").String() + " " + StatusText(result.Get("status").Int()),
			StatusCode:    result.Get("status").Int(),
			Header:        header,
			ContentLength: contentLength,
			Body:          body,
			Request:       req,
		}:
		case <-req.Context().Done():
		}

		return nil
	})
	defer success.Release()
	failure := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		err := fmt.Errorf("net/http: fetch() failed: %s", args[0].String())
		select {
		case errCh <- err:
		case <-req.Context().Done():
		}
		return nil
	})
	defer failure.Release()
	respPromise.Call("then", success, failure)
	select {
	case <-req.Context().Done():
		if ac != js.Undefined() {
			// Abort the Fetch request
			ac.Call("abort")
		}
		return nil, req.Context().Err()
	case resp := <-respCh:
		return resp, nil
	case err := <-errCh:
		return nil, err
	}
}

Como http.RoundTripper o código que implementa a  interface geralmente precisa ser executado simultaneamente em várias goroutines, devemos garantir a segurança do thread do código de implementação.

Os itens acima são os  http.Client principais componentes da implementação subjacente e suas implementações padrão. O foco está em  http.Transport. Ele define o processo completo de uma transação HTTP. Podemos personalizar  Transport a solicitação do cliente HTTP por meio da personalização. Basta entendê-la. No desenvolvimento real, geralmente só precisamos chamar alguns métodos fornecidos no tutorial anterior , a menos que precisemos fazer um desenvolvimento e customização de baixo nível, caso contrário, geralmente não os envolveremos.

Acho que você gosta

Origin blog.csdn.net/wxy_csdn_world/article/details/107444870
Recomendado
Clasificación