成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

圖文講透Golang標準庫 net/http實現原理 - 客戶端

開發 前端
Client.Get() 根據用戶的入參,請求參數 NewRequest使用上下文包裝NewRequestWithContext ,接著通過 Client.Do 方法,處理這個請求。

客戶端的內容將是如何發送請求和接收響應,走完客戶端就把整個流程就完整的串聯起來了!

這次我把調用的核心方法和流程走讀的函數也貼出來,這樣看應該更有邏輯感,重要部分用紅色標記了一下,可以著重看下。

圖片圖片

先了解下核心數據結構Client和Request。

Client結構體

type Client struct { 
    Transport RoundTripper 
    CheckRedirect func(req *Request, via []*Request) error 
    Jar CookieJar 
    Timeout time.Duration
}

四個字段分別是:

  • ? Transport:表示 HTTP 事務,用于處理客戶端的請求連接并等待服務端的響應;
  • ? CheckRedirect:處理重定向的策略
  • ? Jar:管理和存儲請求中的 cookie
  • ? Timeout:超時設置

Request結構體

Request字段較多,這里就列舉一下常見的一些字段

type Request struct {
    Method string
    URL *url.URL
    Header Header
    Body io.ReadCloser
    Host string
    Response *Response
    ...
}
  • ? Method:指定的HTTP方法(GET、POST、PUT等)
  • ? URL:請求路徑
  • ? Header:請求頭
  • ? Body:請求體
  • ? Host:服務器主機
  • ? Response:響應參數

構造請求

var DefaultClient = &Client{}

func Get(url string) (resp *Response, err error) {
    return DefaultClient.Get(url)
}

示例HTTP 的 Get方法會調用到 DefaultClient 的 Get 方法,,然后調用到 Client 的 Get 方法。

DefaultClient 是 Client 的一個空實例(跟DefaultServeMux有點子相似)

圖片圖片

Client.Get

func (c *Client) Get(url string) (resp *Response, err error) {
    req, err := NewRequest("GET", url, nil)
    if err != nil {
        return nil, err
    }
    return c.Do(req)
}

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

Client.Get() 根據用戶的入參,請求參數 NewRequest使用上下文包裝NewRequestWithContext ,接著通過 Client.Do 方法,處理這個請求。

NewRequestWithContext

func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
    ...
    // 解析url
    u, err := urlpkg.Parse(url)
    ...
    rc, ok := body.(io.ReadCloser)
    if !ok && body != nil {
        rc = ioutil.NopCloser(body)
    } 
    u.Host = removeEmptyPort(u.Host)
    req := &Request{
        ctx:        ctx,
        Method:     method,
        URL:        u,
        Proto:      "HTTP/1.1",
        ProtoMajor: 1,
        ProtoMinor: 1,
        Header:     make(Header),
        Body:       rc,
        Host:       u.Host,
    } 
    ...
    return req, nil
}

NewRequestWithContext 函數主要是功能是將請求封裝成一個 Request 結構體并返回,這個結構體的名稱是req。

準備發送請求

構造好的Request結構req,會傳入c.Do()方法。

我們看下發送請求過程調用了哪些方法,用下圖表示下

圖片圖片

?? 其實不管是Get還是Post請求的調用流程都是一樣的,只是對外封裝了Post和Get請求

func (c *Client) do(req *Request) (retres *Response, reterr error) {
    ...
    for {
        ...
        resp, didTimeout, err = send(req, deadline)
        if err != nil {
            return nil, didTimeout, err
        }
    }
    ...
}
//Client 調用 Do 方法處理發送請求最后會調用到 send 函數中
func (c *Client) send(req *Request, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
    resp, didTimeout, err = send(req, c.transport(), deadline)
    if err != nil {
        return nil, didTimeout, err
    }
    ...
    return resp, nil, nil
}

c.transport()方法是為了回去Transport的默認實例 DefaultTransport ,我們看下DefaultTransport長什么樣。

DefaultTransport

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

可以根據需要建立網絡連接,并緩存它們以供后續調用重用,部分參數如下:

  • ? MaxIdleConns:最大空閑連接數
  • ? IdleConnTimeout:空閑連接超時時間
  • ? ExpectContinueTimeout:預計繼續超時

注意這里的RoundTripper是個接口,也就是說 Transport 實現 RoundTripper 接口,該接口方法接收Request,返回Response。

RoundTripper

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

圖片圖片

雖然還沒看完后面邏輯,不過我們猜測RoundTrip方法可能是實際處理客戶端請求的實現。

我們繼續追下后面邏輯,看下是否能驗證這個猜想。

func send(ireq *Request, rt RoundTripper, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
    ...
    resp, err = rt.RoundTrip(req)
    if err != nil {
        ...
    }
    ..
}

?? 你看send函數的第二個參數就是接口類型,調用層傳遞的Transport的實例DefaultTransport。

而rt.RoundTrip()方法的調用具體在net/http/roundtrip.go文件中,這也是RoundTrip接口的實現,代碼如下:

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

Transport.roundTrip 方法概況來說干了這些事:

  • ? 封裝請求transportRequest
  • ? 調用 Transport 的 getConn 方法獲取連接
  • ? 在獲取到連接后,調用 persistConn 的 roundTrip 方法等待請求響應結果
func (t *Transport) roundTrip(req *Request) (*Response, error) {
    ...  
    for {
        ...
        // 請求封裝
        treq := &transportRequest{Request: req, trace: trace, cancelKey: cancelKey} 
        cm, err := t.connectMethodForRequest(treq)
        if err != nil {
            ...
        } 
        // 獲取連接
        pconn, err := t.getConn(treq, cm)
        if err != nil {
            ...
        }
        
        // 等待響應結果
        var resp *Response
        if pconn.alt != nil {
            t.setReqCanceler(cancelKey, nil) 
            resp, err = pconn.alt.RoundTrip(req)
        } else {
            resp, err = pconn.roundTrip(treq)
        }
        ...
    }
}

封裝請求transportRequeste沒啥好說的,因為treq被roundTrip修改,所以這里需要為每次重試重新創建。

獲取連接

獲取連接的方法是 getConn,這里代碼還是比較長的,會有不同的兩種方式去獲取連接:

  1. 1. 調用 queueForIdleConn 排隊等待獲取空閑連接
  2. 2. 如果獲取空閑連接失敗,那么調用 queueForDial 異步創建一個新的連接,并通過channel來接收readdy信號,來確認連接是否構造完成

圖片圖片

getConn

