国产亚洲欧美人成在线,免费视频爱爱太爽了无码,日本免费一区二区三区高清视频 ,国产真实伦对白精彩视频

歡迎您光臨深圳塔燈網(wǎng)絡(luò)科技有限公司!
電話圖標(biāo) 余先生:13699882642

網(wǎng)站百科

為您解碼網(wǎng)站建設(shè)的點點滴滴

第6章 網(wǎng)絡(luò)編程與網(wǎng)絡(luò)框架

發(fā)表日期:2018-02 文章編輯:小燈 瀏覽次數(shù):2919

6.1 公鑰密鑰加密原理

6.1.1 基礎(chǔ)知識

  • 密鑰:一般就是一個字符串或數(shù)字,在加密或者解密時傳遞給加密/解密算法。
  • 對稱加密算法:加密和解密都是使用的同一個密鑰。因此對稱加密算法要保證安全性的話,密鑰要做好保密,只能讓使用的人知道,不能對外公開。
  • 非對稱加密算法:加密使用的密鑰和解密使用的密鑰是不同的。 公鑰密碼體制就是一種非對稱加密算法。

(1) 公鑰密碼體制

分為三個部分:公鑰、私鑰、加密/解密算法

加密解密過程如下:
加密:通過加密算法和公鑰對內(nèi)容(或者說明文)進行加密,得到密文。
解密:通過解密算法和私鑰對密文進行解密,得到明文。

注意:由公鑰加密的內(nèi)容,只能由私鑰進行解密。

公鑰密碼體制的公鑰和算法都是公開的,私鑰是保密的。在實際的使用中,有需要的人會生成一對公鑰和私鑰,把公鑰發(fā)布出去給別人使用,自己保留私鑰。

(2) RSA加密算法

一種公鑰密碼體制,公鑰公開,私鑰保密,它的加密解密算法是公開的。 RSA的這一對公鑰、私鑰都可以用來加密和解密,并且一方加密的內(nèi)容可以由并且只能由對方進行解密。

(3) 簽名

就是在信息的后面再加上一段內(nèi)容,可以證明信息沒有被修改過。

一般是對信息做一個hash計算得到一個hash值(該過程不可逆),在把信息發(fā)送出去時,把這個hash值加密后做為一個簽名和信息一起發(fā)出去。 接收方在收到信息后,會重新計算信息的hash值,并和信息所附帶的hash值(解密后)進行對比,如果一致,就說明信息的內(nèi)容沒有被修改過,因為這里hash計算可以保證不同的內(nèi)容一定會得到不同的hash值,所以只要內(nèi)容一被修改,根據(jù)信息內(nèi)容計算的hash值就會變化。

當(dāng)然,不懷好意的人也可以修改信息內(nèi)容的同時也修改hash值,從而讓它們可以相匹配,為了防止這種情況,hash值一般都會加密后(也就是簽名)再和信息一起發(fā)送,以保證這個hash值不被修改。

6.1.2 基于RSA算法的加密通信的例子

“客戶”->“服務(wù)器”:你好
“服務(wù)器”->“客戶”:你好,我是服務(wù)器
“客戶”->“服務(wù)器”:向我證明你就是服務(wù)器
“服務(wù)器”->“客戶”:你好,我是服務(wù)器{你好,我是服務(wù)器}[私鑰|RSA]
“客戶”->“服務(wù)器”:{我們后面的通信過程,用對稱加密來進行,這里是對稱加密算法和密鑰}[公鑰|RSA]
“服務(wù)器”->“客戶”:{OK,收到!}[密鑰|對稱加密算法]
“客戶”->“服務(wù)器”:{我的帳號是aaa,密碼是123,把我的余額的信息發(fā)給我看看}[密鑰|對稱加密算法]
“服務(wù)器”->“客戶”:{你的余額是100元}[密鑰|對稱加密算法]

總結(jié)一下,RSA加密算法在這個通信過程中所起到的作用主要有兩個:
1. 因為私鑰只有“服務(wù)器”擁有,因此“客戶”可以通過判斷對方是否有私鑰來判斷對方是否是“服務(wù)器”。
2. 客戶端通過RSA的掩護,安全的和服務(wù)器商量好一個對稱加密算法和密鑰來保證后面通信過程內(nèi)容的安全。

但是這里還留有一個問題,“服務(wù)器”要對外發(fā)布公鑰,那“服務(wù)器”如何把公鑰發(fā)送給“客戶”呢?
我們可能會想到以下的兩個方法:
a) 把公鑰放到互聯(lián)網(wǎng)的某個地方的一個下載地址,事先給“客戶”去下載。
b) 每次和“客戶”開始通信時,“服務(wù)器”把公鑰發(fā)給“客戶”。

但是這個兩個方法都有一定的問題,
對于a)方法,“客戶”無法確定這個下載地址是不是“服務(wù)器”發(fā)布的,你憑什么就相信這個
地址下載的東西就是“服務(wù)器”發(fā)布的而不是別人偽造的呢,萬一下載到一個假的怎么辦?另外要所有的“客戶”都在通信前事先去下載公鑰也很不現(xiàn)實。
對于b)方法,也有問題,因為任何人都可以自己生成一對公鑰和私鑰,他只要向“客戶”發(fā)送他
自己的私鑰就可以冒充“服務(wù)器”了。示意如下:

“客戶”->“黑客”:你好//黑客截獲“客戶”發(fā)給“服務(wù)器”的消息
“黑客”->“客戶”:你好,我是服務(wù)器,這個是我的公鑰//黑客自己生成一對公鑰和私鑰,把
公鑰發(fā)給“客戶”,自己保留私鑰
“客戶”->“黑客”:向我證明你就是服務(wù)器
“黑客”->“客戶”:你好,我是服務(wù)器 {你好,我是服務(wù)器}[黑客自己的私鑰|RSA]//客戶收到

“黑客”用私鑰加密的信息后,是可以用“黑客”發(fā)給自己的公鑰解密的,從而會誤認(rèn)為“黑客”是“服務(wù)器”因此“黑客”只需要自己生成一對公鑰和私鑰,然后把公鑰發(fā)送給“客戶”,自己保留私鑰,這樣由于“客戶”可以用黑客的公鑰解密黑客的私鑰加密的內(nèi)容,“客戶”就會相信“黑客”是“服務(wù)器”,從而導(dǎo)致了安全問題。這里問題的根源就在于,大家都可以生成公鑰、私鑰對,無法確認(rèn)公鑰對到底是誰的。 如果能夠確定公鑰到底是誰的,就不會有這個問題了。例如,如果收到“黑客”冒充“服務(wù)器”發(fā)過來的公鑰,經(jīng)過某種檢查,如果能夠發(fā)現(xiàn)這個公鑰不是“服務(wù)器”的就好了。

6.1.3 數(shù)字證書

為了解決上述問題,數(shù)字證書出現(xiàn)了,它可以解決我們上面的問題。先大概看下什么是數(shù)字證書,一個證書包含下面的具體內(nèi)容:

  • 證書的發(fā)布機構(gòu)
  • 證書的有效期
  • 證書所有者(Subject)
  • 公鑰
  • 指紋和指紋算法
  • 簽名算法

指紋和指紋算法
這個是用來保證證書的完整性的,也就是說確保證書沒有被修改過。 其原理就是在發(fā)布證書時,發(fā)布者根據(jù)指紋算法(一個hash算法)計算整個證書的hash值(指紋)并和證書放在一起,使用者在打開證書時,自己也根據(jù)指紋算法計算一下證書的hash值(指紋),如果和剛開始的值對得上,就說明證書沒有被修改過,因為證書的內(nèi)容被修改后,根據(jù)證書的內(nèi)容計算的出的hash值(指紋)是會變化的。
注意,這個指紋會用"SecureTrust CA"這個證書機構(gòu)的私鑰用簽名算法加密后和證書放在一起。

簽名算法
就是指的這個數(shù)字證書的數(shù)字簽名所使用的加密算法,這樣就可以使用證書發(fā)布機構(gòu)的證書里面的公鑰,根據(jù)這個算法對指紋進行解密。指紋的加密結(jié)果就是數(shù)字簽名

數(shù)字證書可以保證數(shù)字證書里的公鑰確實是這個證書的所有者(Subject)的,或者證書可以用來確認(rèn)對方的身份。也就是說,我們拿到一個數(shù)字證書,我們可以判斷出這個數(shù)字證書到底是誰的。至于是如何判斷的,后面會在詳細(xì)討論數(shù)字證書時詳細(xì)解釋?,F(xiàn)在把前面的通信過程使用數(shù)字證書修改為如下:

“客戶”->“服務(wù)器”:你好
“服務(wù)器”->“客戶”:你好,我是服務(wù)器,這里是我的數(shù)字證書//這里用證書代替了公鑰
“客戶”->“服務(wù)器”:向我證明你就是服務(wù)器
“服務(wù)器”->“客戶”:你好,我是服務(wù)器 {你好,我是服務(wù)器}[私鑰|RSA]

在每次發(fā)送信息時,先對信息的內(nèi)容進行一個hash計算得出一個hash值,將信息的內(nèi)容和這個hash值一起加密后發(fā)送。接收方在收到后進行解密得到明文的內(nèi)容和hash值,然后接收方再自己對收到信息內(nèi)容做一次hash計算,與收到的hash值進行對比看是否匹配,如果匹配就說明信息在傳輸過程中沒有被修改過。如果不匹配說明中途有人故意對加密數(shù)據(jù)進行了修改,立刻中斷通話過程后做其它處理。

如何向證書的發(fā)布機構(gòu)去申請證書

舉個例子,假設(shè)我們公司"ABC Company"花了1000塊錢,向一個證書發(fā)布機構(gòu)"SecureTrust CA"為我們自己的公司"ABC Company"申請了一張證書,注意,這個證書發(fā)布機構(gòu)"SecureTrust CA"是一個大家公認(rèn)并被一些權(quán)威機構(gòu)接受的證書發(fā)布機構(gòu),我們的操作系統(tǒng)里面已經(jīng)安裝了"SecureTrust CA"的證書。"SecureTrust CA"在給我們發(fā)布證書時,把Issuer,Public key,Subject,Valid from,Valid to等信息以明文的形式寫到證書里面,然后用一個指紋算法計算出這些數(shù)字證書內(nèi)容的一個指紋,并把指紋和指紋算法用自己的私鑰進行加密,然后和證書的內(nèi)容一起發(fā)布,同時"SecureTrust CA"還會給一個我們公司"ABC Company"的私鑰給到我們。
我們"ABC Company"申請到這個證書后,我們把證書投入使用,我們在通信過程開始時會把證書發(fā)給對方。

