圖文講透Golang標(biāo)準(zhǔn)庫(kù) net/http實(shí)現(xiàn)原理 - 客戶端
客戶端的內(nèi)容將是如何發(fā)送請(qǐng)求和接收響應(yīng),走完客戶端就把整個(gè)流程就完整的串聯(lián)起來(lái)了!
這次我把調(diào)用的核心方法和流程走讀的函數(shù)也貼出來(lái),這樣看應(yīng)該更有邏輯感,重要部分用紅色標(biāo)記了一下,可以著重看下。
圖片
先了解下核心數(shù)據(jù)結(jié)構(gòu)Client和Request。
Client結(jié)構(gòu)體
type Client struct {
Transport RoundTripper
CheckRedirect func(req *Request, via []*Request) error
Jar CookieJar
Timeout time.Duration
}
四個(gè)字段分別是:
- ? Transport:表示 HTTP 事務(wù),用于處理客戶端的請(qǐng)求連接并等待服務(wù)端的響應(yīng);
- ? CheckRedirect:處理重定向的策略
- ? Jar:管理和存儲(chǔ)請(qǐng)求中的 cookie
- ? Timeout:超時(shí)設(shè)置
Request結(jié)構(gòu)體
Request字段較多,這里就列舉一下常見(jiàn)的一些字段
type Request struct {
Method string
URL *url.URL
Header Header
Body io.ReadCloser
Host string
Response *Response
...
}
- ? Method:指定的HTTP方法(GET、POST、PUT等)
- ? URL:請(qǐng)求路徑
- ? Header:請(qǐng)求頭
- ? Body:請(qǐng)求體
- ? Host:服務(wù)器主機(jī)
- ? Response:響應(yīng)參數(shù)
構(gòu)造請(qǐng)求
var DefaultClient = &Client{}
func Get(url string) (resp *Response, err error) {
return DefaultClient.Get(url)
}
示例HTTP 的 Get方法會(huì)調(diào)用到 DefaultClient 的 Get 方法,,然后調(diào)用到 Client 的 Get 方法。
DefaultClient 是 Client 的一個(gè)空實(shí)例(跟DefaultServeMux有點(diǎn)子相似)
圖片
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() 根據(jù)用戶的入?yún)ⅲ?qǐng)求參數(shù) NewRequest使用上下文包裝NewRequestWithContext ,接著通過(guò) Client.Do 方法,處理這個(gè)請(qǐng)求。
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 函數(shù)主要是功能是將請(qǐng)求封裝成一個(gè) Request 結(jié)構(gòu)體并返回,這個(gè)結(jié)構(gòu)體的名稱是req。
準(zhǔn)備發(fā)送請(qǐng)求
構(gòu)造好的Request結(jié)構(gòu)req,會(huì)傳入c.Do()方法。
我們看下發(fā)送請(qǐng)求過(guò)程調(diào)用了哪些方法,用下圖表示下
圖片
?? 其實(shí)不管是Get還是Post請(qǐng)求的調(diào)用流程都是一樣的,只是對(duì)外封裝了Post和Get請(qǐng)求
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 調(diào)用 Do 方法處理發(fā)送請(qǐng)求最后會(huì)調(diào)用到 send 函數(shù)中
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的默認(rèn)實(shí)例 DefaultTransport ,我們看下DefaultTransport長(zhǎng)什么樣。
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,
}
可以根據(jù)需要建立網(wǎng)絡(luò)連接,并緩存它們以供后續(xù)調(diào)用重用,部分參數(shù)如下:
- ? MaxIdleConns:最大空閑連接數(shù)
- ? IdleConnTimeout:空閑連接超時(shí)時(shí)間
- ? ExpectContinueTimeout:預(yù)計(jì)繼續(xù)超時(shí)
注意這里的RoundTripper是個(gè)接口,也就是說(shuō) Transport 實(shí)現(xiàn) RoundTripper 接口,該接口方法接收Request,返回Response。
RoundTripper
type RoundTripper interface {
RoundTrip(*Request) (*Response, error)
}
圖片
雖然還沒(méi)看完后面邏輯,不過(guò)我們猜測(cè)RoundTrip方法可能是實(shí)際處理客戶端請(qǐng)求的實(shí)現(xiàn)。
我們繼續(xù)追下后面邏輯,看下是否能驗(yàn)證這個(gè)猜想。
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函數(shù)的第二個(gè)參數(shù)就是接口類型,調(diào)用層傳遞的Transport的實(shí)例DefaultTransport。
而rt.RoundTrip()方法的調(diào)用具體在net/http/roundtrip.go文件中,這也是RoundTrip接口的實(shí)現(xiàn),代碼如下:
func (t *Transport) RoundTrip(req *Request) (*Response, error) {
return t.roundTrip(req)
}
Transport.roundTrip 方法概況來(lái)說(shuō)干了這些事:
- ? 封裝請(qǐng)求transportRequest
- ? 調(diào)用 Transport 的 getConn 方法獲取連接
- ? 在獲取到連接后,調(diào)用 persistConn 的 roundTrip 方法等待請(qǐng)求響應(yīng)結(jié)果
func (t *Transport) roundTrip(req *Request) (*Response, error) {
...
for {
...
// 請(qǐng)求封裝
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 {
...
}
// 等待響應(yīng)結(jié)果
var resp *Response
if pconn.alt != nil {
t.setReqCanceler(cancelKey, nil)
resp, err = pconn.alt.RoundTrip(req)
} else {
resp, err = pconn.roundTrip(treq)
}
...
}
}
封裝請(qǐng)求transportRequeste沒(méi)啥好說(shuō)的,因?yàn)閠req被roundTrip修改,所以這里需要為每次重試重新創(chuàng)建。
獲取連接
獲取連接的方法是 getConn,這里代碼還是比較長(zhǎng)的,會(huì)有不同的兩種方式去獲取連接:
- 1. 調(diào)用 queueForIdleConn 排隊(duì)等待獲取空閑連接
- 2. 如果獲取空閑連接失敗,那么調(diào)用 queueForDial 異步創(chuàng)建一個(gè)新的連接,并通過(guò)channel來(lái)接收readdy信號(hào),來(lái)確認(rèn)連接是否構(gòu)造完成
圖片
getConn
func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persistConn, err error) {
...
// 初始化wantConn結(jié)構(gòu)體
w := &wantConn{
cm: cm,
key: cm.key(),
ctx: ctx,
ready: make(chan struct{}, 1),
beforeDial: testHookPrePendingDial,
afterDial: testHookPostPendingDial,
}
...
// 獲取空閑連接
if delivered := t.queueForIdleConn(w); delivered {
...
}
// 異步創(chuàng)建新連接
t.queueForDial(w)
select {
// 阻塞等待獲取到連接完成
case <-w.ready:
...
return w.pc, w.err
...
}
queueForIdleConn獲取空閑連接
獲取成功
成功空閑獲取連接Conn流程如下圖
圖片
- 1. 根據(jù)wantConn的key從 transport.idleConn 這個(gè)map中查找,看是否存不存在空閑的 connection 列表
- 2. 獲取到空閑的 connection 列表后,從列表中拿最后一個(gè) connection
- 3. 獲取到連接后會(huì)調(diào)用 wantConn.tryDeliver 方法將連接綁定到 wantConn 請(qǐng)求參數(shù)上
獲取失敗
圖片
當(dāng)不存在該請(qǐng)求的 connection 列表,會(huì)將當(dāng)前 wantConn 加入到名稱為 idleConnWait 的等待空閑map中。
不過(guò)此時(shí)的idleConnWait這個(gè)map的值是個(gè)隊(duì)列
queueForIdleConn方法
從上面的兩張圖解中差不多能看出是如何獲取空閑連接和如何獲取失敗時(shí)如何做的了,這里也貼下代碼體驗(yàn)下,讓大家更清楚里面的實(shí)現(xiàn)邏輯。
//idleConn是map類型,指定key返回切片列表
idleConn map[connectMethodKey][]*persistConn
//idleConnWait,指定key返回隊(duì)列
idleConnWait map[connectMethodKey]wantConnQueue
這里將獲取空閑連接的代碼實(shí)現(xiàn)多進(jìn)行注釋,更好理解一些!
func (t *Transport) queueForIdleConn(w *wantConn) (delivered bool) {
//參數(shù)判斷
if t.DisableKeepAlives {
return false
}
if w == nil {
return false
}
// 計(jì)算空閑連接超時(shí)時(shí)間
var oldTime time.Time
if t.IdleConnTimeout > 0 {
oldTime = time.Now().Add(-t.IdleConnTimeout)
}
//從idleConn根據(jù)w.key找對(duì)應(yīng)的persistConn 列表
if list, ok := t.idleConn[w.key]; ok {
stop := false
delivered := false
for len(list) > 0 && !stop {
// 找到persistConn列表最后一個(gè)
pconn := list[len(list)-1]
// 檢查這個(gè) persistConn 是不是過(guò)期
tooOld := !oldTime.IsZero() && pconn.idleAt.Round(0).Before(oldTime)
if tooOld {
//如果過(guò)期進(jìn)行異步清理
go pconn.closeConnIfStillIdle()
}
// 該 persistConn 被標(biāo)記為 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)
//缺省了最后一個(gè)conn
list = list[:len(list)-1]
}
}
stop = true
}
//對(duì)被獲取連接后的列表進(jìn)行判斷
if len(list) > 0 {
t.idleConn[w.key] = list
} else {
// 如果該 key 對(duì)應(yīng)的空閑列表不存在,那么將該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
}
我們知道了為找到的空閑連接會(huì)被放到空閑 idleConnWait 這個(gè)等待map中,最后會(huì)被Transport.tryPutIdleConn方法將pconne添加到等待新請(qǐng)求的空閑持久連接列表中。
queueForDial創(chuàng)建新連接
queueForDial意思是排隊(duì)等待撥號(hào),為什么說(shuō)是等帶呢,因?yàn)樽罱K的結(jié)果是在ready這個(gè)channel上進(jìn)行通知的。
流程如下圖:
圖片
我們先看下Transport結(jié)構(gòu)體的這兩個(gè)map,名稱不一樣map的屬性和解釋都是一樣的,其中idleConnWait是在沒(méi)查找空閑連接的時(shí)候存放當(dāng)前連接的map。
而connsPerHostWait用在了創(chuàng)建新連接的地方,可以猜測(cè)一下創(chuàng)建新鏈接的地方就是將當(dāng)前的請(qǐng)求放入到 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建立的連接數(shù)沒(méi)達(dá)到上限,執(zhí)行異步建立連接
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
}
//進(jìn)入等待隊(duì)列
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
}
在獲取不到空閑連接之后,會(huì)嘗試去建立連接:
- 1. queueForDial 方法的內(nèi)部會(huì)先校驗(yàn) MaxConnsPerHost 是否未設(shè)置和是否已達(dá)上限
- 1.
- 1. 檢驗(yàn)不通過(guò)則將當(dāng)前的請(qǐng)求放入到 connsPerHostWait 這個(gè)等待map中
- 2. 校驗(yàn)通過(guò)那么會(huì)異步的調(diào)用 dialConnFor 方法創(chuàng)建連接
??那會(huì)不會(huì)queueForDial方法中將idleConnWait和connsPerHostWait打包到等待空閑連接idleConn這個(gè)map中呢?
我們繼續(xù)看dialConnFor的實(shí)現(xiàn),它會(huì)給我們這個(gè)問(wèn)題的答案!
dialConnFor
func (t *Transport) dialConnFor(w *wantConn) {
defer w.afterDial()
//創(chuàng)建 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 先調(diào)用 dialConn 方法創(chuàng)建 TCP 連接
- ? 調(diào)用 tryDeliver 將連接綁定到 wantConn 上,綁定成功的話,就將該鏈接放到空閑連接的idleConn這個(gè)map中
- ? 綁定失敗的話會(huì)調(diào)用decConnsPerHost方法,用遞減密鑰的每主機(jī)連接計(jì)數(shù)方式,繼續(xù)異步調(diào)用Transport.dialConnFor
我們可以追蹤下代碼會(huì)發(fā)現(xiàn)Transport.tryPutIdleConn() 方法就是將persistConn添加到等待的空閑持久連接列表中的實(shí)現(xiàn)。
Transport.dialConn創(chuàng)建連接
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{}),
}
...
// 創(chuàng)建 tcp 連接,給pconn.conn
conn, err := t.dial(ctx, "tcp", cm.addr())
if err != nil {
return nil, wrapErr(err)
}
pconn.conn = conn
...
//開啟兩個(gè)goroutine處理讀寫
go pconn.readLoop()
go pconn.writeLoop()
return pconn, nil
}
?? 看完這個(gè)創(chuàng)建persistConn的代碼是不是心里仿佛懂了什么?
上述代碼中HTTP 連接的創(chuàng)建過(guò)程是建立 tcp 連接,然后為連接異步處理讀寫數(shù)據(jù),最后將創(chuàng)建好的連接返回。
我們可以看到創(chuàng)建的每個(gè)連接會(huì)分別創(chuàng)建兩個(gè)goroutine循環(huán)地進(jìn)行進(jìn)行讀寫的處理,這就是為什么我們連接能接受請(qǐng)求參數(shù)和處理請(qǐng)求的響應(yīng)的關(guān)鍵。
?? 這兩個(gè)協(xié)程功能是這樣的!
- 1. persisConn.writeLoop(),通過(guò) persistConn.writech 通道讀取到客戶端提交的請(qǐng)求,將其發(fā)送到服務(wù)端
- 2. persisConn.readLoop(),讀取來(lái)自服務(wù)端的響應(yīng),并添加到 persistConn.reqCh 通道中,給persistConn.roundTrip 方法接收
想看這兩個(gè)協(xié)程
等待響應(yīng)
persistConn 連接本身創(chuàng)建了兩個(gè)讀寫goroutine,而這兩個(gè)goroutine就是通過(guò)兩個(gè)channel進(jìn)行通信的。
這個(gè)通信就是在persistConn.roundTrip()方法中的進(jìn)行傳遞交互的,其中writech 是用來(lái)寫入請(qǐng)求數(shù)據(jù),reqch是用來(lái)讀取響應(yīng)數(shù)據(jù)。
圖片
func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {
...
// 請(qǐng)求數(shù)據(jù)寫入到 writech channel中
pc.writech <- writeRequest{req, writeErrCh, continueCh}
// 接收響應(yīng)的channel
resc := make(chan responseAndError)
// 接收響應(yīng)的結(jié)構(gòu)體 requestAndChan 寫到 reqch channel中
pc.reqch <- requestAndChan{
req: req.Request,
cancelKey: req.cancelKey,
ch: resc,
...
}
...
for {
...
select {
// 接收到響應(yīng)數(shù)據(jù)
case re := <-resc:
...
// return響應(yīng)數(shù)據(jù)
return re.res, nil
...
}
}
1. 連接獲取到之后,會(huì)調(diào)用連接的 roundTrip 方法,將請(qǐng)求數(shù)據(jù)寫入到 persisConn.writech channel中,而連接 persisConn 中的協(xié)程 writeLoop() 接收到請(qǐng)求后就會(huì)處理請(qǐng)求
2. 響應(yīng)結(jié)構(gòu)體 requestAndChan 寫入到 persisConn.reqch 中
3. 通過(guò)readLoop 接受響應(yīng)數(shù)據(jù),然后讀取 resc channel 的響應(yīng)結(jié)果
4. 接受到響應(yīng)數(shù)據(jù)之后循環(huán)結(jié)束,連接處理完成
好了,net/http標(biāo)準(zhǔn)庫(kù)的客戶端構(gòu)造請(qǐng)求、發(fā)送請(qǐng)求、接受服務(wù)端的請(qǐng)求數(shù)據(jù)流程就講完了,看完之后是否意欲未盡呢?
還別說(shuō),小許也是第一次看是如何實(shí)現(xiàn)的,確實(shí)還是了解到了點(diǎn)東西呢!