Go Http标准库及实现原理|青训营

Go Http标准库及实现原理

本文章的标准库版本为1.20.6

1.整体架构

1.1 架构模式

在http协议下,交互的双方分别为服务端(server)和客户端(client),即熟知的CS模式。本文的研究分别从这两段入手进行展开。

1.2 启动一个简单的CS服务

由于Go的标准库为我们已经封装好了相应的API,所以启动服务的代码非常简单。

go 复制代码
package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/ping", func(writer http.ResponseWriter, request *http.Request) {
		fmt.Println("req:ping")
		writer.Write([]byte("pong"))
	})
	http.ListenAndServe(":8080", nil)
}

如上为服务端(server)的代码,主要工作有两个:

  • http.HandleFunc方法中指定了请求路径(pattern)"/ping"的handler函数
  • 利用http.ListenAndServe方法在本地的8080端口启动了该http服务

至于这两个方法背后隐藏的细节将在第二章中深入探讨。

1.3 在客户端发起http请求

接下来我们通过一个单元测试用例向localhost:8080发送请求。

go 复制代码
package main

import (
	"io/ioutil"
	"net/http"
	"testing"
)

func Test_client(t *testing.T) {
	resp, err := http.Post("http://localhost:8080/ping", "", nil)
	if err != nil {
		t.Error(err)
		return
	}
	body, _ := ioutil.ReadAll(resp.Body)
	defer resp.Body.Close()

	t.Logf("%s", body)
	t.Error("test")
}

返回结果为:

go 复制代码
=== RUN   Test_client
    main_test.go:18: pong
    main_test.go:19: test
--- FAIL: Test_client (0.01s)

服务端成功向客户端返回了pone字符串。

2.服务端

2.1 服务端数据结构

首先我们对http的服务端封装好的数据结构和接口进行分析。

(1)Server类

基于面向对象的思想,服务端模块的属性被封装在Server结构体中。

该结构体中的两个核心字段分别为AddrHandler

go 复制代码
Server struct {
	// Addr optionally specifies the TCP address for the server to listen on,
	// in the form "host:port". If empty, ":http" (port 80) is used.
	// The service names are defined in RFC 6335 and assigned by IANA.
	// See net.Dial for details of the address format.
	Addr string

	Handler Handler // handler to invoke, http.DefaultServeMux if nil
    //···
    //···
}

Addr即为服务端的地址。

Handler则实现了从请求端path到处理函数handler的注册和映射。

如果在声明服务时,Handler字段未显式地声明,即传入的参数为nil,则会使用默认的DefaultServeMux

(2)Handler类

Handler在标准库中是以interfac实现的,接口中只有一个方法ServerHTTP,只要实现了该方法的结构体即可作为一个Handler。

该方法的作用是,根据 http 请求 Request 中的请求路径 path 映射到对应的 handler 处理函数,对请求进行处理和响应。

go 复制代码
type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

(3)ServeMux类

先看源码以及注释

go 复制代码
// ServeMux is an HTTP request multiplexer.
// It matches the URL of each incoming request against a list of registered
// patterns and calls the handler for the pattern that
// most closely matches the URL.
//
type ServeMux struct {
	mu    sync.RWMutex //对ServeMux对象操作时用的锁
	m     map[string]muxEntry
	es    []muxEntry // slice of entries sorted from longest to shortest.
	hosts bool       // whether any patterns contain hostnames
}

在该结构体内部维护了一个从URL映射到handlermap。由于是静态注册,所以不支持动态路由。

(4)muxEntry类

go 复制代码
type muxEntry struct {
	h       Handler
	pattern string
}

muxEntry作为一个handler单元,对pattern和handler进行进一步的封装。

2.2 注册Handler

先来一张流程图看一下大致流程:

在net/http包下会声明一个单例ServeMux,当用户通过http.HandleFunc的方法来注册handler时,handler会被注册到这个DefaultServeMux中。

