Go中的HTTP請求之——HTTP1.1請求流程分析

2021-01-08 新世界雜貨鋪

大綱

前言

http是目前應用最為廣泛, 也是程式設計師接觸最多的協議之一。今天筆者站在GoPher的角度對http1.1的請求流程進行全面的分析。希望讀者讀完此文後, 能夠有以下幾個收穫:

對http1.1的請求流程有一個大概的了解在平時的開發中能夠更好地復用底層TCP連接對http1.1的線頭阻塞能有一個更清楚的認識HTTP1.1流程

今天內容較多, 廢話不多說, 直接上乾貨。

接下來, 筆者將根據流程圖,對除了NewRequest以外的函數進行逐步的展開和分析

(*Client).do

(*Client).do方法的核心代碼是一個沒有結束條件的for循環。

上面的代碼中, 請求第一次進入會調用c.send, 得到響應後會判斷請求是否需要重定向, 如果需要重定向則繼續循環, 否則返迴響應。

進入重定向流程後, 這裡筆者簡單介紹一下checkRedirect函數:

由上可知, 用戶可以自己定義重定向的檢查規則。如果用戶沒有自定義檢查規則, 則重定向次數不能超過10次。

(*Client).send

(*Client).send方法邏輯較為簡單, 主要看用戶有沒有為http.Client的Jar欄位實現CookieJar接口。主要流程如下:

如果實現了CookieJar接口, 為Request添加保存的cookie信息。調用send函數。如果實現了CookieJar接口, 將Response中的cookie信息保存下來。

另外, 我們還需要關注c.transport()的調用。如果用戶未對http.Client指定Transport則會使用go默認的DefaultTransport。

該Transport實現RoundTripper接口。在go中RoundTripper的定義為「執行單個HTTP事務的能力,獲取給定請求的響應」。

send

send函數會檢查request的URL,以及參數的rt, 和header值。如果URL和rt為nil則直接返回錯誤。同時, 如果請求中設置了用戶信息, 還會檢查並設置basic的驗證頭信息,最後調用rt.RoundTrip得到請求的響應。

(*Transport).RoundTrip

(*Transport).RoundTrip的邏輯很簡單,它會調用(*Transport).roundTrip方法,因此本節實際上是對(*Transport).roundTrip方法的分析。

由上可知, 每次for循環, 會判斷請求上下文是否已經取消, 如果沒有取消則繼續進行後續的流程。

先調用t.getConn方法獲取一個persistConn。因為本篇主旨是http1.1,所以我們直接看http1.1的執行分支。根據源碼中的注釋和實際的debug結果,獲取到連接後, 會繼續調用pconn.roundTrip。(*Transport).getConn

筆者認為這一步在http請求中是非常核心的一個步驟,因為只有和server端建立連接後才能進行後續的通信。

由上能夠清楚的知道, 獲取連接分為以下幾個步驟:

調用t.queueForIdleConn獲取一個空閒且可復用的連接,如果獲取成功則直接返回該連接。如果未獲取到空閒連接則調用t.queueForDial開始新建一個連接。等待w.ready關閉,則可以返回新的連接。(*Transport).queueForIdleConn

(*Transport).queueForIdleConn方法會根據請求的connectMethodKey從t.idleConn獲取一個[]*persistConn切片, 並從切片中,根據算法獲取一個有效的空閒連接。如果未獲取到空閒連接,則將wantConn結構體變量放入t.idleConnWait[w.key]等待隊列,此處wantConn結構體變量就是前面提到的w。

connectMethodKey定義和queueForIdleConn部分關鍵代碼如下:

其中w.tryDeliver方法主要作用是將連接協程安全的賦值給w.pc,並關閉w.ready管道。此時我們便可以和(*Transport).getConn中調用queueForIdleConn成功後的返回值對應上。

(*Transport).queueForDial

(*Transport).queueForDial方法包含三個步驟:

如果t.MaxConnsPerHost小於等於0,執行go t.dialConnFor(w)並返回。其中MaxConnsPerHost代表著每個host的最大連接數,小於等於0表示不限制。如果當前host的連接數不超過t.MaxConnsPerHost,對當前host的連接數+1,然後執行go t.dialConnFor(w)並返回。如果當前host的連接數等於t.MaxConnsPerHost,則將wantConn結構體變量放入t.connsPerHostWait[w.key]等待隊列, 此處wantConn結構體變量就是前面提到的w。另外在放入等待隊列前會先清除隊列中已經失效或者不再等待的變量。

(*Transport).dialConnFor

(*Transport).dialConnFor方法調用t.dialConn獲取一個真正的*persistConn。並將這個連接傳遞給w, 如果w已經獲取到了連接,則會傳遞失敗,此時調用t.putOrCloseIdleConn將連接放回空閒連接池。

如果連接獲取錯誤則會調用t.decConnsPerHost減少當前host的連接數。

(*Transport).putOrCloseIdleConn方法

由上可知,將連接放入t.idleConn前,先檢查t.idleConnWait的數量。如果有請求在等待空閒連接, 則將連接復用,沒有空閒連接時,才將連接放入t.idleConn。連接放入t.idleConn後,還會重置連接的可空閒時間。

另外在t.putOrCloseIdleConn函數中還需要注意兩點:

如果用戶自定義了http.client,且將DisableKeepAlives設置為true,或者將MaxIdleConnsPerHost設置為負數,則連接不會放入t.idleConn即連接不能復用。在判斷已有空閒連接數量時, 如果MaxIdleConnsPerHost 不等於0, 則返回用戶設置的數量,否則返回默認值2,詳見上面(*Transport).maxIdleConnsPerHost函數。綜上, 我們知道對於部分有連接數限制的業務, 我們可以為http.Client自定義一個Transport, 並設置Transport的MaxConnsPerHost,MaxIdleConnsPerHost,IdleConnTimeout和DisableKeepAlives從而達到即限制連接數量, 又能保證一定的並發。

(*Transport).decConnsPerHost方法

由上可知, decConnsPerHost方法主要幹了兩件事:

判斷是否有請求在等待撥號, 如果有則執行go t.dialConnFor(w)。如果沒有請求在等待撥號, 則減少當前host的連接數量。(*Transport).dialConn

根據http.Client的默認配置和實際的debug結果,(*Transport).dialConn方法主要邏輯如下:

調用t.dial(ctx, "tcp", cm.addr())創建TCP連接。如果是https的請求, 則對請求建立安全的tls傳輸通道。為persistConn創建讀寫buffer, 如果用戶沒有自定義讀寫buffer的大小, 根據writeBufferSize和readBufferSize方法可知, 讀寫bufffer的大小默認為4096。執行go pconn.readLoop()和go pconn.writeLoop()開啟讀寫循環然後返回連接。

(*persistConn).roundTrip

(*persistConn).roundTrip方法是http1.1請求的核心之一,該方法在這裡獲取真實的Response並返回給上層。

由上可知, (*persistConn).roundTrip方法可以分為三步:

向連接的writech寫入writeRequest:pc.writech <- writeRequest{req, writeErrCh, continueCh}, 參考(*Transport).dialConn可知pc.writech是一個緩衝大小為1的管道,所以會立馬寫入成功。向連接的reqch寫入requestAndChan:pc.reqch <- requestAndChan, pc.reqch和pc.writech一樣都是緩衝大小為1的管道。其中requestAndChan.ch是一個無緩衝的responseAndError管道,(*persistConn).roundTrip就通過這個管道讀取到真實的響應。開啟for循環select, 等待響應或者超時等信息。(*persistConn).writeLoop 寫循環(*persistConn).writeLoop 方法主體邏輯相對簡單, 把用戶的請求寫入連接的寫緩存buffer, 最後再flush就可以了。

(*persistConn).readLoop 讀循環(*persistConn).readLoop有較多的細節, 我們先看代碼, 然後再逐步分析。

由上可知, 只要連接處於活躍狀態, 則這個讀循環會一直開啟, 直到 連接不活躍或者產生其他錯誤才會結束讀循環。

在上述源碼中,pc.readResponse(rc,trace)會從連接的讀buffer中獲取一個請求對應的Response。