func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persistConn, err error) {
    ...
    //  初始化wantConn結構體
    w := &wantConn{
        cm:         cm,
        key:        cm.key(),
        ctx:        ctx,
        ready:      make(chan struct{}, 1),
        beforeDial: testHookPrePendingDial,
        afterDial:  testHookPostPendingDial,
    }
    ...
    // 獲取空閑連接
    if delivered := t.queueForIdleConn(w); delivered {
        ...
    }
 
    // 異步創建新連接
    t.queueForDial(w)
 
    select {
    // 阻塞等待獲取到連接完成
    case <-w.ready:
        ...
        return w.pc, w.err
    ...
}

queueForIdleConn獲取空閑連接

獲取成功

成功空閑獲取連接Conn流程如下圖

圖片圖片

  1. 1. 根據wantConn的key從 transport.idleConn 這個map中查找,看是否存不存在空閑的 connection 列表
  2. 2. 獲取到空閑的 connection 列表后,從列表中拿最后一個 connection
  3. 3. 獲取到連接后會調用 wantConn.tryDeliver 方法將連接綁定到 wantConn 請求參數上

獲取失敗

圖片圖片

當不存在該請求的 connection 列表,會將當前 wantConn 加入到名稱為 idleConnWait 的等待空閑map中。

不過此時的idleConnWait這個map的值是個隊列

queueForIdleConn方法

從上面的兩張圖解中差不多能看出是如何獲取空閑連接和如何獲取失敗時如何做的了,這里也貼下代碼體驗下,讓大家更清楚里面的實現邏輯。

//idleConn是map類型,指定key返回切片列表
idleConn     map[connectMethodKey][]*persistConn 
//idleConnWait,指定key返回隊列
idleConnWait map[connectMethodKey]wantConnQueue

這里將獲取空閑連接的代碼實現多進行注釋,更好理解一些!

func (t *Transport) queueForIdleConn(w *wantConn) (delivered bool) {
    //參數判斷
    if t.DisableKeepAlives {
        return false
    }

    if w == nil { 
        return false
    }
 
    // 計算空閑連接超時時間
    var oldTime time.Time
    if t.IdleConnTimeout > 0 {
        oldTime = time.Now().Add(-t.IdleConnTimeout)
    }
    //從idleConn根據w.key找對應的persistConn 列表
    if list, ok := t.idleConn[w.key]; ok {
        stop := false
        delivered := false
        for len(list) > 0 && !stop {
            // 找到persistConn列表最后一個
            pconn := list[len(list)-1] 
            // 檢查這個 persistConn 是不是過期
            tooOld := !oldTime.IsZero() && pconn.idleAt.Round(0).Before(oldTime)
            if tooOld {
                //如果過期進行異步清理
                go pconn.closeConnIfStillIdle()
            }
            // 該 persistConn 被標記為 broken 或 閑置太久 continue
            if pconn.isBroken() || tooOld { 
                list = list[:len(list)-1]
                continue
            }
            // 嘗試將該 persistConn 寫入到 wantConn(w)中
            delivered = w.tryDeliver(pconn, nil)
            if delivered {
                // 寫入成功,將persistConn從空閑列表中移除
                if pconn.alt != nil { 
                } else { 
                    t.idleLRU.remove(pconn)
                    //缺省了最后一個conn
                    list = list[:len(list)-1]
                }
            }
            stop = true
        }
        //對被獲取連接后的列表進行判斷
        if len(list) > 0 {
            t.idleConn[w.key] = list
        } else {
            // 如果該 key 對應的空閑列表不存在,那么將該key從字典中移除
            delete(t.idleConn, w.key)
        }
        if stop {
            return delivered
        }
    } 
    // 如果找不到空閑的 persistConn
    if t.idleConnWait == nil {
        t.idleConnWait = make(map[connectMethodKey]wantConnQueue)
    }
    // 將該 wantConn添加到等待空閑idleConnWait中
    q := t.idleConnWait[w.key] 
    q.cleanFront()
    q.pushBack(w)
    t.idleConnWait[w.key] = q
    return false
}

我們知道了為找到的空閑連接會被放到空閑 idleConnWait 這個等待map中,最后會被Transport.tryPutIdleConn方法將pconne添加到等待新請求的空閑持久連接列表中。

queueForDial創建新連接

queueForDial意思是排隊等待撥號,為什么說是等帶呢,因為最終的結果是在ready這個channel上進行通知的。

流程如下圖:

圖片圖片

我們先看下Transport結構體的這兩個map,名稱不一樣map的屬性和解釋都是一樣的,其中idleConnWait是在沒查找空閑連接的時候存放當前連接的map。

而connsPerHostWait用在了創建新連接的地方,可以猜測一下創建新鏈接的地方就是將當前的請求放入到 connsPerHostWait 等待map中。

// waiting getConns
idleConnWait map[connectMethodKey]wantConnQueue 
// waiting getConns
connsPerHostWait map[connectMethodKey]wantConnQueue

Transport.queueForDial

func (t *Transport) queueForDial(w *wantConn) {
    w.beforeDial()
    // 小于等于零,意思是限制,直接異步建立連接
    if t.MaxConnsPerHost <= 0 {
        go t.dialConnFor(w)
        return
    }
    ...
    //host建立的連接數沒達到上限,執行異步建立連接
    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
    }
    //進入等待隊列
    if t.connsPerHostWait == nil {
        t.connsPerHostWait = make(map[connectMethodKey]wantConnQueue)
    }
    q := t.connsPerHostWait[w.key]
    q.cleanFront()
    q.pushBack(w)
    t.connsPerHostWait[w.key] = q
}