go 复制代码
// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux

var defaultServeMux ServeMux
go 复制代码
// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	DefaultServeMux.HandleFunc(pattern, handler)
}

ServeMux.HandleFunc 内部会将处理函数 handler 转为实现了 ServeHTTP 方法的 HandlerFunc 类型,将其作为 Handler interface 的实现类注册到 ServeMux 的路由 map 当中。

go 复制代码
// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
	f(w, r)
}

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    // ...
    mux.Handle(pattern, HandlerFunc(handler))
}

实现路由注册的核心逻辑在ServeMux.Handle方法中,其中有两个重点:

  • 方法中将path和handler包装成一个muxEntry类,在以path为key注册到ServeMux.m 这个map中
  • 在响应请求时采用模糊匹配的机制。对于以 '/' 结尾的 path,根据 path 长度将 muxEntry 有序插入到数组 ServeMux.es 中。(模糊匹配将会在下一小节详细说明)
go 复制代码
func (mux *ServeMux) Handle(pattern string, handler Handler) {
	mux.mu.Lock()
	defer mux.mu.Unlock()

	if pattern == "" {
		panic("http: invalid pattern")
	}
	if handler == nil {
		panic("http: nil handler")
	}
	if _, exist := mux.m[pattern]; exist {
		panic("http: multiple registrations for " + pattern)
	}

	if mux.m == nil {
		mux.m = make(map[string]muxEntry)
	}
	e := muxEntry{h: handler, pattern: pattern}
	mux.m[pattern] = e
	if pattern[len(pattern)-1] == '/' {
		mux.es = appendSorted(mux.es, e)
	}

	if pattern[0] != '/' {
		mux.hosts = true
	}
}
go 复制代码
func appendSorted(es []muxEntry, e muxEntry) []muxEntry {
	n := len(es)
	i := sort.Search(n, func(i int) bool {
		return len(es[i].pattern) < len(e.pattern)
	})
	if i == n {
		return append(es, e)
	}
	// we now know that i points at where we want to insert
	es = append(es, muxEntry{}) // try to grow the slice in place, any entry works.
	copy(es[i+1:], es[i:])      // Move shorter entries down
	es[i] = e
	return es
}

2.3 Server,启动!

在之前服务端程序的最后,我们调用了http/net包下的ListenAndServe方法,这就是服务端启动的入口。在程序内部会创建一个Server对象,然后嵌套使用Server.ListenAndServe方法。

go 复制代码
func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

Server.ListenAndServe方法中,会根据用户传入的地址和端口,申请一个监听TCP请求的监听器,继而调用Server.Serve方法。

go 复制代码
func (srv *Server) ListenAndServe() error {
	if srv.shuttingDown() {
		return ErrServerClosed
	}
	addr := srv.Addr
	if addr == "" {
		addr = ":http"
	}
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}
	return srv.Serve(ln)
}

Server.Serve方法的核心就在于它的for + listener.accept机制:

  • 首先将server封装成一个k-v对,存入context中
  • 开启for循环,每一轮循环中调用Listener.Accept方法,相当于在阻塞式地接收请求
  • 每当有一个请求到达后,服务端会创建一个goroutine去异步地执行conn.serve方法处理请求
go 复制代码
var ServerContextKey = &contextKey{"http-server"}

type contextKey struct {
    name string
}

func (srv *Server) Serve(l net.Listener) error {
    //...
	ctx := context.WithValue(baseCtx, ServerContextKey, srv)
	for {
		rw, err := l.Accept()
		//...
		connCtx := ctx
		//...
		c := srv.newConn(rw)
		//...
		go c.serve(connCtx)
	}
}

conn.serve是响应端口的核心方法:

  • 从 conn 中读取到封装到 response 结构体,以及请求参数 http.Request
  • 调用 serveHandler.ServeHTTP 方法,根据请求的 path 为其分配 handler
  • 通过特定 handler 处理并响应请求