對方如何檢查這個證書的確是合法的并且是我們"ABC Company"公司的證書呢?首先應(yīng)用程序(對方通信用的程序,例如IE、OUTLook等)讀取證書中的Issuer(發(fā)布機構(gòu))為"SecureTrust CA" ,然后會在操作系統(tǒng)中受信任的發(fā)布機構(gòu)的證書中去找"SecureTrust CA"的證書,如果找不到,那說明證書的發(fā)布機構(gòu)是個水貨發(fā)布機構(gòu),證書可能有問題,程序會給出一個錯誤信息。 如果在系統(tǒng)中找到了"SecureTrust CA"的證書,那么應(yīng)用程序就會從證書中取出"SecureTrust CA"的公鑰,然后對我們"ABC Company"公司的證書里面的指紋和指紋算法用這個公鑰進行解密,然后使用這個指紋算法計算"ABC Company"證書的指紋,將這個計算的指紋與放在證書中的指紋對比,如果一致,說明"ABC Company"的證書肯定沒有被修改過并且證書是"SecureTrust CA" 發(fā)布的,證書中的公鑰肯定是"ABC Company"的。對方然后就可以放心的使用這個公鑰和我們"ABC Company"進行通信了。

6.2 Http協(xié)議原理

6.2.1 基礎(chǔ)知識

1. TCP/IP協(xié)議族

  • IP協(xié)議:網(wǎng)絡(luò)層協(xié)議,保證了計算機之間可以發(fā)送和接收數(shù)據(jù)。
  • TCP協(xié)議:傳輸層協(xié)議,一種端到端的協(xié)議,建立一個虛擬鏈路用于發(fā)送和接收數(shù)據(jù),基于重發(fā)機制,提供可靠的通信連接。為了方便通信,將報文分割成多個報文段發(fā)送。
  • UDP協(xié)議:傳輸層協(xié)議,一種無連接的協(xié)議,每個數(shù)據(jù)報都是一個獨立的信息,包括完整的源地址或目的地址,它在網(wǎng)絡(luò)上以任何可能的路徑傳往目的地,因此能否到達(dá)目的地,到達(dá)目的地的時間以及內(nèi)容的正確性都是不能被保證的。

通信雙方一方作為服務(wù)器等待客戶提出請求并予以響應(yīng)??蛻魟t在需要服務(wù)時向服務(wù)器提出申請。服務(wù)器一般作為守護進程始終運行,監(jiān)聽網(wǎng)絡(luò)端口,一旦有客戶請求,就會啟動一個服務(wù)進程來響應(yīng)該客戶,同時自己繼續(xù)監(jiān)聽服務(wù)端口,使后來的客戶也能及時得到服務(wù)。一個socket(通常都是server socket)等待建立連接時,另一個socket可以要求進行連接,一旦這兩個socket連接起來,它們就可以進行雙向數(shù)據(jù)傳輸,雙方都可以進行發(fā)送或接收操作。

2. TCP3次握手,4次揮手過程

(1) 建立連接協(xié)議(三次握手)

a)客戶端發(fā)送一個帶SYN標(biāo)志的TCP報文到服務(wù)器。(聽得到嗎?)
b)服務(wù)端回應(yīng)客戶端的報文同時帶ACK(acknowledgement,確認(rèn))標(biāo)志和SYN(synchronize)標(biāo)志。它表示對剛才客戶端SYN報文的回應(yīng);同時又標(biāo)志SYN給客戶端,詢問客戶端是否準(zhǔn)備好進行數(shù)據(jù)通訊。(聽得到,你能聽到我嗎?)
c)客戶必須再次回應(yīng)服務(wù)端一個ACK報文。(聽到了,我們可以說話了)

為什么需要“三次握手”?
在謝希仁著《計算機網(wǎng)絡(luò)》第四版中講“三次握手”的目的是“為了防止已失效的連接請求報文段突然又傳送到了服務(wù)端,因而產(chǎn)生錯誤”。“已失效的連接請求報文段”的產(chǎn)生在這樣一種情況下:client發(fā)出的第一個連接請求報文段并沒有丟失,而是在某個網(wǎng)絡(luò)結(jié)點長時間的滯留了,以致延誤到連接釋放以后的某個時間才到達(dá)server。本來這是一個早已失效的報文段。但server收到此失效的連接請求報文段后,就誤認(rèn)為是client再次發(fā)出的一個新的連接請求。于是就向client發(fā)出確認(rèn)報文段,同意建立連接。假設(shè)不采用“三次握手”,那么只要server發(fā)出確認(rèn),新的連接就建立了。由于現(xiàn)在client并沒有發(fā)出建立連接的請求,因此不會理睬server的確認(rèn),也不會向server發(fā)送數(shù)據(jù)。但server卻以為新的運輸連接已經(jīng)建立,并一直等待client發(fā)來數(shù)據(jù)。這樣,server的很多資源就白白浪費掉了。采用“三次握手”的辦法可以防止上述現(xiàn)象發(fā)生。例如剛才那種情況,client不會向server的確認(rèn)發(fā)出確認(rèn)。server由于收不到確認(rèn),就知道client并沒有要求建立連接?!薄?主要目的防止server端一直等待,浪費資源。

(2) 連接終止協(xié)議(四次揮手)

由于TCP連接是全雙工的,因此每個方向都必須單獨進行關(guān)閉。這原則是當(dāng)一方完成它的數(shù)據(jù)發(fā)送任務(wù)后就能發(fā)送一個FIN來終止這個方向的連接。收到一個 FIN只意味著這一方向上沒有數(shù)據(jù)流動,一個TCP連接在收到一個FIN后仍能發(fā)送數(shù)據(jù)。首先進行關(guān)閉的一方將執(zhí)行主動關(guān)閉,而另一方執(zhí)行被動關(guān)閉。
a) TCP客戶端發(fā)送一個FIN,用來關(guān)閉客戶到服務(wù)器的數(shù)據(jù)傳送(報文段4)。
b) 服務(wù)器收到這個FIN,它發(fā)回一個ACK,確認(rèn)序號為收到的序號加1(報文段5)。和SYN一樣,一個FIN將占用一個序號。
c) 服務(wù)器關(guān)閉客戶端的連接,發(fā)送一個FIN給客戶端(報文段6)。
d) 客戶段發(fā)回ACK報文確認(rèn),并將確認(rèn)序號設(shè)置為收到序號加1(報文段7)。

為什么需要“四次揮手”?
那可能有人會有疑問,在tcp連接握手時為何ACK是和SYN一起發(fā)送,這里ACK卻沒有和FIN一起發(fā)送呢。原因是因為tcp是全雙工模式,接收到FIN時意味將沒有數(shù)據(jù)再發(fā)來,但是還是可以繼續(xù)發(fā)送數(shù)據(jù)。

3. 請求報文

(1) 請求行

由3部分組成,分別為:請求方法、URL以及協(xié)議版本,之間由空格分隔

請求方法:GET、HEAD、PUT、POST等方法,但并不是所有的服務(wù)器都實現(xiàn)了所有的方法,部分方法即便支持,處于安全性的考慮也是不可用的
協(xié)議版本:常用HTTP/1.1

(2) 請求頭部

請求頭部為請求報文添加了一些附加信息,由“名/值”對組成,每行一對,名和值之間使用冒號分隔

  • Host 接受請求的服務(wù)器地址,可以是IP:端口號,也可以是域名
  • User-Agent 發(fā)送請求的應(yīng)用程序名稱
  • Accept-Charset 通知服務(wù)端可以發(fā)送的編碼格式
  • Accept-Encoding 通知服務(wù)端可以發(fā)送的數(shù)據(jù)壓縮格式
  • Accept-Language 通知服務(wù)端可以發(fā)送的語言
  • Range 正文的字節(jié)請求范圍,為斷點續(xù)傳和并行下載提供可能,返回狀態(tài)碼是206
  • Authorization 用于設(shè)置身份認(rèn)證信息
  • Cookie 已有的Cookie

請求頭部的最后會有一個空行,表示請求頭部結(jié)束,接下來為請求正文,這一行非常重要,必不可少

(3) 請求正文

可選部分,比如GET請求就沒有請求正文

4. 響應(yīng)報文

由3部分組成,分別為:協(xié)議版本,狀態(tài)碼,狀態(tài)碼描述,之間由空格分隔

狀態(tài)碼:為3位數(shù)字,2XX表示成功,3XX表示資源重定向,4XX表示客戶端請求出錯,5XX表示服務(wù)端出錯
206狀態(tài)碼表示的是:客戶端通過發(fā)送范圍請求頭Range抓取到了資源的部分?jǐn)?shù)據(jù),得服務(wù)端提供支持

(1) 響應(yīng)頭部
  • Server 服務(wù)器應(yīng)用程序軟件的名稱和版本
  • Content-Type 響應(yīng)正文的類型。如:text/plain、application/json
  • Content-Length 響應(yīng)正文長度
  • Content-Charset 響應(yīng)正文使用的編碼
  • Content-Language 響應(yīng)正文使用的語言
  • Content-Range 正文的字節(jié)位置范圍
  • Accept-Ranges bytes:表明服務(wù)器支持Range請求,單位是字節(jié);none:不支持
  • Set-Cookie 設(shè)置Cookie

正文的內(nèi)容可以用gzip等進行壓縮,以提升傳輸速率

5. 通用首部

(1) Cache-Control

用于操作瀏覽器緩存的工作機制。取值如下:

  • max-age:表示緩存的新鮮時間,在此時間內(nèi)可以直接使用緩存。單位秒。
  • no-cache:不做緩存。
  • max-stale:可以接受過期多少秒的緩存。
(2) Connection

用于管理持久連接。目前大部分瀏覽器都是用http1.1協(xié)議,也就是說默認(rèn)都會發(fā)起Keep-Alive的連接請求。所以是否能完成一個完整的Keep-Alive連接就看服務(wù)器設(shè)置情況。取值如下:

  • keep-alive:使客戶端到服務(wù)器端的連接持續(xù)有效,當(dāng)出現(xiàn)對服務(wù)器的后繼請求時,避免了建立或者重新建立連接。
  • close:每個請求/應(yīng)答客戶和服務(wù)器都要新建一個連接,完成之后立即斷開連接。
(3) Transfer-Encoding

在Http/1.1中,僅對分塊傳輸編碼有效。Transfer-Encoding: chunked 表示輸出的內(nèi)容長度不能確定,普通的靜態(tài)頁面、圖片之類的基本上都用不到這個,但動態(tài)頁面就有可能會用到。一般使用Content-Length就夠了。

Http/1.1 200 OK .... Transfer-Encoding:chunked Connection:keep-alivecf0//16進制,值為3312...3312字節(jié)分塊數(shù)據(jù)...392//16進制,值為914...914字節(jié)分塊數(shù)據(jù)...0 
(4) Content-Encoding

請求體/響應(yīng)體的編碼格式,如gzip

6. HTTP Authentication

兩種常見的Authentication機制:HTTP Basic和Digest。(現(xiàn)在用的并不多,了解一下)

(1) Http Basic

最簡單的Authentication協(xié)議。直接方式告訴服務(wù)器你的用戶名(username)和密碼(password)。

request頭部:

GET /secret HTTP/1.1 Authorization: Basic QWxpY2U6MTIzNDU2//由“Alice:123456”進行Base64編碼以后得到的結(jié)果 ... 

