Http協議之Content-Length

前言html

http協議是互聯網中最重要的協議之一,雖然看上去很簡單,可是實際中常常遇到問題,咱們就已經遇到好幾回了。有長鏈接相關的,有報文解析相關的。對http協議不能只知其一;不知其二,必須透徹理解才行。因此就寫了這個系列分享http協議的問題與經驗。java


問題nginx

咱們的手機App在作更新時會從服務器上下載的一些資源,通常都是一些小文件,更新的代碼差很少是下面這樣的:web

static void update() throws IOException {
    URL url = new URL("http://172.16.59.129:8000/update/test.so");
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    if(conn.getResponseCode() == 200) {
        int totalLength = conn.getContentLength();
	BufferedInputStream in = new BufferedInputStream(conn.getInputStream());
	byte[] buffer = new byte[512];
	int readLength = 0;
	int length = 0;
	while((length=in.read(buffer)) != -1) {
		readLength += length;
		//進度條
		System.out.println(((float)readLength) /((float)(totalLength)));
	}
    }
}


好比上面的代碼更新一個so文件,先經過content-length獲取文件的總大小,而後讀Stream,每讀一段,就計算出當前讀的總大小,除以content-length,用來顯示進度條。瀏覽器


結果weblogic從10升級到12後,content-length一直返回-1,這樣就不能顯示進度條了,可是文件流還能正常讀。把weblogic重啓了,一開始還能返回content-length,一會又是-1了。bash



緣由分析服務器


Http協議的請求報文和回覆報文都有header和body,body就是你要獲取的資源,例如一個html頁面,一個jpeg圖片,而header是用來作某些約定的。例如客戶端與服務端商定一些傳輸格式,客戶端先獲取頭部,得知一些格式信息,而後纔開始讀取body。tcp



客戶端: Accept-Encoding:gzip (給我壓縮一下,我用的是流量,先下載下來我再慢慢解壓吧)
ide

服務端1:Content-Encoding:null(沒有Content-Encoding頭。 我不給壓縮,CPU沒空,你愛要不要)測試

服務端2:Content-Encoding:gzip (給你節省流量,壓縮一下)




客戶端:Connection: keep-alive (大哥,咱好不容易建了個TCP鏈接,下次接着用)

服務端1: Connection: keep-alive (都不容易,接着用)

服務端2: Connection: close (誰跟你接着用,咱們這個TCP是一次性的,下次再找我還得從新連)




http協議沒有三次握手,通常客戶端向服務端請求資源時,以服務端爲準。還有一些header並無協商的過程,而是服務端直接告訴客戶端按什麼來。例如上述的Content-Length,是服務端告訴客戶端body的大小有多大。可是!服務端並不必定能準確的提早告訴你body有多大。服務端要先寫header,再寫body,若是要在header裏把body大小寫進去,就得提早知道body大小。若是這個body是動態生成的,服務端先生成完,再開始寫header,這樣須要不少額外的開銷,因此header裏不必定有content-length。


那客戶端怎麼知道body的大小呢?服務器有三種方式告訴你。


1. 服務器已經知道資源大小,經過content-length這個header告訴你。


Content-Length:1076(body的大小是1076B,你讀取1076B就能夠完成任務了)

Transfer-Encoding: null


2. 服務器無法提早知道資源的大小,或者不肯意花費資源提早計算資源大小,就會把http回覆報文中加一個header叫Transfer-Encoding:chunked,就是分塊傳輸的意思。每一塊都使用固定的格式,前邊是塊的大小,後面是數據,而後最後一塊大小是0。這樣客戶端解析的時候就須要注意去掉一些無用的字段。


Content-Length:null

Transfer-Encoding:chunked (接下來的body我要一塊一塊的傳,每一塊開始是這一塊的大小,等我傳到大小爲0的塊時,就沒了)


3. 服務器不知道資源的大小,同時也不支持chunked的傳輸模式,那麼就既沒有content-length頭,也沒有transfer-encoding頭,這種狀況下必須使用短鏈接,以鏈接結束來標示數據傳輸結束,傳輸結束就能知道大小了。這時候服務器返回的header裏Connection必定是close。


Content-Length:null

Transfer-Encoding:null