go 复制代码
func (c *conn) serve(ctx context.Context) {
    //...
     c.r = &connReader{conn: c}
    c.bufr = newBufioReader(c.r)
    c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)

    for {
        w, err := c.readRequest(ctx)
        // ...
        serverHandler{c.server}.ServeHTTP(w, w.req)
        w.cancelCtx()
        // ...
    }
}

serveHandler.ServeHTTP 方法中,会对 Handler 作判断,倘若其未声明,则取全局单例 DefaultServeMux 进行路由匹配,呼应了 http.HandleFunc 中的处理细节。

go 复制代码
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
	handler := sh.srv.Handler
	if handler == nil {
		handler = DefaultServeMux
	}
	//...

	handler.ServeHTTP(rw, req)
}

接下来,会依次经历ServeMux.ServeHTTpServeMux.HandlerServeMux.handler,最终在ServeMux.match中以Request中的path为pattern在路由表中匹配handler,最终在Handler.ServeHTTP中进行请求的处理和响应。

go 复制代码
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
    // ...
    h, _ := mux.Handler(r)
    h.ServeHTTP(w, r)
}
go 复制代码
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
    // ...
    return mux.handler(host, r.URL.Path)
}
go 复制代码
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
    mux.mu.RLock()
    defer mux.mu.RUnlock()
    
    // ...
    h, pattern = mux.match(path)
    // ...
    return
}

书接上回,当查找路由map未命中handler时,就会启用模糊匹配,两个核心规则如下:

  • 以 '/' 结尾的 pattern 才能被添加到 Server.es 数组中,才有资格参与模糊匹配
  • 模糊匹配时,会找到一个与请求路径 path 前缀完全匹配且长度最长的 pattern,其对应的handler 会作为本次请求的处理函数

在mux.es中,patterns是以由长到短排列的

go 复制代码
// Find a handler on a handler map given a path string.
// Most-specific (longest) pattern wins.
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
	// Check for exact match first.
	v, ok := mux.m[path]
	if ok {
		return v.h, v.pattern
	}

	// Check for longest valid match.  mux.es contains all patterns
	// that end in / sorted from longest to shortest.
	for _, e := range mux.es {
		if strings.HasPrefix(path, e.pattern) {
			return e.h, e.pattern
		}
	}
	return nil, ""
}

至此,服务端的流程已经走完,接下来来到客户端一方。

3.客户端

3.1 客户端数据结构

(1)Client

与服务端相同,客户端也为我们封装了一个Client类

  • Transport:负责http通信部分,也是客户端的核心组件
  • CheckRedirect:详细记录了处理重定向的策略,这次不是我们重点讨论的部分
  • Jar:Cookie管理
  • Timeout:超时设置
go 复制代码
type Client struct {

	Transport RoundTripper

	CheckRedirect func(req *Request, via []*Request) error

	Jar CookieJar

	Timeout time.Duration
}

(2)RoundTripper

RoundTripper 是通信模块的 interface,需要实现方法 Roundtrip,即通过传入请求 Request,与服务端交互后获得响应 Response。

go 复制代码
type RoundTripper interface {
    
	RoundTrip(*Request) (*Response, error)
    
}

(3)Transport

Tranport 是 RoundTripper 的实现类,核心字段包括:

  • idleConn:空闲连接 map,实现复用
  • DialContext:新连接生成器
go 复制代码
type Transport struct {
    idleConn     map[connectMethodKey][]*persistConn // most recently used at end
    // ...
    DialContext func(ctx context.Context, network, addr string) (net.Conn, error)
    // ...
}

(4)Request

http请求的结构体。

go 复制代码
type Request struct {
    // 方法
    Method string
    // 请求路径
    URL *url.URL
    // 请求头
    Header Header
    // 请求参数内容
    Body io.ReadCloser
    // 服务器主机
    Host string
    // query 请求参数
    Form url.Values
    // 响应参数 struct
    Response *Response
    // 请求链路的上下文
    ctx context.Context
    // ...
}

(5)Response

http响应参数结构体。

go 复制代码
type Response struct {
    Status string     // e.g. "200 OK"
    StatusCode int    // e.g. 200
    // http 协议,如:HTTP/1.0
    Proto      string // e.g. "HTTP/1.0"
    // 请求头
    Header Header
    // 响应参数内容  
    Body io.ReadCloser
    // 指向请求参数
    Request *Request
    // ...
}

3.2 方法链路总览

客户端发起一次http请求大致分为一下几部:

  • 构造http请求参数
  • 获取用于与服务端交互的tcp连接
  • 通过tcp连接发送请求参数
  • 通过tcp连接获取响应结果

先来看一下方法执行的流程图:

3.3 Client.Post

调用 net/http 包下的公开方法 Post 时,需要传入服务端地址 url,请求参数格式 contentType 以及请求参数的 io reader。

方法中会使用包下的单例客户端DefaultClient来处理请求。

go 复制代码
// DefaultClient is the default Client and is used by Get, Head, and Post.
var DefaultClient = &Client{}

func Post(url, contentType string, body io.Reader) (resp *Response, err error) {
	return DefaultClient.Post(url, contentType, body)
}

Client.Post方法中,首先会根据用户传入的参数,构建出对应的Request对象,继而通过Client.Do方法处理请求。

go 复制代码
func (c *Client) Post(url, contentType string, body io.Reader) (resp *Response, err error) {
	req, err := NewRequest("POST", url, body)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", contentType)
	return c.Do(req)
}

3.4 NewRequest

NewRequest中会调用NewRequestWithContext方法,根据用户传入的method,url等构建出Request示例。

go 复制代码
func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
    if method == "" {
		// 默认为"GET"请求
		method = "GET"
	}
    // ...
    u, err := urlpkg.Parse(url)
    // ...
    rc, ok := body.(io.ReadCloser)
    // ...
    req := &Request{
        ctx:        ctx,
        Method:     method,
        URL:        u,
        // ...
        Header:     make(Header),
        Body:       rc,
        Host:       u.Host,
    }
    // ...
    return req, nil
}

3.5 Client.Do

在发送请求方法时,会经由Client.DoClient.do辗转,然后进入Client.send方法。

go 复制代码
func (c *Client) Do(req *Request) (*Response, error) {
	return c.do(req)
}
go 复制代码
func (c *Client) do(req *Request) (retres *Response, reterr error) {
    //...
    	var (
		deadline      = c.deadline()
		reqs          []*Request
		resp          *Response
        //...
	)
    for {
        // ...
        var err error       
        if resp, didTimeout, err = c.send(req, deadline); err != nil {
            // ...
        }
        // ...
    }
}

Client.send方法中,会在通过 send 方法发送请求之前和之后,分别对 cookie 进行更新。

go 复制代码
func (c *Client) send(req *Request, deadline time.Time) (resp *Response, didTimeout func() bool, err error){
    //设置cookie
	if c.Jar != nil {
		for _, cookie := range c.Jar.Cookies(req.URL) {
			req.AddCookie(cookie)
		}
	}
    //发送请求
	resp, didTimeout, err = send(req, c.transport(), deadline)
	if err != nil {
		return nil, didTimeout, err
	}
    //更新resp的cookie到请求头中
	if c.Jar != nil {
		if rc := resp.Cookies(); len(rc) > 0 {
			c.Jar.SetCookies(req.URL, rc)
		}
	}
	return resp, nil, nil
}

在调用send方法时,需要传入RoundTripper对象,默认会使用全局单例DefaultTransport 进行注入,核心逻辑位于 Transport.RoundTrip 方法中,其中分为两个步骤:

  • 获取/构造tcp连接
  • 通过tcp连接完成与服务端的交互
