golang http2客户端处理逻辑分析

结构

客户端transport

transport包含了各种配置选项和功能,用于管理与服务器的 HTTP/2 连接和请求

type Transport struct {
   
   
  // 用于创建 TLS 连接的可选拨号函数,支持上下文。如果未设置,将使用 tls.Dial。
  // 如果返回的net.Conn有像tls.Conn的ConnectionState方法,那么将会用于设置http.Response.TLS
	DialTLSContext func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error)

	// Deprecated: Use DialTLSContext instead, which allows the transport
	// to cancel dials as soon as they are no longer needed.
	// If both are set, DialTLSContext takes priority.
	DialTLS func(network, addr string, cfg *tls.Config) (net.Conn, error)

  // TLS客户端配置
	TLSClientConfig *tls.Config

  // 客户端连接池
	ConnPool ClientConnPool

  // 如果为 true,则防止传输请求带有 "Accept-Encoding: gzip" 请求头。当传输自动请求 gzip 并接收到 gzip 响应时,会透明地解码响应体。但如果用户明确请求了 gzip,则不会自动解压。
	DisableCompression bool

  // 是否允许http2请求使用不安全的http
  // Note that this does not enable h2c support.
	AllowHTTP bool

  // 在初始设置帧中发送的 SETTINGS_MAX_HEADER_LIST_SIZE 值,表示允许的响应头的字节数。0 表示使用默认限制(目前为 10MB)。如果要告知对等方无限制,则设置为最高值 0xffffffff。
	MaxHeaderListSize uint32

  // 在初始设置帧中发送的 SETTINGS_MAX_FRAME_SIZE 值,表示发送者愿意接收的最大帧负载大小。如果为 0,则不发送设置,由对等方提供值(应该为 16384)
  	// according to the spec: https://datatracker.ietf.org/doc/html/rfc7540#section-6.5.2.
	// Values are bounded in the range 16k to 16M.
	MaxReadFrameSize uint32

  // 在初始设置帧中发送的 SETTINGS_HEADER_TABLE_SIZE 值,表示用于解码头块的头压缩表的最大大小。如果为 0,则使用默认值 4096。
	MaxDecoderHeaderTableSize uint32

  // 指定用于编码请求头的头压缩表的最大上限。接收到的 SETTINGS_HEADER_TABLE_SIZE 设置将限制在该上限内。如果为 0,则使用默认值 4096。
	MaxEncoderHeaderTableSize uint32

  // 控制是否全局尊重服务端的 SETTINGS_MAX_CONCURRENT_STREAMS。如果为 false,则根据需要创建新的 TCP 连接,每个连接都遵守 SETTINGS_MAX_CONCURRENT_STREAMS 限制。如果为 true,则解释为全局限制,调用 RoundTrip 时会在需要时等待。
	StrictMaxConcurrentStreams bool

  // 空闲(保持活动)连接在关闭之前可以保持空闲的最长时间。0 表示无限制
	IdleConnTimeout time.Duration

  // 在连接上没有接收到帧后进行健康检查(使用 ping 帧)的超时时间。如果为 0,则不进行健康检查。
	ReadIdleTimeout time.Duration

	// 在未收到 Ping 响应后关闭连接的超时时间。默认是 15 秒
	PingTimeout time.Duration

  // 连接关闭的超时时间,超过这个时间就不能写入数据了。
  // 超时从数据可以写入时开始,并在写入任何字节时扩展。
	WriteByteTimeout time.Duration

  // 如果非空,在 HTTP/2 传输错误时调用。用于增量监控指标(如 expvar 或 Prometheus 指标)
	CountError func(errType string)

  // 如果非空,则是使用此传输的标准库传输。其设置将被使用(但不包括 RoundTrip 方法等)
	t1 *http.Transport

  // 用于确保连接池只初始化一次
	connPoolOnce  sync.Once
  // 非空版本的 ConnPool
	connPoolOrDef ClientConnPool

	syncHooks *testSyncHooks
}

客户端连接

客户端连接包含了连接状态、流量控制、请求控制与数据处理等

type ClientConn struct {
   
   
  // 连接关联的Transport
	t             *Transport
  // 低层网络连接
	tconn         net.Conn             // usually *tls.Conn, except specialized impls
  // TLS连接状态
	tlsState      *tls.ConnectionState // nil only for specialized impls
  // 代表连接是否正在被复用
	reused        uint32               
  // 代表连接是否正在被单个http请求使用
	singleUse     bool
  
	getConnCalled bool                 // used by clientConnPool

  // readLoop goroutine读取发生错误
	readerDone chan struct{
   
   } // closed on error
	readerErr  error         // set before readerDone is closed

  // 空闲超时定时器
	idleTimeout time.Duration // or 0 for never
	idleTimer   timer

	mu              sync.Mutex // guards following
	cond            *sync.Cond // hold mu; broadcast on flow/closed changes
  
  // 连接级别的流量控制额度
	flow            outflow    
  // 对端的连接级别的流量控制额度
	inflow          inflow  
  
  // 标记连接不可被复用
	doNotReuse      bool       
  // 表示连接正在被关闭
	closing         bool
  // 表示连接已经被关闭
	closed          bool
  // 表示是否已经收到setting帧
	seenSettings    bool                     
  // 表示是否期待setting帧的回应
	wantSettingsAck bool                     
  // 收到的GOAWAY帧
	goAway          *GoAwayFrame             
	goAwayDebug     string                   // goAway frame's debug data, retained as a string

  // 客户端发起的stream
	streams         map[uint32]*clientStream 
  // 等待请求预留的流数量
	streamsReserved int                      // incr by ReserveNewRequest; decr on RoundTrip
  // 下一个stream id
	nextStreamID    uint32
  // 等待请求数量
	pendingRequests int                       // requests blocked and waiting to be sent because len(streams) == maxConcurrentStreams
  
  // 用于存储正在传输的 ping 数据到通知通道
	pings           map[[8]byte]chan struct{
   
   } // in flight ping data to notification channel
	// 用于从连接中读取数据的缓冲读取器
	br              *bufio.Reader
  
  // 最后一次活动的时间和最后一次空闲的时间
	lastActive      time.Time
	lastIdle        time.Time // time last idle
  
  // 这些字段表示从对端接收到的设置,包括最大帧大小、最大并发流数量、对端最大头列表大小、对端最大头表大小和初始窗口大小
	maxFrameSize           uint32
	maxConcurrentStreams   uint32
	peerMaxHeaderListSize  uint64
	peerMaxHeaderTableSize uint32
	initialWindowSize      uint32

  // 用于控制发送新请求的访问。向 reqHeaderMu 写入以锁定它,从中读取以解锁它
	reqHeaderMu chan struct{
   
   }

	// wmu is held while writing.
	// Acquire BEFORE mu when holding both, to avoid blocking mu on network writes.
	// Only acquire both at the same time when changing peer settings.
	wmu  sync.Mutex
  // 持有网络连接的缓存写入器
	bw   *bufio.Writer
  // 帧编解码器
	fr   *Framer
	werr error        // first write error that has occurred
  
  // 字节缓冲区和一个 HPACK 编码器,用于编码 HTTP 头部
	hbuf bytes.Buffer // HPACK encoder writes into this
	henc *hpack.Encoder

	syncHooks *testSyncHooks // can be nil
}

客户端流

在 HTTP/2 中,一个 TCP 连接可以被划分为多个流,每个流都有一个唯一的标识符。这些流可以并行地传输请求和响应消息,而不需要等待前一个请求或响应完成。这种机制被称为多路复用,它可以显著地提高网络的利用率和性能。

每个 HTTP/2 流都有一个状态,包括 idle、open、half-closed(local)、half-closed(remote)、closed 等。流的状态会随着帧(Frame)的发送和接收而改变。例如,当一个流收到一个包含 END_STREAM 标志的帧时,它的状态会变为 half-closed。