response頭部:

HTTP/1.1 200 OK ... 

因為我們輸入的是正確的用戶名密碼,所以服務(wù)器會返回200,表示驗證成功。如果我們用錯誤的用戶的密碼來發(fā)送請求,則會得到類似如下含有401錯誤的response頭部:

HTTP/1.1 401 Bad credentials WWW-Authenticate: Basic realm="Spring Security Application" ... 
(2) Http Digest

當(dāng)Alice初次訪問服務(wù)器時,并不攜帶密碼。此時服務(wù)器會告知Alice一個隨機生成的字符串(nonce)。然后Alice再將這個字符串與她的密碼123456結(jié)合在一起進行MD5編碼,將編碼以后的結(jié)果發(fā)送給服務(wù)器作為驗證信息。

因為nonce是“每次”(并不一定是每次)隨機生成的,所以Alice在不同的時間訪問服務(wù)器,其編碼使用的nonce值應(yīng)該是不同的,如果攜帶的是相同的nonce編碼后的結(jié)果,服務(wù)器就認(rèn)為其不合法,將拒絕其訪問。

curl和服務(wù)器通信過程:

curl -------- request1:GET ------->> Server curl <<------ response1:nonce ------- Server curl ---- request2:Digest Auth ---->> Server curl <<------- response2:OK --------Server 

request1頭部:

GET /secret HTTP/1.1 ... 

請求1中沒有包含任何用戶名和密碼信息

response1頭部:

HTTP/1.1 401 Full authentication is required to access this resource WWW-Authenticate: Digest realm="Contacts Realm via Digest Authentication", qop="auth",nonce="MTQwMTk3OTkwMDkxMzo3MjdjNDM2NTYzMTU2NTA2NWEzOWU2NzBlNzhmMjkwOA==" ... 

當(dāng)服務(wù)器接收到request1以后,認(rèn)為request1沒有任何的Authentication信息,所以返回401,并且告訴curl nonce的值是MTQwMTk3OTkwMDkxMzo3MjdjNDM2NTYzMTU2NTA2NWEzOWU2NzBlNzhmMjkwOA

request2頭部:

GET /secret HTTP/1.1 Authorization: Digest username="Alice", realm="Contacts Realm via Digest Authentication",nonce="MTQwMTk3OTkwMDkxMzo3MjdjNDM2NTYzMTU2NTA2NWEzOWU2NzBlNzhmMjkwOA==", uri="/secret", cnonce="MTQwMTk3", nc=00000001, qop="auth",response="fd5798940c32e51c128ecf88472151af"... 

curl接收到服務(wù)器的nonce值以后,就可以把如密碼等信息和nonce值放在一起然后進行MD5編碼,得到一個response值,如前面紅色標(biāo)出所示,這樣服務(wù)器就可以通過這個值驗證Alice的密碼是否正確。

response2頭部:

HTTP/1.1 200 OK ... 

當(dāng)我們完成Authentication以后,如果我們再次使用剛才的nonce值,將會收到錯誤信息。Digest Authentication比Basic安全,但是并不是真正的什么都不怕了,Digest Authentication這種容易方式容易收到Man in the Middle式攻擊。

7. 請求體的3種形式

據(jù)應(yīng)用場景的不同,HTTP請求的請求體有三種不同的形式。
第一種:
移動開發(fā)者常見的,請求體是任意類型,服務(wù)器不會解析請求體,請求體的處理需要自己解析,如 POST JSON時候就是這類。
第二種:
這里的格式要求就是URL中Query String的格式要求:多個鍵值對之間用&連接,鍵與值之前用=連接,且只能用ASCII字符,非ASCII字符需使用UrlEncode編碼。
第三種:
請求體被分成為多個部分,文件上傳時會被使用,這種格式最先應(yīng)該是被用于郵件傳輸中,每個字段/文件都被boundary(Content-Type中指定)分成單獨的段,每段以-- 加 boundary開頭,然后是該段的描述頭,描述頭之后空一行接內(nèi)容,請求結(jié)束的標(biāo)制為boundary后面加--。(見下面詳細(xì)說明)

8. http協(xié)議中的多部分對象(multipart/form-data)

默認(rèn)是application/x-www-form-urlencoded,但是在傳輸大型文件的時候效率比較低下。所以需要multipart/form-data。
報文的主體內(nèi)可以包含多部分對象,通常用來發(fā)送圖片、文件或表單等。

Connection: keep-alive Content-Length: 123 X-Requested-With: ShockwaveFlash/16.0.0.296 User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.93 Safari/537.36 Content-Type: multipart/form-data; boundary=Ij5ei4KM7KM7ae0KM7cH2ae0Ij5Ef1 Accept: */* Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.8 Range: bytes=0-1024 Cookie: bdshare_firstime=1409052493497--Ij5ei4KM7KM7ae0KM7cH2ae0Ij5Ef1 Content-Disposition: form-data; name="position"1425264476444 --Ij5ei4KM7KM7ae0KM7cH2ae0Ij5Ef1 Content-Disposition: form-data; name="pics"; filename="file1.txt" Content-Type: text/plain...(file1.txt的數(shù)據(jù))... ue_con_1425264252856 --Ij5ei4KM7KM7ae0KM7cH2ae0Ij5Ef1 Content-Disposition: form-data; name="cm"100672 --Ij5ei4KM7KM7ae0KM7cH2ae0Ij5Ef1-- 

a)在請求頭中Content-Type: multipart/form-data; boundary=Ij5ei4KM7KM7ae0KM7cH2ae0Ij5Ef1是必須的,boundary字符串可以隨意指定
b)上面有3個部分,分別用--boundary進行分隔。Content-Disposition: form-data; name="參數(shù)的名稱" + "\r\n" + "\r\n" + 參數(shù)值
c)--boundary-- 作為結(jié)束

6.2.2Https

(1) Http的缺點

  • 通信使用明文,內(nèi)容可能會被竊聽 —— 加密通信線路
  • 不驗證通信方,可能遭遇偽裝 —— 證書
  • 無法驗證報文的完整性,可能已被篡改 —— 數(shù)字簽名

Http+加密+認(rèn)證+完整性保護=Https

Https就是身披SSL(Secure Socket Layer,安全套接層)協(xié)議這層外殼的Http。當(dāng)使用了SSL之后,Http先和SSL通信,SSL再和TCP通信。

SSL(secure sockets layer):安全套接層,它是在上世紀(jì)90年代中期,由網(wǎng)景公司設(shè)計的,為解決 HTTP 協(xié)議傳輸內(nèi)容會被偷窺(嗅探)和篡改等安全問題而設(shè)計的,到了1999年,SSL成為互聯(lián)網(wǎng)上的標(biāo)準(zhǔn),名稱改為TLS(transport layer security):安全傳輸層協(xié)議,兩者可視為同一種東西的不同階段。

(2) Https的工作原理

HTTPS在傳輸數(shù)據(jù)之前需要客戶端(瀏覽器)與服務(wù)端(網(wǎng)站)之間進行一次握手,在握手過程中將確立雙方加密傳輸數(shù)據(jù)的密碼信息。TLS/SSL協(xié)議不僅僅是一套加密傳輸?shù)膮f(xié)議,更是一件經(jīng)過藝術(shù)家精心設(shè)計的藝術(shù)品,TLS/SSL中使用了非對稱加密,對稱加密以及HASH算法。握手過程的具體描述如下:

  • 瀏覽器將自己支持的一套加密規(guī)則發(fā)送給網(wǎng)站。
  • 網(wǎng)站從中選出一組加密算法與HASH算法,并將自己的身份信息以證書的形式發(fā)回給瀏覽器。證書里面包含了網(wǎng)站地址,加密公鑰,以及證書的頒發(fā)機構(gòu)等信息。
  • 瀏覽器獲得網(wǎng)站證書之后瀏覽器要做以下工作:
    a) 驗證證書的合法性(頒發(fā)證書的機構(gòu)是否合法,證書中包含的網(wǎng)站地址是否與正在訪問的地址一致等),如果證書受信任,則瀏覽器欄里面會顯示一個小鎖頭,否則會給出證書不受信的提示。
    b) 如果證書受信任,或者是用戶接受了不受信的證書,瀏覽器會生成一串隨機數(shù)的密碼,并用證書中提供的公鑰加密。
    c) 使用約定好的HASH算法計算握手消息,并使用生成的隨機數(shù)對消息進行加密,最后將之前生成的所有信息發(fā)送給網(wǎng)站。
  • 網(wǎng)站接收瀏覽器發(fā)來的數(shù)據(jù)之后要做以下的操作:
    a) 使用自己的私鑰將信息解密取出密碼,使用密碼解密瀏覽器發(fā)來的握手消息,并驗證HASH是否與瀏覽器發(fā)來的一致。
    b) 使用密碼加密一段握手消息,發(fā)送給瀏覽器。
  • 瀏覽器解密并計算握手消息的HASH,如果與服務(wù)端發(fā)來的HASH一致,此時握手過程結(jié)束,之后所有的通信數(shù)據(jù)將由之前瀏覽器生成的隨機密碼并利用對稱加密算法進行加密。

這里瀏覽器與網(wǎng)站互相發(fā)送加密的握手消息并驗證,目的是為了保證雙方都獲得了一致的密碼,并且可以正常的加密解密數(shù)據(jù),為后續(xù)真正數(shù)據(jù)的傳輸做一次測試。另外,HTTPS一般使用的加密與HASH算法如下:

  • 非對稱加密算法:RSA,DSA/DSS
  • 對稱加密算法:AES,RC4,3DES
  • HASH算法:MD5,SHA1,SHA256

HTTPS對應(yīng)的通信時序圖如下:

(3) 證書分類

SSL 證書大致分三類:

  • 認(rèn)可的證書頒發(fā)機構(gòu)(如: VeriSign), 或這些機構(gòu)的下屬機構(gòu)頒發(fā)的證書.
  • 沒有得到認(rèn)可的證書頒發(fā)機構(gòu)頒發(fā)的證書.
  • 自簽名證書, 自己通過JDK自帶工具keytool去生成一個證書,分為臨時性的(在開發(fā)階段使用)或在發(fā)布的產(chǎn)品中永久性使用的兩種.

只有第一種, 也就是那些被安卓系統(tǒng)認(rèn)可的機構(gòu)頒發(fā)的證書, 在使用過程中不會出現(xiàn)安全提示。對于向權(quán)威機構(gòu)(簡稱CA,Certificate Authority)申請過證書的網(wǎng)絡(luò)地址,用OkHttp或者HttpsURLConnection都可以直接訪問 ,不需要做額外的事情 。但是申請需要$$ (每年要交 100 到 500 美元不等的費用)。

CA機構(gòu)頒發(fā)的證書有3種類型:
域名型SSL證書(DV SSL):信任等級普通,只需驗證網(wǎng)站的真實性便可頒發(fā)證書保護網(wǎng)站;
企業(yè)型SSL證書(OV SSL):信任等級強,須要驗證企業(yè)的身份,審核嚴(yán)格,安全性更高;
增強型SSL證書(EV SSL):信任等級最高,一般用于銀行證券等金融機構(gòu),審核嚴(yán)格,安全性最高,同時可以激活綠色網(wǎng)址欄。

(4) HTTPS協(xié)議和HTTP協(xié)議的區(qū)別:

  • https協(xié)議需要到ca申請證書,一般免費證書很少,需要交費。
  • http是超文本傳輸協(xié)議,信息是明文傳輸,https 則是具有安全性的ssl加密傳輸協(xié)議。
  • http和https使用的是完全不同的連接方式用的端口也不一樣,前者是80,后者是443。
  • http的連接很簡單,是無狀態(tài)的 。
  • HTTPS協(xié)議是由SSL+HTTP協(xié)議構(gòu)建的可進行加密傳輸、身份認(rèn)證的網(wǎng)絡(luò)協(xié)議, 要比http協(xié)議安全。

6.3 Android SDK支持

6.3.1 InetAddress

(1) 簡介
  • 代表IP地址,還有兩個子類,Inet4Address、Inet6Address
  • 沒有構(gòu)造方法
(2) 方法
  • InetAddress.getByAddress(byte[] addr) 根據(jù)IP地址獲取InetAddress對象,如:new byte[]{127,0,0,1}
  • InetAddress.getByName(String host)根據(jù)主機名獲取InetAddress對象 www.baidu.com 沒有http://
  • InetAddress.getLocalHost()返回本機
  • getHostAddress() String 返回IP地址
  • getHostName() String 返回主機名
  • isReachable(int timeout) boolean 測試是否可以達(dá)到該地址,毫秒數(shù)

2. URLDecoder和URLEncoder

(1) 簡介
  • URLDecoder和URLEncoder用于完成普通字符串和application/x-www-form-urlencoded MIME字符串之間的相互轉(zhuǎn)換
  • 若每個中文占2個字節(jié),每個字節(jié)轉(zhuǎn)換成2個十六進制的數(shù)字,所以每個中文字符轉(zhuǎn)換成“%XX%XX”的形式
(2) 方法
  • URLEncoder.encode(String s, String enc) String
  • URLDecoder.decode(String s, String enc) String

6.3.2 Socket通信

1. Socket通信簡介

Socket又稱套接字,是程序內(nèi)部提供的與外界通信的端口,即端口通信。通過建立socket連接,可為通信雙方的數(shù)據(jù)傳輸傳提供通道。主要特點有數(shù)據(jù)丟失率低,使用簡單且易于移植。

(1) Socket的分類

在TCP/IP協(xié)議族當(dāng)中主要的Socket類型為流套接字(streamsocket)和數(shù)據(jù)報套接字(datagramsocket)。流套接字將TCP作為其端對端協(xié)議,提供了一個可信賴的字節(jié)流服務(wù)。數(shù)據(jù)報套接字使用UDP協(xié)議,提供數(shù)據(jù)打包發(fā)送服務(wù)。

2. Socket 基本通信模型

(1) Socket通信模型
Socket基本通信模型
(2) TCP通信模型
TCP通信模型
(3) UDP通信模型
UDP通信模型

4. Demo

首先添加權(quán)限:

<!--允許應(yīng)用程序改變網(wǎng)絡(luò)狀態(tài)--> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/> <!--允許應(yīng)用程序改變WIFI連接狀態(tài)--> <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/> <!--允許應(yīng)用程序訪問有關(guān)的網(wǎng)絡(luò)信息--> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/><!--允許應(yīng)用程序訪問WIFI網(wǎng)卡的網(wǎng)絡(luò)信息--> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> <!--允許應(yīng)用程序完全使用網(wǎng)絡(luò)--> <uses-permission android:name="android.permission.INTERNET"/> 
(1) 使用TCP協(xié)議通信

客戶端實現(xiàn):

protected void connectServerWithTCPSocket() { Socket socket; try {// 創(chuàng)建一個Socket對象,并指定服務(wù)端的IP及端口號 socket = new Socket("192.168.1.32", 1989); // 創(chuàng)建一個InputStream用戶讀取要發(fā)送的文件。 InputStream inputStream = new FileInputStream("e://a.txt"); // 獲取Socket的OutputStream對象用于發(fā)送數(shù)據(jù)。 OutputStream outputStream = socket.getOutputStream(); // 創(chuàng)建一個byte類型的buffer字節(jié)數(shù)組,用于存放讀取的本地文件 byte buffer[] = new byte[4 * 1024]; int temp = 0; // 循環(huán)讀取文件 while ((temp = inputStream.read(buffer)) != -1) { // 把數(shù)據(jù)寫入到OuputStream對象中 outputStream.write(buffer, 0, temp); } // 發(fā)送讀取的數(shù)據(jù)到服務(wù)端 outputStream.flush(); /** 或創(chuàng)建一個報文,使用BufferedWriter寫入**/ //String socketData = "[2143213;21343fjks;213]"; //BufferedWriter writer = new BufferedWriter(new OutputStreamWriter( //socket.getOutputStream())); //writer.write(socketData.replace("\n", " ") + "\n"); //writer.flush(); } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } 

服務(wù)端實現(xiàn):

public void serverReceviedByTcp() { // 聲明一個ServerSocket對象 ServerSocket serverSocket = null; try { // 創(chuàng)建一個ServerSocket對象,并讓這個Socket在1989端口監(jiān)聽 serverSocket = new ServerSocket(1989); // 調(diào)用ServerSocket的accept()方法,接受客戶端所發(fā)送的請求, // 如果客戶端沒有發(fā)送數(shù)據(jù),那么該線程就阻塞,等到收到數(shù)據(jù),繼續(xù)執(zhí)行。 Socket socket = serverSocket.accept(); // 從Socket當(dāng)中得到InputStream對象,讀取客戶端發(fā)送的數(shù)據(jù) InputStream inputStream = socket.getInputStream(); byte buffer[] = new byte[1024 * 4]; int temp = 0; // 從InputStream當(dāng)中讀取客戶端所發(fā)送的數(shù)據(jù) while ((temp = inputStream.read(buffer)) != -1) { System.out.println(new String(buffer, 0, temp)); } serverSocket.close(); } catch (IOException e) { e.printStackTrace(); } } 

5. 方法

(1) ServerSocket

監(jiān)聽來自于客戶端的Socket連接,如果沒有連接,它將一直處于等待狀態(tài)

  • ServerSocket(int port, int backlog, InetAddress bindAddr)在機器存在多IP的情況下,允許通過bindAddr來指定綁定到哪個IP;backlog是隊列中能接受的最大socket客戶端連接數(shù)(accept()之后將被取出)
  • ServerSocket() 創(chuàng)建非綁定服務(wù)器套接字
  • ServerSocket(int port) 創(chuàng)建綁定到特定端口的服務(wù)器套接字,等價于ServerSocket(port, 50, null)
  • accept() Socket 如果接收到一個客戶端Socket的連接請求,返回一個與客戶端Socket相對應(yīng)的Socket
  • close()
(2) Socket
  • Socket() 創(chuàng)建未連接套接字
  • Socket(String host, int port)
  • Socket(InetAddress address, int port)創(chuàng)建一個流套接字并將其連接到指定 IP 地址的指定端口號,默認(rèn)使用本地主機默認(rèn)的IP和系統(tǒng)動態(tài)分配的端口
  • Socket(InetAddress address, int port, InetAddress localAddr, int localPort)創(chuàng)建一個套接字并將其連接到指定遠(yuǎn)程地址上的指定遠(yuǎn)程端口,多IP時
  • getOutputStream() OutputStream 返回此套接字的輸出流
  • getInputStream() inputStream 返回此套接字的輸入流
  • connect(SocketAddress endpoint, int timeout) 將此套接字連接到服務(wù)器,并指定一個超時值
  • close()

6.3.3HttpURLConnection

1. URL

1)對象代表統(tǒng)一資源定位器,是指向互聯(lián)網(wǎng)“資源”的指針
2)通常由協(xié)議名、主機、端口、資源路徑組成
3)URL包含一個可打開到達(dá)該資源的輸入流,可以將URL理解成為URI的特例

  • URL(String spec)
  • openConnection() URLConnection
  • getProtocol() String
  • getHost() String
  • getPort() int
  • getPath() String 獲取路徑部分,/search.html
  • getFile() String 獲取資源名稱,/search.html?keyword='你好'
  • getQuery() String 獲取查詢字符串,keyword='你好'
  • openStream() InputStream

2. URLConnection

抽象類,表示應(yīng)用程序和URL之間的通信連接,可以向URL發(fā)送請求,讀取URL指向的資源

  • setDoInput(boolean doinput) 發(fā)送POST請求,必須設(shè)置,設(shè)置為true
  • setDoOutput(boolean dooutput) 發(fā)送POST請求,必須設(shè)置,設(shè)置為true
  • setUseCaches(boolean usecaches)是否使用緩存
  • setRequestProperty(String key, String value) 設(shè)置普通的請求屬性
  • setConnectTimeout(int timeout)設(shè)置連接超時的時間
  • setReadTimeout(int timeoutMillis) 讀取輸入流的超時時間
  • connect() 抽象方法,建立實際的連接
  • getHeaderField(String key) String
  • getHeaderFields() Map<String,List<String>> 獲取所有的響應(yīng)頭;getHeaderField(String name)獲取指定的響應(yīng)頭
  • getOutputStream() OutputStream
  • getInputStream() InputStream
  • getContentLength() int

3. HttpURLConnection

(1) 簡介

抽象類,是URLConnection的子類,增加了操作Http資源的便捷方法

  • 對象不能直接構(gòu)造,需要通過URL類中的openConnection()方法來獲得。
  • connect()函數(shù),實際上只是建立了一個與服務(wù)器的TCP連接,并沒有實際發(fā)送HTTP請求。HTTP請求實際上直到我們獲取服務(wù)器響應(yīng)數(shù)據(jù)(如調(diào)用getInputStream()、getResponseCode()等方法)時才正式發(fā)送出去。
  • 對HttpURLConnection對象的配置都需要在connect()方法執(zhí)行之前完成。
  • HttpURLConnection是基于HTTP協(xié)議的,其底層通過socket通信實現(xiàn)。如果不設(shè)置超時(timeout),在網(wǎng)絡(luò)異常的情況下,可能會導(dǎo)致程序僵死而不繼續(xù)往下執(zhí)行。
  • HTTP正文的內(nèi)容是通過OutputStream流寫入的, 向流中寫入的數(shù)據(jù)不會立即發(fā)送到網(wǎng)絡(luò),而是存在于內(nèi)存緩沖區(qū)中,待流關(guān)閉時,根據(jù)寫入的內(nèi)容生成HTTP正文。
  • 調(diào)用getInputStream()方法時,返回一個輸入流,用于從中讀取服務(wù)器對于HTTP請求的返回信息。
  • 我們可以使用connect()方法手動的發(fā)送一個HTTP請求,但是如果要獲取HTTP響應(yīng)的時候,請求就會自動的發(fā)起,比如我們使用getInputStream()方法的時候,所以完全沒有必要調(diào)用connect()方法。