go 复制代码
var DefaultTransport RoundTripper = &Transport{
    // ...
    DialContext: defaultTransportDialContext(&net.Dialer{
        Timeout:   30 * time.Second,
        KeepAlive: 30 * time.Second,
    }),
    // ...
}


func (c *Client) transport() RoundTripper {
    if c.Transport != nil {
        return c.Transport
    }
    return DefaultTransport
}
go 复制代码
func send(ireq *Request, rt RoundTripper, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
    // ...
    resp, err = rt.RoundTrip(req)
    // ...
    return resp, nil, nil
}
go 复制代码
func (t *Transport) RoundTrip(req *Request) (*Response, error) {
    return t.roundTrip(req)
}
go 复制代码
func (t *Transport) roundTrip(req *Request) (*Response, error) {
    // ...
    for {          
        // ...    
        treq := &transportRequest{Request: req, trace: trace, cancelKey: cancelKey}      
        // ...
        pconn, err := t.getConn(treq, cm)        
        // ...
        resp, err = pconn.roundTrip(treq)          
        // ...
    }
}

3.6 Transport.getConn

获取tcp连接的策略可以分为两步:

  • 通过queueForIdleConn方法尝试复用采用相同连接协议、访问相同主机端口的空闲连接
  • 假如无已有连接可用,则调用dialConnFor方法异步创建一个连接,并通过select的方式接收ready channel中的信号,来确认连接工作完成
go 复制代码
func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persistConn, err error) {
	req := treq.Request
	trace := treq.trace
	ctx := req.Context()
	//...
    
    //获取连接的请求参数体
	w := &wantConn{
		cm:         cm,
		key:        cm.key(),//包含http协议、请求地址等
		ctx:        ctx,
		ready:      make(chan struct{}, 1),//用于接收连接的ready信号
		//...
	}
    //假如连接失败,则将tco连接放入队列中供以后复用
	defer func() {
		if err != nil {
			w.cancel(t, err)
		}
	}()

	// Queue for idle connection.
	if delivered := t.queueForIdleConn(w); delivered {
		pc := w.pc
		//...
		return pc, nil
	}

	//...

	// Queue for permission to dial.
	t.queueForDial(w)

	// Wait for completion or cancellation.
	select {
    //通过阻塞的方式等待ready信号
	case <-w.ready:
		//...
		return w.pc, w.err
        //...
    //因为错误接收到cancel信号
    case <-req.Cancel:
		return nil, errRequestCanceledConn
	}
}

(1)复用连接

  • 尝试从Transport.idleConn中获取指向同一服务端的空闲连接
  • 获取到连接后会调用 wantConn.tryDeliver 方法将连接绑定到 wantConn 请求参数上
  • 绑定成功后,会关闭 wantConn.ready channel,以唤醒阻塞读取该 channel 的 goroutine
go 复制代码
func (t *Transport) queueForIdleConn(w *wantConn) (delivered bool) {
    // ...
    if list, ok := t.idleConn[w.key]; ok {
        // ...
        for len(list) > 0 && !stop {
            pconn := list[len(list)-1]
            // ...
            delivered = w.tryDeliver(pconn, nil)
            if delivered {
                // ...
                list = list[:len(list)-1]               
            }
            stop = true
        }
        // ...
        if stop {
            return delivered
        }
    }
   
    // ...    
    return false
}
go 复制代码
func (w *wantConn) tryDeliver(pc *persistConn, err error) bool {
    w.mu.Lock()
    defer w.mu.Unlock()
    // ...
    w.pc = pc
    w.err = err
    // ...
    close(w.ready)
    return true
}

(2)创建连接

queueForDial 方法会异步调用 Transport.dialConnFor 方法,创建新的 tcp 连接。由于是异步操作,所以在上游会通过读 channel 的方式,等待创建操作完成。

