Go - http.Client源碼分析

1. 原由

分析http.Client源碼實現的原由, 是由於在使用以下步驟模擬網站登陸時, 出現了問題, 參考知乎 - go net/http.Client 處理redirect:前端

  1. POST帳號密碼等參數進行登陸git

  2. 下發token, 此token經過cookie下發github

  3. 重定向到主頁/golang

在經過http.Post進行請求, 預期不進行重定向, 可以直接獲取到cookie值, 但實際上go幫咱們處理了重定向, 丟失了cookie數組

分析源碼後, 能夠很輕易地解決這個問題:瀏覽器

// 請求http.calabash.top將被301重定向到https

myClient := http.Client{
    CheckRedirect: func(req *http.Request, via []*http.Request) error {
        return http.ErrUseLastResponse
    },
}
respWithNoRedirect, _ := myClient.Get("http://blog.calabash.top")
respWithRedirect, _ := http.Get("http://blog.calabash.top")

fmt.Println(respWithNoRedirect.StatusCode) // 301
fmt.Println(respWithRedirect.StatusCode)   // 200
複製代碼

2. Client

HTTP客戶端, 其零值爲DefaultClient, 本處分析的核心在於處理重定向的方式bash

type Client struct {
    // 分析範圍以外
    Transport RoundTripper
    // 重定向策略
    CheckRedirect func(req *Request, via []*Request) error
    // Cookie的存儲, 單純的get/set方法
    Jar CookieJar
    // 超時
    Timeout time.Duration
}

var DefaultClient = &Client{}
複製代碼

當碰見重定向時, 除了如下的狀況, Client將轉發全部初始請求頭:cookie

  • 當重定向地址和初始地址的domain不一樣且也不是sub domain時, 請求頭中的cookie, authorization等敏感字段將被忽略閉包

  • 重定向可能會改變初始請求中的cookie值, 所以轉發cookie請求頭時將忽略任何有變化的cookieapp

2.1 重定向的規則: redirectBehavior

func redirectBehavior(reqMethod string, resp *Response, ireq *Request) (redirectMethod string, shouldRedirect, includeBody bool) {
	switch resp.StatusCode {
	case 301, 302, 303:
		redirectMethod = reqMethod
		shouldRedirect = true
		includeBody = false

		if reqMethod != "GET" && reqMethod != "HEAD" {
			redirectMethod = "GET"
		}
	case 307, 308:
		redirectMethod = reqMethod
		shouldRedirect = true
		includeBody = true

		if resp.Header.Get("Location") == "" {
			shouldRedirect = false
			break
		}
		if ireq.GetBody == nil && ireq.outgoingLength() != 0 {
			shouldRedirect = false
		}
	}
	return redirectMethod, shouldRedirect, includeBody
}
複製代碼

由源碼能夠獲得以下的信息:

  • 對於301, 302, 303的狀態碼, 重定向不會附帶請求體, 而且對於非GET/HEAD請求方法會被強制修改成GET請求方法

  • 對於307, 308狀態碼, 重定向不會更改修改請求方法且會重用請求體, Location爲空或Body爲空時, 不該重定向

參考MDN - Http Status Code, 其中有這樣的描述

301: 儘管標準要求瀏覽器在收到該響應並進行重定向時不該該修改http method和body,可是有一些瀏覽器可能會有問題。 因此最好是在應對GET或HEAD方法時使用301,其餘狀況使用308來替代301

302: 即便規範要求瀏覽器在重定向時保證請求方法和請求主體不變,但並非全部的用戶代理都會遵循這一點 因此推薦僅在響應GET或HEAD方法時採用302狀態碼,其餘狀況使用307來替代301

303: 一般做爲PUT或POST操做的返回結果,它表示重定向連接指向的不是新上傳的資源,而是另一個頁面 而請求重定向頁面的方法要老是使用GET

307: 狀態碼307與302之間的惟一區別在於,當發送重定向請求的時候,307狀態碼能夠確保請求方法和消息主體不會發生變化

308: 在重定向過程當中,請求方法和消息主體不會發生改變,然而在返回301狀態碼的狀況下,請求方法有時候會被客戶端錯誤地修改成GET方法。

可以發現MDN的描述和redirectBehavior源碼的設計很是吻合

2.2 重定向的檢查策略: checkRedirect

當沒有指定CheckRedirect函數時, Client會使用defaultCheckRedirect策略: 默認最多重定向十次

func (c *Client) checkRedirect(req *Request, via []*Request) error {
	fn := c.CheckRedirect
	if fn == nil {
		fn = defaultCheckRedirect
	}
	return fn(req, via)
}

func defaultCheckRedirect(req *Request, via []*Request) error {
	if len(via) >= 10 {
		return errors.New("stopped after 10 redirects")
	}
	return nil
}
複製代碼

2.3 處理重定向請求頭: makeHeadersCopier

這個方法經過閉包的方式, 完成了上面提到的對請求頭的處理:

  • 當重定向地址和初始地址的域名不一樣且也不是子域時, 請求頭中的cookie, authorization等敏感字段將被忽略

  • 重定向可能會改變初始請求中的cookie值, 所以轉發cookie請求頭時將忽略任何有變化的cookie

