Http協議之Content-Length


前言

http協議是互聯網中最重要的協議之一,雖然看上去很簡單,但是實際中經常遇到問題,我們就已經遇到好幾次了。有長連接相關的,有報文解析相關的。對http協議不能一知半解,必須透徹理解才行。所以就寫了這個系列分享http協議的問題與經驗。

 

問題

我們的手機App在做更新時會從服務器上下載的一些資源,一般都是一些小文件,更新的代碼差不多是下面這樣的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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了。

 

 

原因分析

 

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

 

 


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

服務端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模式的,可以關掉。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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模式關閉。用下面的方法:

1
2
3
4
5
6
7
8
9
#!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協議的解析時一定得注意考慮多種情況。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM