深入理解 web 協議(一)- http 包體傳輸
本文首發於 vivo網際網路技術 微信公眾號
連結:https://mp.weixin.qq.com/s/WlT8070LlrnSODFRDwZsUQ
作者:吳越
開坑這個系列的原因,主要是在大前端學習的過程中遇到了不少跟web協議有關的問題,之前對這一塊的瞭解僅限於用charles抓個包,基本功欠缺。強迫症發作的我決定這一次徹底將web協議搞懂搞透,如果你遇到了和我一樣的問題,例如
-
對http的瞭解,僅限於charles抓個包。
-
對https的瞭解僅限於大概知道tls,ssl,對稱加密,非對稱加密。真正一次完整的互動過程,無法做到心中有數。
-
對h5的各種快取欄位,瞭解的不夠。
-
移動端各種深度弱網優化的文章因為基本功不紮實的原因各種看不懂。
-
有時作為移動端開發在定技術方案的時候,因為對前端或者伺服器缺乏基本的瞭解,無法據理力爭制定出更完美的方案。
-
移動端中的網頁加載出了問題只能求助於前端工程師,無法第一時間自己定位問題。
-
每次想深度學習web協議的時候,因為不會寫服務端程式導致只能泛泛而讀,隨意找幾篇網上的部落格就得過且過了,並沒有真正解決心中的疑惑。沒有實際動手過。
-
沒有實際對照過瀏覽器和Android端使用okhttp在傳送網路請求的時候有什麼不同。
-
實話說現在okhttp的文章百分之99都忽略了真正實現http協議的部分,基本上都是簡要介紹了下okhttp的設計模式和上層的封裝,這其實對移動端工程師理解web協議本身是一個debuff(我也是其中受害者。)
希望這個系列的文章可以幫助到和我一樣對web協議有困惑的工程師們。本系列文章中所有的服務端程式均使用 Go語言開發完成。抓包工具使用的是wireshark,沒有使用charles是因為charles看不到傳輸層的東西,不利於我理解協議的本質。本系列文章沒有複製貼上網上太多概念性的東西,以程式碼和wireshark抓包為主。概念性的東西需要讀者自行搜尋。實戰有助於真正理解協議本身。
本文主要分為四塊,如果覺得文章過長可以自行選擇感興趣的模組閱讀:
-
chrome network面板的使用:主要以一個移動端工程師的視角來看chrome的network模組,主要列舉了我認為可能會對定位h5問題有幫助的幾個知識點。
-
Connection-keeplive:主要闡述了現在客戶端/前端與服務端互動的方式,簡略介紹了服務端大概的樣子。
-
http 隊頭擁塞: 主要以若干個實驗來理解http 隊頭擁塞的本質,並給出okhttp與瀏覽器在策略上的不同。
-
http 包體傳輸:以若干個實驗來理解http 包體傳輸的過程。
一、chrome network面板的使用
開啟商城的頁面,開啟chrome控制檯。
注意看紅色標註部分,左邊disable cache 代表關閉瀏覽器快取,開啟這個選項之後,每次訪問頁面都會重新拉取而不是使用快取,右邊的online可以下拉選單選擇弱網環境下訪問此頁面。在模擬弱網環境的時候此方法通常非常有效。例如我們正常訪問的時候,耗時僅僅2s。
開啟弱網(fast 3g)
時間膨脹到了26s.
這裡說一下這2個選項的作用,Preserve log主要就是儲存前一個頁面的請求日誌,比如我們在當前頁面a點選了一個超連結訪問了頁面b,那麼頁面a的請求在控制檯就看不到了,如果勾選此選項那麼就可以看到這個前面一個頁面的請求。另外這個Hide data Urls,選項額外說明一下,有些h5頁面的某些資源會直接用base64轉碼以後嵌入到程式碼裡面(比如截圖中data: 開頭的東西),這樣做的好處是可以省略一些http請求,當然壞處就是開啟此選項瀏覽器針對這個資源就沒有快取了,且base64轉碼以後的體積大小要比原大小大4/3。我們可以勾選此選項過濾掉這種我們不想看的東西。
再比如,我們只想看看同一個頁面下某一個域名的請求(這在做競品分析時對競品使用域名數量的分析很有幫助),那我們也可以如下操作:
再比如說我們只想看一下這個頁面的post請求,或者是get請求也可以。
再比如我們可以用 is:from-cache 檢視我們當前頁面哪些資源使用了快取。large-than:(單位是位元組)這個也很有用,通常我們利用這個過濾出超出大小的請求,看看有多少超大的圖片資源(移動端排查h5頁面速度慢的一個手段)。
我們也可以點選其中一個請求,按住shift,注意看藍色的就是我們選定的請求,綠色的代表這個請求是藍色請求的上游,也就是說只有當綠色的請求執行完畢以後才會發出藍色的請求,而紅色的請求就代表只有藍色的請求執行完畢以後才會請求。這種看請求上下游關係的方法是很多時候h5優化的一個技巧。將使用者最關心的資源請求前移,可以極大優化使用者體驗,雖然在某種程度上這種行為並不會在資料上有所提高(例如activity之間跳轉用動畫,application啟動優化用特殊theme等等,本質上耗時都沒有減少,但給使用者的感覺就是頁面和app速度很快)。
這個timing 可以顯示一個請求的詳細分段時間,比如排隊時間,發出請求到第一個請求響應的位元組時間,以及整個response都傳輸完畢的時間等等。有興趣的可以自行搜尋下相關資料。
二、關於Connection:Keep-Alive
在現代伺服器架構中,客戶端的長連線大部分時候並不是直接和源伺服器打交道(所謂源伺服器可以粗略理解為服務端開發兄弟實際程式碼部署的那臺伺服器),而是會經過很多代理伺服器,這些代理伺服器有的負責防火牆,有的負責負載均衡,還有的負責對訊息進行路由分發(例如對客戶端的請求根據客戶端的版本號,ios還是Android等等分別將請求對映到不同的節點上)等等。
客戶端的長連線僅僅意味著客戶端發起的這條tcp連線是和第一層代理伺服器保持連線關係。並不會直接命中到原始伺服器。
再看一張圖:
通常來講,我們的請求客戶端發出以後會經過若干個代理伺服器才會到我們的源伺服器。如果我們的源伺服器想基於客戶端的請求的ip地址來做一些操作,理論上就需要額外的http頭部支援了。因為基於上述的架構圖,我們的源伺服器拿到的地址是跟源伺服器建立tcp連線的代理伺服器的地址,壓根拿不到我們真正發起請求的客戶端ip地址。
http RFC規範中,規定了X-Forwarded-For 用於傳遞真正的ip地址。當然了在實際應用中有些代理伺服器並不遵循此規定,例如Nginx就是利用的X-Real-IP 這個頭部來傳遞真正的ip地址(Nginx預設不開啟此配置,需要手動更改配置項)。
在實際生產環境中,我們是可以在http response中將上述經過的代理伺服器資訊一一返回給客戶端的。
看這個reponse的返回裡面的頭部資訊有一個X-via 裡面的資訊就是代理伺服器的資訊了。
再比如說 我們開啟淘寶的首頁,找個請求。
這裡的代理伺服器資訊就更多了,說明這條請求經過了多個代理伺服器的轉發。另外有時我們在技術會議上會聽到正向代理和反向代理,其實這2種代理都是指的代理伺服器,作用都差不多,只不過應用的場景有一些區別。
正向代理:比如我們kexue上網的時候,這種是我們明確知道我們想訪問外網的網站比如facebook、谷歌等等,我們可以主動將請求轉發到一個代理伺服器上,由代理伺服器來轉發請求給facebook,然後facebook將請求返回給代理伺服器,伺服器再轉發給我們。這種就叫正向代理了。
反向代理:這個其實我們每天都在用,我們訪問的伺服器,99%都是反向代理而來的,現代計算機系統中指的伺服器往往都是指的伺服器叢集了,我們在使用一個功能的時候,根本不知道到底要請求到哪一臺伺服器,通常這種情況都是由Nginx來完成,我們訪問一個網站,dns返回給我們的地址,通常都是一臺Nginx的地址,然後由Nginx自己來負責將這個請求轉發給他覺得應該轉發的那臺伺服器。
這裡我們多次提到了Nginx伺服器和代理伺服器的概念,考慮到很多前端開發可能不太瞭解後端開發的工作,暫且在這裡簡單介紹一下。通常而言我們認為的伺服器開發工程師每天大部分的工作都是在應用伺服器上開發,所謂http的應用伺服器就是指可以動態生成內容的http伺服器。比如 java工程師寫完程式碼以後打出包交給Tomcat,Tomcat本身就是一個應用伺服器。再比如Go語言編譯生成好的可執行檔案,也是一個http的應用伺服器,還有Python的simpleServer等等。而Nginx或者Apache更像是一個單純的http server,這個單純的http server 幾乎沒有動態生成http response的能力,他們只能返回靜態的內容,或者做一次轉發,是一個很單純的http server。嚴格意義上說,不管是Tomcat還是Go語言編譯出來的可執行檔案還是Python等等,本質上他們也是http server,也可以拿來做代理伺服器的,只是通常情況下沒有人這麼幹,因為術業有專攻,這種工作通常而言都是交給Nginx來做。
下圖是Nginx的簡要介紹:用一個Master程序來管理n個worker程序,每個worker程序僅有一個執行緒在執行。
在Nginx之前,多數伺服器都是開啟多執行緒或者多程序的工作模式,然而程序或者執行緒的切換是有成本的,如果訪問量過高,那麼cpu就會消耗大量的資源在建立程序或者建立執行緒,還有執行緒和程序之前的切換上,而Nginx則沒有使用類似的方案,而是採用了“程序池單執行緒”的工作模式,Nginx伺服器在啟動的時候會建立好固定數量的程序,然後在之後的執行中不會再額外建立程序,而且可以將這些程序和cpu繫結起來,完美的使用現代cpu中的多核心能力。
此外,web伺服器有io密集型的特點(注意是io密集不是cpu密集),大部分的耗時都在網路開銷而非cpu計算上,所以Nginx使用了io多路複用的技術,Nginx會將到來的http請求一一打散成一個個碎片,將這些碎片安排到單一的執行緒上,這樣只要發現這個執行緒上的某個碎片進入io等待了就立即切換出去處理其他請求,等確定可讀可寫以後再切回來。這樣就可以最大限度的將cpu的能力利用到極致。注意再次強調這裡的切換不是執行緒切換,你可以把他理解為這個執行緒中要執行的程式裡面有很多go to 的錨點,一旦發現某個執行碎片進入了io等待,就馬上利用go to能力跳轉到其他碎片(這裡的碎片就是指的http請求了)上繼續執行。
其實這個地方Nginx的工作模式有一點點類似於Go語言的協程機制,只不過Go語言中的若干個協程下面並不是只有一個執行緒,也可能有多個。但是思路都是一樣的,就是降低執行緒切換的開銷,儘量用少的執行緒來執行業務上的“高併發”需求。
然而Nginx再優秀,也抵不過歲月的侵蝕,說起來距離今天也有15年的時間了。還是有一些天生缺陷的,比如Nginx只要你修改了配置就必須手動將Nginx程序重啟(master程序),如果你的業務非常龐大,一旦遇到要修改配置的情況,幾百臺甚至幾千臺Nginx手動修改配置重啟不但容易出錯而且重複勞動意義也不大。此外Nginx可擴充套件性一般,因為Nginx是c語言寫的,我們都知道c語言其實還是挺難掌握的,尤其是想要掌握的好更加難。不是每個人都有信心用C語言寫出良好可維護的程式碼。尤其你的程式碼還要跑在Nginx這種每天都要用的基礎服務上。
基於上述缺陷,阿里有一個綽號為“春哥”的程式設計師章亦春,在Nginx的基礎上開發了更為優秀的OpenResty開源專案,也是老羅錘子釋出會上說要贊助的那個開源專案。此專案可以對外暴露Lua指令碼的介面,80後玩過魔獸世界的同學一定對Lua語言不陌生,大名鼎鼎的魔獸世界的外掛機制就是用Lua來完成的。OpenResty出現以後終於可以用Lua指令碼語言來操作我們的Nginx伺服器了,這裡Lua也是用“協程”的概念來完成併發能力,與Go語言也是保持一致的。此外OpenResty對伺服器配置的修改也可以及時生效,不需要再重啟伺服器。大大提高運維的效率。等等等等。。。
三、http協議中“隊頭擁塞”的真相
前文我們數次提到了伺服器,高併發等關鍵字。我們印象中的伺服器都是與高併發這3個字強關聯的。那麼所謂 http中的“隊頭擁塞”到底指的是什麼呢?我們先來看一張圖:
這張網際網路中流傳許久的圖,到底應該怎麼理解?有的同學認為http所謂的擁塞是因為傳輸協議是tcp導致的,因為tcp天生有擁塞的缺點。其實這句話並不全對。考慮如下場景:
-
網路情況很好。
-
客戶端先用socket傳送一組資料a,2s以後傳送資料b。
-
服務端收到資料a 然後開始處理資料a,然後收到資料b,開始處理資料b(這裡當然是開執行緒做)
-
此時服務端處理資料b的執行緒將資料b處理完畢以後開始將b的 responseb發到客戶端,過了一段時間以後資料a的執行緒終於把資料處理完畢也將responsea發給客戶端。
-
在傳送responseb的時候,客戶端甚至還可以同時傳送資料c,d,e。
上述的通訊場景就是完美詮釋tcp作為全雙工傳輸的能力了。相當於客戶端和服務端是有2條傳輸通道在工作。所以從這個角度上來看,tcp不是導致http 協議 “隊頭擁塞”的根本原因。因為大家都知道http使用的傳輸層協議是tcp. 只有在網路環境不好的情況下,tcp作為可靠性協議,確實會出現不停重複傳送資料包和等待資料包確認的情況。但是這不是http “隊頭擁塞“”的根本原因。
從這張圖上看,似乎http 1.x 協議是隻有等前面的http request的 response回來以後 後面的http request 才會發出去。但是這個角度上理解的話,伺服器的效率是不是太低了一點?如果是這樣的話怎麼解釋我們每天開啟網頁的速度都很快,開啟app的速度也很快呢?經過一段時間的探索,我發現上述的圖是針對單tcp連線來說的,所謂的http 隊頭擁塞 是指單條tcp連線上 才會發生。而我們與伺服器的一個域名互動的時候往往不止一條tcp連線。比如說Chrome瀏覽器就默認了最大限度可以和一個域名有6條tcp連線,這樣的話,即使有隊頭擁塞的現象,也可以保證我一臺伺服器最多可以同時處理你這個ip發出來的6條http請求了。
為什麼瀏覽器會限制6條?按照這個理論難道不是越多的tcp連線速度就越快嗎?但如果這樣做每個瀏覽器都針對單域名開多條tcp連線來加快訪問速度的話,伺服器的tcp資源很快就會被耗盡,之後就是拒絕訪問了。然而道高一尺魔高一丈,既然瀏覽器限制了單一域名最多隻能使用6條tcp連線,那乾脆我們在一個頁面訪問多個域名不就行了?實際上單一頁面訪問多域名也是前端優化中的一個點,瀏覽器只能限制你單一域名6條tcp連線,但是可沒限制你一個頁面可以有多個域名,我們多開幾個域名不就相當於多開了幾條tcp連線麼?這樣頁面的訪問速度就會大大增加了。
這裡我們有人可能會覺得好奇,瀏覽器限制了單一域名的tcp連線數量,那麼Android中我們每天使用的okhttp限制了嗎?限制了多少?來看下原始碼:
okhttp中預設對單一域名的tcp連線數量限制為5,且對外暴露了設定這個值的方法。但是問題到這裡還沒完,單一tcp連線上,http為什麼要做成前一個訊息的response回來以後,後面的http request才能發出去?這樣的設計是不是有問題?速度太慢了?還是說我們理解錯了?是不是還有一種可能是:
-
http訊息可以在單一的tcp連線上 不停的傳送,不需要等待前面一個http訊息的返回以後再發送。
-
伺服器接收了http訊息以後先去處理這些訊息,訊息處理完畢準備發response的時候 再判斷一下,一定等到前面到達的request的response先發出去以後 再發,就好像一個先進先出的佇列那樣。這樣似乎也可以符合“隊頭擁塞”的設計?
帶著這個疑問,我做了一組實驗,首先我們寫一段服務端的程式碼,提供fast和slow2個介面,其中slow介面 延遲10秒返回訊息,fast介面延遲5秒返回訊息。
package main
import (
"io"
"net/http"
"os"
"time"
"github.com/labstack/echo"
)
func main() {
e := echo.New()
e.GET("/slow", slowRes)
e.GET("/fast", fastRes)
e.Logger.Fatal(e.Start(":1329"))
}
func fastRes(c echo.Context) error {
println("get fast request!!!!!")
time.Sleep(time.Duration(5) * time.Second)
return c.String(http.StatusOK, "fast reponse")
}
func slowRes(c echo.Context) error {
println("get slow request!!!!!")
time.Sleep(time.Duration(10) * time.Second)
return c.String(http.StatusOK, "slow reponse")
}
然後我們將這個伺服器程式部署在雲上,另外再寫一段Android程式,我們讓這個程式發http請求的時候單一域名只能使用一條tcp連線,並且設定超時時間為20s(否則預設的okhttp響應超時時間太短 等不到伺服器的返回就斷開連線了):
dispatcher = new Dispatcher();
dispatcher.setMaxRequestsPerHost(1);
client = new OkHttpClient.Builder().connectTimeout(20, TimeUnit.SECONDS).readTimeout(20, TimeUnit.SECONDS).dispatcher(dispatcher).build();
new Thread() {
@Override
public void run() {
Request request = new Request.Builder().get().url("http://www.dailyreport.ltd:1329/slow").build();
Call call = client.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.v("wuyue","slow e=="+e.getMessage());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.v("wuyue", "slow reponse==" + response.body().string());
}
});
}
}.start();
new Thread() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Request request = new Request.Builder().get().url("http://www.dailyreport.ltd:1329/fast").build();
Call call = client.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.v("wuyue","fast e=="+e.getMessage());
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.v("wuyue", "fast reponse==" + response.body().string());
}
});
}
}.start();
這裡要注意一定要使用enqueue也就是非同步的方法來發送http請求,否則你設定的域名tcp連線數量限制是失效的。然後我們用wireshark來抓包看看:
這裡可以清晰的看出來,首先這2個http request 都是使用的同一條tcp連線, 都是源埠號60465到伺服器1329. 然後看下time的時間,差不多0s開始傳送了slow的請求,10s左右收到了slow的http response,然後馬上 fast這個介面的request 就發出去了,過了5秒 fast的http response 也返回了。
如果將這個域名tcp數量限制為1改成5 那麼再次抓包執行可以看到:
這個時候就可以清晰的看到,這一次fast大約在slow介面2秒以後就發出去了,並沒有等待slow回來以後再發,且注意看這2條http訊息使用的源埠號是不同的,一個是64683,一個是64684。也就是說這裡使用了不同的tcp連線來傳輸我們的http訊息。
綜上所述,我們可以對http 1.x 中的“隊頭擁塞” 來下結論了:
-
所謂http1.x中的“隊頭擁塞”,除了本身傳輸層協議tcp的原因導致的tcp包擁塞機制以外,更多的是指的http 應用層上的限制。這種限制具體表現在,對於http協議來說,單條tcp連線上客戶端要保證前面一條的request的response返回以後,才能傳送後續的request。
-
http 1.x 中的 “隊頭擁塞” 本質上來說是由http的客戶端來保證實現的。
-
如果你訪問的網頁裡面的請求都指向著同一個域名,那麼不管伺服器有多麼高的併發能力,他也最多隻能同時處理你的6條http請求,因為大多數瀏覽器限制了針對單一域名只能開6條tcp連線。想翻過這個限制提高頁面載入速度只能依靠開啟多域名來實現。
-
雖然okhttp中對外暴露了這個單域名下的tcp連線數的設定,但是也無法通過將這個值調的特別高來增加你應用的請求響應速度,因為大多數伺服器都會限制單一ip的tcp連線數,比如Nginx的預設設定就是10。所以你客戶端單一將這個數值調的特別大也沒用,除非你和伺服器約定好。但是這樣還不如使用多域名方便了。
經過上面的分析,我們得知其實http 1.x協議並沒有完全發揮tcp 全雙工通道的潛能,(也有可能是http協議出現的太早當時的設計者沒有考慮現在的場景)所以從1.1協議開始,又有了一個Pipelining 也就是管道的約定。這個約定可以讓http的客戶端不用等前面一個request的response回來就可以繼續發後面的request。但是各種原因下,現代瀏覽器都沒有開啟這個功能(相關資料感興趣的可以自行查詢Pipelining關鍵字,這裡就不復制貼上了)。我帶著好奇搜尋了一下okhttp的程式碼,想看看他們有沒有類似的實現。最終我們在這個類中找到了線索:
看樣子貌似這個tunnel的命名和我們http1.1中所謂的pipelining好似一個意思?那麼okhttp中是可以使用這個瀏覽器預設關閉的技術了嗎?繼續看程式碼:
我們看到這個值使用的地方是來自於connectTunnel這個方法,我們看看這個方法是在connect方法裡呼叫的:
我們看下這個方法的實現:
/**
* Returns true if this route tunnels HTTPS through an HTTP proxy. See <a
* href="http://www.ietf.org/rfc/rfc2817.txt">RFC 2817, Section 5.2</a>.
*/
public boolean requiresTunnel() {
return address.sslSocketFactory != null && proxy.type() == Proxy.Type.HTTP;
}
從註釋和rfc文件中可以看出來,要開啟這個所謂的tunnel的功能,需要你的目標地址是https的,講白了是tls來做報文的傳輸,此外還需要一個http代理伺服器。同時滿足這2個條件以後才會觸發這部分程式碼。這部分由於涉及到tls協議的相關知識,我們將這一塊的內容放到後續的第三個章節中再來解釋。這裡大家只需要大概清楚tunnel主要用來直接轉發傳輸層的tcp報文到目標伺服器,而不需要經過http的代理伺服器額外進行應用層報文的轉發即可。
四、http包體傳輸的本質
比如說Referer(我在谷歌中搜索github,然後點選github的連結,然後看請求資訊)
這個欄位通常通常被利用做防盜鏈,頁面來源統計分析,快取優化等等。但是要注意的是,這個Referer欄位瀏覽器在自動幫我們新增的時候有一個策略:要麼來源是http 目標也是http,要麼來源是https 目標也是https,一旦出現來源是http目標是https或者反著來的情況,瀏覽器就不會幫我們新增這個欄位了。
此外,在http包體傳輸的時候,定長包體與不定長包體使用的單位是不一樣的。
比如Content-Length這個欄位後面的單位就是10進位制。傳輸的就是這個“Hello, World!”。但是對於Chunk非定長包體來說 這個單位卻是16進位制的,且對於Chunk傳輸方式來說,有一些response的header是等待body傳輸完畢以後才繼續傳的。我們來簡單寫個server端的例子,返回一個叫hellowuyue的response,但是使用chunk的傳輸方式。這裡我簡單使用Go語言來完成對應的程式碼。
package main
import (
"net/http"
"github.com/labstack/echo"
)
func main() {
e := echo.New()
//採用chunk傳輸 不使用預設的定長包體
e.GET("/", func(c echo.Context) error {
c.Response().WriteHeader(http.StatusOK)
c.Response().Write([]byte("hello"))
c.Response().Flush()
c.Response().Write([]byte("wuyue"))
c.Response().Flush()
return nil
})
e.Logger.Fatal(e.Start(":1323"))
}
我們在瀏覽器訪問一下,看看network的展示資訊:
然後我們用wireshark 詳細的看一下chunk的傳輸機制:這裡要注意的是,我沒有選擇將我們服務端的程式碼部署在外網伺服器上,只是簡單的在本地,所以我們要選擇環回地址,不要選擇本地連線。同時監聽1323埠.並且做 port 1323的過濾器。
然後我們來看下wireshark完整的還原過程:
可以看一下這個chunk的結構,每一個chunk的結束都會伴隨著一個0d0a的16進位制,這個我們可以把他理解成就是/r/n 也就是crlf換行符。然後看一下 當chunk全部結束以後 還會有一個end chunked 這裡面 也是包含了一個0d0a 。(這裡篇幅所限就不放ABNF正規化對chunk使用的規範了。有興趣的同學可以自行對照ABNF的規範語法和wireshark實際抓包的內容進行對比加深理解)
最後我們看一下,瀏覽器和服務端在利用form表單上傳檔案時的互動過程以及okhttp完成類似功能時候的異同,加深對包體傳輸的理解。首先我們定義一個非常簡單的html,提供一個表單。
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>上傳檔案</title>
</head>
<body>
<h1>上傳檔案</h1>
<form action="/uploadresult" method="post" enctype="multipart/form-data">
Name: <input type="text" name="name"><br>
Files: <input type="file" name="file"><br><br>
Files2: <input type="file" name="file2"><br><br>
<input type="submit" value="Submit">
</form>
</body>
</html>
然後定義一下我們的服務端:
package main
import (
"io"
"net/http"
"os"
"time"
"github.com/labstack/echo"
)
func main() {
e := echo.New()
//直接返回一個預先定義好的html
e.GET("/uploadtest", func(c echo.Context) error {
return c.File("html/upload.html")
})
//html裡預先定義好點選上傳以後就跳轉到這個uri
e.POST("/uploadresult", getFile)
e.Logger.Fatal(e.Start(":1329"))
}
func getFile(c echo.Context) error {
name := c.FormValue("name")
println("name==" + name)
file, _ := c.FormFile("file")
file2, _ := c.FormFile("file2")
src, _ := file.Open()
src2, _ := file2.Open()
dst, _ := os.Create(file.Filename)
dst2, _ := os.Create(file2.Filename)
io.Copy(dst, src)
io.Copy(dst2, src2)
return c.String(http.StatusOK, "上傳成功")
}
然後我們訪問這個表單,上傳一下檔案以後用wireshark抓個包來體會一下瀏覽器在背後幫我們做的事情。
關於這個Content-Disposition有興趣的可以自行搜尋其含義。
最後我們用okhttp來完成這個操作,看看okhttp做這個操作的時候,wireshark顯示的結果又是什麼樣子:
//注意看 contentType 是需要你手動去設定的,我們這裡故意將這個contentType值寫錯 看看能不能上傳檔案成功
RequestBody requestBody1 = RequestBody.create(MediaType.parse("image/gifccc"), new File("/mnt/sdcard/ccc.txt"));
RequestBody requestBody2 = RequestBody.create(MediaType.parse("text/plain111"), new File("/mnt/sdcard/Gif.gif"));
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM).addFormDataPart("file2", "Gif.gif", requestBody1)
.addFormDataPart("file", "ccc.txt", requestBody2)
.addFormDataPart("name","吳越")
.build();
Request request = new Request.Builder().get().url("http://47.100.237.180:1329/uploadresult").post(requestBody).build();
五、總結
本章節初步介紹瞭如何使用chrome的network面板和wireshark抓包工具進行http協議的分析,重點介紹了http1.x協議中的“隊頭擁塞”的概念,以及該問題的應對方式和瀏覽器的限制策略。在後續的第二個章節中,將會詳細介紹http協議中快取,dns以及websocket的相關知識。在第三個章節中,將會詳細分析http2以及tls協議的每一個細節。
更多內容敬請關注 vivo 網際網路技術 微信公眾號
注:轉載文章請先與微訊號:labs2020 聯