这里之所以采用异步操作进行连接创建,有两部分原因:

  • 一个 tcp 连接并不是一个静态的数据结构,它是有生命周期的,创建过程中会为其创建负责读写的两个守护协程,伴随而生
  • 在上游Transport.queueForIdleConn 方法中,当通过 select 多路复用的方式,接收到其他终止信号时,可以提前调用 wantConn.cancel 方法打断创建连接的 goroutine.。相比于串行化执行而言,这种异步交互的模式,具有更高的灵活度
go 复制代码
func (t *Transport) queueForDial(w *wantConn) {
	w.beforeDial()
    //当每个主机的最大连接数小于等于0时会创建
	if t.MaxConnsPerHost <= 0 {
		go t.dialConnFor(w)
		return
	}

	t.connsPerHostMu.Lock()
	defer t.connsPerHostMu.Unlock()
    
    //当已有的连接数小于最大连接数时会创建
	if n := t.connsPerHost[w.key]; n < t.MaxConnsPerHost {
		if t.connsPerHost == nil {
			t.connsPerHost = make(map[connectMethodKey]int)
		}
		t.connsPerHost[w.key] = n + 1
		go t.dialConnFor(w)
		return
	}

	//...
}

Transport.dialConnFor 方法中,首先调用 Transport.dialConn 创建 tcp 连接 persisConn,接着执行 wantConn.tryDeliver 方法,将连接绑定到 wantConn 上,然后通过关闭 ready channel 操作唤醒上游读 ready channel 的 goroutine。

go 复制代码
func (t *Transport) dialConnFor(w *wantConn) {
	//...

	pc, err := t.dialConn(w.ctx, w.cm)
	delivered := w.tryDeliver(pc, err)
	//...
}

Transport.dialConn 方法包含了创建连接的核心逻辑:

  • 调用 Transport.dial 方法,最终通过 Tranport.DialContext 成员函数,创建好 tcp 连接,封装到 persistConn 当中
  • 异步启动连接的伴生读写协程 readLoop 和 writeLoop 方法,组成提交请求、接收响应的循环
go 复制代码
func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (pconn *persistConn, err error) {
    pconn = &persistConn{
        t:             t,
        reqch:         make(chan requestAndChan, 1),
        writech:       make(chan writeRequest, 1),
        // ...
    }
    
    conn, err := t.dial(ctx, "tcp", cm.addr())
    // ...
    pconn.conn = conn      
    // ...
   
    go pconn.readLoop()
    go pconn.writeLoop()
    return pconn, nil
}
go 复制代码
func (t *Transport) dial(ctx context.Context, network, addr string) (net.Conn, error) {
	if t.DialContext != nil {
		return t.DialContext(ctx, network, addr)
	}
	//...
}

在伴生读协程 persistConn.readLoop 方法中,会读取来自服务端的响应,并添加到 persistConn.reqCh 中,供上游 persistConn.roundTrip 方法接收。

go 复制代码
func (pc *persistConn) readLoop() { 
    // ...
    alive := true
    for alive {
        // ...
        rc := <-pc.reqch
        // ...
        var resp *Response
        // ...
        resp, err = pc.readResponse(rc, trace)
        // ...
        select{
            rc.ch <- responseAndError{res: resp}:
            // ...
        }
        // ...        
    }
}

在伴生协协程 persisConn.writeLoop方法中,会通过 persistConn.writech 读取到客户端提交的请求,然后将其发送到服务端。