Connection:close(我不知道大小,我也用不了chunked,啥時候我關了tcp鏈接,就說明傳輸結束了)



實驗


我經過nginx在虛擬機裏作實驗,默認nginx是支持chunked模式的,能夠關掉。

使用的代碼以下,可能會調整參數。

static void update() throws IOException {
    URL url = new URL("http://172.16.59.129:8000/update/test.so");
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    //conn.setRequestProperty("Accept-Encoding", "gzip");
    //conn.setRequestProperty("Connection", "keep-alive");
    conn.connect();
    if(conn.getResponseCode() == 200) {
        System.out.println(conn.getHeaderFields().keySet());
        System.out.println(conn.getHeaderField("transfer-encoding"));
        System.out.println(conn.getHeaderField("Content-Length"));
        System.out.println(conn.getHeaderField("Content-Encoding"));
        System.out.println(conn.getHeaderField("Connection"));
    }
}


1. nginx在開啓chunked_transfer_encoding的時候


(1) 在reqeust header裏不使用gzip,也就是不加accept-encoding:gzip


test.so文件大小

結果

100B

能正常返回content-length,沒有transfer-encoding頭

69M

能正常返回content-length,沒有transfer-encoding頭

3072M

能正常返回content-length,沒有transfer-encoding頭



能夠發現nginx無論資源多大,若是客戶端不接受gzip的壓縮格式,就不會使用chunked模式,並且跟是否使用短鏈接不要緊。


(2)在request header里加入gzip,accepting-encoding:gzip


test.so文件大小

結果

100B

沒有content-length,transfer-encoding=trunked

69M

沒有content-length,transfer-encoding=trunked

3072M

沒有content-length,transfer-encoding=trunked


能夠看到nginx在開啓chunked_transfer_encoding,而且客戶端接受gzip的時候,會使用chunked模式,nginx開啓gzip後不會計算資源的大小,直接用chunked模式。



2.nginx關閉chunked_transfer_encoding


(1) 在reqeust header裏不使用gzip,也就是不加accept-encoding:gzip

test.so文件大小

結果

100B

能正常返回content-length,沒有transfer-encoding頭

69M

能正常返回content-length,沒有transfer-encoding頭

3072M

能正常返回content-length,沒有transfer-encoding頭


由於能很容易的知道文件大小,因此nginx仍是能返回content-length。



(2)在request header里加入gzip,accepting-encoding:gzip

test.so文件大小

結果

100B

沒有content-length和transfer-encoding頭,不論客戶端connection爲keep-alive仍是close,服務端返回的connection頭都是close

69M

沒有content-length和transfer-encoding頭,不論客戶端connection爲keep-alive仍是close,服務端返回的connection頭都是close

3072M

沒有content-length和transfer-encoding頭,不論客戶端connection爲keep-alive仍是close,服務端返回的connection頭都是close


這就是上面說的第三種狀況,不知道大小,也不支持trunked,那就必須使用短鏈接來標示結束。



問題解決方案


諮詢了中間件組的同事,之前也遇到相似的問題,由於升級了Weblogic致使客戶端解析XML出錯,由於使用了chunked模式,中間有一些格式化的字符,而客戶端解析的代碼並無考慮chunked模式的解析,致使解析出錯。

由於咱們客戶端必須用content-length展現進度,所以不能用chunked模式,Weblogic能夠把chunked模式關閉。用下面的方法:

#!java weblogic.WLST 
connect('username’,'password', 't3://localhost:7001')
edit()
startEdit()
cd("Servers/AdminServer/WebServer/AdminServer")
cmo.setChunkedTransferDisabled(true)
save()
activate()
exit()


改了以後,確實不返回chunked了,可是也沒有content-length,由於Weblogic就是不提早獲取文件大小,而是強制加了connection:close,也就是前邊說的第三種,經過鏈接結束標識數據結束。因爲生產上咱們用了Apache,測試環境爲了方便就直接用的Weblogic,因此只能在測試環境再加個Apache了。


總結

一個好的http客戶端,必須充分實現協議,否則就可能出問題,瀏覽器對於服務端可能產生的各類狀況都很好的作了處理,可是本身實現http協議的解析時必定得注意考慮多種狀況。

相關文章
相關標籤/搜索