Go HTTP框架原理从零开始到还不错 | 青训营

198 阅读15分钟

Go 原生HTTP框架解读

  • HTTP(Hypertext Transfer Protocol)框架作为网络编程的重要工具是众多初学者第一次接触的网络框架,同时Go语言也做了十分优秀的原生支持使得在不引入第三方库的情况下可以用简单的几行代码完成服务端与客户端的搭建工作。在学习过程中,我会非常好奇这些简单命令背后发生的事情,但是深入钻研源码又是一件十分痛苦的事情。
    因此本文系统通过宏观地介绍HTTP框架工作原理,同时辅以必要的源码,一来可以一定程度加深对于网络实现原理的理解,二来阅读官方的代码一定是培养编程习惯养成编程风格最好的途径。
    文章描写较为基础且啰嗦,基于我的书写风格也基于我较低的水平欢迎指正和讨论。

宏观理解

HTTP遵循经典的C-S架构,即服务端(Serve)和客户端(Client)之间通过发起请求与回复进行通信。当然也可以是BS架构,这个在之后讨论。那么我们便可以将服务端和客户端分开来讨论和理解。最后会以思维导图的形式更加清晰地展示这个过程。

1 服务端:

服务端通常搭建在远程服务器上,我们日常会通过浏览器通过输入网址与服务端进行交互,服务端会返回包含各式各样信息的网页。我们可以将服务端的功能抽象为两步,一是构建服务,二是启动服务。
1.1 构建服务:
Q1:我们会自然地发问需要构建一个什么样子的服务呢?
A1:或许我们可以从日常浏览一个网页形成一个比较具体的理解。比如我们浏览某购物网站,这个网站的首页一定有各种不同的按钮,这些按钮点击后都会产生不同的效果。如果将这些效果抽象为一个个函数Func,那么我们就是要构建一个服务,里面有很多的功能函数并且有其对应的入口,他们构成一一对应的关系。
如下图,我们可以发现不同的按钮点击后对应的网址是不同的。
由此我们可以更抽象地描述这个服务具由很多网址或者说是路径(pattern)和处理函数(handler) 的键值对构成。 image.png image.png

我们或许可以想到这么多的键值对应该由一个容器统一存储,并且有一个调度器来统一管理。
Q2:这个容器是什么呢?
A2:前面提到了存放的是键值对,那么可以自然地想到可以用一个map保存pattern-handle的键值对。

这个调度器我们称之为路由处理器,或者是HTTP多路复用器,因为众多不同的HTTP请求都经过它来匹配对应的函数。
Q3:什么是路由呢?
A3:路由是指网络设备通过网络将信息正确传输到指定目的地的方式,简单来说就是对于传入服务端的信息通过一定的方法找到他应该正确去往的地方,在目前这个具体情况里面就是找到地址对应的处理函数。同时路由管理器还会实现前缀匹配等众多功能,也可以自定义特殊的功能,这一部分后面结合具体源码详细描述。

Handl;er.jpg

1.2 启动服务:
启动服务换句话来说就是要实现客户端发来的消息服务端可以接受并处理。
因此可以启动服务转化为两个问题:如何接受消息和如何处理消息
Q4:如何接受消息?
A4:接收消息无非便是用一个For循环不断地监听是否有消息传入,不过此处框架通过被动阻塞的方式防止For循环一直空转长期占用cpu的时间片。同时还使用了一个多路复用的技术,这部分结合代码解释。
Q5:如何处理消息?
A5:结合前面图片的结构,处理消息主要分为两部分的操作,首先通过传来的消息根据pattern找到其对应的handle处理函数。然后需要启动这个处理函数,这样子一个完整的服务端就构建完成了。

2 客户端:

宏观上来看客户端主要是发送请求,这个过程可以抽象为三个子过程:创建请求、获取连接、通过tcp连接进行收发。
2.1 创建请求:
在面向对象的编程思想下,请求应该也是一个对象,所以第一步应该是将对应请求url和body等内容打包成一个请求结构体的实例。
2.2 获取连接:
请求要成功发起需要尝试与服务端建立连接,这里需要注意连接也是一种非常宝贵的资源因此需要尽可能复用而非申请。在这个过程中会有一个tcp连接池,尝试申请连接就是在tcp池中寻找可用连接。
这个过程中会发起判断,如果有可以复用的连接比如目标服务端和内容均相同判则复用连接,如果没有则申请新的连接。
2.3 通过tcp连接进行收发:
建立连接后就可以执行发送请求和接收响应过程,此处会有两个goroutine来负责实现这两个功能并且通过channel进行通信。

以下为客户端和服务端总体宏观流程的思维导图 HTTP实现流程.png

源码分析

1 服务端

1.1 核心数据结构

以下主要先熟悉客户端部分的主要数据结构,以便下一部分介绍代码逻辑时可以更好理解。
Server:

type Server struct {  
    // 监听地址  
    Addr string  
    // 请求多路复用器(路由处理器)  
    Handler Handler // handler to invoke, http.DefaultServeMux if nil  
    ...  
}

Server.png Handler:
Handler就是Server中的参数之一,其对应的数据类型也是服务器部分的一个重要接口

type Handler interface {  
    ServeHTTP(ResponseWriter, *Request)  
}

Handler.png ServeMux:
ServeMux是Handler接口的一个具体实现,实现了请求多路复用器

type ServeMux struct {  
    mu sync.RWMutex  
    m map[string]muxEntry  
    es []muxEntry // slice of entries sorted from longest to shortest.  
    hosts bool // whether any patterns contain hostnames  
}

SeveMux.png muxEntry:
muxEntry也是Handler接口的一个具体实现,表示了一个url和其对应的处理函数

type muxEntry struct {  
    h Handler  
    pattern string  
}

muxentry.png

在这之前要再次明确一下Handler HTTP多路复用器 路由处理器 handler 处理函数之间的区别
Handler = HTTP多路复用器 = 路由处理器
handler = 处理函数
这里区分大小写是因为在代码中通常Handler对应的是多路复用器,小写则是处理函数,因此以此区分。

1.2 服务端注册处理函数(handler)流程

前文中已经提到了服务端的构造分为了创建服务和启动服务两个部分,此处首先介绍创建服务的过程。结合宏观部分的解释和对于数据结构的理解,我们可以发现服务创建的过程主要是需要定义Handler,其中包括了路由处理器以及处理函数。

  1. 关于路由处理器
    Go语言官方提供的HTTP框架中创建服务端时路由处理器可以为空,因为框架提供了一个默认的全局单例DefaultServeMux供我们使用。单例指的是这是有且只有这么一个实例,简单来说就是该数据结构只申明了一个变量。

  2. 关于处理函数
    服务端的构建主要是完成对于处理函数的定义,创建服务端时我们需要声明很多的处理函数,并将其组织并进行管理。该过程主要调用了以下函数HandleFunc,需要注意的是HandleFunc是使用上文提到的全局单例DefaultServeMux来构建服务端的:

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {  
    DefaultServeMux.HandleFunc(pattern, handler)  
}

该函数的使用可以使用以下形式调用:

http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {  
    fmt.Println("req:ping")  
    w.Write([]byte("pone"))  
})

以下为DefaultServeMux的定义:

// DefaultServeMux is the default ServeMux used by Serve.  
var DefaultServeMux = &defaultServeMux  
  
var defaultServeMux ServeMux

我们可以发现defaultServeMux是上文核心数据结构部分提到过的ServeMux类型,这是Handler接口的一个具体实现,起到了路由处理器的作用。
调用HandlerFunc的过程传入了一个路径和一个函数,这里可以比较一个传入的函数Handler接口定义里的函数,可以两者传入的参数是一致的。以此也可以验证路由处理器和我们传入的处理函数是一个接口的两种实现形式。
以下继续观察http.HandleFunc函数内部调用的DefaultServeMux.HandleFunc函数,DefaultServeMux是ServeMux类型的变量,因此我们可以在ServeMux这个类型的方法中寻找到HandleFunc函数:

// HandleFunc registers the handler function for the given pattern.  
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {  
    if handler == nil {  
        panic("http: nil handler")  
    }  
    mux.Handle(pattern, HandlerFunc(handler))  
}

该函数对输入的函数进行了一个简单的检查,然后继续将参数向下传递。不过我们可以注意到传递的过程中对handler做了一个类型转换,这个类型转换过程可以看一下下面这部分的代码:

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

上面这段代码主要实现了一个函数对于自己的调用,看起来好像什么都没做,输入的函数需要经过这样的一个处理才可以变成Handler接口的一个实现。这里声明了HandleFunc的类型,其实就是func(ResponseWriter, *Request)类型的别名。下面是HandleFunc类型的一个方法,使得这个函数可以调用自己。这一部分可以和上文的接口定义相互对应。
这一部分理解了之后再回到ServeMux.HandleFunc函数中最后是调用了ServeMux.Handle函数,这个函数实现了处理函数加入路由处理器的主要逻辑:

// Handle registers the handler for the given pattern.  
// If a handler already exists for pattern, Handle panics.  
func (mux *ServeMux) Handle(pattern string, handler Handler) {  
    mux.mu.Lock()  
    defer mux.mu.Unlock()  
  
    ...
    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  
    }  
}

首先看到前两行,使用到了ServeMux.mu这个参数,可以回顾数据结构部分这个参数是一个读写锁,因为每个ServeMux有一个map用来管理所有的处理函数,这个map的读写是需要进行互斥操作的,以防止对于处理函数的管理出现问题。因此前两行的主要逻辑功能是对map加锁和最后的解锁操作。
中间省略对于输入参数的是否存在的判断,以及此路径(pattern)是否已经有对应的函数存在了。
第8-10行的if判断: 如果是这个服务端第一次加入处理函数那需要首先申请一个map。
第11行: 将输入打包成muxEntry类型,可以回顾核心数据结构部分。
第12行: 将处理函数和对应的路径加入到map中。
第13-15行: 这一部分在实现前缀表达式匹配的功能,详细可以继续研究appendSorted函数以及搜索Go语言http框架前缀树的实现。大致逻辑是当客户端向服务端发出一个请求时,需要根据路径搜索服务端的map寻找其中哪些路径是请求路径的子序列,并返回一个最长的。因此在服务端添加处理函数时需要从长到短对路径进行一个排序,以方便后续的寻找。
以上完成了处理函数的添加过程。

1.3 服务端启动server和运行的过程

对于已经添加完处理函数的服务端,下面来研究如何启动这个客户端。根据前面宏观部分的理解,服务端启动部分主要分为如何接受请求以及如何处理请求两个部分。
Go语言中启动HTTP服务只需要调用ListenAndServe一个函数,函数源码如下:

// ListenAndServe always returns a non-nil error.  
func ListenAndServe(addr string, handler Handler) error {  
    server := &Server{Addr: addr, Handler: handler}  
    return server.ListenAndServe()  
}

可以如下使用:

http.ListenAndServe(":8080", nil)

只需要输入一个监听地址和一个路由处理器(Handler),此处使用nil便会自动调用上文中提到的全局单例。可以看到源码中是先将输入整合成了一个Server类型,上文提到过的数据结构,然后调用了Server.ListenAndServe函数:

// ListenAndServe always returns a non-nil error. After Shutdown or Close,  
// the returned error is ErrServerClosed.
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)  
}

上述代码通过 ln, err := net.Listen("tcp", addr) 给服务分配了一个监听器,监听是否有往目标地址发送的信息。结束了资源分配,通过srv.Serve(ln) 函数启动服务:

// Serve always returns a non-nil error and closes l.  
// After Shutdown or Close, the returned error is ErrServerClosed.  
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)  
    }  
}

Serve函数主体部分是一个for循环,这个for循环不断轮询解决了服务端如何接收请求的问题,当代码运行至循环里rw, err := l.Accept() 程序会被动阻塞直至有请求到来,这样子可以防止空转空耗cpu的资源。当接收到请求之后c := srv.newConn(rw) 会对收到的请求参数做一个封装,然后通过go c.serve(connCtx) 开启一个新的协程来负责处理这个请求,以此解决如何处理请求的问题

// Serve a new connection.  
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()
        ...
    }
}       

这部分代码c.r = &connReader{conn: c} 通过ctx把Request请求读出,这里看的时候可能会疑惑为什么这里还存在一个for循环,这里是因为一次HTTP的连接请求可能会发送多次,以至达成某种条件结束连接。这里可以自行搜索一下HTTP/1.xHTTP/2的区别,同一连接并发处理的区别。serverHandler{c.server}.ServeHTTP(w, w.req) 调用方法运行请求对应的处理函数:

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

要运行处理函数首先要找到路由处理器中管理的处理函数,此处进行判断是否有自定义的路由处理器输入,如果没有则调用我们之前定义过的DefaultServerMux全局单例 然后运行handler.ServeHTTP(rw, req) 由于框架中有众多重名的方法,需要注意这里调用的是全局单例的ServeHTTP方法,全局单例是一个ServeMux类型的变量,因此需要研究ServeMux.ServeHTTP

