為您解碼網(wǎng)站建設(shè)的點(diǎn)點(diǎn)滴滴
發(fā)表日期:2018-02 文章編輯:小燈 瀏覽次數(shù):2911
分為三個(gè)部分:公鑰、私鑰、加密/解密算法
加密解密過(guò)程如下:
加密:通過(guò)加密算法和公鑰對(duì)內(nèi)容(或者說(shuō)明文)進(jìn)行加密,得到密文。
解密:通過(guò)解密算法和私鑰對(duì)密文進(jìn)行解密,得到明文。
注意:由公鑰加密的內(nèi)容,只能由私鑰進(jìn)行解密。
公鑰密碼體制的公鑰和算法都是公開(kāi)的,私鑰是保密的。在實(shí)際的使用中,有需要的人會(huì)生成一對(duì)公鑰和私鑰,把公鑰發(fā)布出去給別人使用,自己保留私鑰。
一種公鑰密碼體制,公鑰公開(kāi),私鑰保密,它的加密解密算法是公開(kāi)的。 RSA的這一對(duì)公鑰、私鑰都可以用來(lái)加密和解密,并且一方加密的內(nèi)容可以由并且只能由對(duì)方進(jìn)行解密。
就是在信息的后面再加上一段內(nèi)容,可以證明信息沒(méi)有被修改過(guò)。
一般是對(duì)信息做一個(gè)hash計(jì)算得到一個(gè)hash值(該過(guò)程不可逆),在把信息發(fā)送出去時(shí),把這個(gè)hash值加密后做為一個(gè)簽名和信息一起發(fā)出去。 接收方在收到信息后,會(huì)重新計(jì)算信息的hash值,并和信息所附帶的hash值(解密后)進(jìn)行對(duì)比,如果一致,就說(shuō)明信息的內(nèi)容沒(méi)有被修改過(guò),因?yàn)檫@里hash計(jì)算可以保證不同的內(nèi)容一定會(huì)得到不同的hash值,所以只要內(nèi)容一被修改,根據(jù)信息內(nèi)容計(jì)算的hash值就會(huì)變化。
當(dāng)然,不懷好意的人也可以修改信息內(nèi)容的同時(shí)也修改hash值,從而讓它們可以相匹配,為了防止這種情況,hash值一般都會(huì)加密后(也就是簽名)再和信息一起發(fā)送,以保證這個(gè)hash值不被修改。
“客戶”->“服務(wù)器”:你好
“服務(wù)器”->“客戶”:你好,我是服務(wù)器
“客戶”->“服務(wù)器”:向我證明你就是服務(wù)器
“服務(wù)器”->“客戶”:你好,我是服務(wù)器{你好,我是服務(wù)器}[私鑰|RSA]
“客戶”->“服務(wù)器”:{我們后面的通信過(guò)程,用對(duì)稱加密來(lái)進(jìn)行,這里是對(duì)稱加密算法和密鑰}[公鑰|RSA]
“服務(wù)器”->“客戶”:{OK,收到!}[密鑰|對(duì)稱加密算法]
“客戶”->“服務(wù)器”:{我的帳號(hào)是aaa,密碼是123,把我的余額的信息發(fā)給我看看}[密鑰|對(duì)稱加密算法]
“服務(wù)器”->“客戶”:{你的余額是100元}[密鑰|對(duì)稱加密算法]
總結(jié)一下,RSA加密算法在這個(gè)通信過(guò)程中所起到的作用主要有兩個(gè):
1. 因?yàn)樗借€只有“服務(wù)器”擁有,因此“客戶”可以通過(guò)判斷對(duì)方是否有私鑰來(lái)判斷對(duì)方是否是“服務(wù)器”。
2. 客戶端通過(guò)RSA的掩護(hù),安全的和服務(wù)器商量好一個(gè)對(duì)稱加密算法和密鑰來(lái)保證后面通信過(guò)程內(nèi)容的安全。
但是這里還留有一個(gè)問(wèn)題,“服務(wù)器”要對(duì)外發(fā)布公鑰,那“服務(wù)器”如何把公鑰發(fā)送給“客戶”呢?
我們可能會(huì)想到以下的兩個(gè)方法:
a) 把公鑰放到互聯(lián)網(wǎng)的某個(gè)地方的一個(gè)下載地址,事先給“客戶”去下載。
b) 每次和“客戶”開(kāi)始通信時(shí),“服務(wù)器”把公鑰發(fā)給“客戶”。
但是這個(gè)兩個(gè)方法都有一定的問(wèn)題,
對(duì)于a)方法,“客戶”無(wú)法確定這個(gè)下載地址是不是“服務(wù)器”發(fā)布的,你憑什么就相信這個(gè)
地址下載的東西就是“服務(wù)器”發(fā)布的而不是別人偽造的呢,萬(wàn)一下載到一個(gè)假的怎么辦?另外要所有的“客戶”都在通信前事先去下載公鑰也很不現(xiàn)實(shí)。
對(duì)于b)方法,也有問(wèn)題,因?yàn)槿魏稳硕伎梢宰约荷梢粚?duì)公鑰和私鑰,他只要向“客戶”發(fā)送他
自己的私鑰就可以冒充“服務(wù)器”了。示意如下:
“客戶”->“黑客”:你好//黑客截獲“客戶”發(fā)給“服務(wù)器”的消息
“黑客”->“客戶”:你好,我是服務(wù)器,這個(gè)是我的公鑰//黑客自己生成一對(duì)公鑰和私鑰,把
公鑰發(fā)給“客戶”,自己保留私鑰
“客戶”->“黑客”:向我證明你就是服務(wù)器
“黑客”->“客戶”:你好,我是服務(wù)器 {你好,我是服務(wù)器}[黑客自己的私鑰|RSA]//客戶收到
“黑客”用私鑰加密的信息后,是可以用“黑客”發(fā)給自己的公鑰解密的,從而會(huì)誤認(rèn)為“黑客”是“服務(wù)器”因此“黑客”只需要自己生成一對(duì)公鑰和私鑰,然后把公鑰發(fā)送給“客戶”,自己保留私鑰,這樣由于“客戶”可以用黑客的公鑰解密黑客的私鑰加密的內(nèi)容,“客戶”就會(huì)相信“黑客”是“服務(wù)器”,從而導(dǎo)致了安全問(wèn)題。這里問(wèn)題的根源就在于,大家都可以生成公鑰、私鑰對(duì),無(wú)法確認(rèn)公鑰對(duì)到底是誰(shuí)的。 如果能夠確定公鑰到底是誰(shuí)的,就不會(huì)有這個(gè)問(wèn)題了。例如,如果收到“黑客”冒充“服務(wù)器”發(fā)過(guò)來(lái)的公鑰,經(jīng)過(guò)某種檢查,如果能夠發(fā)現(xiàn)這個(gè)公鑰不是“服務(wù)器”的就好了。
為了解決上述問(wèn)題,數(shù)字證書(shū)出現(xiàn)了,它可以解決我們上面的問(wèn)題。先大概看下什么是數(shù)字證書(shū),一個(gè)證書(shū)包含下面的具體內(nèi)容:
指紋和指紋算法
這個(gè)是用來(lái)保證證書(shū)的完整性的,也就是說(shuō)確保證書(shū)沒(méi)有被修改過(guò)。 其原理就是在發(fā)布證書(shū)時(shí),發(fā)布者根據(jù)指紋算法(一個(gè)hash算法)計(jì)算整個(gè)證書(shū)的hash值(指紋)并和證書(shū)放在一起,使用者在打開(kāi)證書(shū)時(shí),自己也根據(jù)指紋算法計(jì)算一下證書(shū)的hash值(指紋),如果和剛開(kāi)始的值對(duì)得上,就說(shuō)明證書(shū)沒(méi)有被修改過(guò),因?yàn)樽C書(shū)的內(nèi)容被修改后,根據(jù)證書(shū)的內(nèi)容計(jì)算的出的hash值(指紋)是會(huì)變化的。
注意,這個(gè)指紋會(huì)用"SecureTrust CA"這個(gè)證書(shū)機(jī)構(gòu)的私鑰用簽名算法加密后和證書(shū)放在一起。
簽名算法
就是指的這個(gè)數(shù)字證書(shū)的數(shù)字簽名所使用的加密算法,這樣就可以使用證書(shū)發(fā)布機(jī)構(gòu)的證書(shū)里面的公鑰,根據(jù)這個(gè)算法對(duì)指紋進(jìn)行解密。指紋的加密結(jié)果就是數(shù)字簽名
數(shù)字證書(shū)可以保證數(shù)字證書(shū)里的公鑰確實(shí)是這個(gè)證書(shū)的所有者(Subject)的,或者證書(shū)可以用來(lái)確認(rèn)對(duì)方的身份。也就是說(shuō),我們拿到一個(gè)數(shù)字證書(shū),我們可以判斷出這個(gè)數(shù)字證書(shū)到底是誰(shuí)的。至于是如何判斷的,后面會(huì)在詳細(xì)討論數(shù)字證書(shū)時(shí)詳細(xì)解釋。現(xiàn)在把前面的通信過(guò)程使用數(shù)字證書(shū)修改為如下:
“客戶”->“服務(wù)器”:你好
“服務(wù)器”->“客戶”:你好,我是服務(wù)器,這里是我的數(shù)字證書(shū)//這里用證書(shū)代替了公鑰
“客戶”->“服務(wù)器”:向我證明你就是服務(wù)器
“服務(wù)器”->“客戶”:你好,我是服務(wù)器 {你好,我是服務(wù)器}[私鑰|RSA]
在每次發(fā)送信息時(shí),先對(duì)信息的內(nèi)容進(jìn)行一個(gè)hash計(jì)算得出一個(gè)hash值,將信息的內(nèi)容和這個(gè)hash值一起加密后發(fā)送。接收方在收到后進(jìn)行解密得到明文的內(nèi)容和hash值,然后接收方再自己對(duì)收到信息內(nèi)容做一次hash計(jì)算,與收到的hash值進(jìn)行對(duì)比看是否匹配,如果匹配就說(shuō)明信息在傳輸過(guò)程中沒(méi)有被修改過(guò)。如果不匹配說(shuō)明中途有人故意對(duì)加密數(shù)據(jù)進(jìn)行了修改,立刻中斷通話過(guò)程后做其它處理。
如何向證書(shū)的發(fā)布機(jī)構(gòu)去申請(qǐng)證書(shū)
舉個(gè)例子,假設(shè)我們公司"ABC Company"花了1000塊錢(qián),向一個(gè)證書(shū)發(fā)布機(jī)構(gòu)"SecureTrust CA"為我們自己的公司"ABC Company"申請(qǐng)了一張證書(shū),注意,這個(gè)證書(shū)發(fā)布機(jī)構(gòu)"SecureTrust CA"是一個(gè)大家公認(rèn)并被一些權(quán)威機(jī)構(gòu)接受的證書(shū)發(fā)布機(jī)構(gòu),我們的操作系統(tǒng)里面已經(jīng)安裝了"SecureTrust CA"的證書(shū)。"SecureTrust CA"在給我們發(fā)布證書(shū)時(shí),把Issuer,Public key,Subject,Valid from,Valid to等信息以明文的形式寫(xiě)到證書(shū)里面,然后用一個(gè)指紋算法計(jì)算出這些數(shù)字證書(shū)內(nèi)容的一個(gè)指紋,并把指紋和指紋算法用自己的私鑰進(jìn)行加密,然后和證書(shū)的內(nèi)容一起發(fā)布,同時(shí)"SecureTrust CA"還會(huì)給一個(gè)我們公司"ABC Company"的私鑰給到我們。
我們"ABC Company"申請(qǐng)到這個(gè)證書(shū)后,我們把證書(shū)投入使用,我們?cè)谕ㄐ胚^(guò)程開(kāi)始時(shí)會(huì)把證書(shū)發(fā)給對(duì)方。
對(duì)方如何檢查這個(gè)證書(shū)的確是合法的并且是我們"ABC Company"公司的證書(shū)呢?首先應(yīng)用程序(對(duì)方通信用的程序,例如IE、OUTLook等)讀取證書(shū)中的Issuer(發(fā)布機(jī)構(gòu))為"SecureTrust CA" ,然后會(huì)在操作系統(tǒng)中受信任的發(fā)布機(jī)構(gòu)的證書(shū)中去找"SecureTrust CA"的證書(shū),如果找不到,那說(shuō)明證書(shū)的發(fā)布機(jī)構(gòu)是個(gè)水貨發(fā)布機(jī)構(gòu),證書(shū)可能有問(wèn)題,程序會(huì)給出一個(gè)錯(cuò)誤信息。 如果在系統(tǒng)中找到了"SecureTrust CA"的證書(shū),那么應(yīng)用程序就會(huì)從證書(shū)中取出"SecureTrust CA"的公鑰,然后對(duì)我們"ABC Company"公司的證書(shū)里面的指紋和指紋算法用這個(gè)公鑰進(jìn)行解密,然后使用這個(gè)指紋算法計(jì)算"ABC Company"證書(shū)的指紋,將這個(gè)計(jì)算的指紋與放在證書(shū)中的指紋對(duì)比,如果一致,說(shuō)明"ABC Company"的證書(shū)肯定沒(méi)有被修改過(guò)并且證書(shū)是"SecureTrust CA" 發(fā)布的,證書(shū)中的公鑰肯定是"ABC Company"的。對(duì)方然后就可以放心的使用這個(gè)公鑰和我們"ABC Company"進(jìn)行通信了。
通信雙方一方作為服務(wù)器等待客戶提出請(qǐng)求并予以響應(yīng)。客戶則在需要服務(wù)時(shí)向服務(wù)器提出申請(qǐng)。服務(wù)器一般作為守護(hù)進(jìn)程始終運(yùn)行,監(jiān)聽(tīng)網(wǎng)絡(luò)端口,一旦有客戶請(qǐng)求,就會(huì)啟動(dòng)一個(gè)服務(wù)進(jìn)程來(lái)響應(yīng)該客戶,同時(shí)自己繼續(xù)監(jiān)聽(tīng)服務(wù)端口,使后來(lái)的客戶也能及時(shí)得到服務(wù)。一個(gè)socket(通常都是server socket)等待建立連接時(shí),另一個(gè)socket可以要求進(jìn)行連接,一旦這兩個(gè)socket連接起來(lái),它們就可以進(jìn)行雙向數(shù)據(jù)傳輸,雙方都可以進(jìn)行發(fā)送或接收操作。
a)客戶端發(fā)送一個(gè)帶SYN標(biāo)志的TCP報(bào)文到服務(wù)器。(聽(tīng)得到嗎?)
b)服務(wù)端回應(yīng)客戶端的報(bào)文同時(shí)帶ACK(acknowledgement,確認(rèn))標(biāo)志和SYN(synchronize)標(biāo)志。它表示對(duì)剛才客戶端SYN報(bào)文的回應(yīng);同時(shí)又標(biāo)志SYN給客戶端,詢問(wèn)客戶端是否準(zhǔn)備好進(jìn)行數(shù)據(jù)通訊。(聽(tīng)得到,你能聽(tīng)到我嗎?)
c)客戶必須再次回應(yīng)服務(wù)端一個(gè)ACK報(bào)文。(聽(tīng)到了,我們可以說(shuō)話了)
為什么需要“三次握手”?
在謝希仁著《計(jì)算機(jī)網(wǎng)絡(luò)》第四版中講“三次握手”的目的是“為了防止已失效的連接請(qǐng)求報(bào)文段突然又傳送到了服務(wù)端,因而產(chǎn)生錯(cuò)誤”。“已失效的連接請(qǐng)求報(bào)文段”的產(chǎn)生在這樣一種情況下:client發(fā)出的第一個(gè)連接請(qǐng)求報(bào)文段并沒(méi)有丟失,而是在某個(gè)網(wǎng)絡(luò)結(jié)點(diǎn)長(zhǎng)時(shí)間的滯留了,以致延誤到連接釋放以后的某個(gè)時(shí)間才到達(dá)server。本來(lái)這是一個(gè)早已失效的報(bào)文段。但server收到此失效的連接請(qǐng)求報(bào)文段后,就誤認(rèn)為是client再次發(fā)出的一個(gè)新的連接請(qǐng)求。于是就向client發(fā)出確認(rèn)報(bào)文段,同意建立連接。假設(shè)不采用“三次握手”,那么只要server發(fā)出確認(rèn),新的連接就建立了。由于現(xiàn)在client并沒(méi)有發(fā)出建立連接的請(qǐng)求,因此不會(huì)理睬server的確認(rèn),也不會(huì)向server發(fā)送數(shù)據(jù)。但server卻以為新的運(yùn)輸連接已經(jīng)建立,并一直等待client發(fā)來(lái)數(shù)據(jù)。這樣,server的很多資源就白白浪費(fèi)掉了。采用“三次握手”的辦法可以防止上述現(xiàn)象發(fā)生。例如剛才那種情況,client不會(huì)向server的確認(rèn)發(fā)出確認(rèn)。server由于收不到確認(rèn),就知道client并沒(méi)有要求建立連接。”。 主要目的防止server端一直等待,浪費(fèi)資源。
由于TCP連接是全雙工的,因此每個(gè)方向都必須單獨(dú)進(jìn)行關(guān)閉。這原則是當(dāng)一方完成它的數(shù)據(jù)發(fā)送任務(wù)后就能發(fā)送一個(gè)FIN來(lái)終止這個(gè)方向的連接。收到一個(gè) FIN只意味著這一方向上沒(méi)有數(shù)據(jù)流動(dòng),一個(gè)TCP連接在收到一個(gè)FIN后仍能發(fā)送數(shù)據(jù)。首先進(jìn)行關(guān)閉的一方將執(zhí)行主動(dòng)關(guān)閉,而另一方執(zhí)行被動(dòng)關(guān)閉。
a) TCP客戶端發(fā)送一個(gè)FIN,用來(lái)關(guān)閉客戶到服務(wù)器的數(shù)據(jù)傳送(報(bào)文段4)。
b) 服務(wù)器收到這個(gè)FIN,它發(fā)回一個(gè)ACK,確認(rèn)序號(hào)為收到的序號(hào)加1(報(bào)文段5)。和SYN一樣,一個(gè)FIN將占用一個(gè)序號(hào)。
c) 服務(wù)器關(guān)閉客戶端的連接,發(fā)送一個(gè)FIN給客戶端(報(bào)文段6)。
d) 客戶段發(fā)回ACK報(bào)文確認(rèn),并將確認(rèn)序號(hào)設(shè)置為收到序號(hào)加1(報(bào)文段7)。
為什么需要“四次揮手”?
那可能有人會(huì)有疑問(wèn),在tcp連接握手時(shí)為何ACK是和SYN一起發(fā)送,這里ACK卻沒(méi)有和FIN一起發(fā)送呢。原因是因?yàn)閠cp是全雙工模式,接收到FIN時(shí)意味將沒(méi)有數(shù)據(jù)再發(fā)來(lái),但是還是可以繼續(xù)發(fā)送數(shù)據(jù)。
由3部分組成,分別為:請(qǐng)求方法、URL以及協(xié)議版本,之間由空格分隔
請(qǐng)求方法:GET、HEAD、PUT、POST等方法,但并不是所有的服務(wù)器都實(shí)現(xiàn)了所有的方法,部分方法即便支持,處于安全性的考慮也是不可用的
協(xié)議版本:常用HTTP/1.1
請(qǐng)求頭部為請(qǐng)求報(bào)文添加了一些附加信息,由“名/值”對(duì)組成,每行一對(duì),名和值之間使用冒號(hào)分隔
Host
接受請(qǐng)求的服務(wù)器地址,可以是IP:端口號(hào),也可以是域名User-Agent
發(fā)送請(qǐng)求的應(yīng)用程序名稱Accept-Charset
通知服務(wù)端可以發(fā)送的編碼格式Accept-Encoding
通知服務(wù)端可以發(fā)送的數(shù)據(jù)壓縮格式Accept-Language
通知服務(wù)端可以發(fā)送的語(yǔ)言Range
正文的字節(jié)請(qǐng)求范圍,為斷點(diǎn)續(xù)傳和并行下載提供可能,返回狀態(tài)碼是206Authorization
用于設(shè)置身份認(rèn)證信息Cookie
已有的Cookie請(qǐng)求頭部的最后會(huì)有一個(gè)空行,表示請(qǐng)求頭部結(jié)束,接下來(lái)為請(qǐng)求正文,這一行非常重要,必不可少
可選部分,比如GET請(qǐng)求就沒(méi)有請(qǐng)求正文
由3部分組成,分別為:協(xié)議版本,狀態(tài)碼,狀態(tài)碼描述,之間由空格分隔
狀態(tài)碼:為3位數(shù)字,2XX表示成功,3XX表示資源重定向,4XX表示客戶端請(qǐng)求出錯(cuò),5XX表示服務(wù)端出錯(cuò)
206狀態(tài)碼表示的是:客戶端通過(guò)發(fā)送范圍請(qǐng)求頭Range抓取到了資源的部分?jǐn)?shù)據(jù),得服務(wù)端提供支持
Server
服務(wù)器應(yīng)用程序軟件的名稱和版本Content-Type
響應(yīng)正文的類型。如:text/plain、application/jsonContent-Length
響應(yīng)正文長(zhǎng)度Content-Charset
響應(yīng)正文使用的編碼Content-Language
響應(yīng)正文使用的語(yǔ)言Content-Range
正文的字節(jié)位置范圍Accept-Ranges
bytes:表明服務(wù)器支持Range請(qǐng)求,單位是字節(jié);none:不支持Set-Cookie
設(shè)置Cookie正文的內(nèi)容可以用gzip等進(jìn)行壓縮,以提升傳輸速率
用于操作瀏覽器緩存的工作機(jī)制。取值如下:
用于管理持久連接。目前大部分瀏覽器都是用http1.1協(xié)議,也就是說(shuō)默認(rèn)都會(huì)發(fā)起Keep-Alive的連接請(qǐng)求。所以是否能完成一個(gè)完整的Keep-Alive連接就看服務(wù)器設(shè)置情況。取值如下:
在Http/1.1中,僅對(duì)分塊傳輸編碼有效。Transfer-Encoding: chunked 表示輸出的內(nèi)容長(zhǎng)度不能確定,普通的靜態(tài)頁(yè)面、圖片之類的基本上都用不到這個(gè),但動(dòng)態(tài)頁(yè)面就有可能會(huì)用到。一般使用Content-Length就夠了。
Http/1.1 200 OK .... Transfer-Encoding:chunked Connection:keep-alivecf0//16進(jìn)制,值為3312...3312字節(jié)分塊數(shù)據(jù)...392//16進(jìn)制,值為914...914字節(jié)分塊數(shù)據(jù)...0
請(qǐng)求體/響應(yīng)體的編碼格式,如gzip
兩種常見(jiàn)的Authentication機(jī)制:HTTP Basic和Digest。(現(xiàn)在用的并不多,了解一下)
最簡(jiǎn)單的Authentication協(xié)議。直接方式告訴服務(wù)器你的用戶名(username)和密碼(password)。
request頭部:
GET /secret HTTP/1.1 Authorization: Basic QWxpY2U6MTIzNDU2//由“Alice:123456”進(jìn)行Base64編碼以后得到的結(jié)果 ...
response頭部:
HTTP/1.1 200 OK ...
因?yàn)槲覀冚斎氲氖钦_的用戶名密碼,所以服務(wù)器會(huì)返回200,表示驗(yàn)證成功。如果我們用錯(cuò)誤的用戶的密碼來(lái)發(fā)送請(qǐng)求,則會(huì)得到類似如下含有401錯(cuò)誤的response頭部:
HTTP/1.1 401 Bad credentials WWW-Authenticate: Basic realm="Spring Security Application" ...
當(dāng)Alice初次訪問(wèn)服務(wù)器時(shí),并不攜帶密碼。此時(shí)服務(wù)器會(huì)告知Alice一個(gè)隨機(jī)生成的字符串(nonce)。然后Alice再將這個(gè)字符串與她的密碼123456結(jié)合在一起進(jìn)行MD5編碼,將編碼以后的結(jié)果發(fā)送給服務(wù)器作為驗(yàn)證信息。
因?yàn)閚once是“每次”(并不一定是每次)隨機(jī)生成的,所以Alice在不同的時(shí)間訪問(wèn)服務(wù)器,其編碼使用的nonce值應(yīng)該是不同的,如果攜帶的是相同的nonce編碼后的結(jié)果,服務(wù)器就認(rèn)為其不合法,將拒絕其訪問(wèn)。
curl和服務(wù)器通信過(guò)程:
curl -------- request1:GET ------->> Server curl <<------ response1:nonce ------- Server curl ---- request2:Digest Auth ---->> Server curl <<------- response2:OK --------Server
request1頭部:
GET /secret HTTP/1.1 ...
請(qǐng)求1中沒(méi)有包含任何用戶名和密碼信息
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沒(méi)有任何的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值放在一起然后進(jìn)行MD5編碼,得到一個(gè)response值,如前面紅色標(biāo)出所示,這樣服務(wù)器就可以通過(guò)這個(gè)值驗(yàn)證Alice的密碼是否正確。
response2頭部:
HTTP/1.1 200 OK ...
當(dāng)我們完成Authentication以后,如果我們?cè)俅问褂脛偛诺膎once值,將會(huì)收到錯(cuò)誤信息。Digest Authentication比Basic安全,但是并不是真正的什么都不怕了,Digest Authentication這種容易方式容易收到Man in the Middle式攻擊。
據(jù)應(yīng)用場(chǎng)景的不同,HTTP請(qǐng)求的請(qǐng)求體有三種不同的形式。
第一種:
移動(dòng)開(kāi)發(fā)者常見(jiàn)的,請(qǐng)求體是任意類型,服務(wù)器不會(huì)解析請(qǐng)求體,請(qǐng)求體的處理需要自己解析,如 POST JSON時(shí)候就是這類。
第二種:
這里的格式要求就是URL中Query String的格式要求:多個(gè)鍵值對(duì)之間用&連接,鍵與值之前用=連接,且只能用ASCII字符,非ASCII字符需使用UrlEncode編碼。
第三種:
請(qǐng)求體被分成為多個(gè)部分,文件上傳時(shí)會(huì)被使用,這種格式最先應(yīng)該是被用于郵件傳輸中,每個(gè)字段/文件都被boundary(Content-Type中指定)分成單獨(dú)的段,每段以-- 加 boundary開(kāi)頭,然后是該段的描述頭,描述頭之后空一行接內(nèi)容,請(qǐng)求結(jié)束的標(biāo)制為boundary后面加--。(見(jiàn)下面詳細(xì)說(shuō)明)
默認(rèn)是application/x-www-form-urlencoded,但是在傳輸大型文件的時(shí)候效率比較低下。所以需要multipart/form-data。
報(bào)文的主體內(nèi)可以包含多部分對(duì)象,通常用來(lá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)在請(qǐng)求頭中Content-Type: multipart/form-data; boundary=Ij5ei4KM7KM7ae0KM7cH2ae0Ij5Ef1
是必須的,boundary字符串可以隨意指定
b)上面有3個(gè)部分,分別用--boundary進(jìn)行分隔。Content-Disposition: form-data; name="參數(shù)的名稱" + "\r\n" + "\r\n" + 參數(shù)值
c)--boundary--
作為結(jié)束
Http+加密+認(rèn)證+完整性保護(hù)=Https
Https就是身披SSL(Secure Socket Layer,安全套接層)協(xié)議這層外殼的Http。當(dāng)使用了SSL之后,Http先和SSL通信,SSL再和TCP通信。
SSL(secure sockets layer):安全套接層,它是在上世紀(jì)90年代中期,由網(wǎng)景公司設(shè)計(jì)的,為解決 HTTP 協(xié)議傳輸內(nèi)容會(huì)被偷窺(嗅探)和篡改等安全問(wèn)題而設(shè)計(jì)的,到了1999年,SSL成為互聯(lián)網(wǎng)上的標(biāo)準(zhǔn),名稱改為T(mén)LS(transport layer security):安全傳輸層協(xié)議,兩者可視為同一種東西的不同階段。
HTTPS在傳輸數(shù)據(jù)之前需要客戶端(瀏覽器)與服務(wù)端(網(wǎng)站)之間進(jìn)行一次握手,在握手過(guò)程中將確立雙方加密傳輸數(shù)據(jù)的密碼信息。TLS/SSL協(xié)議不僅僅是一套加密傳輸?shù)膮f(xié)議,更是一件經(jīng)過(guò)藝術(shù)家精心設(shè)計(jì)的藝術(shù)品,TLS/SSL中使用了非對(duì)稱加密,對(duì)稱加密以及HASH算法。握手過(guò)程的具體描述如下:
這里瀏覽器與網(wǎng)站互相發(fā)送加密的握手消息并驗(yàn)證,目的是為了保證雙方都獲得了一致的密碼,并且可以正常的加密解密數(shù)據(jù),為后續(xù)真正數(shù)據(jù)的傳輸做一次測(cè)試。另外,HTTPS一般使用的加密與HASH算法如下:
HTTPS對(duì)應(yīng)的通信時(shí)序圖如下:
SSL 證書(shū)大致分三類:
只有第一種, 也就是那些被安卓系統(tǒng)認(rèn)可的機(jī)構(gòu)頒發(fā)的證書(shū), 在使用過(guò)程中不會(huì)出現(xiàn)安全提示。對(duì)于向權(quán)威機(jī)構(gòu)(簡(jiǎn)稱CA,Certificate Authority)申請(qǐng)過(guò)證書(shū)的網(wǎng)絡(luò)地址,用OkHttp或者HttpsURLConnection都可以直接訪問(wèn) ,不需要做額外的事情 。但是申請(qǐng)需要$$ (每年要交 100 到 500 美元不等的費(fèi)用)。
CA機(jī)構(gòu)頒發(fā)的證書(shū)有3種類型:
域名型SSL證書(shū)(DV SSL):信任等級(jí)普通,只需驗(yàn)證網(wǎng)站的真實(shí)性便可頒發(fā)證書(shū)保護(hù)網(wǎng)站;
企業(yè)型SSL證書(shū)(OV SSL):信任等級(jí)強(qiáng),須要驗(yàn)證企業(yè)的身份,審核嚴(yán)格,安全性更高;
增強(qiáng)型SSL證書(shū)(EV SSL):信任等級(jí)最高,一般用于銀行證券等金融機(jī)構(gòu),審核嚴(yán)格,安全性最高,同時(shí)可以激活綠色網(wǎng)址欄。
InetAddress.getByAddress(byte[] addr)
根據(jù)IP地址獲取InetAddress對(duì)象,如:new byte[]{127,0,0,1}InetAddress.getByName(String host)
根據(jù)主機(jī)名獲取InetAddress對(duì)象 www.baidu.com 沒(méi)有http://InetAddress.getLocalHost()
返回本機(jī)getHostAddress() String
返回IP地址getHostName() String
返回主機(jī)名isReachable(int timeout) boolean
測(cè)試是否可以達(dá)到該地址,毫秒數(shù)application/x-www-form-urlencoded MIME
字符串之間的相互轉(zhuǎn)換URLEncoder.encode(String s, String enc) String
URLDecoder.decode(String s, String enc) String
Socket又稱套接字,是程序內(nèi)部提供的與外界通信的端口,即端口通信。通過(guò)建立socket連接,可為通信雙方的數(shù)據(jù)傳輸傳提供通道。主要特點(diǎn)有數(shù)據(jù)丟失率低,使用簡(jiǎn)單且易于移植。
在TCP/IP協(xié)議族當(dāng)中主要的Socket類型為流套接字(streamsocket)和數(shù)據(jù)報(bào)套接字(datagramsocket)。流套接字將TCP作為其端對(duì)端協(xié)議,提供了一個(gè)可信賴的字節(jié)流服務(wù)。數(shù)據(jù)報(bào)套接字使用UDP協(xié)議,提供數(shù)據(jù)打包發(fā)送服務(wù)。
首先添加權(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)用程序訪問(wèn)有關(guān)的網(wǎng)絡(luò)信息--> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/><!--允許應(yīng)用程序訪問(wèn)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"/>
客戶端實(shí)現(xiàn):
protected void connectServerWithTCPSocket() { Socket socket; try {// 創(chuàng)建一個(gè)Socket對(duì)象,并指定服務(wù)端的IP及端口號(hào) socket = new Socket("192.168.1.32", 1989); // 創(chuàng)建一個(gè)InputStream用戶讀取要發(fā)送的文件。 InputStream inputStream = new FileInputStream("e://a.txt"); // 獲取Socket的OutputStream對(duì)象用于發(fā)送數(shù)據(jù)。 OutputStream outputStream = socket.getOutputStream(); // 創(chuàng)建一個(gè)byte類型的buffer字節(jié)數(shù)組,用于存放讀取的本地文件 byte buffer[] = new byte[4 * 1024]; int temp = 0; // 循環(huán)讀取文件 while ((temp = inputStream.read(buffer)) != -1) { // 把數(shù)據(jù)寫(xiě)入到OuputStream對(duì)象中 outputStream.write(buffer, 0, temp); } // 發(fā)送讀取的數(shù)據(jù)到服務(wù)端 outputStream.flush(); /** 或創(chuàng)建一個(gè)報(bào)文,使用BufferedWriter寫(xiě)入**/ //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ù)端實(shí)現(xiàn):
public void serverReceviedByTcp() { // 聲明一個(gè)ServerSocket對(duì)象 ServerSocket serverSocket = null; try { // 創(chuàng)建一個(gè)ServerSocket對(duì)象,并讓這個(gè)Socket在1989端口監(jiān)聽(tīng) serverSocket = new ServerSocket(1989); // 調(diào)用ServerSocket的accept()方法,接受客戶端所發(fā)送的請(qǐng)求, // 如果客戶端沒(méi)有發(fā)送數(shù)據(jù),那么該線程就阻塞,等到收到數(shù)據(jù),繼續(xù)執(zhí)行。 Socket socket = serverSocket.accept(); // 從Socket當(dāng)中得到InputStream對(duì)象,讀取客戶端發(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(); } }
監(jiān)聽(tīng)來(lái)自于客戶端的Socket連接,如果沒(méi)有連接,它將一直處于等待狀態(tài)
ServerSocket(int port, int backlog, InetAddress bindAddr)
在機(jī)器存在多IP的情況下,允許通過(guò)bindAddr來(lái)指定綁定到哪個(gè)IP;backlog是隊(duì)列中能接受的最大socket客戶端連接數(shù)(accept()之后將被取出)ServerSocket()
創(chuàng)建非綁定服務(wù)器套接字ServerSocket(int port)
創(chuàng)建綁定到特定端口的服務(wù)器套接字,等價(jià)于ServerSocket(port, 50, null)accept() Socket
如果接收到一個(gè)客戶端Socket的連接請(qǐng)求,返回一個(gè)與客戶端Socket相對(duì)應(yīng)的Socketclose()
Socket()
創(chuàng)建未連接套接字Socket(String host, int port)
Socket(InetAddress address, int port)
創(chuàng)建一個(gè)流套接字并將其連接到指定 IP 地址的指定端口號(hào),默認(rèn)使用本地主機(jī)默認(rèn)的IP和系統(tǒng)動(dòng)態(tài)分配的端口Socket(InetAddress address, int port, InetAddress localAddr, int localPort)
創(chuàng)建一個(gè)套接字并將其連接到指定遠(yuǎn)程地址上的指定遠(yuǎn)程端口,多IP時(shí)getOutputStream() OutputStream
返回此套接字的輸出流getInputStream() inputStream
返回此套接字的輸入流connect(SocketAddress endpoint, int timeout)
將此套接字連接到服務(wù)器,并指定一個(gè)超時(shí)值close()
1)對(duì)象代表統(tǒng)一資源定位器,是指向互聯(lián)網(wǎng)“資源”的指針
2)通常由協(xié)議名、主機(jī)、端口、資源路徑組成
3)URL包含一個(gè)可打開(kāi)到達(dá)該資源的輸入流,可以將URL理解成為URI的特例
URL(String spec)
openConnection() URLConnection
getProtocol() String
getHost() String
getPort() int
getPath() String
獲取路徑部分,/search.htmlgetFile() String
獲取資源名稱,/search.html?keyword='你好'getQuery() String
獲取查詢字符串,keyword='你好'openStream() InputStream
抽象類,表示應(yīng)用程序和URL之間的通信連接,可以向URL發(fā)送請(qǐng)求,讀取URL指向的資源
setDoInput(boolean doinput)
發(fā)送POST請(qǐng)求,必須設(shè)置,設(shè)置為truesetDoOutput(boolean dooutput)
發(fā)送POST請(qǐng)求,必須設(shè)置,設(shè)置為truesetUseCaches(boolean usecaches)
是否使用緩存setRequestProperty(String key, String value)
設(shè)置普通的請(qǐng)求屬性setConnectTimeout(int timeout)
設(shè)置連接超時(shí)的時(shí)間setReadTimeout(int timeoutMillis)
讀取輸入流的超時(shí)時(shí)間connect()
抽象方法,建立實(shí)際的連接getHeaderField(String key) String
getHeaderFields() Map<String,List<String>>
獲取所有的響應(yīng)頭;getHeaderField(String name)獲取指定的響應(yīng)頭getOutputStream() OutputStream
getInputStream() InputStream
getContentLength() int
抽象類,是URLConnection的子類,增加了操作Http資源的便捷方法
setRequestMethod(String method)
setInstanceFollowRedirects(boolean followRedirects)
getResponseCode() int
獲取服務(wù)器的響應(yīng)碼getResponseMessage() String
獲取響應(yīng)消息getRequestMethod() String
獲取發(fā)送請(qǐng)求的方法,GET或POSTdisconnect()
抽象方法步驟:
①:創(chuàng)建URL對(duì)象
②:獲取URL對(duì)象指向資源的大小(getContentLength()方法)
③:在本地創(chuàng)建一個(gè)與網(wǎng)路資源相同大小的空文件
④:計(jì)算每條線程應(yīng)該下載網(wǎng)絡(luò)資源的哪個(gè)部分(從哪個(gè)字節(jié)開(kāi)始,到哪個(gè)字節(jié)結(jié)束)
⑤:依次創(chuàng)建,啟動(dòng)多條線程來(lái)下載網(wǎng)絡(luò)資源的指定部分
使用GET方式訪問(wèn)HTTP
public static void main(String[] args) { try { // 1. 得到訪問(wèn)地址的URL URL url = new URL( "http://localhost:8080/Servlet/do_login.do?username=test&password=123456"); // 2. 得到網(wǎng)絡(luò)訪問(wèn)對(duì)象java.net.HttpURLConnection HttpURLConnection connection = (HttpURLConnection) url .openConnection(); /* 3. 設(shè)置請(qǐng)求參數(shù)(過(guò)期時(shí)間,輸入、輸出流、訪問(wèn)方式),以流的形式進(jìn)行連接 */ // 設(shè)置是否向HttpURLConnection輸出 connection.setDoOutput(false); // 設(shè)置是否從httpUrlConnection讀入 connection.setDoInput(true); // 設(shè)置請(qǐng)求方式 connection.setRequestMethod("GET"); // 設(shè)置是否使用緩存 connection.setUseCaches(true); // 設(shè)置此 HttpURLConnection 實(shí)例是否應(yīng)該自動(dòng)執(zhí)行 HTTP 重定向 connection.setInstanceFollowRedirects(true); // 設(shè)置超時(shí)時(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. 斷開(kāi)連接,釋放資源 connection.disconnect(); // 顯示響應(yīng)結(jié)果 System.out.println(msg); } catch (IOException e) { e.printStackTrace(); } }
使用POST方式訪問(wèn)HTTP
public static void main(String[] args) { try { // 1. 獲取訪問(wèn)地址URL URL url = new URL("http://localhost:8080/Servlet/do_login.do"); // 2. 創(chuàng)建HttpURLConnection對(duì)象 HttpURLConnection connection = (HttpURLConnection) url .openConnection(); /* 3. 設(shè)置請(qǐng)求參數(shù)等 */ // 請(qǐng)求方式 connection.setRequestMethod("POST"); // 超時(shí)時(shí)間 connection.setConnectTimeout(3000); // 設(shè)置是否輸出 connection.setDoOutput(true); // 設(shè)置是否讀入 connection.setDoInput(true); // 設(shè)置是否使用緩存 connection.setUseCaches(false); // 設(shè)置此 HttpURLConnection 實(shí)例是否應(yīng)該自動(dòng)執(zhí)行 HTTP 重定向 connection.setInstanceFollowRedirects(true); // 設(shè)置使用標(biāo)準(zhǔn)編碼格式編碼參數(shù)的名-值對(duì) connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); // 連接 connection.connect(); /* 4. 處理輸入輸出 */ // 寫(xiě)入?yún)?shù)到請(qǐng)求中 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. 斷開(kāi)連接 connection.disconnect();// 處理結(jié)果 System.out.println(msg); } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }
(Android6.0之后的SDK中移除了對(duì)于HttpClient的支持,僅作了解)
在一般情況下,如果只是需要向Web站點(diǎn)的某個(gè)簡(jiǎn)單頁(yè)面提交請(qǐng)求并獲取服務(wù)器響應(yīng),HttpURLConnection完全可以勝任。但在絕大部分情況下,Web站點(diǎn)的網(wǎng)頁(yè)可能沒(méi)這么簡(jiǎn)單,這些頁(yè)面并不是通過(guò)一個(gè)簡(jiǎn)單的URL就可訪問(wèn)的,可能需要用戶登錄而且具有相應(yīng)的權(quán)限才可訪問(wèn)該頁(yè)面。在這種情況下,就需要涉及Session、Cookie的處理了,如果打算使用HttpURLConnection來(lái)處理這些細(xì)節(jié),當(dāng)然也是可能實(shí)現(xiàn)的,只是處理起來(lái)難度就大了。
為了更好地處理向Web站點(diǎn)請(qǐng)求,包括處理Session、Cookie等細(xì)節(jié)問(wèn)題,Apache開(kāi)源組織提供了一個(gè)HttpClient項(xiàng)目。HttpClient就是一個(gè)增強(qiáng)版的HttpURLConnection,HttpURLConnection可以做的事情HttpClient全部可以做;HttpURLConnection沒(méi)有提供的有些功能,HttpClient也提供了,但它只是關(guān)注于如何發(fā)送請(qǐng)求、接收響應(yīng),以及管理HTTP連接。
步驟:
①:創(chuàng)建HttpClient對(duì)象
②:需要GET請(qǐng)求,創(chuàng)建HttpCet對(duì)象;需要POST請(qǐng)求,創(chuàng)建HttpPost對(duì)象
③:需要發(fā)送請(qǐng)求參數(shù),調(diào)用HttpGet、HttpPost共同的setParams(HttpParams params),HttpPost對(duì)象還可以使用setEntity(HttpEntity entity)方法來(lái)設(shè)置請(qǐng)求參數(shù)。
④:調(diào)用HttpClient對(duì)象的execute(HttpUriRequest request) HttpResponse
發(fā)送請(qǐng)求,執(zhí)行該方法返回一個(gè)HttpResponse。
⑤:調(diào)用HttpResponse的getAllHeaders()
,getHeader(String name)
獲取響應(yīng)頭;調(diào)用HttpResponse的getEntity()獲取HttpEntity對(duì)象(包裝了服務(wù)器的響應(yīng)內(nèi)容)
public static void main(String[] args) { // 1. 創(chuàng)建HttpClient對(duì)象 CloseableHttpClient httpClient = HttpClientBuilder.create().build(); // 2. 創(chuàng)建HttpGet對(duì)象 HttpGet httpGet = new HttpGet( "http://localhost:8080/Servlet/do_login.do?username=test&password=123456"); CloseableHttpResponse response = null; try { // 3. 執(zhí)行GET請(qǐng)求 response = httpClient.execute(httpGet); System.out.println(response.getStatusLine()); // 4. 獲取響應(yīng)實(shí)體 HttpEntity entity = response.getEntity(); // 5. 處理響應(yīng)實(shí)體 if (entity != null) { System.out.println("長(zhǎng)度:" + 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(); } } }
public static void main(String[] args) { // 1. 創(chuàng)建HttpClient對(duì)象 CloseableHttpClient httpClient = HttpClientBuilder.create().build(); // 2. 創(chuàng)建HttpPost對(duì)象 HttpPost post = new HttpPost( "http://localhost:8080/Servlet/do_login.do"); // 3. 設(shè)置POST請(qǐng)求傳遞參數(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í)行請(qǐng)求并處理響應(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(); } } }
HttpClient是Apache基金會(huì)的一個(gè)開(kāi)源網(wǎng)絡(luò)庫(kù),功能十分強(qiáng)大,API數(shù)量眾多,但正是由于龐大的API數(shù)量使得我們很難在不破壞兼容性的情況下對(duì)它進(jìn)行升級(jí)和擴(kuò)展,所以Android團(tuán)隊(duì)在提升和優(yōu)化HttpClient方面的工作態(tài)度并不積極。官方在Android 2.3以后就不建議用了,并且在Android 5.0以后廢棄了HttpClient,在Android 6.0更是刪除了HttpClient。
HttpURLConnection是一種多用途、輕量極的HTTP客戶端,提供的API比較簡(jiǎn)單,可以容易地去使用和擴(kuò)展。不過(guò)在Android 2.2版本之前,HttpURLConnection一直存在著一些令人厭煩的bug。比如說(shuō)對(duì)一個(gè)可讀的InputStream調(diào)用close()方法時(shí),就有可能會(huì)導(dǎo)致連接池失效了。那么我們通常的解決辦法就是直接禁用掉連接池的功能。因此一般推薦是在2.2之前使用HttpClient,因?yàn)槠鋌ug較少。在2.2之后推薦使用HttpURLConnection,因?yàn)锳PI簡(jiǎn)單、體積小、有壓縮和緩存機(jī)制,并且Android團(tuán)隊(duì)后續(xù)會(huì)繼續(xù)優(yōu)化HttpURLConnection。
自從Android4.4開(kāi)始,google已經(jīng)開(kāi)始將源碼中的HttpURLConnection替換為OkHttp,而市面上流行的Retrofit同樣是使用OkHttp進(jìn)行再次封裝而來(lái)的。
OkHttp是一個(gè)快速、高效的網(wǎng)絡(luò)請(qǐng)求庫(kù),它的設(shè)計(jì)和實(shí)現(xiàn)的首要目標(biāo)便是高效,有如下特性:
http請(qǐng)求包含:請(qǐng)求方法, 請(qǐng)求地址, 請(qǐng)求協(xié)議, 請(qǐng)求頭, 請(qǐng)求體這五部分。這些都在okhttp3.Request的類中有體現(xiàn), 這個(gè)類正是代表http請(qǐng)求的類。
public final class Request { final HttpUrl url;//請(qǐng)求地址 final String method;//請(qǐng)求方法 final Headers headers;//請(qǐng)求頭 final RequestBody body;//請(qǐng)求體 final Object tag; ... }
Http響應(yīng)由訪問(wèn)協(xié)議, 響應(yīng)碼, 描述信息, 響應(yīng)頭, 響應(yīng)體來(lái)組成。
public final class Response implements Closeable { final Request request;//持有的請(qǐng)求 final Protocol protocol;//訪問(wèn)協(xié)議 final int code;//響應(yīng)碼 final String message;//描述信息 final Handshake handshake;//SSL/TLS握手協(xié)議驗(yàn)證時(shí)的信息, final Headers headers;//響應(yīng)頭 final ResponseBody body;//響應(yīng)體 ... }
OkHttpClient()
OkHttpClient(OkHttpClient.Builder builder)
newCall(Request request) Call
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是一個(gè)接口hostnameVerifier(HostnameVerifier hostnameVerifier)
HostnameVerifier是一個(gè)接口,只有boolean verify(String hostname, SSLSession session)
sslSocketFactory(SSLSocketFactory sslSocketFactory, X509TrustManager trustManager)
Request(Request.Builder builder)
addHeader(String name, String value)
添加鍵值對(duì),不會(huì)覆蓋header(String name, String value)
添加鍵值對(duì),會(huì)覆蓋url(String url)
method(String method, RequestBody body)
post(RequestBody body)
本質(zhì):method("POST", body)build() Request
create(MediaType contentType, final File file) RequestBody
create(MediaType contentType, String content) RequestBody
create(MediaType contentType, byte[] content) RequestBody
RequestBody的子類
add(String name, String value) FormBody.Builder
build() FormBody
RequestBody的子類
Builder()
Builder(String boundary)
setType(MediaType type)
addPart(Headers headers, RequestBody body)
addFormDataPart(String name, String filename, RequestBody body)
build() MultipartBody
Call負(fù)責(zé)發(fā)送請(qǐng)求和讀取響應(yīng)
enqueue(Callback responseCallback)
加入調(diào)度隊(duì)列,異步執(zhí)行execute() Response
同步執(zhí)行cancel()
body() ResponseBody
code() int
http請(qǐng)求的狀態(tài)碼isSuccessful()
code為2XX時(shí),返回true,否則falseheaders() Headers
string() String
bytes() byte[]
byteStream() InputStream
charStream() Reader
contentLength() long
RequestBody的數(shù)據(jù)格式都要指定Content-Type,就是指定MIME,常見(jiàn)的有三種:
方法:
parse(String string) MediaType
參數(shù) | 說(shuō)明 |
---|---|
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 | 二進(jìn)制流數(shù)據(jù) |
application/x-www-form-urlencoded | 普通表單數(shù)據(jù) |
multipart/form-data | 表單數(shù)據(jù)里有文件 |
Request經(jīng)常都要攜帶Cookie,request創(chuàng)建時(shí)可以通過(guò)header設(shè)置參數(shù),Cookie也是參數(shù)之一。就像下面這樣:
Request request = new Request.Builder() .url(url) .header("Cookie", "xxx") .build();
然后可以從返回的response里得到新的Cookie,你可能得想辦法把Cookie保存起來(lái)。
但是OkHttp可以不用我們管理Cookie,自動(dòng)攜帶,保存和更新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這個(gè)參數(shù)也不用去response獲取新Cookie什么的了。還能通過(guò)cookieStore獲取當(dāng)前保存的Cookie。
最后,new OkHttpClient()只是一種快速創(chuàng)建OkHttpClient的方式,更標(biāo)準(zhǔn)的是使用OkHttpClient.Builder()。后者可以設(shè)置一堆參數(shù),例如超時(shí)時(shí)間什么的。
//step 1: 創(chuàng)建 OkHttpClient 對(duì)象 OkHttpClient okHttpClient = new OkHttpClient(); //step 2: 創(chuàng)建一個(gè)請(qǐng)求,不指定請(qǐng)求方法時(shí)默認(rèn)是GET。 Request.Builder requestBuilder = new Request.Builder().url("http://www.baidu.com"); //可以省略,默認(rèn)是GET請(qǐng)求 requestBuilder.method("GET", null); //step 3:創(chuàng)建 Call 對(duì)象 Call call = okHttpClient.newCall(requestBuilder.build()); //step 4: 開(kāi)始異步請(qǐng)求 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的對(duì)象與Request的對(duì)象建立起來(lái)聯(lián)系,使用okHttpClient的newCall()方法得到一個(gè)Call對(duì)象,這個(gè)Call對(duì)象的作用就是相當(dāng)于將請(qǐng)求封裝成了一個(gè)任務(wù),既然是任務(wù),自然就會(huì)有execute()
和cancel()
等方法。
最后,我們希望以異步的方式去執(zhí)行請(qǐng)求,所以我們調(diào)用的是call.enqueue,將call加入調(diào)度隊(duì)列,然后等待任務(wù)執(zhí)行完成,我們?cè)贑allback中即可得到結(jié)果。但要注意的是,call的回調(diào)是子線程,所以是不能直接操作界面的。當(dāng)請(qǐng)求成功時(shí)就會(huì)回調(diào)onResponse()方法,我們可以看到返回的結(jié)果是Response對(duì)象,在此我們比較關(guān)注的是請(qǐng)求中的返回體body(ResponseBody類型),大多數(shù)的情況下我們希望獲得字符串從而進(jìn)行json解析獲得數(shù)據(jù),所以可以通過(guò)body.string()的方式獲得字符串。如果希望獲得返回的二進(jìn)制字節(jié)數(shù)組,則調(diào)用response.body().bytes();如果你想拿到返回的inputStream,則調(diào)用response.body().byteStream()。
調(diào)用Call#execute()
方法,在主線程運(yùn)行
//step1: 同樣的需要?jiǎng)?chuàng)建一個(gè)OkHttpClick對(duì)象 OkHttpClient okHttpClient = new OkHttpClient(); //step2: 創(chuàng)建 FormBody.Builder FormBody formBody = new FormBody.Builder() .add("name", "dsd") //添加鍵值對(duì) .build(); //step3: 創(chuàng)建請(qǐng)求 Request request = new Request.Builder().url("http://www.baidu.com") .post(formBody) .build() //step4: 建立聯(lián)系 創(chuàng)建Call對(duì)象 okHttpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { }@Override public void onResponse(Call call, Response response) throws IOException { } });
// step 1: 創(chuàng)建 OkHttpClient 對(duì)象 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 獲取請(qǐng)求體 RequestBody requestBody = RequestBody.create(MEDIATYPE, file); //step 3:創(chuàng)建請(qǐ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 { } });
以流的方式POST提交請(qǐng)求體. 請(qǐng)求體的內(nèi)容由流寫(xiě)入產(chǎn)生. 這個(gè)例子是流直接寫(xiě)入Okio的BufferedSink. 你的程序可能會(huì)使用OutputStream, 你可以使用BufferedSink.outputStream()來(lái)獲取. OkHttp的底層對(duì)流和字節(jié)的操作都是基于Okio庫(kù), Okio庫(kù)也是Square開(kāi)發(fā)的另一個(gè)IO庫(kù), 填補(bǔ)I/O和NIO的空缺, 目的是提供簡(jiǎn)單便于使用的接口來(lái)操作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()); }
下面是使用HTTP POST提交請(qǐng)求到服務(wù). 這個(gè)例子提交了一個(gè)markdown文檔到web服務(wù), 以HTML方式渲染markdown. 因?yàn)檎麄€(gè)請(qǐng)求體都在內(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()); }
MultipartBody.Builder可以構(gòu)建復(fù)雜的請(qǐng)求體, 與HTML文件上傳形式兼容. 多塊請(qǐng)求體中每塊請(qǐng)求都是一個(gè)請(qǐng)求體, 可以定義自己的請(qǐng)求頭. 這些請(qǐng)求頭可以用來(lái)描述這塊請(qǐng)求, 例如它的Content-Disposition. 如果Content-Length和Content-Type可用的話, 他們會(huì)被自動(dòng)添加到請(qǐng)求頭中.
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()); }
典型的HTTP頭是一個(gè)Map<String, String> : 每個(gè)字段都有一個(gè)或沒(méi)有值. 但是一些頭允許多個(gè)值。
當(dāng)寫(xiě)請(qǐng)求頭的時(shí)候, 使用header(name, value)可以設(shè)置唯一的name、value. 如果已經(jīng)有值, 舊的將被移除, 然后添加新的. 使用addHeader(name, value)可以添加多值(添加, 不移除已有的).
當(dāng)讀取響應(yīng)頭時(shí), 使用header(name)返回最后出現(xiàn)的name、value. 通常情況這也是唯一的name、value. 如果沒(méi)有值, 那么header(name)
將返回null. 如果想讀取字段對(duì)應(yīng)的所有值, 使用headers(name)`會(huì)返回一個(gè)list.
為了獲取所有的Header, Headers類支持按index訪問(wèn).
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")); }
Gson是一個(gè)在JSON和Java對(duì)象之間轉(zhuǎn)換非常方便的api庫(kù). 這里我們用Gson來(lái)解析Github API的JSON響應(yīng).
注意: ResponseBody.charStream()使用響應(yīng)頭Content-Type指定的字符集來(lái)解析響應(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; }
OKHTTP如果要設(shè)置緩存,首要的條件就是設(shè)置一個(gè)緩存文件夾,在Android中為了安全起見(jiàn),一般設(shè)置為私密數(shù)據(jù)空間。通過(guò)getExternalCacheDir()獲取。
如然后通過(guò)調(diào)用OKHttpClient.Builder中的cache()方法。如下面代碼所示:
//緩存文件夾 File cacheFile = new File(getExternalCacheDir().toString(),"cache"); //緩存大小為10M int cacheSize = 10 * 1024 * 1024; //創(chuàng)建緩存對(duì)象 Cache cache = new Cache(cacheFile,cacheSize);OkHttpClient client = new OkHttpClient.Builder() .cache(cache) .build();
設(shè)置好Cache我們就可以正常訪問(wèn)了。我們可以通過(guò)獲取到的Response對(duì)象拿到它正常的消息和緩存的消息。
Response的消息有兩種類型,CacheResponse和NetworkResponse。CacheResponse代表從緩存取到的消息,NetworkResponse代表直接從服務(wù)端返回的消息。
示例代碼如下:
private void testCache(){ //緩存文件夾 File cacheFile = new File(getExternalCacheDir().toString(),"cache"); //緩存大小為10M int cacheSize = 10 * 1024 * 1024; //創(chuàng)建緩存對(duì)象 final Cache cache = new Cache(cacheFile,cacheSize);new Thread(new Runnable() { @Override public void run() { OkHttpClient client = new OkHttpClient.Builder() .cache(cache) .build(); //官方的一個(gè)示例的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ò)請(qǐng)求 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ò)請(qǐng)求 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();}
我們?cè)谏厦娴拇a中,用同一個(gè)url地址分別進(jìn)行了兩次網(wǎng)絡(luò)訪問(wèn),然后分別用Log打印它們的信息。打印的結(jié)果主要說(shuō)明了一個(gè)現(xiàn)象,第一次訪問(wèn)的時(shí)候,Response的消息是NetworkResponse消息,此時(shí)CacheResponse的值為Null.而第二次訪問(wèn)的時(shí)候Response是CahceResponse,而此時(shí)NetworkResponse為空。也就說(shuō)明了上面的示例代碼能夠進(jìn)行網(wǎng)絡(luò)請(qǐng)求的緩存。
其實(shí)控制緩存的消息頭往往是服務(wù)端返回的信息中添加的如”Cache-Control:max-age=60”。所以,會(huì)有兩種情況。
第一種辦法當(dāng)然很好,只要服務(wù)器在返回消息的時(shí)候添加好Cache-Control相關(guān)的消息便好。
第二種情況,就很麻煩,你真的無(wú)法左右別人的行為。怎么辦呢?好在OKHTTP能夠很輕易地處理這種情況。那就是定義一個(gè)攔截器,
人為地添加Response中的消息頭,然后再傳遞給用戶,這樣用戶拿到的Response就有了我們理想當(dāng)中的消息頭Headers,從而達(dá)到控制緩存的意圖,正所謂移花接木。
因?yàn)閿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è)置緩存時(shí)間為60秒,并移除了pragma消息頭,移除它的原因是因?yàn)閜ragma也是控制緩存的一個(gè)消息頭屬性 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)建緩存對(duì)象 final Cache cache = new Cache(cacheFile,cacheSize);OkHttpClient client = new OkHttpClient.Builder() .addNetworkInterceptor(new CacheInterceptor()) .cache(cache) .build(); ....... }
代碼后面部分有省略。主要通過(guò)在OkHttpClient.Builder()中addNetworkInterceptor()中添加。而這樣也挺簡(jiǎn)單的,就幾步完成了緩存代碼。
攔截器進(jìn)行緩存的缺點(diǎn)
那么,問(wèn)題來(lái)了。
因?yàn)镺KHTTP開(kāi)發(fā)建議是同一個(gè)APP,用同一個(gè)OKHTTPCLIENT對(duì)象這是為了只有一個(gè)緩存文件訪問(wèn)入口。這個(gè)很容易理解,單例模式嘛。但是問(wèn)題攔截器是在OKHttpClient.Builder當(dāng)中添加的。如果在攔截器中定義緩存的方法會(huì)導(dǎo)致圖片的緩存和新聞列表的緩存時(shí)間是一樣的,這顯然是不合理的,真實(shí)的情況不應(yīng)該是圖片請(qǐng)求有它的緩存時(shí)間,新聞列表請(qǐng)求有它的緩存時(shí)間,應(yīng)該是每一個(gè)Request有它的緩存時(shí)間。 那么,有解決的方案嗎? 有的,okhttp官方有建議的方法。
okhttp官方文檔建議緩存方法
okhttp中建議用CacheControl這個(gè)類來(lái)進(jìn)行緩存策略的制定。
它內(nèi)部有兩個(gè)很重要的靜態(tài)實(shí)例。
/**強(qiáng)制使用網(wǎng)絡(luò)請(qǐng)求*/ public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();/*** 強(qiáng)制性使用本地緩存,如果本地緩存不滿足條件,則會(huì)返回code為504*/ public static final CacheControl FORCE_CACHE = new Builder() .onlyIfCached() .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS) .build();
我們看到FORCE_NETWORK常量用來(lái)強(qiáng)制使用網(wǎng)絡(luò)請(qǐng)求。FORCE_CACHE只取本地的緩存。它們本身都是CacheControl對(duì)象,由內(nèi)部的Buidler對(duì)象構(gòu)造。下面我們來(lái)看看CacheControl.Builder
CacheControl.Builder
它有如下方法:
noCache()
//不使用緩存,用網(wǎng)絡(luò)請(qǐng)求noStore()
//不使用緩存,也不存儲(chǔ)緩存onlyIfCached()
//只使用緩存noTransform()
//禁止轉(zhuǎn)碼maxAge(10, TimeUnit.MILLISECONDS)
//設(shè)置超時(shí)時(shí)間為10ms。maxStale(10, TimeUnit.SECONDS)
//超時(shí)之外的超時(shí)時(shí)間為10sminFresh(10, TimeUnit.SECONDS)
//超時(shí)時(shí)間為當(dāng)前時(shí)間加上10秒鐘。知道了CacheControl的相關(guān)信息,那么它怎么使用呢?不同于攔截器設(shè)置緩存,CacheControl是針對(duì)Request的,所以它可以針對(duì)每個(gè)請(qǐng)求設(shè)置不同的緩存策略。比如圖片和新聞列表。下面代碼展示如何用CacheControl設(shè)置一個(gè)60秒的超時(shí)時(shí)間。
private void testCacheControl(){ //緩存文件夾 File cacheFile = new File(getExternalCacheDir().toString(),"cache"); //緩存大小為10M int cacheSize = 10 * 1024 * 1024; //創(chuàng)建緩存對(duì)象 final Cache cache = new Cache(cacheFile,cacheSize);new Thread(new Runnable() { @Override public void run() { OkHttpClient client = new OkHttpClient.Builder() .cache(cache) .build(); //設(shè)置緩存時(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();}
強(qiáng)制使用緩存
前面有講CacheControl.FORCE_CACHE這個(gè)常量。
public static final CacheControl FORCE_CACHE = new Builder() .onlyIfCached() .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
它內(nèi)部其實(shí)就是調(diào)用onlyIfCached()和maxStale方法。
它的使用方法為
Request request = new Request.Builder() .url("http://blog.csdn.net/briblue") .cacheControl(Cache.FORCE_CACHE) .build();
但是如前面后提到的,如果緩存不符合條件會(huì)返回504.這個(gè)時(shí)候我們要根據(jù)情況再進(jìn)行編碼,如緩存不行就再進(jìn)行一次網(wǎng)絡(luò)請(qǐng)求。
Response forceCacheResponse = client.newCall(request).execute();if (forceCacheResponse.code() != 504) {// 資源已經(jīng)緩存了,可以直接使用} else {// 資源沒(méi)有緩存,或者是緩存不符合條件了。}
不使用緩存
前面也有講CacheControl.FORCE_NETWORK這個(gè)常量。
public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();
它的內(nèi)部其實(shí)是調(diào)用noCache()方法,也就是不緩存的意思。
它的使用方法為
Request request = new Request.Builder() .url("http://blog.csdn.net/briblue") .cacheControl(Cache.FORCE_NETWORK) .build();
還有一種情況將maxAge設(shè)置為0,也不會(huì)取緩存,直接走網(wǎng)絡(luò)。
Request request = new Request.Builder() .url("http://blog.csdn.net/briblue") .cacheControl(new CacheControl.Builder() .maxAge(0, TimeUnit.SECONDS)) .build();
使用Call.cancel()可以立即停止掉一個(gè)正在執(zhí)行的call. 如果一個(gè)線程正在寫(xiě)請(qǐng)求或者讀響應(yīng), 將會(huì)引發(fā)IOException. 當(dāng)call沒(méi)有必要的時(shí)候, 使用這個(gè)api可以節(jié)約網(wǎng)絡(luò)資源. 例如當(dāng)用戶離開(kāi)一個(gè)應(yīng)用時(shí), 不管同步還是異步的call都可以取消.
你可以通過(guò)tags來(lái)同時(shí)取消多個(gè)請(qǐng)求. 當(dāng)你構(gòu)建一請(qǐng)求時(shí), 使用RequestBuilder.tag(tag)來(lái)分配一個(gè)標(biāo)簽, 之后你就可以用OkHttpClient.cancel(tag)來(lái)取消所有帶有這個(gè)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); } }
沒(méi)有響應(yīng)時(shí)使用超時(shí)結(jié)束call. 沒(méi)有響應(yīng)的原因可能是客戶點(diǎn)鏈接問(wèn)題、服務(wù)器可用性問(wèn)題或者這之間的其他東西. OkHttp支持連接超時(shí), 讀取超時(shí)和寫(xiě)入超時(shí).
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); }
使用OkHttpClient, 所有的HTTP Client配置包括代理設(shè)置、超時(shí)設(shè)置、緩存設(shè)置. 當(dāng)你需要為單個(gè)call改變配置的時(shí)候, 調(diào)用OkHttpClient.newBuilder(). 這個(gè)api將會(huì)返回一個(gè)builder, 這個(gè)builder和原始的client共享相同的連接池, 分發(fā)器和配置.
下面的例子中,我們讓一個(gè)請(qǐng)求是500ms的超時(shí)、另一個(gè)是3000ms的超時(shí)。
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).
使用HTTP AUTH需要在server端配置http auth信息, 其過(guò)程如下:
OkHttp會(huì)自動(dòng)重試未驗(yàn)證的請(qǐng)求. 當(dāng)響應(yīng)是401 Not Authorized時(shí),Authenticator會(huì)被要求提供證書(shū). Authenticator的實(shí)現(xiàn)中需要建立一個(gè)新的包含證書(shū)的請(qǐng)求. 如果沒(méi)有證書(shū)可用, 返回null來(lái)跳過(guò)嘗試.
使用Response.challenges()來(lái)獲得任何authentication challenges的 schemes 和 realms. 當(dāng)完成一個(gè)Basic challenge, 使用Credentials.basic(username, password)來(lái)解碼請(qǐng)求頭.
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)證無(wú)法工作時(shí), 為了避免多次重試, 你可以返回空來(lái)放棄認(rèn)證. 例如, 當(dāng)exact credentials已經(jīng)嘗試過(guò), 你可能會(huì)直接想跳過(guò)認(rèn)證, 可以這樣做:
if (credential.equals(response.request().header("Authorization"))) { return null; // If we already failed with these credentials, don't retry.}
當(dāng)重試次數(shù)超過(guò)定義的次數(shù), 你若想跳過(guò)認(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; }
OkHttp使用完全教程
日期:2018-04 瀏覽次數(shù):6761
日期:2017-02 瀏覽次數(shù):3435
日期:2017-09 瀏覽次數(shù):3654
日期:2017-12 瀏覽次數(shù):3526
日期:2018-12 瀏覽次數(shù):4815
日期:2016-12 瀏覽次數(shù):4582
日期:2017-07 瀏覽次數(shù):13642
日期:2017-12 瀏覽次數(shù):3505
日期:2018-06 瀏覽次數(shù):4265
日期:2018-05 瀏覽次數(shù):4442
日期:2017-12 瀏覽次數(shù):3556
日期:2017-06 瀏覽次數(shù):3979
日期:2018-01 瀏覽次數(shù):3941
日期:2016-12 瀏覽次數(shù):3908
日期:2018-08 瀏覽次數(shù):4423
日期:2017-12 瀏覽次數(shù):3705
日期:2016-09 瀏覽次數(shù):6404
日期:2018-07 瀏覽次數(shù):3205
日期:2016-12 瀏覽次數(shù):3230
日期:2018-10 瀏覽次數(shù):3377
日期:2018-10 瀏覽次數(shù):3479
日期:2018-09 瀏覽次數(shù):3577
日期:2018-02 瀏覽次數(shù):3595
日期:2015-05 瀏覽次數(shù):3518
日期:2018-09 瀏覽次數(shù):3305
日期:2018-06 瀏覽次數(shù):3433
日期:2017-02 瀏覽次數(shù):3869
日期:2018-02 瀏覽次數(shù):4334
日期:2018-02 瀏覽次數(shù):4171
日期:2016-12 瀏覽次數(shù):3571
Copyright ? 2013-2018 Tadeng NetWork Technology Co., LTD. All Rights Reserved.