(2) 方法
  • setRequestMethod(String method)
  • setInstanceFollowRedirects(boolean followRedirects)
  • getResponseCode() int 獲取服務(wù)器的響應(yīng)碼
  • getResponseMessage() String 獲取響應(yīng)消息
  • getRequestMethod() String 獲取發(fā)送請求的方法,GET或POST
  • disconnect() 抽象方法
(3) 實現(xiàn)多線程下載

步驟:
①:創(chuàng)建URL對象
②:獲取URL對象指向資源的大?。╣etContentLength()方法)
③:在本地創(chuàng)建一個與網(wǎng)路資源相同大小的空文件
④:計算每條線程應(yīng)該下載網(wǎng)絡(luò)資源的哪個部分(從哪個字節(jié)開始,到哪個字節(jié)結(jié)束)
⑤:依次創(chuàng)建,啟動多條線程來下載網(wǎng)絡(luò)資源的指定部分

(4) 實例

使用GET方式訪問HTTP

public static void main(String[] args) { try { // 1. 得到訪問地址的URL URL url = new URL( "http://localhost:8080/Servlet/do_login.do?username=test&password=123456"); // 2. 得到網(wǎng)絡(luò)訪問對象java.net.HttpURLConnection HttpURLConnection connection = (HttpURLConnection) url .openConnection(); /* 3. 設(shè)置請求參數(shù)(過期時間,輸入、輸出流、訪問方式),以流的形式進行連接 */ // 設(shè)置是否向HttpURLConnection輸出 connection.setDoOutput(false); // 設(shè)置是否從httpUrlConnection讀入 connection.setDoInput(true); // 設(shè)置請求方式 connection.setRequestMethod("GET"); // 設(shè)置是否使用緩存 connection.setUseCaches(true); // 設(shè)置此 HttpURLConnection 實例是否應(yīng)該自動執(zhí)行 HTTP 重定向 connection.setInstanceFollowRedirects(true); // 設(shè)置超時時間 connection.setConnectTimeout(3000); // 連接 connection.connect(); // 4. 得到響應(yīng)狀態(tài)碼的返回值 responseCode int code = connection.getResponseCode(); // 5. 如果返回值正常,數(shù)據(jù)在網(wǎng)絡(luò)中是以流的形式得到服務(wù)端返回的數(shù)據(jù) String msg = ""; if (code == 200) { // 正常響應(yīng) // 從流中讀取響應(yīng)信息 BufferedReader reader = new BufferedReader( new InputStreamReader(connection.getInputStream())); String line = null;while ((line = reader.readLine()) != null) { // 循環(huán)從流中讀取 msg += line + "\n"; } reader.close(); // 關(guān)閉流 } // 6. 斷開連接,釋放資源 connection.disconnect(); // 顯示響應(yīng)結(jié)果 System.out.println(msg); } catch (IOException e) { e.printStackTrace(); } } 

使用POST方式訪問HTTP

public static void main(String[] args) { try { // 1. 獲取訪問地址URL URL url = new URL("http://localhost:8080/Servlet/do_login.do"); // 2. 創(chuàng)建HttpURLConnection對象 HttpURLConnection connection = (HttpURLConnection) url .openConnection(); /* 3. 設(shè)置請求參數(shù)等 */ // 請求方式 connection.setRequestMethod("POST"); // 超時時間 connection.setConnectTimeout(3000); // 設(shè)置是否輸出 connection.setDoOutput(true); // 設(shè)置是否讀入 connection.setDoInput(true); // 設(shè)置是否使用緩存 connection.setUseCaches(false); // 設(shè)置此 HttpURLConnection 實例是否應(yīng)該自動執(zhí)行 HTTP 重定向 connection.setInstanceFollowRedirects(true); // 設(shè)置使用標(biāo)準(zhǔn)編碼格式編碼參數(shù)的名-值對 connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); // 連接 connection.connect(); /* 4. 處理輸入輸出 */ // 寫入?yún)?shù)到請求中 String params = "username=test&password=123456"; OutputStream out = connection.getOutputStream(); out.write(params.getBytes()); out.flush(); out.close(); // 從連接中讀取響應(yīng)信息 String msg = ""; int code = connection.getResponseCode(); if (code == 200) { BufferedReader reader = new BufferedReader( new InputStreamReader(connection.getInputStream())); String line;while ((line = reader.readLine()) != null) { msg += line + "\n"; } reader.close(); } // 5. 斷開連接 connection.disconnect();// 處理結(jié)果 System.out.println(msg); } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } 

4. HttpClient

(Android6.0之后的SDK中移除了對于HttpClient的支持,僅作了解)
在一般情況下,如果只是需要向Web站點的某個簡單頁面提交請求并獲取服務(wù)器響應(yīng),HttpURLConnection完全可以勝任。但在絕大部分情況下,Web站點的網(wǎng)頁可能沒這么簡單,這些頁面并不是通過一個簡單的URL就可訪問的,可能需要用戶登錄而且具有相應(yīng)的權(quán)限才可訪問該頁面。在這種情況下,就需要涉及Session、Cookie的處理了,如果打算使用HttpURLConnection來處理這些細(xì)節(jié),當(dāng)然也是可能實現(xiàn)的,只是處理起來難度就大了。

為了更好地處理向Web站點請求,包括處理Session、Cookie等細(xì)節(jié)問題,Apache開源組織提供了一個HttpClient項目。HttpClient就是一個增強版的HttpURLConnection,HttpURLConnection可以做的事情HttpClient全部可以做;HttpURLConnection沒有提供的有些功能,HttpClient也提供了,但它只是關(guān)注于如何發(fā)送請求、接收響應(yīng),以及管理HTTP連接。

(1) HttpClient的使用

步驟:
①:創(chuàng)建HttpClient對象
②:需要GET請求,創(chuàng)建HttpCet對象;需要POST請求,創(chuàng)建HttpPost對象
③:需要發(fā)送請求參數(shù),調(diào)用HttpGet、HttpPost共同的setParams(HttpParams params),HttpPost對象還可以使用setEntity(HttpEntity entity)方法來設(shè)置請求參數(shù)。
④:調(diào)用HttpClient對象的execute(HttpUriRequest request) HttpResponse發(fā)送請求,執(zhí)行該方法返回一個HttpResponse。
⑤:調(diào)用HttpResponse的getAllHeaders(),getHeader(String name)獲取響應(yīng)頭;調(diào)用HttpResponse的getEntity()獲取HttpEntity對象(包裝了服務(wù)器的響應(yīng)內(nèi)容)

(2) 使用GET方式訪問HTTP
public static void main(String[] args) { // 1. 創(chuàng)建HttpClient對象 CloseableHttpClient httpClient = HttpClientBuilder.create().build(); // 2. 創(chuàng)建HttpGet對象 HttpGet httpGet = new HttpGet( "http://localhost:8080/Servlet/do_login.do?username=test&password=123456"); CloseableHttpResponse response = null; try { // 3. 執(zhí)行GET請求 response = httpClient.execute(httpGet); System.out.println(response.getStatusLine()); // 4. 獲取響應(yīng)實體 HttpEntity entity = response.getEntity(); // 5. 處理響應(yīng)實體 if (entity != null) { System.out.println("長度:" + entity.getContentLength()); System.out.println("內(nèi)容:" + EntityUtils.toString(entity)); } } catch (ClientProtocolException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { // 6. 釋放資源 try { response.close(); httpClient.close(); } catch (IOException e) { e.printStackTrace(); } } } 
(3) 使用POST方式訪問HTTP
public static void main(String[] args) { // 1. 創(chuàng)建HttpClient對象 CloseableHttpClient httpClient = HttpClientBuilder.create().build(); // 2. 創(chuàng)建HttpPost對象 HttpPost post = new HttpPost( "http://localhost:8080/Servlet/do_login.do"); // 3. 設(shè)置POST請求傳遞參數(shù) List<NameValuePair> params = new ArrayList<NameValuePair>(); params.add(new BasicNameValuePair("username", "test")); params.add(new BasicNameValuePair("password", "12356")); try { UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params); post.setEntity(entity); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } // 4. 執(zhí)行請求并處理響應(yīng) try { CloseableHttpResponse response = httpClient.execute(post); HttpEntity entity = response.getEntity(); if (entity != null) { System.out.println("響應(yīng)內(nèi)容:"); System.out.println(EntityUtils.toString(entity)); } response.close(); } catch (ClientProtocolException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { // 釋放資源 try { httpClient.close(); } catch (IOException e) { e.printStackTrace(); } } } 

6.4 OkHttp

6.4.1 簡介

HttpClient是Apache基金會的一個開源網(wǎng)絡(luò)庫,功能十分強大,API數(shù)量眾多,但正是由于龐大的API數(shù)量使得我們很難在不破壞兼容性的情況下對它進行升級和擴展,所以Android團隊在提升和優(yōu)化HttpClient方面的工作態(tài)度并不積極。官方在Android 2.3以后就不建議用了,并且在Android 5.0以后廢棄了HttpClient,在Android 6.0更是刪除了HttpClient。

HttpURLConnection是一種多用途、輕量極的HTTP客戶端,提供的API比較簡單,可以容易地去使用和擴展。不過在Android 2.2版本之前,HttpURLConnection一直存在著一些令人厭煩的bug。比如說對一個可讀的InputStream調(diào)用close()方法時,就有可能會導(dǎo)致連接池失效了。那么我們通常的解決辦法就是直接禁用掉連接池的功能。因此一般推薦是在2.2之前使用HttpClient,因為其bug較少。在2.2之后推薦使用HttpURLConnection,因為API簡單、體積小、有壓縮和緩存機制,并且Android團隊后續(xù)會繼續(xù)優(yōu)化HttpURLConnection。

自從Android4.4開始,google已經(jīng)開始將源碼中的HttpURLConnection替換為OkHttp,而市面上流行的Retrofit同樣是使用OkHttp進行再次封裝而來的。

OkHttp是一個快速、高效的網(wǎng)絡(luò)請求庫,它的設(shè)計和實現(xiàn)的首要目標(biāo)便是高效,有如下特性:

  • 支持HTTP/2, HTTP/2通過使用多路復(fù)用技術(shù)在一個單獨的TCP連接上支持并發(fā), 通過在一個連接上一次性發(fā)送多個請求來發(fā)送或接收數(shù)據(jù);
  • 如果HTTP/2不可用, 連接池復(fù)用技術(shù)也可以極大減少延時;
  • 支持Gzip壓縮響應(yīng)體,降低傳輸內(nèi)容的大?。?/li>
  • 支持Http緩存,避免重復(fù)請求;
  • 如果您的服務(wù)器配置了多個IP地址, 當(dāng)?shù)谝粋€IP連接失敗的時候, OkHttp會自動嘗試下一個IP;
  • 使用Okio來簡化數(shù)據(jù)的訪問與存儲,提高性能;
  • OkHttp還處理了代理服務(wù)器問題和SSL握手失敗問題;

6.4.2 OkHttp類與Http請求響應(yīng)的映射

1. Http請求

http請求包含:請求方法, 請求地址, 請求協(xié)議, 請求頭, 請求體這五部分。這些都在okhttp3.Request的類中有體現(xiàn), 這個類正是代表http請求的類。

public final class Request { final HttpUrl url;//請求地址 final String method;//請求方法 final Headers headers;//請求頭 final RequestBody body;//請求體 final Object tag; ... } 

2. Http響應(yīng)

Http響應(yīng)由訪問協(xié)議, 響應(yīng)碼, 描述信息, 響應(yīng)頭, 響應(yīng)體來組成。

public final class Response implements Closeable { final Request request;//持有的請求 final Protocol protocol;//訪問協(xié)議 final int code;//響應(yīng)碼 final String message;//描述信息 final Handshake handshake;//SSL/TLS握手協(xié)議驗證時的信息, final Headers headers;//響應(yīng)頭 final ResponseBody body;//響應(yīng)體 ... } 

6.4.3 相關(guān)方法

1. OkHttpClient

  • OkHttpClient()
  • OkHttpClient(OkHttpClient.Builder builder)
  • newCall(Request request) Call
OkHttpClient.Builder
  • connectTimeout(long timeout, TimeUnit unit)
  • readTimeout(long timeout, TimeUnit unit)
  • writeTimeout(long timeout, TimeUnit unit)
  • pingInterval(long interval, TimeUnit unit)
  • cache(Cache cache) 入?yún)⑷纾?code>new Cache(File directory, long maxSize)
  • cookieJar(CookieJar cookieJar) CookieJar是一個接口
  • hostnameVerifier(HostnameVerifier hostnameVerifier) HostnameVerifier是一個接口,只有boolean verify(String hostname, SSLSession session)
  • sslSocketFactory(SSLSocketFactory sslSocketFactory, X509TrustManager trustManager)

2. Request

  • Request(Request.Builder builder)
Request.Builder
  • addHeader(String name, String value) 添加鍵值對,不會覆蓋
  • header(String name, String value) 添加鍵值對,會覆蓋
  • url(String url)
  • method(String method, RequestBody body)
  • post(RequestBody body) 本質(zhì):method("POST", body)
  • build() Request
RequestBody
  • create(MediaType contentType, final File file) RequestBody
  • create(MediaType contentType, String content) RequestBody
  • create(MediaType contentType, byte[] content) RequestBody
FormBody

RequestBody的子類

FormBody.Builder
  • add(String name, String value) FormBody.Builder
  • build() FormBody
MultipartBody

RequestBody的子類

MultipartBody.Builder
  • Builder()
  • Builder(String boundary)
  • setType(MediaType type)
  • addPart(Headers headers, RequestBody body)
  • addFormDataPart(String name, String filename, RequestBody body)
  • build() MultipartBody

3. Call

Call負(fù)責(zé)發(fā)送請求和讀取響應(yīng)

  • enqueue(Callback responseCallback)加入調(diào)度隊列,異步執(zhí)行
  • execute() Response 同步執(zhí)行
  • cancel()

4. Response

  • body() ResponseBody
  • code() int http請求的狀態(tài)碼
  • isSuccessful() code為2XX時,返回true,否則false
  • headers() Headers
ResponseBody
  • string() String
  • bytes() byte[]
  • byteStream() InputStream
  • charStream() Reader
  • contentLength() long

5. MediaType

RequestBody的數(shù)據(jù)格式都要指定Content-Type,就是指定MIME,常見的有三種:

  • application/x-www-form-urlencoded 數(shù)據(jù)是個普通表單(默認(rèn))
  • multipart/form-data 數(shù)據(jù)里有文件
  • application/json 數(shù)據(jù)是個json

方法:

  • parse(String string) MediaType
參數(shù) 說明
text/html HTML格式
text/plain 純文本格式
image/gif gif圖片格式
image/jpeg jpg圖片格式
image/png png圖片格式
application/json JSON數(shù)據(jù)格式
application/pdf pdf格式
application/msword Word文檔格式
application/octet-stream 二進制流數(shù)據(jù)
application/x-www-form-urlencoded 普通表單數(shù)據(jù)
multipart/form-data 表單數(shù)據(jù)里有文件

6. 自動管理Cookie

Request經(jīng)常都要攜帶Cookie,request創(chuàng)建時可以通過header設(shè)置參數(shù),Cookie也是參數(shù)之一。就像下面這樣:

Request request = new Request.Builder() .url(url) .header("Cookie", "xxx") .build(); 

然后可以從返回的response里得到新的Cookie,你可能得想辦法把Cookie保存起來。
但是OkHttp可以不用我們管理Cookie,自動攜帶,保存和更新Cookie。
方法是在創(chuàng)建OkHttpClient設(shè)置管理Cookie的CookieJar:

private final HashMap<String, List<Cookie>> cookieStore = new HashMap<>(); OkHttpClient okHttpClient = new OkHttpClient.Builder() .cookieJar(new CookieJar() { @Override public void saveFromResponse(HttpUrl httpUrl, List<Cookie> list) { cookieStore.put(httpUrl.host(), list); }@Override public List<Cookie> loadForRequest(HttpUrl httpUrl) { List<Cookie> cookies = cookieStore.get(httpUrl.host()); return cookies != null ? cookies : new ArrayList<Cookie>(); } }) .build(); 

這樣以后發(fā)送Request都不用管Cookie這個參數(shù)也不用去response獲取新Cookie什么的了。還能通過cookieStore獲取當(dāng)前保存的Cookie。
最后,new OkHttpClient()只是一種快速創(chuàng)建OkHttpClient的方式,更標(biāo)準(zhǔn)的是使用OkHttpClient.Builder()。后者可以設(shè)置一堆參數(shù),例如超時時間什么的。

6.4.4 get請求

1. 異步的get請求

//step 1: 創(chuàng)建 OkHttpClient 對象 OkHttpClient okHttpClient = new OkHttpClient(); //step 2: 創(chuàng)建一個請求,不指定請求方法時默認(rèn)是GET。 Request.Builder requestBuilder = new Request.Builder().url("http://www.baidu.com"); //可以省略,默認(rèn)是GET請求 requestBuilder.method("GET", null); //step 3:創(chuàng)建 Call 對象 Call call = okHttpClient.newCall(requestBuilder.build()); //step 4: 開始異步請求 call.enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { }@Override public void onResponse(Call call, Response response) throws IOException { //獲得返回體 ResponseBody body = response.body(); } }); 

首先要將OkHttpClient的對象與Request的對象建立起來聯(lián)系,使用okHttpClient的newCall()方法得到一個Call對象,這個Call對象的作用就是相當(dāng)于將請求封裝成了一個任務(wù),既然是任務(wù),自然就會有execute()cancel()等方法。

最后,我們希望以異步的方式去執(zhí)行請求,所以我們調(diào)用的是call.enqueue,將call加入調(diào)度隊列,然后等待任務(wù)執(zhí)行完成,我們在Callback中即可得到結(jié)果。但要注意的是,call的回調(diào)是子線程,所以是不能直接操作界面的。當(dāng)請求成功時就會回調(diào)onResponse()方法,我們可以看到返回的結(jié)果是Response對象,在此我們比較關(guān)注的是請求中的返回體body(ResponseBody類型),大多數(shù)的情況下我們希望獲得字符串從而進行json解析獲得數(shù)據(jù),所以可以通過body.string()的方式獲得字符串。如果希望獲得返回的二進制字節(jié)數(shù)組,則調(diào)用response.body().bytes();如果你想拿到返回的inputStream,則調(diào)用response.body().byteStream()。

2. 同步的get請求

調(diào)用Call#execute()方法,在主線程運行

6.4.5 post請求

1. Post上傳表單(鍵值對)

//step1: 同樣的需要創(chuàng)建一個OkHttpClick對象 OkHttpClient okHttpClient = new OkHttpClient(); //step2: 創(chuàng)建 FormBody.Builder FormBody formBody = new FormBody.Builder() .add("name", "dsd") //添加鍵值對 .build(); //step3: 創(chuàng)建請求 Request request = new Request.Builder().url("http://www.baidu.com") .post(formBody) .build() //step4: 建立聯(lián)系 創(chuàng)建Call對象 okHttpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { }@Override public void onResponse(Call call, Response response) throws IOException { } }); 

2. Post異步上傳文件

// step 1: 創(chuàng)建 OkHttpClient 對象 OkHttpClient okHttpClient = new OkHttpClient(); //step 2:創(chuàng)建 RequestBody 以及所需的參數(shù) //2.1 獲取文件 File file = new File(Environment.getExternalStorageDirectory() + "test.txt"); //2.2 創(chuàng)建 MediaType 設(shè)置上傳文件類型 MediaType MEDIATYPE = MediaType.parse("text/plain; charset=utf-8"); //2.3 獲取請求體 RequestBody requestBody = RequestBody.create(MEDIATYPE, file); //step 3:創(chuàng)建請求 Request request = new Request.Builder().url("http://www.baidu.com") .post(requestBody) .build(); //step 4 建立聯(lián)系 okHttpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { }@Override public void onResponse(Call call, Response response) throws IOException { } }); 

3. Post方式提交流

以流的方式POST提交請求體. 請求體的內(nèi)容由流寫入產(chǎn)生. 這個例子是流直接寫入Okio的BufferedSink. 你的程序可能會使用OutputStream, 你可以使用BufferedSink.outputStream()來獲取. OkHttp的底層對流和字節(jié)的操作都是基于Okio庫, Okio庫也是Square開發(fā)的另一個IO庫, 填補I/O和NIO的空缺, 目的是提供簡單便于使用的接口來操作IO.

public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("text/x-markdown; charset=utf-8"); private final OkHttpClient client = new OkHttpClient();public void run() throws Exception { RequestBody requestBody = new RequestBody() { @Override public MediaType contentType() { return MEDIA_TYPE_MARKDOWN; }@Override public void writeTo(BufferedSink sink) throws IOException { sink.writeUtf8("Numbers\n"); sink.writeUtf8("-------\n"); for (int i = 2; i <= 997; i++) { sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i))); } }private String factor(int n) { for (int i = 2; i < n; i++) { int x = n / i; if (x * i == n) return factor(x) + " × " + i; } return Integer.toString(n); } };Request request = new Request.Builder() .url("https://api.github.com/markdown/raw") .post(requestBody) .build();Response response = client.newCall(request).execute(); if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println(response.body().string()); } 

4. Post方式提交String

下面是使用HTTP POST提交請求到服務(wù). 這個例子提交了一個markdown文檔到web服務(wù), 以HTML方式渲染markdown. 因為整個請求體都在內(nèi)存中, 因此避免使用此api提交大文檔(大于1MB).

public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("text/x-markdown; charset=utf-8");private final OkHttpClient client = new OkHttpClient();public void run() throws Exception { String postBody = "" + "Releases\n" + "--------\n" + "\n" + " * _1.0_ May 6, 2013\n" + " * _1.1_ June 15, 2013\n" + " * _1.2_ August 11, 2013\n";Request request = new Request.Builder() .url("https://api.github.com/markdown/raw") .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody)) .build();Response response = client.newCall(request).execute(); if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);System.out.println(response.body().string()); } 