// ServeHTTP dispatches the request to the handler whose  
// pattern most closely matches the request URL.  
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {  
    if r.RequestURI == "*" {  
        if r.ProtoAtLeast(1, 1) {  
            w.Header().Set("Connection", "close")  
        }  
        w.WriteHeader(StatusBadRequest)  
        return  
    }  
    h, _ := mux.Handler(r)  
    h.ServeHTTP(w, r)  
}

这里需要注意函数的最后两句代码,h, _ := mux.Handler(r) 在路由处理器内部对于请求路径进行了查询,返回的h就是处理函数,最后调用处理函数。最后来研究一下mux.Handler方法是如何实现处理函数的查询的:

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
    // ...
    return mux.handler(host, r.URL.Path)
}

func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
    mux.mu.RLock()
    defer mux.mu.RUnlock()
    
    // ...
    h, pattern = mux.match(path)
    // ...
    return
}

查询时handler函数首先会添加一个共享锁,此处可以对比前文中注册处理函数时的读写锁学习,以此保护异步读时的安全,最后调用h, pattern = mux.match(path) 进行路径匹配:

func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    v, ok := mux.m[path]
    if ok {
        return v.h, v.pattern
    }

    // ServeMux.es 本身是按照 pattern 的长度由大到小排列的
    for _, e := range mux.es {
        if strings.HasPrefix(path, e.pattern) {
            return e.h, e.pattern
        }
    }
    return nil""
}

这里可以看见首先通过v, ok := mux.m[path] 进行了精准匹配,如果精准匹配失败了则进行前缀匹配,这里可以联系我们前面提到的,处理函数注册时pattern是由长到短排序的,可以一次匹配最长子序列,以满足模糊查询时候的需求。
至此服务端创建和运行过程中大致的源码逻辑已经介绍完毕。

2 客户端

2.1 核心数据结构

与服务端相同客户端也首先来熟悉一些数据结构。
Client:
与服务端的Server类似,服务端的全部服务也被封装在Client中。

type Client struct {
    ...
    Transport RoundTripper
    ...
    Jar CookieJar
    ...
    Timeout time.Duration
}

Client.png RoundTripper:
RoundTripper是一个接口可以类比一下Handler,用于实现通讯

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

RoundTripper.png Transport:
Transport是RoundTripper的实现类,是默认的通讯模块,负责生成新的连接和管理连接。

type Transport struct {
    idleConn     map[connectMethodKey][]*persistConn // most recently used at end
    ...
    DialContext func(ctx context.Context, network, addr string) (net.Conn, error)
    ...
}

Transport.png Request:
请求参数的结构体

type Request struct {
    Method string
    URL *url.URL
    Header Header
    Body io.ReadCloser
    Host string
    Form url.Values
    Response *Response
    ctx context.Context
    ...
}

Request.png Response:
响应参数的结构体

type Response struct {
    StatusCode int    // e.g. 200
    Proto      string // e.g. "HTTP/1.0"
    Header Header
    Body io.ReadCloser
    Request *Request
    ...
}

Response.png

2.2 构造请求

根据前文中提到的客户端发送请求主要分为构造请求建立连接进行通信 几个步骤,我们可以通过跟踪发送一个POST请求的源码实现来理解各个阶段的实现:

resp, err := http.Post("http://localhost:8080/ping", "", nil)

这句代码向我们之前构造的http服务器发送请求,其中端口和请求路径是根据服务端的定义来书写的,传入的参数分别是服务端地址url,请求参数格式contenType以及请求参数的io reader。

var DefaultClient = &Client{}

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

在不定义Client的情况下,Client就是前文提到的数据结构,客户端会自动调用一个全局单例DefaultClient来处理请求,调用Client.Post:

func (c *Client) Post(url, contentType string, body io.Reader) (resp *Response, err error) {
    req, err := NewRequest("POST", url, body)
    // ...
    req.Header.Set("Content-Type", contentType)
    return c.Do(req)
}

在该方法中会将传入的参数进行组合,并且调用NewRequest方法创建一个新的请求,req.Header.Set用于设置Header头部,然后调用c.Do方法来处理请求,NewREquest方法的实现如下:

// NewRequest wraps NewRequestWithContext using context.Background.  
func NewRequest(method, url string, body io.Reader) (*Request, error) {  
    return NewRequestWithContext(context.Background(), method, url, body)  
}

func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
    ...
    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
}