go 复制代码
func (pc *persistConn) writeLoop() {    
    for {
        select {
        case wr := <-pc.writech:
            // ...
            err := wr.req.Request.write(pc.bw, pc.isProxy, wr.req.extra, pc.waitForContinue(wr.continueCh))
            // ...       
    }
}

(3)归还连接

有复用连接的能力,就必然存在归还连接的机制。

首先,在构造新连接中途,倘若被打断,则可能会将连接放回队列以供复用:

go 复制代码
func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persistConn, err error) {
    // ...
    // 倘若连接获取失败,在 wantConn.cancel 方法中,会尝试将 tcp 连接放回队列中以供后续复用
    defer func() {
        if err != nil {
            w.cancel(t, err)
        }
    }()
    // ...
}
go 复制代码
func (w *wantConn) cancel(t *Transport, err error) {
   // ...
    if pc != nil {
        t.putOrCloseIdleConn(pc)
    }
}
go 复制代码
func (t *Transport) putOrCloseIdleConn(pconn *persistConn) {
	if err := t.tryPutIdleConn(pconn); err != nil {
		pconn.close(err)
	}
}
go 复制代码
func (t *Transport) tryPutIdleConn(pconn *persistConn) error {
    // ...
    key := pconn.cacheKey
    // ...
    t.idleConn[key] = append(idles, pconn)
    // ...
    return nil
}

其次,假如与服务端的一轮交流结束,也会将连接放回以供复用。

go 复制代码
func (pc *persistConn) readLoop() {
    tryPutIdleConn := func(trace *httptrace.ClientTrace) bool {
        if err := pc.t.tryPutIdleConn(pc); err != nil {
            // ...
        }
        // ...
    }
    
    // ...
    alive := true
    for alive {
        // ...
        select {
        case bodyEOF := <-waitForBodyRead:
            // ...
            tryPutIdleConn(trace)
            // ...
        }           
    } 
}

3.7 persistConn.roundTrip

在上一小节有提到,一个persistConn是有生命周期的,在创建的过程中会为其维护负责读写的两个守护线程,来与上游调用的channel进行通信。

其中承上启下的方法就是persistConn.roundTrip

  • 首先将 http 请求通过 persistConn.writech 发送给连接的守护协程 writeLoop,并进一步传送到服务端
  • 其次通过读取 resc channel,接收由守护协程 readLoop 代理转发的客户端响应数据
go 复制代码
func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {
    // ...
    pc.writech <- writeRequest{req, writeErrCh, continueCh}
    resc := make(chan responseAndError)
    pc.reqch <- requestAndChan{
        req:        req.Request,
        cancelKey:  req.cancelKey,
        ch:         resc,
        // ...
    }
    // ...
    for {       
        select {
        // ...
        case re := <-resc:
            // ...
            return re.res, nil
        // ...
        }
    }
}

4.结语

本文差不多到此结束了,感谢up主"小徐先生1212"的视频帮助我熟悉了net/http原生库,同时为我之后的web框架学习打好了基础。

相关推荐
Find24 天前
MaxKB 集成langchain + Vue + PostgreSQL 的 本地大模型+本地知识库 构建私有大模型 | MarsCode AI刷题
青训营笔记
理tan王子24 天前
伴学笔记 AI刷题 14.数组元素之和最小化 | 豆包MarsCode AI刷题
青训营笔记
理tan王子24 天前
伴学笔记 AI刷题 25.DNA序列编辑距离 | 豆包MarsCode AI刷题
青训营笔记
理tan王子24 天前
伴学笔记 AI刷题 9.超市里的货物架调整 | 豆包MarsCode AI刷题
青训营笔记
夭要7夜宵1 个月前
分而治之,主题分片Partition | 豆包MarsCode AI刷题
青训营笔记
三六1 个月前
刷题漫漫路(二)| 豆包MarsCode AI刷题
青训营笔记
tabzzz1 个月前
突破Zustand的局限性:与React ContentAPI搭配使用
前端·青训营笔记
Serendipity5651 个月前
Go 语言入门指南——单元测试 | 豆包MarsCode AI刷题;
青训营笔记
wml1 个月前
前端实践-使用React实现简单代办事项列表 | 豆包MarsCode AI刷题
青训营笔记
用户44710308932421 个月前
详解前端框架中的设计模式 | 豆包MarsCode AI刷题
青训营笔记