Android HTTP必知必会
HTTP協(xié)議使用如此廣泛,開(kāi)發(fā)者務(wù)必要做到“知”,“會(huì)”。
引子
用curl請(qǐng)求百度首頁(yè)全解析的過(guò)程:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | @feng ? jayfeng.com (master) ? curl -v http://www.baidu.com > ~/http_get.txt * Rebuilt URL to: http://www.baidu.com/ % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying 119.75.217.109... * Connected to www.baidu.com (119.75.217.109) port 80 (#0) > GET / HTTP/1.1 > Host: www.baidu.com > User-Agent: curl/7.43.0 > Accept: */* > < HTTP/1.1 200 OK < Date: Thu, 28 Jan 2016 14:53:51 GMT < Content-Type: text/html; charset=utf-8 < Transfer-Encoding: chunked < Connection: Keep-Alive < Vary: Accept-Encoding < Set-Cookie: BAIDUID=D75C20ED3D7551221E1C32F79C698867:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com < Set-Cookie: BIDUPSID=D75C20ED3D7551221E1C32F79C698867; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com < Set-Cookie: PSTM=1453992831; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com < Set-Cookie: BDSVRTM=0; path=/ < Set-Cookie: BD_HOME=0; path=/ < Set-Cookie: H_PS_PSSID=18880_1458_18879_12824_18205_18777_17000_17072_15544_11476_10634; path=/; domain=.baidu.com < P3P: CP=" OTI DSP COR IVA OUR IND COM " < Cache-Control: private < Cxy_all: baidu+1257c154f891fc3a17374fed141622bd < Expires: Thu, 28 Jan 2016 14:53:00 GMT < X-Powered-By: HPHP < Server: BWS/1.1 < X-UA-Compatible: IE=Edge,chrome=1 < BDPAGETYPE: 1 < BDQID: 0xe5ff10b4000dd380 < BDUSERID: 0 < { [2880 bytes data] 100 98345 0 98345 0 0 665k 0 --:--:-- --:--:-- --:--:-- 671k * Connection #0 to host www.baidu.com left intact |
示意圖
把上面的過(guò)程畫(huà)成示意圖如下:
但是那些代碼到底是什么意思呢?
聽(tīng)我慢慢說(shuō)來(lái)。
結(jié)構(gòu)
說(shuō)起來(lái)http的結(jié)構(gòu)確實(shí)是簡(jiǎn)單,從上面的示意圖大概也能看出來(lái),包括三部分(請(qǐng)求和響應(yīng)用/區(qū)分):
| 1 2 3 4 5 6 7 8 9 10 | - - - - - - - - - - - - - - - - - - - - - - - - - - | Request Line / Response Line | - - - - - - - - - - - - - - - - - - - - - - - - - - | ... | | Request Header / Response Header | | ... | - - - - - - - - - - - - - - - - - - - - - - - - - - | Optional Request Body / Optional Response Body | | ... | - - - - - - - - - - - - - - - - - - - - - - - - - - |
1. 請(qǐng)求行/狀態(tài)行
以上面百度為例子,請(qǐng)求行是:
| 1 2 | // 包括了基本的請(qǐng)求方法: GET,請(qǐng)求資源路徑: /, HTTP協(xié)議版本: HTTP/1.1 > GET / HTTP/1.1 |
狀態(tài)行是:
| 1 2 | // 包括服務(wù)器響應(yīng)的HTTP協(xié)議版本: HTTP/1.1, 響應(yīng)狀態(tài)碼: 200, 狀態(tài)碼描述: OK < HTTP/1.1 200 OK |
2. 首部
首部可分為請(qǐng)求首部,響應(yīng)首部, 實(shí)體首部,非正式首部,但是這些首部會(huì)有一些相同名稱(chēng)的首部,我們把它們定位為通用首部。
請(qǐng)求首部:
| 1 | > User-Agent: curl/7.43.0 |
響應(yīng)首部:
| 1 | < Connection: Keep-Alive |
實(shí)體首部:
| 1 | Content-Type: text/html; charset=utf-8 |
非正式首部:
| 1 | Set-Cookie: BDSVRTM=0; path=/ |
更多首部,下一節(jié)會(huì)專(zhuān)門(mén)詳解。
3. 實(shí)體內(nèi)容
對(duì)于請(qǐng)求消息,如果是POST請(qǐng)求,可以設(shè)置請(qǐng)求內(nèi)容:傳參,甚至上傳文件。
對(duì)于響應(yīng)消息,返回的主體內(nèi)容,就是響應(yīng)內(nèi)容:網(wǎng)頁(yè),圖片等資源都是。
首部字段概覽
從HTTP的結(jié)構(gòu)來(lái)看,HTTP的重頭戲當(dāng)屬那些預(yù)定義的首部了。
| 通用首部字段 | 請(qǐng)求報(bào)文和響應(yīng)報(bào)文兩方都會(huì)使用的首部 |
| CacheControl | 控制緩存的行 |
| Connection | 允許客戶(hù)端和服務(wù)器指定與請(qǐng)求/響應(yīng)連接有關(guān)的選項(xiàng) |
| Date | 報(bào)文創(chuàng)建時(shí)間 |
| Progma | 報(bào)文指令 |
| Trailer | 報(bào)文末端的首部一覽 |
| Transfer-Encoding | 指定報(bào)文主體的傳輸編碼方式 |
| Upgrade | 升級(jí)為其它協(xié)議 |
| Via | 代理服務(wù)器的相關(guān)信息 |
| Warning | 錯(cuò)誤通知 |
| 請(qǐng)求首部字段 | 從客戶(hù)端向服務(wù)器端發(fā)送請(qǐng)求報(bào)文時(shí)使用的首部。補(bǔ)充了請(qǐng)求的附加內(nèi)容、客戶(hù)端信息、響應(yīng)內(nèi)容相關(guān)優(yōu)先級(jí)等信息 |
| Accept | 用戶(hù)代理可處理的媒體類(lèi)型 |
| Accept-Charset | 優(yōu)先的字符集 |
| Content-Encoding | 優(yōu)先的內(nèi)容編碼 |
| Connectionntent-Language | 優(yōu)先的語(yǔ)言 |
| Authorization | Web認(rèn)證信息 |
| Expect | 期待服務(wù)器的特定行為 |
| From | 用戶(hù)的電子郵箱地址 |
| Host | 請(qǐng)求資源所在服務(wù)器 |
| If-Match | 比較實(shí)體標(biāo)記(ETag) |
| If-Modified-Since | 比較資源的更新時(shí)間 |
| If-None-Match | 比較實(shí)體標(biāo)記較實(shí)體標(biāo)記(與If-Match相反) |
| If-Range | 資源未更新時(shí)發(fā)送實(shí)體Byte的范圍請(qǐng)求 |
| If-Unmodified-Since | 比較資源的更新??間(與If-Modified-Since相反) |
| Max-Forwards | 最大傳輸逐跳數(shù) |
| Proxy-Authorization | 代理服務(wù)器要求客戶(hù)端的認(rèn)證信息 |
| Range | 實(shí)體的字節(jié)范圍請(qǐng)求 |
| Referer | 對(duì)請(qǐng)求中URI的原始獲取方 |
| TE | 傳輸編碼的優(yōu)先級(jí) |
| User-Agent | HTTP客戶(hù)端程序的信息 |
| 響應(yīng)首部字段 | 從服務(wù)器端向客戶(hù)端返回響應(yīng)報(bào)文時(shí)使用的首部。補(bǔ)充了響應(yīng)的附加內(nèi)容,也會(huì)要求客戶(hù)端附加額外的內(nèi)容信息 |
| Accept-Ranges | 是否接受字節(jié)范圍請(qǐng)求 |
| Agente | 推算資源創(chuàng)建經(jīng)過(guò)時(shí)間 |
| Etag | 資源的匹配信息 |
| Location | 令客戶(hù)端重定向至指定URI |
| Proxy-Authenticate | 代理服務(wù)器對(duì)客戶(hù)端的認(rèn)證信息 |
| Retry-After | 對(duì)再次發(fā)起請(qǐng)求的時(shí)機(jī)要求 |
| Server | HTTP服務(wù)器的安裝信息 |
| Vary | 代理服務(wù)器緩存的管理信息 |
| WWW-Authenticate | 服務(wù)器對(duì)客戶(hù)端的認(rèn)證信息 |
| 實(shí)體首部字段 | 針對(duì)請(qǐng)求報(bào)文和響應(yīng)報(bào)文的實(shí)體部分使用的首部。補(bǔ)充了資源內(nèi)容更新時(shí)間等與實(shí)體相關(guān)的信息 |
| Allow | 資源可支持的HTTP方法 |
| Content-Encoding | 實(shí)體主體適用的編碼方式 |
| Content-Language | 實(shí)體主體的自然語(yǔ)言 |
| Content-Length | 實(shí)體主體的大小 |
| Content-Location | 替代對(duì)應(yīng)資源的URI |
| Content-MD5 | 實(shí)體主體的報(bào)文摘要 |
| Content-Rangesge | 實(shí)體主體的位置范圍 |
| Content-Type | 實(shí)體主體的媒體類(lèi)型 |
| Expires | 實(shí)體主體過(guò)期的日期時(shí)間 |
| Last-Modified | 資源的最后修改日期時(shí)間 |
對(duì)一些常用字段深入了解是很有必要,這里不做詳述,有些字段單獨(dú)拿出來(lái)就能另外再寫(xiě)一篇文章了,請(qǐng)參考文末附錄。
常見(jiàn)狀態(tài)碼
HTTP狀態(tài)碼標(biāo)明客戶(hù)端HTTP請(qǐng)求的返回結(jié)果,結(jié)果是否正確,應(yīng)該怎么處理等信息。
| 200 | OK |
| 301 | Moved Permanently |
| 302 | Found |
| 304 | Not Modified |
| 307 | Temporary Redirect |
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 410 | Gone |
| 500 | Internal Server Error |
| 501 | Not Implemented |
值得注意的幾個(gè)熱點(diǎn)
1. 持久連接
在這個(gè)無(wú)網(wǎng)不沖浪,推送滿(mǎn)天飛的年代,理解持久連接的概念非常重要。
引用wiki的解釋:
HTTP持久連接(HTTP persistent connection,也稱(chēng)作HTTP keep-alive或HTTP connection reuse)是使用同一個(gè)TCP連接來(lái)發(fā)送和接收多個(gè)HTTP請(qǐng)求/應(yīng)答,而不是為每一個(gè)新的請(qǐng)求/應(yīng)答打開(kāi)新的連接的方法。
可以說(shuō),http1.1相對(duì)于http1.0的一個(gè)最大的改進(jìn)就是默認(rèn)支持http持久連接了。
在android客戶(hù)端中如果要關(guān)閉持久連接(以google http client為例)
| 1 | request.getHeaders().set("Connection", "close"); |
另外,關(guān)于持久連接造成EOFException的問(wèn)題,我一直沒(méi)用找到可靠的解決方案,okhttp的issues下關(guān)于這個(gè)討論也是很熱鬧:
EOFException in RealBufferedSource.readUtf8LineStrict
EOFException in RealBufferedSource.readUtf8LineStrict(): 0-bytes in stream
EOFException in RealBufferedSource.readUtf8LineStrict(): corrupt stream
但是,像xutils3這樣的修復(fù)方案是真的對(duì)嗎?
嘗試修復(fù)Android4.4之前HttpUrlConnection偶發(fā)的EOFException問(wèn)題
直接把4.4之前的長(zhǎng)連接給關(guān)閉了,雖然干凈了,但是是否會(huì)對(duì)性能造成影響?這個(gè)問(wèn)題的解法是否要聯(lián)調(diào)一下服務(wù)器的keepalive_timeout?如果真的和keepalive_timeout,keepalive_timeout設(shè)置應(yīng)該設(shè)置多少(這個(gè)值不能設(shè)置太大,否則可能會(huì)把服務(wù)器搞掛)?
請(qǐng)高手賜教。
2. 斷點(diǎn)續(xù)傳
斷點(diǎn)續(xù)傳的原理其實(shí)非常簡(jiǎn)單,就是利用HTTP的請(qǐng)求首部中的Range字段。
第一步,計(jì)算本地文件大小。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | FileInputStream fis = null; try { // 讀取本地文件 fis = new FileInputStream(dest); // currentSize就是本地文件大小 currentSize = fis.available(); } catch (IOException e) { throw e; } finally { if (fis != null) { fis.close(); } } |
第二步,設(shè)置Range值,明確告知服務(wù)器從哪里接著下載。
| 1 2 3 4 5 6 7 | HttpURLConnection conn; ... // 如果本地文件存在,設(shè)置RANGE為"bytes=currentSize-", -后面不寫(xiě)具體值,表示接著下載到文件結(jié)尾 if (currentSize > 0) { conn.setRequestProperty("RANGE", "bytes=" + currentSize + "-"); } ... |
完整代碼請(qǐng)參考:http之download方法
PS: 這里只是說(shuō)明原理,如果是可變文件(比如圖片資源一般定義為不變文件),還要考慮文件校驗(yàn)。
3. 上傳文件
對(duì)上傳文件的理解程度某個(gè)意義上就代表了你對(duì)HTTP結(jié)構(gòu)的理解程度。
第一步,為了后續(xù)代碼可讀性,先定義幾個(gè)常量。
| 1 2 3 4 | String BOUNDARY = "--------------" + UUID.randomUUID().toString(); String PREFIX = "--", String LINEND = "\r\n"; String MULTIPART_FROM_DATA = "multipart/form-data"; |
第二步,定義Content-Type。
Content-Type為”multipart/form-data”,因?yàn)橛形募荒芤远M(jìn)制的形式傳輸。同時(shí)定義內(nèi)容分隔符。
| 1 2 | // ${bound} 是一個(gè)占位符, 為了表示唯一,可以用一些特殊的隨機(jī)組合,比如---------------4365423423423423 Content-Type: multipart/form-data; boundary=${bound} |
第三步,傳參數(shù)(可選)。
傳文件并不是說(shuō)就不能再傳參數(shù)了。
| 1 2 3 4 5 6 7 8 9 10 11 | for (Map.Entry<String, String> entry : params.entrySet()) { sb.append(PREFIX); sb.append(BOUNDARY); sb.append(LINEND); sb.append("Content-Disposition: form-data; name=\"" + entry.getKey() + "\"" + LINEND); sb.append("Content-Type: text/plain; charset=GBK" + LINEND); sb.append("Content-Transfer-Encoding: 8bit" + LINEND); sb.append(LINEND); sb.append(entry.getValue()); sb.append(LINEND); } |
第四步,傳文件。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | for (Map.Entry<String, File> file : files.entrySet()) { StringBuilder sb1 = new StringBuilder(); sb1.append(PREFIX); sb1.append(BOUNDARY); sb1.append(LINEND); // 添加文件描述 sb1.append("Content-Disposition: form-data; name=\"uploadfile\"; filename=\"" + file.getValue().getName() + "\"" + LINEND); sb1.append("Content-Type: application/octet-stream; charset=GBK" + LINEND); sb1.append(LINEND); os.write(sb1.toString().getBytes()); is = new FileInputStream(file.getValue()); byte[] buffer = new byte[1024]; int len = 0; while ((len = is.read(buffer)) != -1) { os.write(buffer, 0, len); } is.close(); os.write(LINEND.getBytes()); } |
第五步,末尾邊界。
特別寫(xiě)出這一步是為了強(qiáng)調(diào),請(qǐng)務(wù)必注意各個(gè)段落的分割。
| 1 2 | byte[] end_data = (PREFIX + BOUNDARY + PREFIX + LINEND).getBytes(); os.write(end_data); |
可以看的出來(lái),所謂上傳文件,就是以二進(jìn)制的形式把這些參數(shù),文件等數(shù)據(jù)以一定邊界區(qū)分并拼裝在一起發(fā)送給服務(wù)器。
完整代碼請(qǐng)參考:http之upload方法
關(guān)于上傳如果想了解更多,可以學(xué)習(xí)一下lite http的部分源碼:lite http之content
4. Last Modified和ETag
通過(guò)Last Modified作為服務(wù)器文件的時(shí)間戳,來(lái)判斷服務(wù)器文件是否有更新。
| 1 2 3 4 5 6 7 8 9 10 11 | public static long getLastModified(URL url) throws IOException { HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(TIME_OUT); connection.setReadTimeout(TIME_OUT); long lastModified = connection.getLastModified(); connection.disconnect(); return lastModified; } |
ETag,其實(shí)和Last Modified一樣,只不過(guò)它不是時(shí)間戳而是一串標(biāo)志量,也可以判斷服務(wù)器文件是否發(fā)生變化。
這個(gè)我沒(méi)有使用過(guò),這里不細(xì)講。
具體請(qǐng)參考:?ETag使用效果對(duì)比&經(jīng)驗(yàn)分享?、?對(duì)站點(diǎn)服務(wù)器如何配置ETag
5. HTTPS
HTTPS是在HTTP層之下添加了SSL層,大大增強(qiáng)了數(shù)據(jù)傳輸?shù)陌踩浴?br /> 在android中,如何解析https的接口呢?(以下代碼因?yàn)槭嵌嗄昵按a,可能有些地方欠缺嚴(yán)謹(jǐn),僅供學(xué)習(xí)參考)
第一步,生成客戶(hù)端私鑰。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | if [ -z $1 ]; then echo "Usage: importcert.sh <CA cert PEM file>" exit 1 fi CACERT=$1 BCJAR=bcprov-jdk16-145.jar TRUSTSTORE=../app/src/main/res/raw/mytruststore.bks ALIAS=`openssl x509 -inform PEM -subject_hash -noout -in $CACERT` if [ -f $TRUSTSTORE ]; then rm $TRUSTSTORE || exit 1 fi echo "Adding certificate to $TRUSTSTORE..." keytool -import -v -trustcacerts -alias $ALIAS \ -file $CACERT \ -keystore $TRUSTSTORE -storetype BKS \ -providerclass org.bouncycastle.jce.provider.BouncyCastleProvider \ -providerpath $BCJAR \ -storepass 123456abc echo "" echo "Added '$CACERT' with alias '$ALIAS' to $TRUSTSTORE..." |
使用這個(gè)腳本,利用pem文件,最終在res/raw目錄下生成一個(gè)mytruststore.bks文件。
第二步,根據(jù)私鑰和密碼生成SSLSocketFactory:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // 為了更好的性能,這里使用全局靜態(tài)變量 private static SSLSocketFactory sCustomerSSLSocketFactory = null; public static SSLSocketFactory getCustomerSSLSocketFactory(Context context) { if (sCustomerSSLSocketFactory != null) { return sCustomerSSLSocketFactory; } try { KeyStore trusted = KeyStore.getInstance("BKS"); InputStream in = context.getResources().openRawResource(R.raw.mytruststore); try { trusted.load(in, "aike_client".toCharArray()); } finally { in.close(); } sCustomerSSLSocketFactory = new SSLSocketFactory(trusted); sCustomerSSLSocketFactory.setHostnameVerifier(new AllowAllHostnameVerifier()); return sCustomerSSLSocketFactory; } catch(Exception e) { throw new AssertionError(e); } } |
第三步,在google http client中使用SSLSocketFactory。
| 1 2 3 4 5 6 7 | ApacheHttpTransport.Builder builder = new ApacheHttpTransport.Builder(); HttpRequestFactory httpRequestFactory = builder .setSocketFactory(AppConfig.getCustomerSSLSocketFactory(mContext)) .build() .createRequestFactory(); HttpRequest request = httpRequestFactory.buildPostRequest(url, content); ... |
至此https的基本使用流程大概是這樣的。
小結(jié)
通過(guò)對(duì)HTTP結(jié)構(gòu)和首部的深入學(xué)習(xí),相信大家對(duì)http協(xié)議的理解會(huì)上一個(gè)臺(tái)階。
如果有興趣,可自行去拓展學(xué)習(xí)一下HTTP2.0,SPDY,WebSocket等。
附錄
[1].?What really happens when you navigate to a URL
[2].?HTTP專(zhuān)題 by Jerry Qu
[3].?HTTP/2專(zhuān)題 by Jerry Qu
[4].?HTTP 協(xié)議中的 Transfer-Encoding
[5].?Http 協(xié)議中的Range請(qǐng)求頭例子
[6].?HTTP 2.0的那些事
[7].?HTTP持久連接
原文地址: http://jayfeng.com/2016/01/08/Android%20HTTP%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A/
總結(jié)
以上是生活随笔為你收集整理的Android HTTP必知必会的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 理解Android Java垃圾回收机制
- 下一篇: Android打包的那些事