5、Post方式提交分塊請求

MultipartBody.Builder可以構(gòu)建復(fù)雜的請求體, 與HTML文件上傳形式兼容. 多塊請求體中每塊請求都是一個請求體, 可以定義自己的請求頭. 這些請求頭可以用來描述這塊請求, 例如它的Content-Disposition. 如果Content-Length和Content-Type可用的話, 他們會被自動添加到請求頭中.

private static final String IMGUR_CLIENT_ID = "..."; private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");private final OkHttpClient client = new OkHttpClient();public void run() throws Exception { // Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image RequestBody requestBody = new MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart("title", "Square Logo") .addFormDataPart("image", "logo-square.png", RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png"))) .build();Request request = new Request.Builder() .header("Authorization", "Client-ID " + IMGUR_CLIENT_ID) .url("https://api.imgur.com/3/image") .post(requestBody) .build();Response response = client.newCall(request).execute(); if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);System.out.println(response.body().string()); } 

六、其他用法

1、提取響應(yīng)頭

典型的HTTP頭是一個Map<String, String> : 每個字段都有一個或沒有值. 但是一些頭允許多個值。
當(dāng)寫請求頭的時候, 使用header(name, value)可以設(shè)置唯一的name、value. 如果已經(jīng)有值, 舊的將被移除, 然后添加新的. 使用addHeader(name, value)可以添加多值(添加, 不移除已有的).
當(dāng)讀取響應(yīng)頭時, 使用header(name)返回最后出現(xiàn)的name、value. 通常情況這也是唯一的name、value. 如果沒有值, 那么header(name)將返回null. 如果想讀取字段對應(yīng)的所有值, 使用headers(name)`會返回一個list.
為了獲取所有的Header, Headers類支持按index訪問.

private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { Request request = new Request.Builder() .url("https://api.github.com/repos/square/okhttp/issues") .header("User-Agent", "OkHttp Headers.java") .addHeader("Accept", "application/json; q=0.5") .addHeader("Accept", "application/vnd.github.v3+json") .build(); Response response = client.newCall(request).execute(); if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println("Server: " + response.header("Server")); System.out.println("Date: " + response.header("Date")); System.out.println("Vary: " + response.headers("Vary")); } 

2、使用Gson來解析JSON響應(yīng)

Gson是一個在JSON和Java對象之間轉(zhuǎn)換非常方便的api庫. 這里我們用Gson來解析Github API的JSON響應(yīng).
注意: ResponseBody.charStream()使用響應(yīng)頭Content-Type指定的字符集來解析響應(yīng)體. 默認(rèn)是UTF-8.

private final OkHttpClient client = new OkHttpClient(); private final Gson gson = new Gson();public void run() throws Exception { Request request = new Request.Builder() .url("https://api.github.com/gists/c2a7c39532239ff261be") .build(); Response response = client.newCall(request).execute(); if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);Gist gist = gson.fromJson(response.body().charStream(), Gist.class); for (Map.Entry<String, GistFile> entry : gist.files.entrySet()) { System.out.println(entry.getKey()); System.out.println(entry.getValue().content); } }static class Gist { Map<String, GistFile> files; }static class GistFile { String content; } 

3、響應(yīng)緩存

OKHTTP如果要設(shè)置緩存,首要的條件就是設(shè)置一個緩存文件夾,在Android中為了安全起見,一般設(shè)置為私密數(shù)據(jù)空間。通過getExternalCacheDir()獲取。
如然后通過調(diào)用OKHttpClient.Builder中的cache()方法。如下面代碼所示:

//緩存文件夾 File cacheFile = new File(getExternalCacheDir().toString(),"cache"); //緩存大小為10M int cacheSize = 10 * 1024 * 1024; //創(chuàng)建緩存對象 Cache cache = new Cache(cacheFile,cacheSize);OkHttpClient client = new OkHttpClient.Builder() .cache(cache) .build(); 

設(shè)置好Cache我們就可以正常訪問了。我們可以通過獲取到的Response對象拿到它正常的消息和緩存的消息。

Response的消息有兩種類型,CacheResponse和NetworkResponse。CacheResponse代表從緩存取到的消息,NetworkResponse代表直接從服務(wù)端返回的消息。
示例代碼如下:

private void testCache(){ //緩存文件夾 File cacheFile = new File(getExternalCacheDir().toString(),"cache"); //緩存大小為10M int cacheSize = 10 * 1024 * 1024; //創(chuàng)建緩存對象 final Cache cache = new Cache(cacheFile,cacheSize);new Thread(new Runnable() { @Override public void run() { OkHttpClient client = new OkHttpClient.Builder() .cache(cache) .build(); //官方的一個示例的url String url = "http://publicobject.com/helloworld.txt";Request request = new Request.Builder() .url(url) .build(); Call call1 = client.newCall(request); Response response1 = null; try { //第一次網(wǎng)絡(luò)請求 response1 = call1.execute(); Log.i(TAG, "testCache: response1 :"+response1.body().string()); Log.i(TAG, "testCache: response1 cache :"+response1.cacheResponse()); Log.i(TAG, "testCache: response1 network :"+response1.networkResponse()); response1.body().close(); } catch (IOException e) { e.printStackTrace(); }Call call12 = client.newCall(request);try { //第二次網(wǎng)絡(luò)請求 Response response2 = call12.execute(); Log.i(TAG, "testCache: response2 :"+response2.body().string()); Log.i(TAG, "testCache: response2 cache :"+response2.cacheResponse()); Log.i(TAG, "testCache: response2 network :"+response2.networkResponse()); Log.i(TAG, "testCache: response1 equals response2:"+response2.equals(response1)); response2.body().close(); } catch (IOException e) { e.printStackTrace(); } } }).start();} 

我們在上面的代碼中,用同一個url地址分別進行了兩次網(wǎng)絡(luò)訪問,然后分別用Log打印它們的信息。打印的結(jié)果主要說明了一個現(xiàn)象,第一次訪問的時候,Response的消息是NetworkResponse消息,此時CacheResponse的值為Null.而第二次訪問的時候Response是CahceResponse,而此時NetworkResponse為空。也就說明了上面的示例代碼能夠進行網(wǎng)絡(luò)請求的緩存。

其實控制緩存的消息頭往往是服務(wù)端返回的信息中添加的如”Cache-Control:max-age=60”。所以,會有兩種情況。

  1. 客戶端和服務(wù)端開發(fā)能夠很好溝通,按照達(dá)成一致的協(xié)議,服務(wù)端按照規(guī)定添加緩存相關(guān)的消息頭。
  2. 客戶端與服務(wù)端的開發(fā)根本就不是同一家公司,沒有辦法也不可能要求服務(wù)端按照客戶端的意愿進行開發(fā)。

第一種辦法當(dāng)然很好,只要服務(wù)器在返回消息的時候添加好Cache-Control相關(guān)的消息便好。

第二種情況,就很麻煩,你真的無法左右別人的行為。怎么辦呢?好在OKHTTP能夠很輕易地處理這種情況。那就是定義一個攔截器,
人為地添加Response中的消息頭,然后再傳遞給用戶,這樣用戶拿到的Response就有了我們理想當(dāng)中的消息頭Headers,從而達(dá)到控制緩存的意圖,正所謂移花接木。

緩存之?dāng)r截器

因為攔截器可以拿到Request和Response,所以可以輕而易舉地加工這些東西。在這里我們?nèi)藶榈靥砑覥ache-Control消息頭。

class CacheInterceptor implements Interceptor{@Override public Response intercept(Chain chain) throws IOException {Response originResponse = chain.proceed(chain.request());//設(shè)置緩存時間為60秒,并移除了pragma消息頭,移除它的原因是因為pragma也是控制緩存的一個消息頭屬性 return originResponse.newBuilder().removeHeader("pragma") .header("Cache-Control","max-age=60").build(); } } 

定義好攔截器中后,我們可以添加到OKHttpClient中了。

private void testCacheInterceptor(){ //緩存文件夾 File cacheFile = new File(getExternalCacheDir().toString(),"cache"); //緩存大小為10M int cacheSize = 10 * 1024 * 1024; //創(chuàng)建緩存對象 final Cache cache = new Cache(cacheFile,cacheSize);OkHttpClient client = new OkHttpClient.Builder() .addNetworkInterceptor(new CacheInterceptor()) .cache(cache) .build(); ....... } 

代碼后面部分有省略。主要通過在OkHttpClient.Builder()中addNetworkInterceptor()中添加。而這樣也挺簡單的,就幾步完成了緩存代碼。

攔截器進行緩存的缺點

  1. 網(wǎng)絡(luò)訪問請求的資源是文本信息,如新聞列表,這類信息經(jīng)常變動,一天更新好幾次,它們用的緩存時間應(yīng)該就很短。
  2. 網(wǎng)絡(luò)訪問請求的資源是圖片或者視頻,它們變動很少,或者是長期不變動,那么它們用的緩存時間就應(yīng)該很長。

那么,問題來了。
因為OKHTTP開發(fā)建議是同一個APP,用同一個OKHTTPCLIENT對象這是為了只有一個緩存文件訪問入口。這個很容易理解,單例模式嘛。但是問題攔截器是在OKHttpClient.Builder當(dāng)中添加的。如果在攔截器中定義緩存的方法會導(dǎo)致圖片的緩存和新聞列表的緩存時間是一樣的,這顯然是不合理的,真實的情況不應(yīng)該是圖片請求有它的緩存時間,新聞列表請求有它的緩存時間,應(yīng)該是每一個Request有它的緩存時間。 那么,有解決的方案嗎? 有的,okhttp官方有建議的方法。

okhttp官方文檔建議緩存方法

okhttp中建議用CacheControl這個類來進行緩存策略的制定。
它內(nèi)部有兩個很重要的靜態(tài)實例。

/**強制使用網(wǎng)絡(luò)請求*/ public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();/*** 強制性使用本地緩存,如果本地緩存不滿足條件,則會返回code為504*/ public static final CacheControl FORCE_CACHE = new Builder() .onlyIfCached() .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS) .build(); 

我們看到FORCE_NETWORK常量用來強制使用網(wǎng)絡(luò)請求。FORCE_CACHE只取本地的緩存。它們本身都是CacheControl對象,由內(nèi)部的Buidler對象構(gòu)造。下面我們來看看CacheControl.Builder

CacheControl.Builder

它有如下方法:

  • noCache()//不使用緩存,用網(wǎng)絡(luò)請求
  • noStore()//不使用緩存,也不存儲緩存
  • onlyIfCached()//只使用緩存
  • noTransform()//禁止轉(zhuǎn)碼
  • maxAge(10, TimeUnit.MILLISECONDS)//設(shè)置超時時間為10ms。
  • maxStale(10, TimeUnit.SECONDS)//超時之外的超時時間為10s
  • minFresh(10, TimeUnit.SECONDS)//超時時間為當(dāng)前時間加上10秒鐘。

知道了CacheControl的相關(guān)信息,那么它怎么使用呢?不同于攔截器設(shè)置緩存,CacheControl是針對Request的,所以它可以針對每個請求設(shè)置不同的緩存策略。比如圖片和新聞列表。下面代碼展示如何用CacheControl設(shè)置一個60秒的超時時間。

private void testCacheControl(){ //緩存文件夾 File cacheFile = new File(getExternalCacheDir().toString(),"cache"); //緩存大小為10M int cacheSize = 10 * 1024 * 1024; //創(chuàng)建緩存對象 final Cache cache = new Cache(cacheFile,cacheSize);new Thread(new Runnable() { @Override public void run() { OkHttpClient client = new OkHttpClient.Builder() .cache(cache) .build(); //設(shè)置緩存時間為60秒 CacheControl cacheControl = new CacheControl.Builder() .maxAge(60, TimeUnit.SECONDS) .build(); Request request = new Request.Builder() .url("http://blog.csdn.net/briblue") .cacheControl(cacheControl) .build(); try { Response response = client.newCall(request).execute(); response.body().close(); } catch (IOException e) { e.printStackTrace(); } } }).start();} 

強制使用緩存

前面有講CacheControl.FORCE_CACHE這個常量。

public static final CacheControl FORCE_CACHE = new Builder() .onlyIfCached() .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS) 

它內(nèi)部其實就是調(diào)用onlyIfCached()和maxStale方法。
它的使用方法為

Request request = new Request.Builder() .url("http://blog.csdn.net/briblue") .cacheControl(Cache.FORCE_CACHE) .build(); 

但是如前面后提到的,如果緩存不符合條件會返回504.這個時候我們要根據(jù)情況再進行編碼,如緩存不行就再進行一次網(wǎng)絡(luò)請求。

Response forceCacheResponse = client.newCall(request).execute();if (forceCacheResponse.code() != 504) {// 資源已經(jīng)緩存了,可以直接使用} else {// 資源沒有緩存,或者是緩存不符合條件了。}

不使用緩存

前面也有講CacheControl.FORCE_NETWORK這個常量。

public static final CacheControl FORCE_NETWORK = new Builder().noCache().build(); 

它的內(nèi)部其實是調(diào)用noCache()方法,也就是不緩存的意思。
它的使用方法為

Request request = new Request.Builder() .url("http://blog.csdn.net/briblue") .cacheControl(Cache.FORCE_NETWORK) .build(); 

還有一種情況將maxAge設(shè)置為0,也不會取緩存,直接走網(wǎng)絡(luò)。

Request request = new Request.Builder() .url("http://blog.csdn.net/briblue") .cacheControl(new CacheControl.Builder() .maxAge(0, TimeUnit.SECONDS)) .build(); 

4、取消一個Call

使用Call.cancel()可以立即停止掉一個正在執(zhí)行的call. 如果一個線程正在寫請求或者讀響應(yīng), 將會引發(fā)IOException. 當(dāng)call沒有必要的時候, 使用這個api可以節(jié)約網(wǎng)絡(luò)資源. 例如當(dāng)用戶離開一個應(yīng)用時, 不管同步還是異步的call都可以取消.
你可以通過tags來同時取消多個請求. 當(dāng)你構(gòu)建一請求時, 使用RequestBuilder.tag(tag)來分配一個標(biāo)簽, 之后你就可以用OkHttpClient.cancel(tag)來取消所有帶有這個tag的call.

 private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);private final OkHttpClient client = new OkHttpClient();public void run() throws Exception { Request request = new Request.Builder() .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay. .build();final long startNanos = System.nanoTime(); final Call call = client.newCall(request);// Schedule a job to cancel the call in 1 second. executor.schedule(new Runnable() { @Override public void run() { System.out.printf("%.2f Canceling call.%n", (System.nanoTime() - startNanos) / 1e9f); call.cancel(); System.out.printf("%.2f Canceled call.%n", (System.nanoTime() - startNanos) / 1e9f); } }, 1, TimeUnit.SECONDS);try { System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f); Response response = call.execute(); System.out.printf("%.2f Call was expected to fail, but completed: %s%n", (System.nanoTime() - startNanos) / 1e9f, response); } catch (IOException e) { System.out.printf("%.2f Call failed as expected: %s%n", (System.nanoTime() - startNanos) / 1e9f, e); } } 

5、超時

沒有響應(yīng)時使用超時結(jié)束call. 沒有響應(yīng)的原因可能是客戶點鏈接問題、服務(wù)器可用性問題或者這之間的其他東西. OkHttp支持連接超時, 讀取超時和寫入超時.

private final OkHttpClient client;public ConfigureTimeouts() throws Exception { client = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build(); }public void run() throws Exception { Request request = new Request.Builder() .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay. .build();Response response = client.newCall(request).execute(); System.out.println("Response completed: " + response); } 

6、每個call的配置

使用OkHttpClient, 所有的HTTP Client配置包括代理設(shè)置、超時設(shè)置、緩存設(shè)置. 當(dāng)你需要為單個call改變配置的時候, 調(diào)用OkHttpClient.newBuilder(). 這個api將會返回一個builder, 這個builder和原始的client共享相同的連接池, 分發(fā)器和配置.
下面的例子中,我們讓一個請求是500ms的超時、另一個是3000ms的超時。

private final OkHttpClient client = new OkHttpClient();public void run() throws Exception { Request request = new Request.Builder() .url("http://httpbin.org/delay/1") // This URL is served with a 1 second delay. .build();try { // Copy to customize OkHttp for this request. OkHttpClient copy = client.newBuilder() .readTimeout(500, TimeUnit.MILLISECONDS) .build();Response response = copy.newCall(request).execute(); System.out.println("Response 1 succeeded: " + response); } catch (IOException e) { System.out.println("Response 1 failed: " + e); }try { // Copy to customize OkHttp for this request. OkHttpClient copy = client.newBuilder() .readTimeout(3000, TimeUnit.MILLISECONDS) .build();Response response = copy.newCall(request).execute(); System.out.println("Response 2 succeeded: " + response); } catch (IOException e) { System.out.println("Response 2 failed: " + e); } } 

七、處理驗證

這部分和HTTP AUTH有關(guān).

1、HTTP AUTH

使用HTTP AUTH需要在server端配置http auth信息, 其過程如下:

  • 客戶端發(fā)送http請求(沒有Authorization header)
  • 服務(wù)器發(fā)現(xiàn)配置了http auth, 于是檢查request里面有沒有"Authorization"的http header
    如果有, 則判斷Authorization里面的內(nèi)容是否在用戶列表里面, Authorization header的典型數(shù)據(jù)為"Authorization: Basic jdhaHY0=", 其中Basic表示基礎(chǔ)認(rèn)證, jdhaHY0=是base64編碼的"user:passwd"字符串. 如果沒有,或者用戶密碼不對,則返回http code 401頁面給客戶端.
  • 標(biāo)準(zhǔn)的http瀏覽器在收到401頁面之后, 應(yīng)該彈出一個對話框讓用戶輸入帳號密碼; 并在用戶點確認(rèn)的時候再次發(fā)出請求, 這次請求里面將帶上Authorization header.
  • 服務(wù)器端認(rèn)證通過,并返回頁面
  • 瀏覽器顯示頁面
    一次典型的訪問場景是:

2、OkHttp認(rèn)證

OkHttp會自動重試未驗證的請求. 當(dāng)響應(yīng)是401 Not Authorized時,Authenticator會被要求提供證書. Authenticator的實現(xiàn)中需要建立一個新的包含證書的請求. 如果沒有證書可用, 返回null來跳過嘗試.
使用Response.challenges()來獲得任何authentication challenges的 schemes 和 realms. 當(dāng)完成一個Basic challenge, 使用Credentials.basic(username, password)來解碼請求頭.

private final OkHttpClient client;public Authenticate() { client = new OkHttpClient.Builder() .authenticator(new Authenticator() { @Override public Request authenticate(Route route, Response response) throws IOException { System.out.println("Authenticating for response: " + response); System.out.println("Challenges: " + response.challenges()); String credential = Credentials.basic("jesse", "password1"); return response.request().newBuilder() .header("Authorization", credential) .build(); } }) .build(); }public void run() throws Exception { Request request = new Request.Builder() .url("http://publicobject.com/secrets/hellosecret.txt") .build();Response response = client.newCall(request).execute(); if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);System.out.println(response.body().string()); } 

當(dāng)認(rèn)證無法工作時, 為了避免多次重試, 你可以返回空來放棄認(rèn)證. 例如, 當(dāng)exact credentials已經(jīng)嘗試過, 你可能會直接想跳過認(rèn)證, 可以這樣做:

if (credential.equals(response.request().header("Authorization"))) { return null; // If we already failed with these credentials, don't retry.} 

當(dāng)重試次數(shù)超過定義的次數(shù), 你若想跳過認(rèn)證, 可以這樣做:

if (responseCount(response) >= 3) { return null; // If we've failed 3 times, give up. }private int responseCount(Response response) { int result = 1; while ((response = response.priorResponse()) != null) { result++; } return result; } 

參考文獻(xiàn)

OkHttp使用完全教程


本頁內(nèi)容由塔燈網(wǎng)絡(luò)科技有限公司通過網(wǎng)絡(luò)收集編輯所得,所有資料僅供用戶學(xué)習(xí)參考,本站不擁有所有權(quán),如您認(rèn)為本網(wǎng)頁中由涉嫌抄襲的內(nèi)容,請及時與我們聯(lián)系,并提供相關(guān)證據(jù),工作人員會在5工作日內(nèi)聯(lián)系您,一經(jīng)查實,本站立刻刪除侵權(quán)內(nèi)容。本文鏈接:http://jstctz.cn/20440.html
相關(guān)開發(fā)語言
 八年  行業(yè)經(jīng)驗

多一份參考,總有益處

聯(lián)系深圳網(wǎng)站公司塔燈網(wǎng)絡(luò),免費獲得網(wǎng)站建設(shè)方案及報價

咨詢相關(guān)問題或預(yù)約面談,可以通過以下方式與我們聯(lián)系

業(yè)務(wù)熱線:余經(jīng)理:13699882642

Copyright ? 2013-2018 Tadeng NetWork Technology Co., LTD. All Rights Reserved.    

  • QQ咨詢
  • 在線咨詢
  • 官方微信
  • 聯(lián)系電話
    座機0755-29185426
    手機13699882642
  • 預(yù)約上門
  • 返回頂部