6.1 公鑰密鑰加密原理
6.1.1 基礎(chǔ)知識(shí)
- 密鑰:一般就是一個(gè)字符串或數(shù)字,在加密或者解密時(shí)傳遞給加密/解密算法接箫。
- 對(duì)稱加密算法:加密和解密都是使用的同一個(gè)密鑰等曼。因此對(duì)稱加密算法要保證安全性的話,密鑰要做好保密际插,只能讓使用的人知道,不能對(duì)外公開显设。
- 非對(duì)稱加密算法:加密使用的密鑰和解密使用的密鑰是不同的框弛。 公鑰密碼體制就是一種非對(duì)稱加密算法。
(1) 公鑰密碼體制
分為三個(gè)部分:公鑰捕捂、私鑰瑟枫、加密/解密算法
加密解密過程如下:
加密:通過加密算法和公鑰對(duì)內(nèi)容(或者說明文)進(jìn)行加密,得到密文指攒。
解密:通過解密算法和私鑰對(duì)密文進(jìn)行解密慷妙,得到明文。
注意:由公鑰加密的內(nèi)容允悦,只能由私鑰進(jìn)行解密膝擂。
公鑰密碼體制的公鑰和算法都是公開的,私鑰是保密的澡屡。在實(shí)際的使用中猿挚,有需要的人會(huì)生成一對(duì)公鑰和私鑰,把公鑰發(fā)布出去給別人使用驶鹉,自己保留私鑰绩蜻。
(2) RSA加密算法
一種公鑰密碼體制,公鑰公開室埋,私鑰保密办绝,它的加密解密算法是公開的伊约。 RSA的這一對(duì)公鑰、私鑰都可以用來加密和解密孕蝉,并且一方加密的內(nèi)容可以由并且只能由對(duì)方進(jìn)行解密屡律。
(3) 簽名
就是在信息的后面再加上一段內(nèi)容,可以證明信息沒有被修改過降淮。
一般是對(duì)信息做一個(gè)hash計(jì)算得到一個(gè)hash值(該過程不可逆)超埋,在把信息發(fā)送出去時(shí),把這個(gè)hash值加密后做為一個(gè)簽名和信息一起發(fā)出去佳鳖。 接收方在收到信息后霍殴,會(huì)重新計(jì)算信息的hash值,并和信息所附帶的hash值(解密后)進(jìn)行對(duì)比系吩,如果一致来庭,就說明信息的內(nèi)容沒有被修改過,因?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值不被修改。
6.1.2 基于RSA算法的加密通信的例子
“客戶”->“服務(wù)器”:你好
“服務(wù)器”->“客戶”:你好但壮,我是服務(wù)器
“客戶”->“服務(wù)器”:向我證明你就是服務(wù)器
“服務(wù)器”->“客戶”:你好冀泻,我是服務(wù)器{你好,我是服務(wù)器}[私鑰|RSA]
“客戶”->“服務(wù)器”:{我們后面的通信過程蜡饵,用對(duì)稱加密來進(jìn)行弹渔,這里是對(duì)稱加密算法和密鑰}[公鑰|RSA]
“服務(wù)器”->“客戶”:{OK,收到溯祸!}[密鑰|對(duì)稱加密算法]
“客戶”->“服務(wù)器”:{我的帳號(hào)是aaa肢专,密碼是123,把我的余額的信息發(fā)給我看看}[密鑰|對(duì)稱加密算法]
“服務(wù)器”->“客戶”:{你的余額是100元}[密鑰|對(duì)稱加密算法]
總結(jié)一下焦辅,RSA加密算法在這個(gè)通信過程中所起到的作用主要有兩個(gè):
1. 因?yàn)樗借€只有“服務(wù)器”擁有博杖,因此“客戶”可以通過判斷對(duì)方是否有私鑰來判斷對(duì)方是否是“服務(wù)器”。
2. 客戶端通過RSA的掩護(hù)筷登,安全的和服務(wù)器商量好一個(gè)對(duì)稱加密算法和密鑰來保證后面通信過程內(nèi)容的安全剃根。
但是這里還留有一個(gè)問題,“服務(wù)器”要對(duì)外發(fā)布公鑰前方,那“服務(wù)器”如何把公鑰發(fā)送給“客戶”呢狈醉?
我們可能會(huì)想到以下的兩個(gè)方法:
a) 把公鑰放到互聯(lián)網(wǎng)的某個(gè)地方的一個(gè)下載地址廉油,事先給“客戶”去下載。
b) 每次和“客戶”開始通信時(shí)苗傅,“服務(wù)器”把公鑰發(fā)給“客戶”抒线。
但是這個(gè)兩個(gè)方法都有一定的問題,
對(duì)于a)方法渣慕,“客戶”無法確定這個(gè)下載地址是不是“服務(wù)器”發(fā)布的嘶炭,你憑什么就相信這個(gè)
地址下載的東西就是“服務(wù)器”發(fā)布的而不是別人偽造的呢,萬一下載到一個(gè)假的怎么辦摇庙?另外要所有的“客戶”都在通信前事先去下載公鑰也很不現(xiàn)實(shí)旱物。
對(duì)于b)方法,也有問題卫袒,因?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)致了安全問題。這里問題的根源就在于仲吏,大家都可以生成公鑰不铆、私鑰對(duì),無法確認(rèn)公鑰對(duì)到底是誰的裹唆。 如果能夠確定公鑰到底是誰的誓斥,就不會(huì)有這個(gè)問題了。例如许帐,如果收到“黑客”冒充“服務(wù)器”發(fā)過來的公鑰劳坑,經(jīng)過某種檢查,如果能夠發(fā)現(xiàn)這個(gè)公鑰不是“服務(wù)器”的就好了舞吭。
6.1.3 數(shù)字證書
為了解決上述問題泡垃,數(shù)字證書出現(xiàn)了析珊,它可以解決我們上面的問題。先大概看下什么是數(shù)字證書蔑穴,一個(gè)證書包含下面的具體內(nèi)容:
- 證書的發(fā)布機(jī)構(gòu)
- 證書的有效期
- 證書所有者(Subject)
- 公鑰
- 指紋和指紋算法
- 簽名算法
指紋和指紋算法
這個(gè)是用來保證證書的完整性的忠寻,也就是說確保證書沒有被修改過。 其原理就是在發(fā)布證書時(shí)存和,發(fā)布者根據(jù)指紋算法(一個(gè)hash算法)計(jì)算整個(gè)證書的hash值(指紋)并和證書放在一起奕剃,使用者在打開證書時(shí),自己也根據(jù)指紋算法計(jì)算一下證書的hash值(指紋)捐腿,如果和剛開始的值對(duì)得上纵朋,就說明證書沒有被修改過,因?yàn)樽C書的內(nèi)容被修改后茄袖,根據(jù)證書的內(nèi)容計(jì)算的出的hash值(指紋)是會(huì)變化的操软。
注意,這個(gè)指紋會(huì)用"SecureTrust CA"這個(gè)證書機(jī)構(gòu)的私鑰用簽名算法加密后和證書放在一起宪祥。
簽名算法
就是指的這個(gè)數(shù)字證書的數(shù)字簽名所使用的加密算法聂薪,這樣就可以使用證書發(fā)布機(jī)構(gòu)的證書里面的公鑰,根據(jù)這個(gè)算法對(duì)指紋進(jìn)行解密翔悠。指紋的加密結(jié)果就是數(shù)字簽名
數(shù)字證書可以保證數(shù)字證書里的公鑰確實(shí)是這個(gè)證書的所有者(Subject)的,或者證書可以用來確認(rèn)對(duì)方的身份剑刑。也就是說,我們拿到一個(gè)數(shù)字證書汁针,我們可以判斷出這個(gè)數(shù)字證書到底是誰的卓练。至于是如何判斷的,后面會(huì)在詳細(xì)討論數(shù)字證書時(shí)詳細(xì)解釋。現(xiàn)在把前面的通信過程使用數(shù)字證書修改為如下:
“客戶”->“服務(wù)器”:你好
“服務(wù)器”->“客戶”:你好忌穿,我是服務(wù)器,這里是我的數(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ì)比看是否匹配躬翁,如果匹配就說明信息在傳輸過程中沒有被修改過。如果不匹配說明中途有人故意對(duì)加密數(shù)據(jù)進(jìn)行了修改盯拱,立刻中斷通話過程后做其它處理盒发。
如何向證書的發(fā)布機(jī)構(gòu)去申請(qǐng)證書
舉個(gè)例子,假設(shè)我們公司"ABC Company"花了1000塊錢狡逢,向一個(gè)證書發(fā)布機(jī)構(gòu)"SecureTrust CA"為我們自己的公司"ABC Company"申請(qǐng)了一張證書宁舰,注意,這個(gè)證書發(fā)布機(jī)構(gòu)"SecureTrust CA"是一個(gè)大家公認(rèn)并被一些權(quán)威機(jī)構(gòu)接受的證書發(fā)布機(jī)構(gòu)奢浑,我們的操作系統(tǒng)里面已經(jīng)安裝了"SecureTrust CA"的證書蛮艰。"SecureTrust CA"在給我們發(fā)布證書時(shí),把Issuer,Public key,Subject,Valid from,Valid to等信息以明文的形式寫到證書里面雀彼,然后用一個(gè)指紋算法計(jì)算出這些數(shù)字證書內(nèi)容的一個(gè)指紋壤蚜,并把指紋和指紋算法用自己的私鑰進(jìn)行加密,然后和證書的內(nèi)容一起發(fā)布详羡,同時(shí)"SecureTrust CA"還會(huì)給一個(gè)我們公司"ABC Company"的私鑰給到我們仍律。
我們"ABC Company"申請(qǐng)到這個(gè)證書后,我們把證書投入使用实柠,我們?cè)谕ㄐ胚^程開始時(shí)會(huì)把證書發(fā)給對(duì)方水泉。
對(duì)方如何檢查這個(gè)證書的確是合法的并且是我們"ABC Company"公司的證書呢?首先應(yīng)用程序(對(duì)方通信用的程序,例如IE草则、OUTLook等)讀取證書中的Issuer(發(fā)布機(jī)構(gòu))為"SecureTrust CA" 钢拧,然后會(huì)在操作系統(tǒng)中受信任的發(fā)布機(jī)構(gòu)的證書中去找"SecureTrust CA"的證書,如果找不到炕横,那說明證書的發(fā)布機(jī)構(gòu)是個(gè)水貨發(fā)布機(jī)構(gòu)源内,證書可能有問題,程序會(huì)給出一個(gè)錯(cuò)誤信息份殿。 如果在系統(tǒng)中找到了"SecureTrust CA"的證書膜钓,那么應(yīng)用程序就會(huì)從證書中取出"SecureTrust CA"的公鑰,然后對(duì)我們"ABC Company"公司的證書里面的指紋和指紋算法用這個(gè)公鑰進(jìn)行解密卿嘲,然后使用這個(gè)指紋算法計(jì)算"ABC Company"證書的指紋颂斜,將這個(gè)計(jì)算的指紋與放在證書中的指紋對(duì)比,如果一致拾枣,說明"ABC Company"的證書肯定沒有被修改過并且證書是"SecureTrust CA" 發(fā)布的沃疮,證書中的公鑰肯定是"ABC Company"的。對(duì)方然后就可以放心的使用這個(gè)公鑰和我們"ABC Company"進(jìn)行通信了梅肤。
6.2 Http協(xié)議原理
6.2.1 基礎(chǔ)知識(shí)
1. TCP/IP協(xié)議族
- IP協(xié)議:網(wǎng)絡(luò)層協(xié)議司蔬,保證了計(jì)算機(jī)之間可以發(fā)送和接收數(shù)據(jù)。
- TCP協(xié)議:傳輸層協(xié)議姨蝴,一種端到端的協(xié)議俊啼,建立一個(gè)虛擬鏈路用于發(fā)送和接收數(shù)據(jù),基于重發(fā)機(jī)制左医,提供可靠的通信連接吨些。為了方便通信,將報(bào)文分割成多個(gè)報(bào)文段發(fā)送炒辉。
- UDP協(xié)議:傳輸層協(xié)議,一種無連接的協(xié)議泉手,每個(gè)數(shù)據(jù)報(bào)都是一個(gè)獨(dú)立的信息黔寇,包括完整的源地址或目的地址,它在網(wǎng)絡(luò)上以任何可能的路徑傳往目的地斩萌,因此能否到達(dá)目的地缝裤,到達(dá)目的地的時(shí)間以及內(nèi)容的正確性都是不能被保證的。
通信雙方一方作為服務(wù)器等待客戶提出請(qǐng)求并予以響應(yīng)颊郎”锓桑客戶則在需要服務(wù)時(shí)向服務(wù)器提出申請(qǐng)。服務(wù)器一般作為守護(hù)進(jìn)程始終運(yùn)行姆吭,監(jiān)聽網(wǎng)絡(luò)端口榛做,一旦有客戶請(qǐng)求,就會(huì)啟動(dòng)一個(gè)服務(wù)進(jìn)程來響應(yīng)該客戶,同時(shí)自己繼續(xù)監(jiān)聽服務(wù)端口检眯,使后來的客戶也能及時(shí)得到服務(wù)厘擂。一個(gè)socket(通常都是server socket)等待建立連接時(shí),另一個(gè)socket可以要求進(jìn)行連接锰瘸,一旦這兩個(gè)socket連接起來刽严,它們就可以進(jìn)行雙向數(shù)據(jù)傳輸,雙方都可以進(jìn)行發(fā)送或接收操作避凝。
2. TCP3次握手舞萄,4次揮手過程
(1) 建立連接協(xié)議(三次握手)
a)客戶端發(fā)送一個(gè)帶SYN標(biāo)志的TCP報(bào)文到服務(wù)器。(聽得到嗎管削?)
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給客戶端把还,詢問客戶端是否準(zhǔn)備好進(jìn)行數(shù)據(jù)通訊。(聽得到茸俭,你能聽到我嗎吊履?)
c)客戶必須再次回應(yīng)服務(wù)端一個(gè)ACK報(bào)文。(聽到了调鬓,我們可以說話了)
為什么需要“三次握手”艇炎?
在謝希仁著《計(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)文段并沒有丟失,而是在某個(gè)網(wǎng)絡(luò)結(jié)點(diǎn)長(zhǎng)時(shí)間的滯留了虹脯,以致延誤到連接釋放以后的某個(gè)時(shí)間才到達(dá)server驴娃。本來這是一個(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并沒有發(fā)出建立連接的請(qǐng)求旷档,因此不會(huì)理睬server的確認(rèn),也不會(huì)向server發(fā)送數(shù)據(jù)歇拆。但server卻以為新的運(yùn)輸連接已經(jīng)建立鞋屈,并一直等待client發(fā)來數(shù)據(jù)范咨。這樣,server的很多資源就白白浪費(fèi)掉了谐区。采用“三次握手”的辦法可以防止上述現(xiàn)象發(fā)生湖蜕。例如剛才那種情況,client不會(huì)向server的確認(rèn)發(fā)出確認(rèn)宋列。server由于收不到確認(rèn)昭抒,就知道client并沒有要求建立連接×墩龋”灭返。 主要目的防止server端一直等待,浪費(fèi)資源坤邪。
(2) 連接終止協(xié)議(四次揮手)
由于TCP連接是全雙工的熙含,因此每個(gè)方向都必須單獨(dú)進(jìn)行關(guān)閉。這原則是當(dāng)一方完成它的數(shù)據(jù)發(fā)送任務(wù)后就能發(fā)送一個(gè)FIN來終止這個(gè)方向的連接艇纺。收到一個(gè) FIN只意味著這一方向上沒有數(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,用來關(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ì)有疑問,在tcp連接握手時(shí)為何ACK是和SYN一起發(fā)送胀溺,這里ACK卻沒有和FIN一起發(fā)送呢。原因是因?yàn)閠cp是全雙工模式皆看,接收到FIN時(shí)意味將沒有數(shù)據(jù)再發(fā)來仓坞,但是還是可以繼續(xù)發(fā)送數(shù)據(jù)。
3. 請(qǐng)求報(bào)文
(1) 請(qǐng)求行
由3部分組成腰吟,分別為:請(qǐng)求方法无埃、URL以及協(xié)議版本徙瓶,之間由空格分隔
請(qǐng)求方法:GET、HEAD嫉称、PUT侦镇、POST等方法,但并不是所有的服務(wù)器都實(shí)現(xiàn)了所有的方法织阅,部分方法即便支持壳繁,處于安全性的考慮也是不可用的
協(xié)議版本:常用HTTP/1.1
(2) 請(qǐng)求頭部
請(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ā)送的語言 -
Range
正文的字節(jié)請(qǐng)求范圍渣触,為斷點(diǎn)續(xù)傳和并行下載提供可能,返回狀態(tài)碼是206 -
Authorization
用于設(shè)置身份認(rèn)證信息 -
Cookie
已有的Cookie
請(qǐng)求頭部的最后會(huì)有一個(gè)空行壹若,表示請(qǐng)求頭部結(jié)束嗅钻,接下來為請(qǐng)求正文,這一行非常重要店展,必不可少
(3) 請(qǐng)求正文
可選部分养篓,比如GET請(qǐng)求就沒有請(qǐng)求正文
4. 響應(yīng)報(bào)文
由3部分組成,分別為:協(xié)議版本壁查,狀態(tài)碼觉至,狀態(tài)碼描述,之間由空格分隔
狀態(tài)碼:為3位數(shù)字睡腿,2XX表示成功语御,3XX表示資源重定向,4XX表示客戶端請(qǐng)求出錯(cuò)席怪,5XX表示服務(wù)端出錯(cuò)
206狀態(tài)碼表示的是:客戶端通過發(fā)送范圍請(qǐng)求頭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)正文長(zhǎng)度 -
Content-Charset
響應(yīng)正文使用的編碼 -
Content-Language
響應(yīng)正文使用的語言 -
Content-Range
正文的字節(jié)位置范圍 -
Accept-Ranges
bytes:表明服務(wù)器支持Range請(qǐng)求,單位是字節(jié)哄尔;none:不支持 -
Set-Cookie
設(shè)置Cookie
正文的內(nèi)容可以用gzip等進(jìn)行壓縮,以提升傳輸速率
5. 通用首部
(1) Cache-Control
用于操作瀏覽器緩存的工作機(jī)制舶衬。取值如下:
- max-age:表示緩存的新鮮時(shí)間重贺,在此時(shí)間內(nèi)可以直接使用緩存。單位秒声怔。
- no-cache:不做緩存态贤。
- max-stale:可以接受過期多少秒的緩存。
(2) Connection
用于管理持久連接醋火。目前大部分瀏覽器都是用http1.1協(xié)議悠汽,也就是說默認(rèn)都會(huì)發(fā)起Keep-Alive的連接請(qǐng)求箱吕。所以是否能完成一個(gè)完整的Keep-Alive連接就看服務(wù)器設(shè)置情況。取值如下:
- keep-alive:使客戶端到服務(wù)器端的連接持續(xù)有效柿冲,當(dāng)出現(xiàn)對(duì)服務(wù)器的后繼請(qǐng)求時(shí)茬高,避免了建立或者重新建立連接。
- close:每個(gè)請(qǐng)求/應(yīng)答客戶和服務(wù)器都要新建一個(gè)連接假抄,完成之后立即斷開連接怎栽。
(3) Transfer-Encoding
在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-alive
cf0 //16進(jìn)制蛉签,值為3312
...3312字節(jié)分塊數(shù)據(jù)...
392 //16進(jìn)制胡陪,值為914
...914字節(jié)分塊數(shù)據(jù)...
0
(4) Content-Encoding
請(qǐng)求體/響應(yīng)體的編碼格式,如gzip
6. HTTP Authentication
兩種常見的Authentication機(jī)制:HTTP Basic和Digest碍舍。(現(xiàn)在用的并不多柠座,了解一下)
(1) Http Basic
最簡(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ò)誤的用戶的密碼來發(fā)送請(qǐng)求吹泡,則會(huì)得到類似如下含有401錯(cuò)誤的response頭部:
HTTP/1.1 401 Bad credentials
WWW-Authenticate: Basic realm="Spring Security Application"
...
(2) Http Digest
當(dāng)Alice初次訪問服務(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ù)器,其編碼使用的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
...
請(qǐng)求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值放在一起然后進(jìn)行MD5編碼,得到一個(gè)response值癣防,如前面紅色標(biāo)出所示蜗巧,這樣服務(wù)器就可以通過這個(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式攻擊。
7. 請(qǐng)求體的3種形式
據(jù)應(yīng)用場(chǎng)景的不同挫鸽,HTTP請(qǐng)求的請(qǐng)求體有三種不同的形式说敏。
第一種:
移動(dòng)開發(fā)者常見的,請(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開頭上遥,然后是該段的描述頭搏屑,描述頭之后空一行接內(nèi)容,請(qǐng)求結(jié)束的標(biāo)制為boundary后面加--粉楚。(見下面詳細(xì)說明)
8. http協(xié)議中的多部分對(duì)象(multipart/form-data)
默認(rèn)是application/x-www-form-urlencoded辣恋,但是在傳輸大型文件的時(shí)候效率比較低下。所以需要multipart/form-data模软。
報(bào)文的主體內(nèi)可以包含多部分對(duì)象伟骨,通常用來發(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é)束
6.2.2Https
(1) Http的缺點(diǎn)
- 通信使用明文逛腿,內(nèi)容可能會(huì)被竊聽 —— 加密通信線路
- 不驗(yàn)證通信方稀并,可能遭遇偽裝 —— 證書
- 無法驗(yàn)證報(bào)文的完整性,可能已被篡改 —— 數(shù)字簽名
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ì)被偷窺(嗅探)和篡改等安全問題而設(shè)計(jì)的,到了1999年粱年,SSL成為互聯(lián)網(wǎng)上的標(biāo)準(zhǔn)售滤,名稱改為TLS(transport layer security):安全傳輸層協(xié)議,兩者可視為同一種東西的不同階段逼泣。
(2) Https的工作原理
HTTPS在傳輸數(shù)據(jù)之前需要客戶端(瀏覽器)與服務(wù)端(網(wǎng)站)之間進(jìn)行一次握手趴泌,在握手過程中將確立雙方加密傳輸數(shù)據(jù)的密碼信息。TLS/SSL協(xié)議不僅僅是一套加密傳輸?shù)膮f(xié)議拉庶,更是一件經(jīng)過藝術(shù)家精心設(shè)計(jì)的藝術(shù)品嗜憔,TLS/SSL中使用了非對(duì)稱加密,對(duì)稱加密以及HASH算法氏仗。握手過程的具體描述如下:
- 瀏覽器將自己支持的一套加密規(guī)則發(fā)送給網(wǎng)站吉捶。
- 網(wǎng)站從中選出一組加密算法與HASH算法,并將自己的身份信息以證書的形式發(fā)回給瀏覽器皆尔。證書里面包含了網(wǎng)站地址呐舔,加密公鑰,以及證書的頒發(fā)機(jī)構(gòu)等信息慷蠕。
- 瀏覽器獲得網(wǎng)站證書之后瀏覽器要做以下工作:
a) 驗(yàn)證證書的合法性(頒發(fā)證書的機(jī)構(gòu)是否合法珊拼,證書中包含的網(wǎng)站地址是否與正在訪問的地址一致等),如果證書受信任流炕,則瀏覽器欄里面會(huì)顯示一個(gè)小鎖頭澎现,否則會(huì)給出證書不受信的提示。
b) 如果證書受信任每辟,或者是用戶接受了不受信的證書剑辫,瀏覽器會(huì)生成一串隨機(jī)數(shù)的密碼,并用證書中提供的公鑰加密渠欺。
c) 使用約定好的HASH算法計(jì)算握手消息妹蔽,并使用生成的隨機(jī)數(shù)對(duì)消息進(jìn)行加密,最后將之前生成的所有信息發(fā)送給網(wǎng)站。 - 網(wǎng)站接收瀏覽器發(fā)來的數(shù)據(jù)之后要做以下的操作:
a) 使用自己的私鑰將信息解密取出密碼胳岂,使用密碼解密瀏覽器發(fā)來的握手消息编整,并驗(yàn)證HASH是否與瀏覽器發(fā)來的一致。
b) 使用密碼加密一段握手消息乳丰,發(fā)送給瀏覽器闹击。 - 瀏覽器解密并計(jì)算握手消息的HASH,如果與服務(wù)端發(fā)來的HASH一致成艘,此時(shí)握手過程結(jié)束,之后所有的通信數(shù)據(jù)將由之前瀏覽器生成的隨機(jī)密碼并利用對(duì)稱加密算法進(jìn)行加密贺归。
這里瀏覽器與網(wǎng)站互相發(fā)送加密的握手消息并驗(yàn)證淆两,目的是為了保證雙方都獲得了一致的密碼,并且可以正常的加密解密數(shù)據(jù)拂酣,為后續(xù)真正數(shù)據(jù)的傳輸做一次測(cè)試秋冰。另外,HTTPS一般使用的加密與HASH算法如下:
- 非對(duì)稱加密算法:RSA婶熬,DSA/DSS
- 對(duì)稱加密算法:AES剑勾,RC4,3DES
- HASH算法:MD5虽另,SHA1饺谬,SHA256
HTTPS對(duì)應(yīng)的通信時(shí)序圖如下:
(3) 證書分類
SSL 證書大致分三類:
- 認(rèn)可的證書頒發(fā)機(jī)構(gòu)(如: VeriSign), 或這些機(jī)構(gòu)的下屬機(jī)構(gòu)頒發(fā)的證書.
- 沒有得到認(rèn)可的證書頒發(fā)機(jī)構(gòu)頒發(fā)的證書.
- 自簽名證書列肢, 自己通過JDK自帶工具keytool去生成一個(gè)證書,分為臨時(shí)性的(在開發(fā)階段使用)或在發(fā)布的產(chǎn)品中永久性使用的兩種.
只有第一種, 也就是那些被安卓系統(tǒng)認(rèn)可的機(jī)構(gòu)頒發(fā)的證書, 在使用過程中不會(huì)出現(xiàn)安全提示虽抄。對(duì)于向權(quán)威機(jī)構(gòu)(簡(jiǎn)稱CA,Certificate Authority)申請(qǐng)過證書的網(wǎng)絡(luò)地址,用OkHttp或者HttpsURLConnection都可以直接訪問 骇径,不需要做額外的事情 躯肌。但是申請(qǐng)需要$$ (每年要交 100 到 500 美元不等的費(fèi)用)。
CA機(jī)構(gòu)頒發(fā)的證書有3種類型:
域名型SSL證書(DV SSL):信任等級(jí)普通破衔,只需驗(yàn)證網(wǎng)站的真實(shí)性便可頒發(fā)證書保護(hù)網(wǎng)站清女;
企業(yè)型SSL證書(OV SSL):信任等級(jí)強(qiáng),須要驗(yàn)證企業(yè)的身份晰筛,審核嚴(yán)格嫡丙,安全性更高;
增強(qiáng)型SSL證書(EV SSL):信任等級(jí)最高读第,一般用于銀行證券等金融機(jī)構(gòu)曙博,審核嚴(yán)格,安全性最高怜瞒,同時(shí)可以激活綠色網(wǎng)址欄父泳。
(4) HTTPS協(xié)議和HTTP協(xié)議的區(qū)別:
- https協(xié)議需要到ca申請(qǐng)證書般哼,一般免費(fèi)證書很少,需要交費(fèi)惠窄。
- http是超文本傳輸協(xié)議蒸眠,信息是明文傳輸,https 則是具有安全性的ssl加密傳輸協(xié)議杆融。
- http和https使用的是完全不同的連接方式用的端口也不一樣,前者是80,后者是443楞卡。
- http的連接很簡(jiǎn)單,是無狀態(tài)的 。
- HTTPS協(xié)議是由SSL+HTTP協(xié)議構(gòu)建的可進(jìn)行加密傳輸脾歇、身份認(rèn)證的網(wǎng)絡(luò)協(xié)議蒋腮, 要比http協(xié)議安全。
6.3 Android SDK支持
6.3.1 InetAddress
(1) 簡(jiǎn)介
- 代表IP地址藕各,還有兩個(gè)子類徽惋,Inet4Address、Inet6Address
- 沒有構(gòu)造方法
(2) 方法
-
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 沒有http:// -
InetAddress.getLocalHost()
返回本機(jī) -
getHostAddress() String
返回IP地址 -
getHostName() String
返回主機(jī)名 -
isReachable(int timeout) boolean
測(cè)試是否可以達(dá)到該地址,毫秒數(shù)
2. URLDecoder和URLEncoder
(1) 簡(jiǎn)介
- URLDecoder和URLEncoder用于完成普通字符串和
application/x-www-form-urlencoded MIME
字符串之間的相互轉(zhuǎn)換 - 若每個(gè)中文占2個(gè)字節(jié)踢京,每個(gè)字節(jié)轉(zhuǎn)換成2個(gè)十六進(jìn)制的數(shù)字誉碴,所以每個(gè)中文字符轉(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通信簡(jiǎn)介
Socket又稱套接字,是程序內(nèi)部提供的與外界通信的端口瓣距,即端口通信黔帕。通過建立socket連接,可為通信雙方的數(shù)據(jù)傳輸傳提供通道蹈丸。主要特點(diǎn)有數(shù)據(jù)丟失率低成黄,使用簡(jiǎn)單且易于移植。
(1) Socket的分類
在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ù)闻伶。
2. Socket 基本通信模型
(1) Socket通信模型
(2) TCP通信模型
(3) 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é)議通信
客戶端實(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ù)寫入到OuputStream對(duì)象中
outputStream.write(buffer, 0, temp);
}
// 發(fā)送讀取的數(shù)據(jù)到服務(wù)端
outputStream.flush();
/** 或創(chuàng)建一個(gè)報(bào)文女嘲,使用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ù)端實(shí)現(xiàn):
public void serverReceviedByTcp() {
// 聲明一個(gè)ServerSocket對(duì)象
ServerSocket serverSocket = null;
try {
// 創(chuàng)建一個(gè)ServerSocket對(duì)象畜份,并讓這個(gè)Socket在1989端口監(jiān)聽
serverSocket = new ServerSocket(1989);
// 調(diào)用ServerSocket的accept()方法,接受客戶端所發(fā)送的請(qǐng)求欣尼,
// 如果客戶端沒有發(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();
}
}
5. 方法
(1) ServerSocket
監(jiān)聽來自于客戶端的Socket連接,如果沒有連接驯绎,它將一直處于等待狀態(tài)
-
ServerSocket(int port, int backlog, InetAddress bindAddr)
在機(jī)器存在多IP的情況下完慧,允許通過bindAddr來指定綁定到哪個(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)的Socket close()
(2) Socket
-
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()
6.3.3HttpURLConnection
1. URL
1)對(duì)象代表統(tǒng)一資源定位器,是指向互聯(lián)網(wǎng)“資源”的指針
2)通常由協(xié)議名演熟、主機(jī)鞭执、端口、資源路徑組成
3)URL包含一個(gè)可打開到達(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ā)送請(qǐng)求座云,讀取URL指向的資源
-
setDoInput(boolean doinput)
發(fā)送POST請(qǐng)求疙赠,必須設(shè)置,設(shè)置為true -
setDoOutput(boolean dooutput)
發(fā)送POST請(qǐng)求朦拖,必須設(shè)置圃阳,設(shè)置為true -
setUseCaches(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
3. HttpURLConnection
(1) 簡(jiǎn)介
抽象類限佩,是URLConnection的子類,增加了操作Http資源的便捷方法
- 對(duì)象不能直接構(gòu)造裸弦,需要通過URL類中的openConnection()方法來獲得祟同。
- connect()函數(shù),實(shí)際上只是建立了一個(gè)與服務(wù)器的TCP連接理疙,并沒有實(shí)際發(fā)送HTTP請(qǐng)求晕城。HTTP請(qǐng)求實(shí)際上直到我們獲取服務(wù)器響應(yīng)數(shù)據(jù)(如調(diào)用getInputStream()、getResponseCode()等方法)時(shí)才正式發(fā)送出去窖贤。
- 對(duì)HttpURLConnection對(duì)象的配置都需要在connect()方法執(zhí)行之前完成砖顷。
- HttpURLConnection是基于HTTP協(xié)議的贰锁,其底層通過socket通信實(shí)現(xiàn)。如果不設(shè)置超時(shí)(timeout)滤蝠,在網(wǎng)絡(luò)異常的情況下豌熄,可能會(huì)導(dǎo)致程序僵死而不繼續(xù)往下執(zhí)行。
- HTTP正文的內(nèi)容是通過OutputStream流寫入的物咳, 向流中寫入的數(shù)據(jù)不會(huì)立即發(fā)送到網(wǎng)絡(luò)锣险,而是存在于內(nèi)存緩沖區(qū)中,待流關(guān)閉時(shí)览闰,根據(jù)寫入的內(nèi)容生成HTTP正文芯肤。
- 調(diào)用getInputStream()方法時(shí),返回一個(gè)輸入流压鉴,用于從中讀取服務(wù)器對(duì)于HTTP請(qǐng)求的返回信息崖咨。
- 我們可以使用connect()方法手動(dòng)的發(fā)送一個(gè)HTTP請(qǐng)求,但是如果要獲取HTTP響應(yīng)的時(shí)候油吭,請(qǐng)求就會(huì)自動(dòng)的發(fā)起击蹲,比如我們使用getInputStream()方法的時(shí)候,所以完全沒有必要調(diào)用connect()方法婉宰。
(2) 方法
setRequestMethod(String method)
setInstanceFollowRedirects(boolean followRedirects)
-
getResponseCode() int
獲取服務(wù)器的響應(yīng)碼 -
getResponseMessage() String
獲取響應(yīng)消息 -
getRequestMethod() String
獲取發(fā)送請(qǐng)求的方法歌豺,GET或POST -
disconnect()
抽象方法
(3) 實(shí)現(xiàn)多線程下載
步驟:
①:創(chuàng)建URL對(duì)象
②:獲取URL對(duì)象指向資源的大小(getContentLength()方法)
③:在本地創(chuàng)建一個(gè)與網(wǎng)路資源相同大小的空文件
④:計(jì)算每條線程應(yīng)該下載網(wǎng)絡(luò)資源的哪個(gè)部分(從哪個(gè)字節(jié)開始芍阎,到哪個(gè)字節(jié)結(jié)束)
⑤:依次創(chuàng)建,啟動(dòng)多條線程來下載網(wǎng)絡(luò)資源的指定部分
(4) 實(shí)例
使用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ò)訪問對(duì)象java.net.HttpURLConnection
HttpURLConnection connection = (HttpURLConnection) url
.openConnection();
/* 3. 設(shè)置請(qǐng)求參數(shù)(過期時(shí)間缨恒,輸入谴咸、輸出流、訪問方式)骗露,以流的形式進(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. 斷開連接,釋放資源
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對(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. 處理輸入輸出 */
// 寫入?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. 斷開連接
connection.disconnect();
// 處理結(jié)果
System.out.println(msg);
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
4. HttpClient
(Android6.0之后的SDK中移除了對(duì)于HttpClient的支持萧锉,僅作了解)
在一般情況下珊随,如果只是需要向Web站點(diǎn)的某個(gè)簡(jiǎn)單頁(yè)面提交請(qǐng)求并獲取服務(wù)器響應(yīng),HttpURLConnection完全可以勝任柿隙。但在絕大部分情況下叶洞,Web站點(diǎn)的網(wǎng)頁(yè)可能沒這么簡(jiǎn)單宛逗,這些頁(yè)面并不是通過一個(gè)簡(jiǎn)單的URL就可訪問的峦椰,可能需要用戶登錄而且具有相應(yīng)的權(quán)限才可訪問該頁(yè)面。在這種情況下赁豆,就需要涉及Session波附、Cookie的處理了艺晴,如果打算使用HttpURLConnection來處理這些細(xì)節(jié)昼钻,當(dāng)然也是可能實(shí)現(xiàn)的,只是處理起來難度就大了封寞。
為了更好地處理向Web站點(diǎn)請(qǐng)求然评,包括處理Session、Cookie等細(xì)節(jié)問題狈究,Apache開源組織提供了一個(gè)HttpClient項(xiàng)目碗淌。HttpClient就是一個(gè)增強(qiáng)版的HttpURLConnection,HttpURLConnection可以做的事情HttpClient全部可以做谦炒;HttpURLConnection沒有提供的有些功能贯莺,HttpClient也提供了,但它只是關(guān)注于如何發(fā)送請(qǐng)求宁改、接收響應(yīng)缕探,以及管理HTTP連接。
(1) HttpClient的使用
步驟:
①:創(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)方法來設(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)容)
(2) 使用GET方式訪問HTTP
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();
}
}
}
(3) 使用POST方式訪問HTTP
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();
}
}
}
6.4 OkHttp
6.4.1 簡(jiǎn)介
HttpClient是Apache基金會(huì)的一個(gè)開源網(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ò)展舱污。不過在Android 2.2版本之前,HttpURLConnection一直存在著一些令人厭煩的bug弥虐。比如說對(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開始番捂,google已經(jīng)開始將源碼中的HttpURLConnection替換為OkHttp,而市面上流行的Retrofit同樣是使用OkHttp進(jìn)行再次封裝而來的江解。
OkHttp是一個(gè)快速设预、高效的網(wǎng)絡(luò)請(qǐng)求庫(kù),它的設(shè)計(jì)和實(shí)現(xiàn)的首要目標(biāo)便是高效犁河,有如下特性:
- 支持HTTP/2, HTTP/2通過使用多路復(fù)用技術(shù)在一個(gè)單獨(dú)的TCP連接上支持并發(fā), 通過在一個(gè)連接上一次性發(fā)送多個(gè)請(qǐng)求來發(fā)送或接收數(shù)據(jù)鳖枕;
- 如果HTTP/2不可用, 連接池復(fù)用技術(shù)也可以極大減少延時(shí);
- 支持Gzip壓縮響應(yīng)體桨螺,降低傳輸內(nèi)容的大斜龇;
- 支持Http緩存灭翔,避免重復(fù)請(qǐng)求魏烫;
- 如果您的服務(wù)器配置了多個(gè)IP地址, 當(dāng)?shù)谝粋€(gè)IP連接失敗的時(shí)候, OkHttp會(huì)自動(dòng)嘗試下一個(gè)IP;
- 使用Okio來簡(jiǎn)化數(shù)據(jù)的訪問與存儲(chǔ)肝箱,提高性能哄褒;
- OkHttp還處理了代理服務(wù)器問題和SSL握手失敗問題;
6.4.2 OkHttp類與Http請(qǐng)求響應(yīng)的映射
1. Http請(qǐng)求
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;
...
}
2. Http響應(yīng)
Http響應(yīng)由訪問協(xié)議, 響應(yīng)碼, 描述信息, 響應(yīng)頭, 響應(yīng)體來組成糊治。
public final class Response implements Closeable {
final Request request;//持有的請(qǐng)求
final Protocol protocol;//訪問協(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)體
...
}
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是一個(gè)接口 -
hostnameVerifier(HostnameVerifier hostnameVerifier)
HostnameVerifier是一個(gè)接口唱矛,只有boolean verify(String hostname, SSLSession session)
sslSocketFactory(SSLSocketFactory sslSocketFactory, X509TrustManager trustManager)
2. Request
Request(Request.Builder builder)
Request.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
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ā)送請(qǐng)求和讀取響應(yīng)
-
enqueue(Callback responseCallback)
加入調(diào)度隊(duì)列绎谦,異步執(zhí)行 -
execute() Response
同步執(zhí)行 cancel()
4. Response
body() ResponseBody
-
code() int
http請(qǐng)求的狀態(tài)碼 -
isSuccessful()
code為2XX時(shí),返回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ù)是個(gè)普通表單(默認(rèn))
- multipart/form-data 數(shù)據(jù)里有文件
- application/json 數(shù)據(jù)是個(gè)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 | 二進(jìn)制流數(shù)據(jù) |
application/x-www-form-urlencoded | 普通表單數(shù)據(jù) |
multipart/form-data | 表單數(shù)據(jù)里有文件 |
6. 自動(dòng)管理Cookie
Request經(jīng)常都要攜帶Cookie刷允,request創(chuàng)建時(shí)可以通過header設(shè)置參數(shù)冤留,Cookie也是參數(shù)之一碧囊。就像下面這樣:
Request request = new Request.Builder()
.url(url)
.header("Cookie", "xxx")
.build();
然后可以從返回的response里得到新的Cookie,你可能得想辦法把Cookie保存起來纤怒。
但是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什么的了熄驼。還能通過cookieStore獲取當(dāng)前保存的Cookie。
最后烘豹,new OkHttpClient()只是一種快速創(chuàng)建OkHttpClient的方式瓜贾,更標(biāo)準(zhǔn)的是使用OkHttpClient.Builder()。后者可以設(shè)置一堆參數(shù)携悯,例如超時(shí)時(shí)間什么的祭芦。
6.4.4 get請(qǐng)求
1. 異步的get請(qǐng)求
//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: 開始異步請(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ì)象建立起來聯(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ù)静浴,所以可以通過body.string()的方式獲得字符串。如果希望獲得返回的二進(jìn)制字節(jié)數(shù)組挤渐,則調(diào)用response.body().bytes()苹享;如果你想拿到返回的inputStream,則調(diào)用response.body().byteStream()浴麻。
2. 同步的get請(qǐng)求
調(diào)用Call#execute()
方法得问,在主線程運(yùn)行
6.4.5 post請(qǐng)求
1. Post上傳表單(鍵值對(duì))
//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 {
}
});
2. Post異步上傳文件
// 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 {
}
});
3. Post方式提交流
以流的方式POST提交請(qǐng)求體. 請(qǐng)求體的內(nèi)容由流寫入產(chǎn)生. 這個(gè)例子是流直接寫入Okio的BufferedSink. 你的程序可能會(huì)使用OutputStream, 你可以使用BufferedSink.outputStream()來獲取. OkHttp的底層對(duì)流和字節(jié)的操作都是基于Okio庫(kù), Okio庫(kù)也是Square開發(fā)的另一個(gè)IO庫(kù), 填補(bǔ)I/O和NIO的空缺, 目的是提供簡(jiǎn)單便于使用的接口來操作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提交請(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());
}
5囤攀、Post方式提交分塊請(qǐng)求
MultipartBody.Builder可以構(gòu)建復(fù)雜的請(qǐng)求體, 與HTML文件上傳形式兼容. 多塊請(qǐng)求體中每塊請(qǐng)求都是一個(gè)請(qǐng)求體, 可以定義自己的請(qǐng)求頭. 這些請(qǐng)求頭可以用來描述這塊請(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());
}
六、其他用法
1宫纬、提取響應(yīng)頭
典型的HTTP頭是一個(gè)Map<String, String> : 每個(gè)字段都有一個(gè)或沒有值. 但是一些頭允許多個(gè)值抚岗。
當(dāng)寫請(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. 如果沒有值, 那么header(name)
將返回null. 如果想讀取字段對(duì)應(yīng)的所有值, 使用headers(name)`會(huì)返回一個(gè)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是一個(gè)在JSON和Java對(duì)象之間轉(zhuǎn)換非常方便的api庫(kù). 這里我們用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è)置一個(gè)緩存文件夾,在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)建緩存對(duì)象
Cache cache = new Cache(cacheFile,cacheSize);
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache)
.build();
設(shè)置好Cache我們就可以正常訪問了硅急。我們可以通過獲取到的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ò)訪問,然后分別用Log打印它們的信息吩屹。打印的結(jié)果主要說明了一個(gè)現(xiàn)象跪另,第一次訪問的時(shí)候,Response的消息是NetworkResponse消息煤搜,此時(shí)CacheResponse的值為Null.而第二次訪問的時(shí)候Response是CahceResponse免绿,而此時(shí)NetworkResponse為空。也就說明了上面的示例代碼能夠進(jìn)行網(wǎng)絡(luò)請(qǐng)求的緩存擦盾。
其實(shí)控制緩存的消息頭往往是服務(wù)端返回的信息中添加的如”Cache-Control:max-age=60”嘲驾。所以,會(huì)有兩種情況厌衙。
- 客戶端和服務(wù)端開發(fā)能夠很好溝通距淫,按照達(dá)成一致的協(xié)議绞绒,服務(wù)端按照規(guī)定添加緩存相關(guān)的消息頭婶希。
- 客戶端與服務(wù)端的開發(fā)根本就不是同一家公司,沒有辦法也不可能要求服務(wù)端按照客戶端的意愿進(jìn)行開發(fā)蓬衡。
第一種辦法當(dāng)然很好喻杈,只要服務(wù)器在返回消息的時(shí)候添加好Cache-Control相關(guān)的消息便好彤枢。
第二種情況,就很麻煩筒饰,你真的無法左右別人的行為缴啡。怎么辦呢?好在OKHTTP能夠很輕易地處理這種情況瓷们。那就是定義一個(gè)攔截器业栅,
人為地添加Response中的消息頭,然后再傳遞給用戶谬晕,這樣用戶拿到的Response就有了我們理想當(dāng)中的消息頭Headers碘裕,從而達(dá)到控制緩存的意圖,正所謂移花接木攒钳。
緩存之?dāng)r截器
因?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();
.......
}
代碼后面部分有省略。主要通過在OkHttpClient.Builder()中addNetworkInterceptor()中添加实愚。而這樣也挺簡(jiǎn)單的旷偿,就幾步完成了緩存代碼。
攔截器進(jìn)行緩存的缺點(diǎn)
- 網(wǎng)絡(luò)訪問請(qǐng)求的資源是文本信息爆侣,如新聞列表萍程,這類信息經(jīng)常變動(dòng),一天更新好幾次兔仰,它們用的緩存時(shí)間應(yīng)該就很短茫负。
- 網(wǎng)絡(luò)訪問請(qǐng)求的資源是圖片或者視頻,它們變動(dòng)很少乎赴,或者是長(zhǎng)期不變動(dòng)忍法,那么它們用的緩存時(shí)間就應(yīng)該很長(zhǎng)。
那么榕吼,問題來了饿序。
因?yàn)镺KHTTP開發(fā)建議是同一個(gè)APP,用同一個(gè)OKHTTPCLIENT對(duì)象這是為了只有一個(gè)緩存文件訪問入口羹蚣。這個(gè)很容易理解原探,單例模式嘛。但是問題攔截器是在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è)類來進(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常量用來強(qiáng)制使用網(wǎng)絡(luò)請(qǐng)求甘畅。FORCE_CACHE只取本地的緩存。它們本身都是CacheControl對(duì)象往弓,由內(nèi)部的Buidler對(duì)象構(gòu)造疏唾。下面我們來看看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í)間為10s -
minFresh(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 {
// 資源沒有緩存番甩,或者是緩存不符合條件了。
}
不使用緩存
前面也有講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();
4么翰、取消一個(gè)Call
使用Call.cancel()可以立即停止掉一個(gè)正在執(zhí)行的call. 如果一個(gè)線程正在寫請(qǐng)求或者讀響應(yīng), 將會(huì)引發(fā)IOException. 當(dāng)call沒有必要的時(shí)候, 使用這個(gè)api可以節(jié)約網(wǎng)絡(luò)資源. 例如當(dāng)用戶離開一個(gè)應(yīng)用時(shí), 不管同步還是異步的call都可以取消.
你可以通過tags來同時(shí)取消多個(gè)請(qǐng)求. 當(dāng)你構(gòu)建一請(qǐng)求時(shí), 使用RequestBuilder.tag(tag)來分配一個(gè)標(biāo)簽, 之后你就可以用OkHttpClient.cancel(tag)來取消所有帶有這個(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);
}
}
5牺汤、超時(shí)
沒有響應(yīng)時(shí)使用超時(shí)結(jié)束call. 沒有響應(yīng)的原因可能是客戶點(diǎn)鏈接問題、服務(wù)器可用性問題或者這之間的其他東西. OkHttp支持連接超時(shí), 讀取超時(shí)和寫入超時(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);
}
6浩嫌、每個(gè)call的配置
使用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);
}
}
七骚腥、處理驗(yàn)證
這部分和HTTP AUTH有關(guān).
1敦间、HTTP AUTH
使用HTTP AUTH需要在server端配置http auth信息, 其過程如下:
- 客戶端發(fā)送http請(qǐng)求(沒有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"字符串. 如果沒有,或者用戶密碼不對(duì)束铭,則返回http code 401頁(yè)面給客戶端. - 標(biāo)準(zhǔn)的http瀏覽器在收到401頁(yè)面之后, 應(yīng)該彈出一個(gè)對(duì)話框讓用戶輸入帳號(hào)密碼; 并在用戶點(diǎn)確認(rèn)的時(shí)候再次發(fā)出請(qǐng)求, 這次請(qǐng)求里面將帶上Authorization header.
- 服務(wù)器端認(rèn)證通過廓块,并返回頁(yè)面
- 瀏覽器顯示頁(yè)面
一次典型的訪問場(chǎng)景是:
2、OkHttp認(rèn)證
OkHttp會(huì)自動(dòng)重試未驗(yàn)證的請(qǐng)求. 當(dāng)響應(yīng)是401 Not Authorized時(shí)契沫,Authenticator會(huì)被要求提供證書. Authenticator的實(shí)現(xiàn)中需要建立一個(gè)新的包含證書的請(qǐng)求. 如果沒有證書可用, 返回null來跳過嘗試.
使用Response.challenges()來獲得任何authentication challenges的 schemes 和 realms. 當(dāng)完成一個(gè)Basic challenge, 使用Credentials.basic(username, password)來解碼請(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)證無法工作時(shí), 為了避免多次重試, 你可以返回空來放棄認(rèn)證. 例如, 當(dāng)exact credentials已經(jīng)嘗試過, 你可能會(huì)直接想跳過認(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;
}