在獲取不到空閑連接之后,會嘗試去建立連接:

  1. 1. queueForDial 方法的內部會先校驗 MaxConnsPerHost 是否未設置和是否已達上限
  2. 1.
  1. 1. 檢驗不通過則將當前的請求放入到 connsPerHostWait 這個等待map中
  2. 2. 校驗通過那么會異步的調用 dialConnFor 方法創建連接

??那會不會queueForDial方法中將idleConnWait和connsPerHostWait打包到等待空閑連接idleConn這個map中呢?

我們繼續看dialConnFor的實現,它會給我們這個問題的答案!

dialConnFor

func (t *Transport) dialConnFor(w *wantConn) {
    defer w.afterDial()
    //創建 persistConn
    pc, err := t.dialConn(w.ctx, w.cm)
    //綁定到 wantConn
    delivered := w.tryDeliver(pc, err)
    if err == nil && (!delivered || pc.alt != nil) {
        //綁定wantConn失敗
        //放到存放空閑連接idleConn的map中
        t.putOrCloseIdleConn(pc)
    }
    if err != nil {
        t.decConnsPerHost(w.key)
    }
}
  • ? dialConnFor 先調用 dialConn 方法創建 TCP 連接
  • ? 調用 tryDeliver 將連接綁定到 wantConn 上,綁定成功的話,就將該鏈接放到空閑連接的idleConn這個map中
  • ? 綁定失敗的話會調用decConnsPerHost方法,用遞減密鑰的每主機連接計數方式,繼續異步調用Transport.dialConnFor

我們可以追蹤下代碼會發現Transport.tryPutIdleConn() 方法就是將persistConn添加到等待的空閑持久連接列表中的實現。

Transport.dialConn創建連接

func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (pconn *persistConn, err error) {
    pconn = &persistConn{
        t:             t,
        cacheKey:      cm.key(),
        reqch:         make(chan requestAndChan, 1),
        writech:       make(chan writeRequest, 1),
        closech:       make(chan struct{}),
        writeErrCh:    make(chan error, 1),
        writeLoopDone: make(chan struct{}),
    }
    ...
    // 創建 tcp 連接,給pconn.conn
    conn, err := t.dial(ctx, "tcp", cm.addr())
    if err != nil {
        return nil, wrapErr(err)
    }
    pconn.conn = conn
    ...
    //開啟兩個goroutine處理讀寫
    go pconn.readLoop()
    go pconn.writeLoop()
    return pconn, nil
}

?? 看完這個創建persistConn的代碼是不是心里仿佛懂了什么?

上述代碼中HTTP 連接的創建過程是建立 tcp 連接,然后為連接異步處理讀寫數據,最后將創建好的連接返回。

我們可以看到創建的每個連接會分別創建兩個goroutine循環地進行進行讀寫的處理,這就是為什么我們連接能接受請求參數和處理請求的響應的關鍵。

?? 這兩個協程功能是這樣的!

  1. 1. persisConn.writeLoop(),通過 persistConn.writech 通道讀取到客戶端提交的請求,將其發送到服務端
  2. 2. persisConn.readLoop(),讀取來自服務端的響應,并添加到 persistConn.reqCh 通道中,給persistConn.roundTrip 方法接收

想看這兩個協程

等待響應

persistConn 連接本身創建了兩個讀寫goroutine,而這兩個goroutine就是通過兩個channel進行通信的。