讀到響應之後判斷請求是否是HEAD請求或者響應內容為空,如果是HEAD請求或者響應內容為空則將響應寫入rc.ch, 並將連接放入idleConn(此處因為篇幅的原因省略了源碼內容, 正常請求的邏輯也有寫響應和將連接放入idleConn兩個步驟)。

如果不是HEAD請求並且響應內容不為空即!hasBody || bodyWritable為false:

創建一個緩衝大小為2的等待響應被讀取的管道waitForBodyRead:waitForBodyRead := make(chan bool, 2)將響應的Body修改為bodyEOFSignal結構體。通過上面的源碼我們可以知道,此時的resp.Body中有earlyCloseFn和fn兩個函數。earlyCloseFn函數會向waitForBodyRead管道寫入false, fn函數會判斷響應是否讀完, 如果已經讀完則向waitForBodyRead寫入true否則寫入false。將修改後的響應寫入rc.ch。其中rc.ch從rc := <-pc.reqch獲取,而pc.reqch正是前面(*persistConn).roundTrip函數寫入的requestAndChan。requestAndChan.ch是一個無緩衝的responseAndError管道,(*persistConn).roundTrip通過這個管道讀取到真實的響應。select 讀取 waitForBodyRead被寫入的值。如果讀到到的是true則可以調用tryPutIdleConn(此方法會調用前面提到的(*Transport).tryPutIdleConn方法)將連接放入idleConn從而復用連接。waitForBodyRead寫入true的原因我們已經知道了,但是被寫入true的時機我們尚不明確。

由上述源碼可知, 只有當調用方完整的讀取了響應,該連接才能夠被復用。因此在http1.1中,一個連接上的請求,只有等前一個請求處理完之後才能繼續下一個請求。如果前面的請求處理較慢, 則後面的請求必須等待, 這就是http1.1中的線頭阻塞。

根據上面的邏輯, 我們GoPher在平時的開發中如果遇到了不關心響應的請求, 也一定要記得把響應body讀完以保證連接的復用性。筆者在這裡給出一個demo:

以上,就是筆者整理的HTTP1.1的請求流程。

注意

筆者本著嚴謹的態度, 特此提醒:上述流程中筆者對很多細節並未詳細提及或者僅一筆帶過,希望讀者酌情參考。

總結

在go中發起http1.1的請求時, 如果遇到不關心響應的請求,請務必完整讀取響應內容以保證連接的復用性。如果遇到對連接數有限制的業務,可以通過自定義http.Client的Transport, 並設置Transport的MaxConnsPerHost,MaxIdleConnsPerHost,IdleConnTimeout和DisableKeepAlives的值,來控制連接數。如果對於重定向業務邏輯有需求,可以自定義http.Client的CheckRedirect。在http1.1中,一個連接上的請求,只有等前一個請求處理完之後才能繼續下一個請求。如果前面的請求處理較慢, 則後面的請求必須等待, 這就是http1.1中的線頭阻塞。注: 寫本文時, 筆者所用go版本為: go1.14.2

生命不息, 探索不止, 後續將持續更新有關於go的技術探索

原創不易, 卑微求關注收藏二連.

