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

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è)子類徽惋,Inet4AddressInet6Address
  • 沒有構(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通信模型
Socket基本通信模型
(2) TCP通信模型
TCP通信模型
(3) UDP通信模型
UDP通信模型

4. Demo

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

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

客戶端實(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ì)有兩種情況厌衙。

  1. 客戶端和服務(wù)端開發(fā)能夠很好溝通距淫,按照達(dá)成一致的協(xié)議绞绒,服務(wù)端按照規(guī)定添加緩存相關(guān)的消息頭婶希。
  2. 客戶端與服務(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)

  1. 網(wǎng)絡(luò)訪問請(qǐng)求的資源是文本信息爆侣,如新聞列表萍程,這類信息經(jīng)常變動(dòng),一天更新好幾次兔仰,它們用的緩存時(shí)間應(yīng)該就很短茫负。
  2. 網(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;
  }

參考文獻(xiàn)

OkHttp使用完全教程

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末带猴,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子懈万,更是在濱河造成了極大的恐慌拴清,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,013評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件会通,死亡現(xiàn)場(chǎng)離奇詭異口予,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)涕侈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,205評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門苹威,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人驾凶,你說我怎么就攤上這事牙甫。” “怎么了调违?”我有些...
    開封第一講書人閱讀 152,370評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵窟哺,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我技肩,道長(zhǎng)且轨,這世上最難降的妖魔是什么浮声? 我笑而不...
    開封第一講書人閱讀 55,168評(píng)論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮旋奢,結(jié)果婚禮上泳挥,老公的妹妹穿的比我還像新娘。我一直安慰自己至朗,他們只是感情好屉符,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,153評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著锹引,像睡著了一般矗钟。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上嫌变,一...
    開封第一講書人閱讀 48,954評(píng)論 1 283
  • 那天吨艇,我揣著相機(jī)與錄音,去河邊找鬼腾啥。 笑死东涡,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的倘待。 我是一名探鬼主播软啼,決...
    沈念sama閱讀 38,271評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼延柠!你這毒婦竟也來了祸挪?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,916評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤贞间,失蹤者是張志新(化名)和其女友劉穎贿条,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體增热,經(jīng)...
    沈念sama閱讀 43,382評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡整以,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,877評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了峻仇。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片公黑。...
    茶點(diǎn)故事閱讀 37,989評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖摄咆,靈堂內(nèi)的尸體忽然破棺而出凡蚜,到底是詐尸還是另有隱情,我是刑警寧澤吭从,帶...
    沈念sama閱讀 33,624評(píng)論 4 322
  • 正文 年R本政府宣布朝蜘,位于F島的核電站,受9級(jí)特大地震影響涩金,放射性物質(zhì)發(fā)生泄漏谱醇。R本人自食惡果不足惜暇仲,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,209評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望副渴。 院中可真熱鬧奈附,春花似錦、人聲如沸煮剧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,199評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽轿秧。三九已至中跌,卻和暖如春咨堤,著一層夾襖步出監(jiān)牢的瞬間菇篡,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,418評(píng)論 1 260
  • 我被黑心中介騙來泰國(guó)打工一喘, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留驱还,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,401評(píng)論 2 352
  • 正文 我出身青樓凸克,卻偏偏與公主長(zhǎng)得像议蟆,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子萎战,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,700評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理口注,服務(wù)發(fā)現(xiàn)李茫,斷路器,智...
    卡卡羅2017閱讀 134,599評(píng)論 18 139
  • 1.OkHttp源碼解析(一):OKHttp初階2 OkHttp源碼解析(二):OkHttp連接的"前戲"——HT...
    隔壁老李頭閱讀 20,811評(píng)論 24 176
  • 國(guó)家電網(wǎng)公司企業(yè)標(biāo)準(zhǔn)(Q/GDW)- 面向?qū)ο蟮挠秒娦畔?shù)據(jù)交換協(xié)議 - 報(bào)批稿:20170802 前言: 排版 ...
    庭說閱讀 10,868評(píng)論 6 13
  • 一、概念(載錄于:http://www.cnblogs.com/EricaMIN1987_IT/p/3837436...
    yuantao123434閱讀 8,328評(píng)論 6 152
  • 一住進(jìn)這個(gè)小區(qū)苦丁,我就很留意空地上的綠植,源于從小對(duì)花花草草的喜愛掺逼。 注意到小路邊一排綠化樹坑赡,葉片很熟悉,但不敢確定...
    清荷的簡(jiǎn)書閱讀 660評(píng)論 0 1