這個通信就是在persistConn.roundTrip()方法中的進行傳遞交互的,其中writech 是用來寫入請求數據,reqch是用來讀取響應數據。

圖片圖片

func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {
    ...
    // 請求數據寫入到 writech channel中
    pc.writech <- writeRequest{req, writeErrCh, continueCh}

    // 接收響應的channel
    resc := make(chan responseAndError)
    // 接收響應的結構體 requestAndChan 寫到 reqch channel中
    pc.reqch <- requestAndChan{
        req:        req.Request,
        cancelKey:  req.cancelKey,
        ch:         resc,
        ...
    }
    ...
    for {
        ...
        select { 
            // 接收到響應數據
        case re := <-resc:
            ...
            // return響應數據
            return re.res, nil
        ...
    }
}

1. 連接獲取到之后,會調用連接的 roundTrip 方法,將請求數據寫入到 persisConn.writech channel中,而連接 persisConn 中的協程 writeLoop() 接收到請求后就會處理請求

2. 響應結構體 requestAndChan 寫入到 persisConn.reqch 中

3. 通過readLoop 接受響應數據,然后讀取 resc channel 的響應結果

4. 接受到響應數據之后循環結束,連接處理完成

好了,net/http標準庫的客戶端構造請求、發送請求、接受服務端的請求數據流程就講完了,看完之后是否意欲未盡呢?

還別說,小許也是第一次看是如何實現的,確實還是了解到了點東西呢!

責任編輯:武曉燕 來源: 小許code
相關推薦

2024-01-29 08:04:48

Golang標準庫服務端

2021-10-18 05:00:38

語言GoRequestHTTP

2021-05-07 15:28:03

Kafka客戶端Sarama

2022-02-20 23:15:46

gRPCGolang語言

2023-10-12 07:54:02

.NETXamarin框架

2009-08-18 15:43:56

ASP.NET生成客戶端腳本

2009-07-24 17:31:56

ASP.NET AJA

2021-08-01 23:18:21

Redis Golang命令

2020-03-24 15:15:29

HttpClientOkHttpJava

2024-05-09 08:30:57

OkHttpHTTP客戶端

2025-03-14 09:20:46

2023-10-11 07:00:44

高可用程序客戶端

2024-05-29 07:30:41

2024-10-10 15:54:44

.NET開源Redis

2022-04-20 08:32:09

RabbitMQ流控制

2009-10-15 10:46:03

PPC客戶端程序VB.NET創建

2021-09-22 15:46:29

虛擬桌面瘦客戶端胖客戶端

2024-09-14 08:16:24

Redis客戶端性能

2009-02-04 17:39:14

ibmdwWebSphereDataPower

2011-08-17 10:10:59

點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 精品国产一区二区三区日日嗨 | 午夜亚洲 | 精品国产高清一区二区三区 | 成人在线精品 | 欧区一欧区二欧区三免费 | 超碰最新在线 | 中文字幕二区三区 | 成人久草| 亚洲美女在线一区 | 午夜影院在线视频 | 天天av天天好逼 | 亚洲日韩欧美一区二区在线 | 欧美在线国产精品 | 日韩av免费看 | 99热视| 91视频久久久久 | 日本高清不卡视频 | 91久久精品日日躁夜夜躁国产 | 国产精品一卡 | 中文字幕乱码视频32 | jizz视频 | 91精品国产乱码久久蜜臀 | 亚洲一区二区三区国产 | 久久久国产一区二区三区 | 99久久99热这里只有精品 | 日韩不卡一区二区 | 性一爱一乱一交一视频 | 国产成人99久久亚洲综合精品 | 欧美精品一区在线发布 | 成人国产精品色哟哟 | 欧美日韩在线精品 | caoporn国产| 久久久久久久一区 | 二区三区av | 91色在线| 可以看黄的视频 | 久久精品一级 | 欧美一区二区大片 | 欧美中文字幕在线 | 欧美三级免费观看 | 亚洲国产精品久久久久秋霞不卡 |