func (c *Client) makeHeadersCopier(ireq *Request) func(*Request) {
	// 克隆一份header
	var (
		ireqhdr  = ireq.Header.Clone()
		icookies map[string][]*Cookie
	)
	// 再維護一個cookie哈希表
	if c.Jar != nil && ireq.Header.Get("Cookie") != "" {
		icookies = make(map[string][]*Cookie)
		for _, c := range ireq.Cookies() {
			icookies[c.Name] = append(icookies[c.Name], c)
		}
	}
	// The previous request
	preq := ireq 
	
	// 調用返回一個接受req的函數, 用於對拷貝header進行處理
	return func(req *Request) {
		if c.Jar != nil && icookies != nil {
			var changed bool
			resp := req.Response
			// 若是響應中的set-cookie操做設定的cookie名稱存在於cookie哈希表中, 從哈希表中刪除它
			for _, c := range resp.Cookies() {
				if _, ok := icookies[c.Name]; ok {
					delete(icookies, c.Name)
					changed = true
				}
			}
			// 忽略全部變化的cookie, 從新組裝cookie請求頭字段
			if changed {
				ireqhdr.Del("Cookie")
				var ss []string
				for _, cs := range icookies {
					for _, c := range cs {
						ss = append(ss, c.Name+"="+c.Value)
					}
				}
				sort.Strings(ss) // Ensure deterministic headers
				ireqhdr.Set("Cookie", strings.Join(ss, "; "))
			}
		}

		for k, vv := range ireqhdr {
		        // 對於非同域或子域, 敏感請求頭的處理
			if shouldCopyHeaderOnRedirect(k, preq.URL, req.URL) {
				req.Header[k] = vv
			}
		}
		// Update previous Request with the current request
		preq = req 
	}
}
複製代碼

順便貼一下對於非同域或子域, 敏感請求頭的處理, 比較簡單易懂

func shouldCopyHeaderOnRedirect(headerKey string, initial, dest *url.URL) bool {
	switch CanonicalHeaderKey(headerKey) {
	case "Authorization", "Www-Authenticate", "Cookie", "Cookie2":
		ihost := canonicalAddr(initial)
		dhost := canonicalAddr(dest)
		return isDomainOrSubdomain(dhost, ihost)
	}
	// All other headers are copied:
	return true
}

func isDomainOrSubdomain(sub, parent string) bool {
	if sub == parent {
		return true
	}
	if !strings.HasSuffix(sub, parent) {
		return false
	}
	return sub[len(sub)-len(parent)-1] == '.'
}
複製代碼

2.4 http.Get等方法的背後

從源碼中能夠找到, http.Get等方法都是DefaultClient包裝後的一層

var DefaultClient = &Client{}

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

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

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

func Head(url string) (resp *Response, err error) {
	return DefaultClient.Head(url)
}
複製代碼

而這些方法最終調用的都是Client.Do方法, 這部分能夠說十分開胃了

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

func (c *Client) PostForm(url string, data url.Values) (resp *Response, err error) {
	return c.Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
}

func (c *Client) Head(url string) (resp *Response, err error) {
	req, err := NewRequest("HEAD", url, nil)
	if err != nil {
		return nil, err
	}
	return c.Do(req)
}
複製代碼

2.5 Client.Do方法

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

開始分析Client.do方法, 該方法大概200行, 爲了不對流程分析的影響, 將去除錯誤處理部分代碼, 簡化代碼以下

func (c *Client) do(req *Request) (retres *Response, reterr error) {
	var (
		deadline      = c.deadline()
		reqs          []*Request
		resp          *Response
		copyHeaders   = c.makeHeadersCopier(req)
		reqBodyClosed = false
		redirectMethod string
		includeBody    bool
	)
	for {
		if len(reqs) > 0 {
			loc := resp.Header.Get("Location")
			ireq := reqs[0]
			req = &Request{
				Method:   redirectMethod,
				Response: resp,
				URL:      u,
				Header:   make(Header),
				Host:     host,
				Cancel:   ireq.Cancel,
				ctx:      ireq.ctx,
			}
			if includeBody && ireq.GetBody != nil {
				req.Body, err = ireq.GetBody()
				req.ContentLength = ireq.ContentLength
			}

			copyHeaders(req)

			err = c.checkRedirect(req, reqs)
			if err == ErrUseLastResponse {
				return resp, nil
			}
			resp.Body.Close()
		}

		reqs = append(reqs, req)
		var err error
		var didTimeout func() bool
		resp, didTimeout, err = c.send(req, deadline)

		var shouldRedirect bool
		redirectMethod, shouldRedirect, includeBody = redirectBehavior(req.Method, resp, reqs[0])
		if !shouldRedirect {
			return resp, nil
		}

		req.closeBody()
	}
}
複製代碼
  1. 拷貝req中的所有headers, 返回一個函數copyHeaders, 出現重定向時根據規則處理一部分請求頭字段

  2. reqs = append(reqs, req), 將請求記錄到reqs數組中, 第一個req必定是最原始的請求, 後面的req必定都是重定向的~

  3. resp = c.send(req), 正兒八經發起請求, 獲得resp

  4. redirectBehavior(req.Method, resp, reqs[0]), 根據響應判斷是否須要重定向, 若是不須要, 流程結束, 若是須要, 繼續向下

  5. 進入if len(reqs) > 0分支, 開始對重定向的處理

  6. resp中獲取Location字段, 結合原始請求reqs[0]組裝出新的重定向請求, 並賦值給req

  7. copyHeaders(req), 比對reqs[0]req, 根據上面提到的兩條規則去除特定的字段

  8. c.checkRedirect(req, reqs)判斷是否符合重定向策略, 若是不符合, 返回最後一個resp, 再也不繼續重定向

  9. 再次執行步驟1-4

3. 總結

源碼地址

雖然是個前端, 第一次看Go源碼, 體驗仍是很是爽的, 800行代碼, 400行註釋, 量也不是很大QAQ。

總結一下經驗:

  1. 帶着問題看源碼, 便於肯定方向

  2. 沿着主分支分析, 去除/跳過一些旁枝末節和錯誤處理的代碼

  3. DEBUG是能最快肯定流程的方式

相關文章
相關標籤/搜索