此处method传入了POST指的是请求类型,url为服务端路径,body为入参。

2.3 建立连接与通信

创建完请求之后可以开始建立与服务端之间的通信,这部分由上述Client.Do方法的逻辑完成:

func (c *Client) Do(req *Request) (*Response, error) {
    return c.do(req)
}

func (c *Client) do(req *Request) (retres *Response, reterr error) {
    var (
        deadline      = c.deadline()
        resp          *Response
        ...
    )    
    for {
        ...
        var err error       
        if resp, didTimeout, err = c.send(req, deadline); err != nil {
            ...
        }
        ...
    }
}

Client.do中会调用c.send正式发起连接:

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, nilnil
}

发送请求前到c.Jar != nil部分将cookie设置到请求头中,完成前处理,发送请求后到部分会讲resp中的cookie更新至请求头中完成后处理。在send方法中会需要注入RoundTripper模块,默认会使用DefaultTransport进行注入:

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
}

这里可以结合上文中对于RoundTripper数据结构进行的分析进行理解,接下来回到上文中调用了一个函数send,resp, didTimeout, err = send(req, c.transport(), deadline),这个函数中会调用Transport.RoundTrip方法,该方法又会调用Transport.roundTrip这部分调用的过程省略主要关注roundTrip方法:

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)          
        ...
    }
}

pconn, err := t.getConn(treq, cm) 部分请求了一个tcp连接赋值给了pconn,resp, err = pconn.roundTrip(treq) 又调用了这个连接的roundTrip方法。首先来分析getConn方法如何建立连接:

func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persistConn, err error) {
    // 获取连接的请求参数体
    w := &wantConn{
        cm:         cm,
        // key 由 http 协议、服务端地址等信息组成
        key:        cm.key(),
        ctx:        ctx,
        // 标识连接构造成功的信号发射器
        ready:      make(chan struct{}, 1),
    }
    // 倘若连接获取失败,在 wantConn.cancel 方法中,会尝试将 tcp 连接放回队列中以供后续复用
    defer func() {
        if err != nil {
            w.cancel(t, err)
        }
    }()
    // 尝试复用指向相同服务端地址的空闲连接
    if delivered := t.queueForIdleConn(w); delivered {
        pc := w.pc
        ...
        return pc, nil
    }
    // 异步构造新的连接
    t.queueForDial(w)
    select {
    // 通过阻塞等待信号的方式,等待连接获取完成
    case <-w.ready:
        ...
        return w.pc, w.err
    ...
    }
}

在阅读这部分源码前,我们需要理解TCP连接是相对宝贵的资源,所以在可以复用的情况下要尽量复用。对于采用相同协议、访问相同服务端的连接,如果处于空闲状态我们可以尝试进行复用。如果没有可以复用的连接,则需要通过queueForIdleConn方法异步地创建一个新的连接,此处通过一个channel信号来监测是否已经完成了构造连接的工作。

func (t *Transport) queueForDial(w *wantConn) {
    ...
    go t.dialConnFor(w) 
    ...
}

这里创建过程采用异步操作主要有两部分原因,首先tcp连接不是一个静态的过程,是具有生命周期的会有两个协程相伴而生,因此需要一段时间分配资源,异步可以增加灵活性。另一方面上层函数可以通过select多路复用的方式接收到其他终止信号,这样可以提前打断连接的创建,可以增加创建过程的灵活度。

func (t *Transport) dialConnFor(w *wantConn) {
    ...
    pc, err := t.dialConn(w.ctx, w.cm)
    delivered := w.tryDeliver(pc, err)
    ...
}

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

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
}

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

  • 调用 Transport.dial 方法,最终通过 Tranport.DialContext 成员函数,创建好 tcp 连接,封装到 persistConn 当中
  • 异步启动连接的伴生读写协程 readLoop 和 writeLoop 方法,组成提交请求、接收响应的循环
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}:
            ...
        }
        ...        
    }
    
}

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))
            // ...       
    }
}

在伴生读协程 persisConn.readLoop 方法中,会读取来自服务端的响应,并添加到persistConn.reqCh 中,供上游 persistConn.roundTrip 方法接收.在伴生协协程 persisConn.writeLoop方法中,会通过 persistConn.writech 读取到客户端提交的请求,然后将其发送到服务端.
至此连接的创建可以正常开启通信

参考文章:Golang HTTP 标准库实现原理 公众号
参考视频:Golang HTTP 标准库实现原理 Bilibili