相關焦點

  • Go發起HTTP2.0請求流程分析(前篇)
    Go中的HTTP請求之——HTTP1.1請求流程分析之後,中間斷斷續續,歷時近一月,終於才敢開始碼字寫下本文閱讀建議HTTP2.0在建立TCP連接和安全的TLS傳輸通道與HTTP1.1的流程基本一致。所以筆者建議沒有看過Go中的HTTP請求之——HTTP1.1請求流程分析這篇文章的先去補一下課,本文會基於前一篇文章僅介紹和HTTP2.0相關的邏輯。
  • Jmeter之HTTP請求與響應
    HTTP請求詳解一個http請求指從客戶端到服務端的請求信息,我們可以通過瀏覽器的F12鍵,可以看到以下信息:1.請求地址:uri>2.請求方法:HEAD,GET,POST,PUT,OPTIONS,DELETE,PATCH3.HTTP協議/版本:可以打開瀏覽器按下F12仔細查看4.請求頭
  • 一次完整的http請求詳解
    Http請求的一次詳解:(1) 客戶端輸入URL(2) 客戶端檢測緩存:有緩存且較新,客戶端直接讀取本地緩存進行資源展示有緩存但是不新,準備http請求包,發送至服務端進行緩存校驗備註:http1.0中Expire、http1.1中是Cache-Control根據發起http請求: 請求報文包含:
  • Linux使用epoll異步發送http請求
    http是基於tcp的協議,在發送http請求之前,要先與伺服器建立tcp連接,然後才可以發送HTTP請求。HTTP請求的頭部,就是一些以\r\n分割的字符串。第一行為GET、POST方法,之後的每一行為冒號分割的鍵值對,表示http請求的一些信息。
  • 從一個HTTP請求來讀懂HTTP、TCP協議
    一個HTTP請求的分層解析流程http://www.dumain.com 服務端只認ip地址,瀏覽器將域名解析出來,看下瀏覽器裡有沒有域名對應DNS的緩存,有的話直接拿到服務端的ip地址,沒有的話去本地的host文件看有沒有配置,沒有配置的話才會發起一個DNS請求用來獲取伺服器ip地址。
  • Linux使用epoll控制多個socket發送http請求
    1,下圖的client_connect()函數在START狀態時調用,與伺服器異步建立連接。connect之後把寫事件EPOLLOUT加入epoll描述符監控,然後把狀態更新為CONNECTING連接中。
  • 如何減少HTTP請求並加快網站訪問速度?
    每當有人訪問您網站上的頁面時,瀏覽器都必須請求大量文件。這些HTTP請求直接影響網頁的加載速度。通常,更少的HTTP請求意味著網站加載速度更快。 現在,網站的加載速度是搜尋引擎排名的重要因素。平均而言,媒體頁面加載速度為谷歌的10個結果只是1.65秒。這凸顯了擁有快速加載網站的重要性。
  • 淺度測評:requests、aiohttp、httpx 我應該用哪一個?
    在 Python 眾多的 HTTP 客戶端中,最有名的莫過於requests、aiohttp和httpx。在不藉助其他第三方庫的情況下,requests只能發送同步請求;aiohttp只能發送異步請求;httpx既能發送同步請求,又能發送異步請求。
  • HTTP協議之狀態碼詳解
    狀態碼位於HTTP Response 的第一行中,會返回一個」三位數字的狀態碼「和一個「狀態消息」。 」三位數字的狀態碼「便於程序進行處理, 「狀態消息」更便於人理解。如下圖, 當客戶端請求一個不存在的URL的時候, Web伺服器會返回 「HTTP/1.1 404 Not Found」 告訴瀏覽器客戶端。 伺服器無法找到所請求的URL。
  • 如何對手機http進行抓包?Fiddler工具超好用
    在抓取的信息中可以看到接口請求方式、接口請求URL、接口請求參數、接口返回參數。下面是如何使用fiddler對手機APP進行抓包:1、fiddler手機抓包原理在本機開啟了一個http的代理伺服器,然後它會轉發所有的http請求和響應。Fiddler 是以代理web 伺服器的形式工作的,它使用代理地址:127.0.0.1,埠:8888。
  • web安全-HTTP截取工具講解(1/3)(連載)
    很多網站為了減少伺服器端的壓力,在後臺方面減少驗證,而在web前端使用JS進行驗證,殊不知這樣增加了很多安全隱患,在滲透測試中會對HTTP請求的攔截進行分析發現一些隱秘的漏洞。intruder:自定義配置的對web應用程式自動化攻擊、枚舉分析程序相應的工具項。repeater:是一個手動操作請求的選項,你可以修改請求內容後重新發送請求到伺服器。sequencer:是一個用來分析不可預知的應用程式會話令牌和重要數據項的隨機性工具。
  • IT挑戰高薪必備網絡常識-如何理解HTTP協議是無狀態的
    1.無連接HTTP的設計者有意利用這種特點將協議設計為請求時建連接、請求完釋放連接,以儘快將資源釋放出來服務其他客戶端。隨著時間的推移,網頁變得越來越複雜,裡面可能嵌入了很多圖片,這時候每次訪問圖片都需要建立一次 TCP 連接就顯得很低效。
  • 軟體測試之TCP、HTTP協議必知必會,面試必備!
    HTTPS協議詳解3.1 HTTP協議概述3.2 HTTP協議之請求報文3.3 HTTP協議之響應報文3.4 HTTP協議 無狀態、無連接特點及解決方案3.5 HTTPS協議詳解1.3 網絡傳輸過程數據在網絡傳輸的過程中,本質是一個封裝和解封裝的過程。例如:用戶使用瀏覽器向百度伺服器發送請求過程中,就是用戶的數據從應用層一路封裝到物理層,伺服器得到物理層的比特流後,一路解封裝得到對應數據。
  • 物聯網應用層協議選擇和分析--MQTT、CoAP 、HTTP、XMPP、SoAP
    從應用方向來分析,主要區別有以下幾點: 1、MQTT協議不支持帶有類型或者其它幫助Clients理解的標籤信息,也就是說所有MQTT Clients必須要知道消息格式。而CoAP協議則相反,因為CoAP內置發現支持和內容協商,這樣便能允許設備相互窺測以找到數據交換的方式。 2、MQTT是長連接而CoAP是無連接。
  • 你還在為 HTTP 的這些概念頭疼嗎?
    因為計算機網絡中是可以有第三者出現的,也就是緩存伺服器,這個指令通過影響請求/響應中的緩存伺服器從而達到控制緩存的目的;不僅有緩存伺服器,還有瀏覽器內部緩存也會影響鏈路的緩存。這個標頭中可以出現許多單獨的指令,其詳細信息可以在 RFC 2616 中找到,即使這是常規標頭,某些指令也只能出現在請求或響應中。
  • HTTP3 為什麼比 HTTP2 靠譜?| 技術頭條
    伴隨著計算機網絡和瀏覽器的誕生,HTTP1.0也隨之而來,處於計算機網絡中的應用層,HTTP是建立在TCP協議之上,所以HTTP協議的瓶頸及其優化技巧都是基於TCP協議本身的特性,例如TCP建立連接的3次握手和斷開連接的4次揮手以及每次建立連接帶來的RTT延遲時間。HTTP/1.x的缺陷連接無法復用:連接無法復用會導致每次請求都經歷三次握手和慢啟動。
  • 通信網絡技術:RPC服務和HTTP服務的區別分析
    通信網絡技術:RPC服務和HTTP服務的區別分析 浮生憶夢 發表於 2020-11-26 13:52:51 很長時間以來都沒有怎麼好好搞清楚 RPC(即 Remote
  • 跨域請求產生錯誤的原因及處理方法
    [❌] https://m.example.com -> 不同域 [❌] https://example.com:3000 -> 埠不同 [❌] http解決方案關於跨域請求的解決方案有很多,例如 JSONP,也就是通過 HTML 中沒有跨域限制的標籤如 img、script 等,再通過指定回調函數,將響應的內容介接回 JavaScript 中;或是通過 iframe,繞過跨域保護獲取目標資源等。下面僅說明兩種常見也相對正規的解決方式。1.
  • 有了HTTP,為什麼還要RPC?
    OSI 網絡七層模型在說 RPC 和 HTTP 的區別之前,我覺的有必要了解一下 OSI 的七層網絡結構模型(雖然實際應用中基本上都是五層)。定義了用於在網絡中進行通信和傳輸數據的接口。 第二層:表示層。定義不同的系統中數據的傳輸格式,編碼和解碼規範等。 第三層:會話層。管理用戶的會話,控制用戶間邏輯連接的建立和中斷。 第四層:傳輸層。管理著網絡中的端到端的數據傳輸。 第五層:網絡層。定義網絡設備間如何傳輸數據。
  • 面試官不講武德,上來就問我Chrome底層原理和HTTP協議
    學習掌握:瀏覽器中的網絡流程,頁面渲染過程,JavaScript執行流程,以及Web安全理論。線程是依附於進程的,而進程中使用多線程並行處理能提升運算效率。線程之間共享進程中的數據。當一個進程關閉後,作業系統會回收進程所佔用的內存。目前的多進程架構瀏覽器Chrome包括,1個瀏覽器主進程,1個GPU進程,1個網絡進程,多個渲染進程和多個插件進程。so,打開一個頁面,為啥有4個進程?