此外,HTTP/2 还引入了流控制(Flow Control)机制,可以防止发送方过快地发送数据,导致接收方无法处理。流控制是在每个流的基础上进行的,也就是说,每个流都有自己的流控制窗口。

type clientStream struct {
   
   
  // 表示这个流所属的客户端连接
	cc *ClientConn

	// Fields of Request that we may access even after the response body is closed.
  // 分别表示请求的上下文和请求的取消通道
	ctx       context.Context
	reqCancel <-chan struct{
   
   }

  // 追踪http请求的生命周期
	trace         *httptrace.ClientTrace // or nil
  // 流ID
	ID            uint32
  // 带缓冲的管道,用于存储流控制的响应负载
	bufPipe       pipe 
  // 表示是否请求了 Gzip 压缩
	requestedGzip bool
  // 表示是否是 HEAD 请求
	isHead        bool

  // 用于表示流是否应该立即结束
	abortOnce sync.Once
	abort     chan struct{
   
   } // closed to signal stream should end immediately
	abortErr  error         // set if abort is closed

  // 这两个通道分别在对端发送 END_STREAM 标志和流进入关闭状态后关闭
	peerClosed chan struct{
   
   } 
	donec      chan struct{
   
   } 
  
  // 缓冲通道,如果收到 100 响应,会写入这个通道
	on100      chan struct{
   
   }

  // 两个字段分别表示是否收到了响应头和响应
	respHeaderRecv chan struct{
   
   }  
	res            *http.Response

  // 表示出流和入流的流量控制
	flow        outflow // guarded by cc.mu
	inflow      inflow  // guarded by cc.mu
  
  // 分别表示剩余的字节数和读取错误
	bytesRemain int64   // -1 means unknown; owned by transportResponseBody.Read
	readErr     error   // sticky read error; owned by transportResponseBody.Read

  // 表示请求体、请求体的内容长度和请求体的关闭通道
	reqBody              io.ReadCloser
	reqBodyContentLength int64         // -1 means unknown
	reqBodyClosed        chan struct{
   
   } // guarded by cc.mu; non-nil on Close, closed when done

  // 表示是否向对端发送了 END_STREAM 标志和头部。
	// owned by writeRequest:
	sentEndStream bool
	sentHeaders   bool

  // 这些字段表示读取循环的状态
	// owned by clientConnReadLoop:
	firstByte    bool  // got the first response byte
	pastHeaders  bool  // got first MetaHeadersFrame (actual headers)
	pastTrailers bool  // got optional second MetaHeadersFrame (trailers)
	num1xx       uint8 // number of 1xx responses seen
	readClosed   bool  // peer sent an END_STREAM flag
	readAborted  bool  // read loop reset the stream

  // 表示累积的尾部和客户端的响应尾部
	trailer    http.Header  // accumulated trailers
	resTrailer *http.Header // client's Response.Trailer
}

客户端发起请求

客户端发起请求分为两部分:数据读取与处理工作交给了连接的读循环goroutine,而发起请求则交由流执行

无论是读响应还是写请求都是通过客户端连接中帧处理器所属的网络连接中获取的。

一个客户端连接对应一个网络连接,一个客户端连接对应多个流,而一个请求对应一个流。

客户端连接通过判断是否超出最大并发流限制去创建新流处理,一个请求通过是否有可复用的客户端连接来判断是否要重新创建,而流是不能被复用的

整个客户端请求流程如下

  1. 尝试在连接池中获取可复用的连接

  2. 如不能,则创建新的客户端连接,建立网络连接,并设置初始参数

    客户端连接在创建之后,开启新协程,从帧处理器循环读取帧,流的缓存管道绑定响应体后,数据被添加到对应流的缓存管道中。

  3. 开启新流,写入请求到对应客户端连接的帧处理器缓冲区中,并在接收到响应头后更新响应的请求、TLS等信息

// RoundTripOpt is like RoundTrip, but takes options.
func (t *Transport) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Response, error) {
   
   
  // 检查请求的url是否支持http2
	if !(req.URL.Scheme == "https" || (req.URL.Scheme == "http" && t.AllowHTTP)) {
   
   
		return nil, errors.New("http2: unsupported scheme")
	}

  // 尝试获取连接
	addr := authorityAddr(req.URL.Scheme, req.URL.Host)
	for retry := 0; ; retry++ {
   
   
		cc, err := t.connPool().GetClientConn(req, addr)
		if err != nil {
   
   
			t.vlogf("http2: Transport failed to get client conn for %s: %v", addr, err)
			return nil, err
		}
		reused := !atomic.CompareAndSwapUint32(&cc.reused, 0, 1)
		traceGotConn(req, cc, reused)
    
    // 客户端连接发起请求
		res, err := cc.RoundTrip(req)
    
    // 若发生错误最多重试6次
		if err != nil && retry <= 6 {
   
   
			roundTripErr := err
			if req, err = shouldRetryRequest(req, err); err == nil {
   
   
				// After the first retry, do exponential backoff with 10% jitter.
				if retry == 0 {
   
   
					t.vlogf("RoundTrip retrying after failure: %v", roundTripErr)
					continue
				}
        
        // 对重试间隔进行指数回退
				backoff := float64(uint(1) << (uint(retry) - 1))
				backoff += backoff * (0.1 * mathrand.Float64())
				d := time.Second * time.Duration(backoff)
        
        // 等待重试间隔时间
				var tm timer
				if t.syncHooks != nil {
   
   
					// ... syncHooks block
				} else {
   
   
					tm = newTimeTimer(d)
				}
				select {
   
   
				case <-tm.C():
					t.vlogf("RoundTrip retrying after failure: %v", roundTripErr)
					continue
				case <-req.Context().Done():
					tm.Stop()
					err = req.Context().Err()
				}
			}
		}
    
    // 超过重试次数返回错误
		if err != nil {
   
   
			t.vlogf("RoundTrip failure: %v", err)
			return nil, err
		}
		return res, nil
	}
}

客户端连接获取

尝试先从连接池获取客户端连接,若不存在则拨号建立新连接,并启动协程读取处理数据

那么在什么情况下会创建新连接呢?

  • 请求独占连接且设置了dialOnMiss(一般都会设置)

  • 连接池中不存在已经建立的连接,或者连接不能再承载新请求了(比如已经创建的流数量+等待的请求*2已经超过了int32的最大值)

func (p *clientConnPool) getClientConn(req *http.Request, addr string, dialOnMiss bool) (*ClientConn, error) {
   
   
	// TODO(dneil): Dial a new connection when t.DisableKeepAlives is set?
  // 若请求为完成后关闭其连接且允许缺失时拨号,则创建新的连接返回
  // 由于是一个请求独占的连接,所以该连接不通过连接池创建、管理
	if isConnectionCloseRequest(req) && dialOnMiss {
   
   
		// It gets its own connection.
		traceGetConn(req, addr)
		const singleUse = true
		cc, err := p.t.dialClientConn(req.Context(), addr, singleUse)
		if err != nil {
   
   
			return nil, err
		}
		return cc, nil
	}
  
  
	for {
   
   
    // 遍历连接池中指定地址的连接列表,查找是否有可用的连接。如果找到可用的连接,则标记它为已经被新请求占用,然后返回该连接
		p.mu.Lock()
		for _, cc := range p.conns[addr] {
   
   
      // 判断是否能创建新请求,并预留并发流(防止连接被关闭)
			if cc.ReserveNewRequest() {
   
   
				// When a connection is presented to us by the net/http package,
				// the GetConn hook has already been called.
				// Don't call it a second time here.
				if !cc.getConnCalled {
   
   
					traceGetConn(req, addr)
				}
				cc.getConnCalled = false
				p.mu.Unlock()
				return cc, nil
			}
		}
    
    // 如果连接池中没有可用的连接,并且不允许在缺失时拨号,则返回错误 
		if !dialOnMiss {
   
   
			p.mu.Unlock()
			return nil, ErrNoCachedConn
		}
    
		traceGetConn(req, addr)
    
    // 连接池中拨号创建连接
		call := p.getStartDialLocked(</
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值