為您解碼網(wǎng)站建設(shè)的點點滴滴
發(fā)表日期:2018-02 文章編輯:小燈 瀏覽次數(shù):2919
分為三個部分:公鑰、私鑰、加密/解密算法
加密解密過程如下:
加密:通過加密算法和公鑰對內(nèi)容(或者說明文)進行加密,得到密文。
解密:通過解密算法和私鑰對密文進行解密,得到明文。
注意:由公鑰加密的內(nèi)容,只能由私鑰進行解密。
公鑰密碼體制的公鑰和算法都是公開的,私鑰是保密的。在實際的使用中,有需要的人會生成一對公鑰和私鑰,把公鑰發(fā)布出去給別人使用,自己保留私鑰。
一種公鑰密碼體制,公鑰公開,私鑰保密,它的加密解密算法是公開的。 RSA的這一對公鑰、私鑰都可以用來加密和解密,并且一方加密的內(nèi)容可以由并且只能由對方進行解密。
就是在信息的后面再加上一段內(nèi)容,可以證明信息沒有被修改過。
一般是對信息做一個hash計算得到一個hash值(該過程不可逆),在把信息發(fā)送出去時,把這個hash值加密后做為一個簽名和信息一起發(fā)出去。 接收方在收到信息后,會重新計算信息的hash值,并和信息所附帶的hash值(解密后)進行對比,如果一致,就說明信息的內(nèi)容沒有被修改過,因為這里hash計算可以保證不同的內(nèi)容一定會得到不同的hash值,所以只要內(nèi)容一被修改,根據(jù)信息內(nèi)容計算的hash值就會變化。
當(dāng)然,不懷好意的人也可以修改信息內(nèi)容的同時也修改hash值,從而讓它們可以相匹配,為了防止這種情況,hash值一般都會加密后(也就是簽名)再和信息一起發(fā)送,以保證這個hash值不被修改。
“客戶”->“服務(wù)器”:你好
“服務(wù)器”->“客戶”:你好,我是服務(wù)器
“客戶”->“服務(wù)器”:向我證明你就是服務(wù)器
“服務(wù)器”->“客戶”:你好,我是服務(wù)器{你好,我是服務(wù)器}[私鑰|RSA]
“客戶”->“服務(wù)器”:{我們后面的通信過程,用對稱加密來進行,這里是對稱加密算法和密鑰}[公鑰|RSA]
“服務(wù)器”->“客戶”:{OK,收到!}[密鑰|對稱加密算法]
“客戶”->“服務(wù)器”:{我的帳號是aaa,密碼是123,把我的余額的信息發(fā)給我看看}[密鑰|對稱加密算法]
“服務(wù)器”->“客戶”:{你的余額是100元}[密鑰|對稱加密算法]
總結(jié)一下,RSA加密算法在這個通信過程中所起到的作用主要有兩個:
1. 因為私鑰只有“服務(wù)器”擁有,因此“客戶”可以通過判斷對方是否有私鑰來判斷對方是否是“服務(wù)器”。
2. 客戶端通過RSA的掩護,安全的和服務(wù)器商量好一個對稱加密算法和密鑰來保證后面通信過程內(nèi)容的安全。
但是這里還留有一個問題,“服務(wù)器”要對外發(fā)布公鑰,那“服務(wù)器”如何把公鑰發(fā)送給“客戶”呢?
我們可能會想到以下的兩個方法:
a) 把公鑰放到互聯(lián)網(wǎng)的某個地方的一個下載地址,事先給“客戶”去下載。
b) 每次和“客戶”開始通信時,“服務(wù)器”把公鑰發(fā)給“客戶”。
但是這個兩個方法都有一定的問題,
對于a)方法,“客戶”無法確定這個下載地址是不是“服務(wù)器”發(fā)布的,你憑什么就相信這個
地址下載的東西就是“服務(wù)器”發(fā)布的而不是別人偽造的呢,萬一下載到一個假的怎么辦?另外要所有的“客戶”都在通信前事先去下載公鑰也很不現(xiàn)實。
對于b)方法,也有問題,因為任何人都可以自己生成一對公鑰和私鑰,他只要向“客戶”發(fā)送他
自己的私鑰就可以冒充“服務(wù)器”了。示意如下:
“客戶”->“黑客”:你好//黑客截獲“客戶”發(fā)給“服務(wù)器”的消息
“黑客”->“客戶”:你好,我是服務(wù)器,這個是我的公鑰//黑客自己生成一對公鑰和私鑰,把
公鑰發(fā)給“客戶”,自己保留私鑰
“客戶”->“黑客”:向我證明你就是服務(wù)器
“黑客”->“客戶”:你好,我是服務(wù)器 {你好,我是服務(wù)器}[黑客自己的私鑰|RSA]//客戶收到
“黑客”用私鑰加密的信息后,是可以用“黑客”發(fā)給自己的公鑰解密的,從而會誤認(rèn)為“黑客”是“服務(wù)器”因此“黑客”只需要自己生成一對公鑰和私鑰,然后把公鑰發(fā)送給“客戶”,自己保留私鑰,這樣由于“客戶”可以用黑客的公鑰解密黑客的私鑰加密的內(nèi)容,“客戶”就會相信“黑客”是“服務(wù)器”,從而導(dǎo)致了安全問題。這里問題的根源就在于,大家都可以生成公鑰、私鑰對,無法確認(rèn)公鑰對到底是誰的。 如果能夠確定公鑰到底是誰的,就不會有這個問題了。例如,如果收到“黑客”冒充“服務(wù)器”發(fā)過來的公鑰,經(jīng)過某種檢查,如果能夠發(fā)現(xiàn)這個公鑰不是“服務(wù)器”的就好了。
為了解決上述問題,數(shù)字證書出現(xiàn)了,它可以解決我們上面的問題。先大概看下什么是數(shù)字證書,一個證書包含下面的具體內(nèi)容:
指紋和指紋算法
這個是用來保證證書的完整性的,也就是說確保證書沒有被修改過。 其原理就是在發(fā)布證書時,發(fā)布者根據(jù)指紋算法(一個hash算法)計算整個證書的hash值(指紋)并和證書放在一起,使用者在打開證書時,自己也根據(jù)指紋算法計算一下證書的hash值(指紋),如果和剛開始的值對得上,就說明證書沒有被修改過,因為證書的內(nèi)容被修改后,根據(jù)證書的內(nèi)容計算的出的hash值(指紋)是會變化的。
注意,這個指紋會用"SecureTrust CA"這個證書機構(gòu)的私鑰用簽名算法加密后和證書放在一起。
簽名算法
就是指的這個數(shù)字證書的數(shù)字簽名所使用的加密算法,這樣就可以使用證書發(fā)布機構(gòu)的證書里面的公鑰,根據(jù)這個算法對指紋進行解密。指紋的加密結(jié)果就是數(shù)字簽名
數(shù)字證書可以保證數(shù)字證書里的公鑰確實是這個證書的所有者(Subject)的,或者證書可以用來確認(rèn)對方的身份。也就是說,我們拿到一個數(shù)字證書,我們可以判斷出這個數(shù)字證書到底是誰的。至于是如何判斷的,后面會在詳細(xì)討論數(shù)字證書時詳細(xì)解釋?,F(xiàn)在把前面的通信過程使用數(shù)字證書修改為如下:
“客戶”->“服務(wù)器”:你好
“服務(wù)器”->“客戶”:你好,我是服務(wù)器,這里是我的數(shù)字證書//這里用證書代替了公鑰
“客戶”->“服務(wù)器”:向我證明你就是服務(wù)器
“服務(wù)器”->“客戶”:你好,我是服務(wù)器 {你好,我是服務(wù)器}[私鑰|RSA]
在每次發(fā)送信息時,先對信息的內(nèi)容進行一個hash計算得出一個hash值,將信息的內(nèi)容和這個hash值一起加密后發(fā)送。接收方在收到后進行解密得到明文的內(nèi)容和hash值,然后接收方再自己對收到信息內(nèi)容做一次hash計算,與收到的hash值進行對比看是否匹配,如果匹配就說明信息在傳輸過程中沒有被修改過。如果不匹配說明中途有人故意對加密數(shù)據(jù)進行了修改,立刻中斷通話過程后做其它處理。
如何向證書的發(fā)布機構(gòu)去申請證書
舉個例子,假設(shè)我們公司"ABC Company"花了1000塊錢,向一個證書發(fā)布機構(gòu)"SecureTrust CA"為我們自己的公司"ABC Company"申請了一張證書,注意,這個證書發(fā)布機構(gòu)"SecureTrust CA"是一個大家公認(rèn)并被一些權(quán)威機構(gòu)接受的證書發(fā)布機構(gòu),我們的操作系統(tǒng)里面已經(jīng)安裝了"SecureTrust CA"的證書。"SecureTrust CA"在給我們發(fā)布證書時,把Issuer,Public key,Subject,Valid from,Valid to等信息以明文的形式寫到證書里面,然后用一個指紋算法計算出這些數(shù)字證書內(nèi)容的一個指紋,并把指紋和指紋算法用自己的私鑰進行加密,然后和證書的內(nèi)容一起發(fā)布,同時"SecureTrust CA"還會給一個我們公司"ABC Company"的私鑰給到我們。
我們"ABC Company"申請到這個證書后,我們把證書投入使用,我們在通信過程開始時會把證書發(fā)給對方。
對方如何檢查這個證書的確是合法的并且是我們"ABC Company"公司的證書呢?首先應(yīng)用程序(對方通信用的程序,例如IE、OUTLook等)讀取證書中的Issuer(發(fā)布機構(gòu))為"SecureTrust CA" ,然后會在操作系統(tǒng)中受信任的發(fā)布機構(gòu)的證書中去找"SecureTrust CA"的證書,如果找不到,那說明證書的發(fā)布機構(gòu)是個水貨發(fā)布機構(gòu),證書可能有問題,程序會給出一個錯誤信息。 如果在系統(tǒng)中找到了"SecureTrust CA"的證書,那么應(yīng)用程序就會從證書中取出"SecureTrust CA"的公鑰,然后對我們"ABC Company"公司的證書里面的指紋和指紋算法用這個公鑰進行解密,然后使用這個指紋算法計算"ABC Company"證書的指紋,將這個計算的指紋與放在證書中的指紋對比,如果一致,說明"ABC Company"的證書肯定沒有被修改過并且證書是"SecureTrust CA" 發(fā)布的,證書中的公鑰肯定是"ABC Company"的。對方然后就可以放心的使用這個公鑰和我們"ABC Company"進行通信了。
通信雙方一方作為服務(wù)器等待客戶提出請求并予以響應(yīng)??蛻魟t在需要服務(wù)時向服務(wù)器提出申請。服務(wù)器一般作為守護進程始終運行,監(jiān)聽網(wǎng)絡(luò)端口,一旦有客戶請求,就會啟動一個服務(wù)進程來響應(yīng)該客戶,同時自己繼續(xù)監(jiān)聽服務(wù)端口,使后來的客戶也能及時得到服務(wù)。一個socket(通常都是server socket)等待建立連接時,另一個socket可以要求進行連接,一旦這兩個socket連接起來,它們就可以進行雙向數(shù)據(jù)傳輸,雙方都可以進行發(fā)送或接收操作。
a)客戶端發(fā)送一個帶SYN標(biāo)志的TCP報文到服務(wù)器。(聽得到嗎?)
b)服務(wù)端回應(yīng)客戶端的報文同時帶ACK(acknowledgement,確認(rèn))標(biāo)志和SYN(synchronize)標(biāo)志。它表示對剛才客戶端SYN報文的回應(yīng);同時又標(biāo)志SYN給客戶端,詢問客戶端是否準(zhǔn)備好進行數(shù)據(jù)通訊。(聽得到,你能聽到我嗎?)
c)客戶必須再次回應(yīng)服務(wù)端一個ACK報文。(聽到了,我們可以說話了)
為什么需要“三次握手”?
在謝希仁著《計算機網(wǎng)絡(luò)》第四版中講“三次握手”的目的是“為了防止已失效的連接請求報文段突然又傳送到了服務(wù)端,因而產(chǎn)生錯誤”。“已失效的連接請求報文段”的產(chǎn)生在這樣一種情況下:client發(fā)出的第一個連接請求報文段并沒有丟失,而是在某個網(wǎng)絡(luò)結(jié)點長時間的滯留了,以致延誤到連接釋放以后的某個時間才到達(dá)server。本來這是一個早已失效的報文段。但server收到此失效的連接請求報文段后,就誤認(rèn)為是client再次發(fā)出的一個新的連接請求。于是就向client發(fā)出確認(rèn)報文段,同意建立連接。假設(shè)不采用“三次握手”,那么只要server發(fā)出確認(rèn),新的連接就建立了。由于現(xiàn)在client并沒有發(fā)出建立連接的請求,因此不會理睬server的確認(rèn),也不會向server發(fā)送數(shù)據(jù)。但server卻以為新的運輸連接已經(jīng)建立,并一直等待client發(fā)來數(shù)據(jù)。這樣,server的很多資源就白白浪費掉了。采用“三次握手”的辦法可以防止上述現(xiàn)象發(fā)生。例如剛才那種情況,client不會向server的確認(rèn)發(fā)出確認(rèn)。server由于收不到確認(rèn),就知道client并沒有要求建立連接?!薄?主要目的防止server端一直等待,浪費資源。
由于TCP連接是全雙工的,因此每個方向都必須單獨進行關(guān)閉。這原則是當(dāng)一方完成它的數(shù)據(jù)發(fā)送任務(wù)后就能發(fā)送一個FIN來終止這個方向的連接。收到一個 FIN只意味著這一方向上沒有數(shù)據(jù)流動,一個TCP連接在收到一個FIN后仍能發(fā)送數(shù)據(jù)。首先進行關(guān)閉的一方將執(zhí)行主動關(guān)閉,而另一方執(zhí)行被動關(guān)閉。
a) TCP客戶端發(fā)送一個FIN,用來關(guān)閉客戶到服務(wù)器的數(shù)據(jù)傳送(報文段4)。
b) 服務(wù)器收到這個FIN,它發(fā)回一個ACK,確認(rèn)序號為收到的序號加1(報文段5)。和SYN一樣,一個FIN將占用一個序號。
c) 服務(wù)器關(guān)閉客戶端的連接,發(fā)送一個FIN給客戶端(報文段6)。
d) 客戶段發(fā)回ACK報文確認(rèn),并將確認(rèn)序號設(shè)置為收到序號加1(報文段7)。
為什么需要“四次揮手”?
那可能有人會有疑問,在tcp連接握手時為何ACK是和SYN一起發(fā)送,這里ACK卻沒有和FIN一起發(fā)送呢。原因是因為tcp是全雙工模式,接收到FIN時意味將沒有數(shù)據(jù)再發(fā)來,但是還是可以繼續(xù)發(fā)送數(shù)據(jù)。
由3部分組成,分別為:請求方法、URL以及協(xié)議版本,之間由空格分隔
請求方法:GET、HEAD、PUT、POST等方法,但并不是所有的服務(wù)器都實現(xiàn)了所有的方法,部分方法即便支持,處于安全性的考慮也是不可用的
協(xié)議版本:常用HTTP/1.1
請求頭部為請求報文添加了一些附加信息,由“名/值”對組成,每行一對,名和值之間使用冒號分隔
Host
接受請求的服務(wù)器地址,可以是IP:端口號,也可以是域名User-Agent
發(fā)送請求的應(yīng)用程序名稱Accept-Charset
通知服務(wù)端可以發(fā)送的編碼格式Accept-Encoding
通知服務(wù)端可以發(fā)送的數(shù)據(jù)壓縮格式Accept-Language
通知服務(wù)端可以發(fā)送的語言Range
正文的字節(jié)請求范圍,為斷點續(xù)傳和并行下載提供可能,返回狀態(tài)碼是206Authorization
用于設(shè)置身份認(rèn)證信息Cookie
已有的Cookie請求頭部的最后會有一個空行,表示請求頭部結(jié)束,接下來為請求正文,這一行非常重要,必不可少
可選部分,比如GET請求就沒有請求正文
由3部分組成,分別為:協(xié)議版本,狀態(tài)碼,狀態(tài)碼描述,之間由空格分隔
狀態(tài)碼:為3位數(shù)字,2XX表示成功,3XX表示資源重定向,4XX表示客戶端請求出錯,5XX表示服務(wù)端出錯
206狀態(tài)碼表示的是:客戶端通過發(fā)送范圍請求頭Range抓取到了資源的部分?jǐn)?shù)據(jù),得服務(wù)端提供支持
Server
服務(wù)器應(yīng)用程序軟件的名稱和版本Content-Type
響應(yīng)正文的類型。如:text/plain、application/jsonContent-Length
響應(yīng)正文長度Content-Charset
響應(yīng)正文使用的編碼Content-Language
響應(yīng)正文使用的語言Content-Range
正文的字節(jié)位置范圍Accept-Ranges
bytes:表明服務(wù)器支持Range請求,單位是字節(jié);none:不支持Set-Cookie
設(shè)置Cookie正文的內(nèi)容可以用gzip等進行壓縮,以提升傳輸速率
用于操作瀏覽器緩存的工作機制。取值如下:
用于管理持久連接。目前大部分瀏覽器都是用http1.1協(xié)議,也就是說默認(rèn)都會發(fā)起Keep-Alive的連接請求。所以是否能完成一個完整的Keep-Alive連接就看服務(wù)器設(shè)置情況。取值如下:
在Http/1.1中,僅對分塊傳輸編碼有效。Transfer-Encoding: chunked 表示輸出的內(nèi)容長度不能確定,普通的靜態(tài)頁面、圖片之類的基本上都用不到這個,但動態(tài)頁面就有可能會用到。一般使用Content-Length就夠了。
Http/1.1 200 OK .... Transfer-Encoding:chunked Connection:keep-alivecf0//16進制,值為3312...3312字節(jié)分塊數(shù)據(jù)...392//16進制,值為914...914字節(jié)分塊數(shù)據(jù)...0
請求體/響應(yīng)體的編碼格式,如gzip
兩種常見的Authentication機制:HTTP Basic和Digest。(現(xiàn)在用的并不多,了解一下)
最簡單的Authentication協(xié)議。直接方式告訴服務(wù)器你的用戶名(username)和密碼(password)。
request頭部:
GET /secret HTTP/1.1 Authorization: Basic QWxpY2U6MTIzNDU2//由“Alice:123456”進行Base64編碼以后得到的結(jié)果 ...
response頭部:
HTTP/1.1 200 OK ...
因為我們輸入的是正確的用戶名密碼,所以服務(wù)器會返回200,表示驗證成功。如果我們用錯誤的用戶的密碼來發(fā)送請求,則會得到類似如下含有401錯誤的response頭部:
HTTP/1.1 401 Bad credentials WWW-Authenticate: Basic realm="Spring Security Application" ...
當(dāng)Alice初次訪問服務(wù)器時,并不攜帶密碼。此時服務(wù)器會告知Alice一個隨機生成的字符串(nonce)。然后Alice再將這個字符串與她的密碼123456結(jié)合在一起進行MD5編碼,將編碼以后的結(jié)果發(fā)送給服務(wù)器作為驗證信息。
因為nonce是“每次”(并不一定是每次)隨機生成的,所以Alice在不同的時間訪問服務(wù)器,其編碼使用的nonce值應(yīng)該是不同的,如果攜帶的是相同的nonce編碼后的結(jié)果,服務(wù)器就認(rèn)為其不合法,將拒絕其訪問。
curl和服務(wù)器通信過程:
curl -------- request1:GET ------->> Server curl <<------ response1:nonce ------- Server curl ---- request2:Digest Auth ---->> Server curl <<------- response2:OK --------Server
request1頭部:
GET /secret HTTP/1.1 ...
請求1中沒有包含任何用戶名和密碼信息
response1頭部:
HTTP/1.1 401 Full authentication is required to access this resource WWW-Authenticate: Digest realm="Contacts Realm via Digest Authentication", qop="auth",nonce="MTQwMTk3OTkwMDkxMzo3MjdjNDM2NTYzMTU2NTA2NWEzOWU2NzBlNzhmMjkwOA==" ...
當(dāng)服務(wù)器接收到request1以后,認(rèn)為request1沒有任何的Authentication信息,所以返回401,并且告訴curl nonce的值是MTQwMTk3OTkwMDkxMzo3MjdjNDM2NTYzMTU2NTA2NWEzOWU2NzBlNzhmMjkwOA
request2頭部:
GET /secret HTTP/1.1 Authorization: Digest username="Alice", realm="Contacts Realm via Digest Authentication",nonce="MTQwMTk3OTkwMDkxMzo3MjdjNDM2NTYzMTU2NTA2NWEzOWU2NzBlNzhmMjkwOA==", uri="/secret", cnonce="MTQwMTk3", nc=00000001, qop="auth",response="fd5798940c32e51c128ecf88472151af"...
curl接收到服務(wù)器的nonce值以后,就可以把如密碼等信息和nonce值放在一起然后進行MD5編碼,得到一個response值,如前面紅色標(biāo)出所示,這樣服務(wù)器就可以通過這個值驗證Alice的密碼是否正確。
response2頭部:
HTTP/1.1 200 OK ...
當(dāng)我們完成Authentication以后,如果我們再次使用剛才的nonce值,將會收到錯誤信息。Digest Authentication比Basic安全,但是并不是真正的什么都不怕了,Digest Authentication這種容易方式容易收到Man in the Middle式攻擊。
據(jù)應(yīng)用場景的不同,HTTP請求的請求體有三種不同的形式。
第一種:
移動開發(fā)者常見的,請求體是任意類型,服務(wù)器不會解析請求體,請求體的處理需要自己解析,如 POST JSON時候就是這類。
第二種:
這里的格式要求就是URL中Query String的格式要求:多個鍵值對之間用&連接,鍵與值之前用=連接,且只能用ASCII字符,非ASCII字符需使用UrlEncode編碼。
第三種:
請求體被分成為多個部分,文件上傳時會被使用,這種格式最先應(yīng)該是被用于郵件傳輸中,每個字段/文件都被boundary(Content-Type中指定)分成單獨的段,每段以-- 加 boundary開頭,然后是該段的描述頭,描述頭之后空一行接內(nèi)容,請求結(jié)束的標(biāo)制為boundary后面加--。(見下面詳細(xì)說明)
默認(rèn)是application/x-www-form-urlencoded,但是在傳輸大型文件的時候效率比較低下。所以需要multipart/form-data。
報文的主體內(nèi)可以包含多部分對象,通常用來發(fā)送圖片、文件或表單等。
Connection: keep-alive Content-Length: 123 X-Requested-With: ShockwaveFlash/16.0.0.296 User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.93 Safari/537.36 Content-Type: multipart/form-data; boundary=Ij5ei4KM7KM7ae0KM7cH2ae0Ij5Ef1 Accept: */* Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.8 Range: bytes=0-1024 Cookie: bdshare_firstime=1409052493497--Ij5ei4KM7KM7ae0KM7cH2ae0Ij5Ef1 Content-Disposition: form-data; name="position"1425264476444 --Ij5ei4KM7KM7ae0KM7cH2ae0Ij5Ef1 Content-Disposition: form-data; name="pics"; filename="file1.txt" Content-Type: text/plain...(file1.txt的數(shù)據(jù))... ue_con_1425264252856 --Ij5ei4KM7KM7ae0KM7cH2ae0Ij5Ef1 Content-Disposition: form-data; name="cm"100672 --Ij5ei4KM7KM7ae0KM7cH2ae0Ij5Ef1--
a)在請求頭中Content-Type: multipart/form-data; boundary=Ij5ei4KM7KM7ae0KM7cH2ae0Ij5Ef1
是必須的,boundary字符串可以隨意指定
b)上面有3個部分,分別用--boundary進行分隔。Content-Disposition: form-data; name="參數(shù)的名稱" + "\r\n" + "\r\n" + 參數(shù)值
c)--boundary--
作為結(jié)束
Http+加密+認(rèn)證+完整性保護=Https
Https就是身披SSL(Secure Socket Layer,安全套接層)協(xié)議這層外殼的Http。當(dāng)使用了SSL之后,Http先和SSL通信,SSL再和TCP通信。
SSL(secure sockets layer):安全套接層,它是在上世紀(jì)90年代中期,由網(wǎng)景公司設(shè)計的,為解決 HTTP 協(xié)議傳輸內(nèi)容會被偷窺(嗅探)和篡改等安全問題而設(shè)計的,到了1999年,SSL成為互聯(lián)網(wǎng)上的標(biāo)準(zhǔn),名稱改為TLS(transport layer security):安全傳輸層協(xié)議,兩者可視為同一種東西的不同階段。
HTTPS在傳輸數(shù)據(jù)之前需要客戶端(瀏覽器)與服務(wù)端(網(wǎng)站)之間進行一次握手,在握手過程中將確立雙方加密傳輸數(shù)據(jù)的密碼信息。TLS/SSL協(xié)議不僅僅是一套加密傳輸?shù)膮f(xié)議,更是一件經(jīng)過藝術(shù)家精心設(shè)計的藝術(shù)品,TLS/SSL中使用了非對稱加密,對稱加密以及HASH算法。握手過程的具體描述如下:
這里瀏覽器與網(wǎng)站互相發(fā)送加密的握手消息并驗證,目的是為了保證雙方都獲得了一致的密碼,并且可以正常的加密解密數(shù)據(jù),為后續(xù)真正數(shù)據(jù)的傳輸做一次測試。另外,HTTPS一般使用的加密與HASH算法如下:
HTTPS對應(yīng)的通信時序圖如下:
SSL 證書大致分三類:
只有第一種, 也就是那些被安卓系統(tǒng)認(rèn)可的機構(gòu)頒發(fā)的證書, 在使用過程中不會出現(xiàn)安全提示。對于向權(quán)威機構(gòu)(簡稱CA,Certificate Authority)申請過證書的網(wǎng)絡(luò)地址,用OkHttp或者HttpsURLConnection都可以直接訪問 ,不需要做額外的事情 。但是申請需要$$ (每年要交 100 到 500 美元不等的費用)。
CA機構(gòu)頒發(fā)的證書有3種類型:
域名型SSL證書(DV SSL):信任等級普通,只需驗證網(wǎng)站的真實性便可頒發(fā)證書保護網(wǎng)站;
企業(yè)型SSL證書(OV SSL):信任等級強,須要驗證企業(yè)的身份,審核嚴(yán)格,安全性更高;
增強型SSL證書(EV SSL):信任等級最高,一般用于銀行證券等金融機構(gòu),審核嚴(yán)格,安全性最高,同時可以激活綠色網(wǎng)址欄。
InetAddress.getByAddress(byte[] addr)
根據(jù)IP地址獲取InetAddress對象,如:new byte[]{127,0,0,1}InetAddress.getByName(String host)
根據(jù)主機名獲取InetAddress對象 www.baidu.com 沒有http://InetAddress.getLocalHost()
返回本機getHostAddress() String
返回IP地址getHostName() String
返回主機名isReachable(int timeout) boolean
測試是否可以達(dá)到該地址,毫秒數(shù)application/x-www-form-urlencoded MIME
字符串之間的相互轉(zhuǎn)換URLEncoder.encode(String s, String enc) String
URLDecoder.decode(String s, String enc) String
Socket又稱套接字,是程序內(nèi)部提供的與外界通信的端口,即端口通信。通過建立socket連接,可為通信雙方的數(shù)據(jù)傳輸傳提供通道。主要特點有數(shù)據(jù)丟失率低,使用簡單且易于移植。
在TCP/IP協(xié)議族當(dāng)中主要的Socket類型為流套接字(streamsocket)和數(shù)據(jù)報套接字(datagramsocket)。流套接字將TCP作為其端對端協(xié)議,提供了一個可信賴的字節(jié)流服務(wù)。數(shù)據(jù)報套接字使用UDP協(xié)議,提供數(shù)據(jù)打包發(fā)送服務(wù)。
首先添加權(quán)限:
<!--允許應(yīng)用程序改變網(wǎng)絡(luò)狀態(tài)--> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/> <!--允許應(yīng)用程序改變WIFI連接狀態(tài)--> <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/> <!--允許應(yīng)用程序訪問有關(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"/>
客戶端實現(xiàn):
protected void connectServerWithTCPSocket() { Socket socket; try {// 創(chuàng)建一個Socket對象,并指定服務(wù)端的IP及端口號 socket = new Socket("192.168.1.32", 1989); // 創(chuàng)建一個InputStream用戶讀取要發(fā)送的文件。 InputStream inputStream = new FileInputStream("e://a.txt"); // 獲取Socket的OutputStream對象用于發(fā)送數(shù)據(jù)。 OutputStream outputStream = socket.getOutputStream(); // 創(chuàng)建一個byte類型的buffer字節(jié)數(shù)組,用于存放讀取的本地文件 byte buffer[] = new byte[4 * 1024]; int temp = 0; // 循環(huán)讀取文件 while ((temp = inputStream.read(buffer)) != -1) { // 把數(shù)據(jù)寫入到OuputStream對象中 outputStream.write(buffer, 0, temp); } // 發(fā)送讀取的數(shù)據(jù)到服務(wù)端 outputStream.flush(); /** 或創(chuàng)建一個報文,使用BufferedWriter寫入**/ //String socketData = "[2143213;21343fjks;213]"; //BufferedWriter writer = new BufferedWriter(new OutputStreamWriter( //socket.getOutputStream())); //writer.write(socketData.replace("\n", " ") + "\n"); //writer.flush(); } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }
服務(wù)端實現(xiàn):
public void serverReceviedByTcp() { // 聲明一個ServerSocket對象 ServerSocket serverSocket = null; try { // 創(chuàng)建一個ServerSocket對象,并讓這個Socket在1989端口監(jiān)聽 serverSocket = new ServerSocket(1989); // 調(diào)用ServerSocket的accept()方法,接受客戶端所發(fā)送的請求, // 如果客戶端沒有發(fā)送數(shù)據(jù),那么該線程就阻塞,等到收到數(shù)據(jù),繼續(xù)執(zhí)行。 Socket socket = serverSocket.accept(); // 從Socket當(dāng)中得到InputStream對象,讀取客戶端發(fā)送的數(shù)據(jù) InputStream inputStream = socket.getInputStream(); byte buffer[] = new byte[1024 * 4]; int temp = 0; // 從InputStream當(dāng)中讀取客戶端所發(fā)送的數(shù)據(jù) while ((temp = inputStream.read(buffer)) != -1) { System.out.println(new String(buffer, 0, temp)); } serverSocket.close(); } catch (IOException e) { e.printStackTrace(); } }
監(jiān)聽來自于客戶端的Socket連接,如果沒有連接,它將一直處于等待狀態(tài)
ServerSocket(int port, int backlog, InetAddress bindAddr)
在機器存在多IP的情況下,允許通過bindAddr來指定綁定到哪個IP;backlog是隊列中能接受的最大socket客戶端連接數(shù)(accept()之后將被取出)ServerSocket()
創(chuàng)建非綁定服務(wù)器套接字ServerSocket(int port)
創(chuàng)建綁定到特定端口的服務(wù)器套接字,等價于ServerSocket(port, 50, null)accept() Socket
如果接收到一個客戶端Socket的連接請求,返回一個與客戶端Socket相對應(yīng)的Socketclose()
Socket()
創(chuàng)建未連接套接字Socket(String host, int port)
Socket(InetAddress address, int port)
創(chuàng)建一個流套接字并將其連接到指定 IP 地址的指定端口號,默認(rèn)使用本地主機默認(rèn)的IP和系統(tǒng)動態(tài)分配的端口Socket(InetAddress address, int port, InetAddress localAddr, int localPort)
創(chuàng)建一個套接字并將其連接到指定遠(yuǎn)程地址上的指定遠(yuǎn)程端口,多IP時getOutputStream() OutputStream
返回此套接字的輸出流getInputStream() inputStream
返回此套接字的輸入流connect(SocketAddress endpoint, int timeout)
將此套接字連接到服務(wù)器,并指定一個超時值close()
1)對象代表統(tǒng)一資源定位器,是指向互聯(lián)網(wǎng)“資源”的指針
2)通常由協(xié)議名、主機、端口、資源路徑組成
3)URL包含一個可打開到達(dá)該資源的輸入流,可以將URL理解成為URI的特例
URL(String spec)
openConnection() URLConnection
getProtocol() String
getHost() String
getPort() int
getPath() String
獲取路徑部分,/search.htmlgetFile() String
獲取資源名稱,/search.html?keyword='你好'getQuery() String
獲取查詢字符串,keyword='你好'openStream() InputStream
抽象類,表示應(yīng)用程序和URL之間的通信連接,可以向URL發(fā)送請求,讀取URL指向的資源
setDoInput(boolean doinput)
發(fā)送POST請求,必須設(shè)置,設(shè)置為truesetDoOutput(boolean dooutput)
發(fā)送POST請求,必須設(shè)置,設(shè)置為truesetUseCaches(boolean usecaches)
是否使用緩存setRequestProperty(String key, String value)
設(shè)置普通的請求屬性setConnectTimeout(int timeout)
設(shè)置連接超時的時間setReadTimeout(int timeoutMillis)
讀取輸入流的超時時間connect()
抽象方法,建立實際的連接getHeaderField(String key) String
getHeaderFields() Map<String,List<String>>
獲取所有的響應(yīng)頭;getHeaderField(String name)獲取指定的響應(yīng)頭getOutputStream() OutputStream
getInputStream() InputStream
getContentLength() int
抽象類,是URLConnection的子類,增加了操作Http資源的便捷方法
setRequestMethod(String method)
setInstanceFollowRedirects(boolean followRedirects)
getResponseCode() int
獲取服務(wù)器的響應(yīng)碼getResponseMessage() String
獲取響應(yīng)消息getRequestMethod() String
獲取發(fā)送請求的方法,GET或POSTdisconnect()
抽象方法步驟:
①:創(chuàng)建URL對象
②:獲取URL對象指向資源的大?。╣etContentLength()方法)
③:在本地創(chuàng)建一個與網(wǎng)路資源相同大小的空文件
④:計算每條線程應(yīng)該下載網(wǎng)絡(luò)資源的哪個部分(從哪個字節(jié)開始,到哪個字節(jié)結(jié)束)
⑤:依次創(chuàng)建,啟動多條線程來下載網(wǎng)絡(luò)資源的指定部分
使用GET方式訪問HTTP
public static void main(String[] args) { try { // 1. 得到訪問地址的URL URL url = new URL( "http://localhost:8080/Servlet/do_login.do?username=test&password=123456"); // 2. 得到網(wǎng)絡(luò)訪問對象java.net.HttpURLConnection HttpURLConnection connection = (HttpURLConnection) url .openConnection(); /* 3. 設(shè)置請求參數(shù)(過期時間,輸入、輸出流、訪問方式),以流的形式進行連接 */ // 設(shè)置是否向HttpURLConnection輸出 connection.setDoOutput(false); // 設(shè)置是否從httpUrlConnection讀入 connection.setDoInput(true); // 設(shè)置請求方式 connection.setRequestMethod("GET"); // 設(shè)置是否使用緩存 connection.setUseCaches(true); // 設(shè)置此 HttpURLConnection 實例是否應(yīng)該自動執(zhí)行 HTTP 重定向 connection.setInstanceFollowRedirects(true); // 設(shè)置超時時間 connection.setConnectTimeout(3000); // 連接 connection.connect(); // 4. 得到響應(yīng)狀態(tài)碼的返回值 responseCode int code = connection.getResponseCode(); // 5. 如果返回值正常,數(shù)據(jù)在網(wǎng)絡(luò)中是以流的形式得到服務(wù)端返回的數(shù)據(jù) String msg = ""; if (code == 200) { // 正常響應(yīng) // 從流中讀取響應(yīng)信息 BufferedReader reader = new BufferedReader( new InputStreamReader(connection.getInputStream())); String line = null;while ((line = reader.readLine()) != null) { // 循環(huán)從流中讀取 msg += line + "\n"; } reader.close(); // 關(guān)閉流 } // 6. 斷開連接,釋放資源 connection.disconnect(); // 顯示響應(yīng)結(jié)果 System.out.println(msg); } catch (IOException e) { e.printStackTrace(); } }
使用POST方式訪問HTTP
public static void main(String[] args) { try { // 1. 獲取訪問地址URL URL url = new URL("http://localhost:8080/Servlet/do_login.do"); // 2. 創(chuàng)建HttpURLConnection對象 HttpURLConnection connection = (HttpURLConnection) url .openConnection(); /* 3. 設(shè)置請求參數(shù)等 */ // 請求方式 connection.setRequestMethod("POST"); // 超時時間 connection.setConnectTimeout(3000); // 設(shè)置是否輸出 connection.setDoOutput(true); // 設(shè)置是否讀入 connection.setDoInput(true); // 設(shè)置是否使用緩存 connection.setUseCaches(false); // 設(shè)置此 HttpURLConnection 實例是否應(yīng)該自動執(zhí)行 HTTP 重定向 connection.setInstanceFollowRedirects(true); // 設(shè)置使用標(biāo)準(zhǔn)編碼格式編碼參數(shù)的名-值對 connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); // 連接 connection.connect(); /* 4. 處理輸入輸出 */ // 寫入?yún)?shù)到請求中 String params = "username=test&password=123456"; OutputStream out = connection.getOutputStream(); out.write(params.getBytes()); out.flush(); out.close(); // 從連接中讀取響應(yīng)信息 String msg = ""; int code = connection.getResponseCode(); if (code == 200) { BufferedReader reader = new BufferedReader( new InputStreamReader(connection.getInputStream())); String line;while ((line = reader.readLine()) != null) { msg += line + "\n"; } reader.close(); } // 5. 斷開連接 connection.disconnect();// 處理結(jié)果 System.out.println(msg); } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }
(Android6.0之后的SDK中移除了對于HttpClient的支持,僅作了解)
在一般情況下,如果只是需要向Web站點的某個簡單頁面提交請求并獲取服務(wù)器響應(yīng),HttpURLConnection完全可以勝任。但在絕大部分情況下,Web站點的網(wǎng)頁可能沒這么簡單,這些頁面并不是通過一個簡單的URL就可訪問的,可能需要用戶登錄而且具有相應(yīng)的權(quán)限才可訪問該頁面。在這種情況下,就需要涉及Session、Cookie的處理了,如果打算使用HttpURLConnection來處理這些細(xì)節(jié),當(dāng)然也是可能實現(xiàn)的,只是處理起來難度就大了。
為了更好地處理向Web站點請求,包括處理Session、Cookie等細(xì)節(jié)問題,Apache開源組織提供了一個HttpClient項目。HttpClient就是一個增強版的HttpURLConnection,HttpURLConnection可以做的事情HttpClient全部可以做;HttpURLConnection沒有提供的有些功能,HttpClient也提供了,但它只是關(guān)注于如何發(fā)送請求、接收響應(yīng),以及管理HTTP連接。
步驟:
①:創(chuàng)建HttpClient對象
②:需要GET請求,創(chuàng)建HttpCet對象;需要POST請求,創(chuàng)建HttpPost對象
③:需要發(fā)送請求參數(shù),調(diào)用HttpGet、HttpPost共同的setParams(HttpParams params),HttpPost對象還可以使用setEntity(HttpEntity entity)方法來設(shè)置請求參數(shù)。
④:調(diào)用HttpClient對象的execute(HttpUriRequest request) HttpResponse
發(fā)送請求,執(zhí)行該方法返回一個HttpResponse。
⑤:調(diào)用HttpResponse的getAllHeaders()
,getHeader(String name)
獲取響應(yīng)頭;調(diào)用HttpResponse的getEntity()獲取HttpEntity對象(包裝了服務(wù)器的響應(yīng)內(nèi)容)
public static void main(String[] args) { // 1. 創(chuàng)建HttpClient對象 CloseableHttpClient httpClient = HttpClientBuilder.create().build(); // 2. 創(chuàng)建HttpGet對象 HttpGet httpGet = new HttpGet( "http://localhost:8080/Servlet/do_login.do?username=test&password=123456"); CloseableHttpResponse response = null; try { // 3. 執(zhí)行GET請求 response = httpClient.execute(httpGet); System.out.println(response.getStatusLine()); // 4. 獲取響應(yīng)實體 HttpEntity entity = response.getEntity(); // 5. 處理響應(yīng)實體 if (entity != null) { System.out.println("長度:" + entity.getContentLength()); System.out.println("內(nèi)容:" + EntityUtils.toString(entity)); } } catch (ClientProtocolException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { // 6. 釋放資源 try { response.close(); httpClient.close(); } catch (IOException e) { e.printStackTrace(); } } }
public static void main(String[] args) { // 1. 創(chuàng)建HttpClient對象 CloseableHttpClient httpClient = HttpClientBuilder.create().build(); // 2. 創(chuàng)建HttpPost對象 HttpPost post = new HttpPost( "http://localhost:8080/Servlet/do_login.do"); // 3. 設(shè)置POST請求傳遞參數(shù) List<NameValuePair> params = new ArrayList<NameValuePair>(); params.add(new BasicNameValuePair("username", "test")); params.add(new BasicNameValuePair("password", "12356")); try { UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params); post.setEntity(entity); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } // 4. 執(zhí)行請求并處理響應(yīng) try { CloseableHttpResponse response = httpClient.execute(post); HttpEntity entity = response.getEntity(); if (entity != null) { System.out.println("響應(yīng)內(nèi)容:"); System.out.println(EntityUtils.toString(entity)); } response.close(); } catch (ClientProtocolException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { // 釋放資源 try { httpClient.close(); } catch (IOException e) { e.printStackTrace(); } } }
HttpClient是Apache基金會的一個開源網(wǎng)絡(luò)庫,功能十分強大,API數(shù)量眾多,但正是由于龐大的API數(shù)量使得我們很難在不破壞兼容性的情況下對它進行升級和擴展,所以Android團隊在提升和優(yōu)化HttpClient方面的工作態(tài)度并不積極。官方在Android 2.3以后就不建議用了,并且在Android 5.0以后廢棄了HttpClient,在Android 6.0更是刪除了HttpClient。
HttpURLConnection是一種多用途、輕量極的HTTP客戶端,提供的API比較簡單,可以容易地去使用和擴展。不過在Android 2.2版本之前,HttpURLConnection一直存在著一些令人厭煩的bug。比如說對一個可讀的InputStream調(diào)用close()方法時,就有可能會導(dǎo)致連接池失效了。那么我們通常的解決辦法就是直接禁用掉連接池的功能。因此一般推薦是在2.2之前使用HttpClient,因為其bug較少。在2.2之后推薦使用HttpURLConnection,因為API簡單、體積小、有壓縮和緩存機制,并且Android團隊后續(xù)會繼續(xù)優(yōu)化HttpURLConnection。
自從Android4.4開始,google已經(jīng)開始將源碼中的HttpURLConnection替換為OkHttp,而市面上流行的Retrofit同樣是使用OkHttp進行再次封裝而來的。
OkHttp是一個快速、高效的網(wǎng)絡(luò)請求庫,它的設(shè)計和實現(xiàn)的首要目標(biāo)便是高效,有如下特性:
http請求包含:請求方法, 請求地址, 請求協(xié)議, 請求頭, 請求體這五部分。這些都在okhttp3.Request的類中有體現(xiàn), 這個類正是代表http請求的類。
public final class Request { final HttpUrl url;//請求地址 final String method;//請求方法 final Headers headers;//請求頭 final RequestBody body;//請求體 final Object tag; ... }
Http響應(yīng)由訪問協(xié)議, 響應(yīng)碼, 描述信息, 響應(yīng)頭, 響應(yīng)體來組成。
public final class Response implements Closeable { final Request request;//持有的請求 final Protocol protocol;//訪問協(xié)議 final int code;//響應(yīng)碼 final String message;//描述信息 final Handshake handshake;//SSL/TLS握手協(xié)議驗證時的信息, final Headers headers;//響應(yīng)頭 final ResponseBody body;//響應(yīng)體 ... }
OkHttpClient()
OkHttpClient(OkHttpClient.Builder builder)
newCall(Request request) Call
connectTimeout(long timeout, TimeUnit unit)
readTimeout(long timeout, TimeUnit unit)
writeTimeout(long timeout, TimeUnit unit)
pingInterval(long interval, TimeUnit unit)
cache(Cache cache)
入?yún)⑷纾?code>new Cache(File directory, long maxSize) cookieJar(CookieJar cookieJar)
CookieJar是一個接口hostnameVerifier(HostnameVerifier hostnameVerifier)
HostnameVerifier是一個接口,只有boolean verify(String hostname, SSLSession session)
sslSocketFactory(SSLSocketFactory sslSocketFactory, X509TrustManager trustManager)
Request(Request.Builder builder)
addHeader(String name, String value)
添加鍵值對,不會覆蓋header(String name, String value)
添加鍵值對,會覆蓋url(String url)
method(String method, RequestBody body)
post(RequestBody body)
本質(zhì):method("POST", body)build() Request
create(MediaType contentType, final File file) RequestBody
create(MediaType contentType, String content) RequestBody
create(MediaType contentType, byte[] content) RequestBody
RequestBody的子類
add(String name, String value) FormBody.Builder
build() FormBody
RequestBody的子類
Builder()
Builder(String boundary)
setType(MediaType type)
addPart(Headers headers, RequestBody body)
addFormDataPart(String name, String filename, RequestBody body)
build() MultipartBody
Call負(fù)責(zé)發(fā)送請求和讀取響應(yīng)
enqueue(Callback responseCallback)
加入調(diào)度隊列,異步執(zhí)行execute() Response
同步執(zhí)行cancel()
body() ResponseBody
code() int
http請求的狀態(tài)碼isSuccessful()
code為2XX時,返回true,否則falseheaders() Headers
string() String
bytes() byte[]
byteStream() InputStream
charStream() Reader
contentLength() long
RequestBody的數(shù)據(jù)格式都要指定Content-Type,就是指定MIME,常見的有三種:
方法:
parse(String string) MediaType
參數(shù) | 說明 |
---|---|
text/html | HTML格式 |
text/plain | 純文本格式 |
image/gif | gif圖片格式 |
image/jpeg | jpg圖片格式 |
image/png | png圖片格式 |
application/json | JSON數(shù)據(jù)格式 |
application/pdf | pdf格式 |
application/msword | Word文檔格式 |
application/octet-stream | 二進制流數(shù)據(jù) |
application/x-www-form-urlencoded | 普通表單數(shù)據(jù) |
multipart/form-data | 表單數(shù)據(jù)里有文件 |
Request經(jīng)常都要攜帶Cookie,request創(chuàng)建時可以通過header設(shè)置參數(shù),Cookie也是參數(shù)之一。就像下面這樣:
Request request = new Request.Builder() .url(url) .header("Cookie", "xxx") .build();
然后可以從返回的response里得到新的Cookie,你可能得想辦法把Cookie保存起來。
但是OkHttp可以不用我們管理Cookie,自動攜帶,保存和更新Cookie。
方法是在創(chuàng)建OkHttpClient設(shè)置管理Cookie的CookieJar:
private final HashMap<String, List<Cookie>> cookieStore = new HashMap<>(); OkHttpClient okHttpClient = new OkHttpClient.Builder() .cookieJar(new CookieJar() { @Override public void saveFromResponse(HttpUrl httpUrl, List<Cookie> list) { cookieStore.put(httpUrl.host(), list); }@Override public List<Cookie> loadForRequest(HttpUrl httpUrl) { List<Cookie> cookies = cookieStore.get(httpUrl.host()); return cookies != null ? cookies : new ArrayList<Cookie>(); } }) .build();
這樣以后發(fā)送Request都不用管Cookie這個參數(shù)也不用去response獲取新Cookie什么的了。還能通過cookieStore獲取當(dāng)前保存的Cookie。
最后,new OkHttpClient()只是一種快速創(chuàng)建OkHttpClient的方式,更標(biāo)準(zhǔn)的是使用OkHttpClient.Builder()。后者可以設(shè)置一堆參數(shù),例如超時時間什么的。
//step 1: 創(chuàng)建 OkHttpClient 對象 OkHttpClient okHttpClient = new OkHttpClient(); //step 2: 創(chuàng)建一個請求,不指定請求方法時默認(rèn)是GET。 Request.Builder requestBuilder = new Request.Builder().url("http://www.baidu.com"); //可以省略,默認(rèn)是GET請求 requestBuilder.method("GET", null); //step 3:創(chuàng)建 Call 對象 Call call = okHttpClient.newCall(requestBuilder.build()); //step 4: 開始異步請求 call.enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { }@Override public void onResponse(Call call, Response response) throws IOException { //獲得返回體 ResponseBody body = response.body(); } });
首先要將OkHttpClient的對象與Request的對象建立起來聯(lián)系,使用okHttpClient的newCall()方法得到一個Call對象,這個Call對象的作用就是相當(dāng)于將請求封裝成了一個任務(wù),既然是任務(wù),自然就會有execute()
和cancel()
等方法。
最后,我們希望以異步的方式去執(zhí)行請求,所以我們調(diào)用的是call.enqueue,將call加入調(diào)度隊列,然后等待任務(wù)執(zhí)行完成,我們在Callback中即可得到結(jié)果。但要注意的是,call的回調(diào)是子線程,所以是不能直接操作界面的。當(dāng)請求成功時就會回調(diào)onResponse()方法,我們可以看到返回的結(jié)果是Response對象,在此我們比較關(guān)注的是請求中的返回體body(ResponseBody類型),大多數(shù)的情況下我們希望獲得字符串從而進行json解析獲得數(shù)據(jù),所以可以通過body.string()的方式獲得字符串。如果希望獲得返回的二進制字節(jié)數(shù)組,則調(diào)用response.body().bytes();如果你想拿到返回的inputStream,則調(diào)用response.body().byteStream()。
調(diào)用Call#execute()
方法,在主線程運行
//step1: 同樣的需要創(chuàng)建一個OkHttpClick對象 OkHttpClient okHttpClient = new OkHttpClient(); //step2: 創(chuàng)建 FormBody.Builder FormBody formBody = new FormBody.Builder() .add("name", "dsd") //添加鍵值對 .build(); //step3: 創(chuàng)建請求 Request request = new Request.Builder().url("http://www.baidu.com") .post(formBody) .build() //step4: 建立聯(lián)系 創(chuàng)建Call對象 okHttpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { }@Override public void onResponse(Call call, Response response) throws IOException { } });
// step 1: 創(chuàng)建 OkHttpClient 對象 OkHttpClient okHttpClient = new OkHttpClient(); //step 2:創(chuàng)建 RequestBody 以及所需的參數(shù) //2.1 獲取文件 File file = new File(Environment.getExternalStorageDirectory() + "test.txt"); //2.2 創(chuàng)建 MediaType 設(shè)置上傳文件類型 MediaType MEDIATYPE = MediaType.parse("text/plain; charset=utf-8"); //2.3 獲取請求體 RequestBody requestBody = RequestBody.create(MEDIATYPE, file); //step 3:創(chuàng)建請求 Request request = new Request.Builder().url("http://www.baidu.com") .post(requestBody) .build(); //step 4 建立聯(lián)系 okHttpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { }@Override public void onResponse(Call call, Response response) throws IOException { } });
以流的方式POST提交請求體. 請求體的內(nèi)容由流寫入產(chǎn)生. 這個例子是流直接寫入Okio的BufferedSink. 你的程序可能會使用OutputStream, 你可以使用BufferedSink.outputStream()來獲取. OkHttp的底層對流和字節(jié)的操作都是基于Okio庫, Okio庫也是Square開發(fā)的另一個IO庫, 填補I/O和NIO的空缺, 目的是提供簡單便于使用的接口來操作IO.
public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("text/x-markdown; charset=utf-8"); private final OkHttpClient client = new OkHttpClient();public void run() throws Exception { RequestBody requestBody = new RequestBody() { @Override public MediaType contentType() { return MEDIA_TYPE_MARKDOWN; }@Override public void writeTo(BufferedSink sink) throws IOException { sink.writeUtf8("Numbers\n"); sink.writeUtf8("-------\n"); for (int i = 2; i <= 997; i++) { sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i))); } }private String factor(int n) { for (int i = 2; i < n; i++) { int x = n / i; if (x * i == n) return factor(x) + " × " + i; } return Integer.toString(n); } };Request request = new Request.Builder() .url("https://api.github.com/markdown/raw") .post(requestBody) .build();Response response = client.newCall(request).execute(); if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println(response.body().string()); }
下面是使用HTTP POST提交請求到服務(wù). 這個例子提交了一個markdown文檔到web服務(wù), 以HTML方式渲染markdown. 因為整個請求體都在內(nèi)存中, 因此避免使用此api提交大文檔(大于1MB).
public static final MediaType MEDIA_TYPE_MARKDOWN = MediaType.parse("text/x-markdown; charset=utf-8");private final OkHttpClient client = new OkHttpClient();public void run() throws Exception { String postBody = "" + "Releases\n" + "--------\n" + "\n" + " * _1.0_ May 6, 2013\n" + " * _1.1_ June 15, 2013\n" + " * _1.2_ August 11, 2013\n";Request request = new Request.Builder() .url("https://api.github.com/markdown/raw") .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody)) .build();Response response = client.newCall(request).execute(); if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);System.out.println(response.body().string()); }
MultipartBody.Builder可以構(gòu)建復(fù)雜的請求體, 與HTML文件上傳形式兼容. 多塊請求體中每塊請求都是一個請求體, 可以定義自己的請求頭. 這些請求頭可以用來描述這塊請求, 例如它的Content-Disposition. 如果Content-Length和Content-Type可用的話, 他們會被自動添加到請求頭中.
private static final String IMGUR_CLIENT_ID = "..."; private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");private final OkHttpClient client = new OkHttpClient();public void run() throws Exception { // Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image RequestBody requestBody = new MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart("title", "Square Logo") .addFormDataPart("image", "logo-square.png", RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png"))) .build();Request request = new Request.Builder() .header("Authorization", "Client-ID " + IMGUR_CLIENT_ID) .url("https://api.imgur.com/3/image") .post(requestBody) .build();Response response = client.newCall(request).execute(); if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);System.out.println(response.body().string()); }
典型的HTTP頭是一個Map<String, String> : 每個字段都有一個或沒有值. 但是一些頭允許多個值。
當(dāng)寫請求頭的時候, 使用header(name, value)可以設(shè)置唯一的name、value. 如果已經(jīng)有值, 舊的將被移除, 然后添加新的. 使用addHeader(name, value)可以添加多值(添加, 不移除已有的).
當(dāng)讀取響應(yīng)頭時, 使用header(name)返回最后出現(xiàn)的name、value. 通常情況這也是唯一的name、value. 如果沒有值, 那么header(name)
將返回null. 如果想讀取字段對應(yīng)的所有值, 使用headers(name)`會返回一個list.
為了獲取所有的Header, Headers類支持按index訪問.
private final OkHttpClient client = new OkHttpClient(); public void run() throws Exception { Request request = new Request.Builder() .url("https://api.github.com/repos/square/okhttp/issues") .header("User-Agent", "OkHttp Headers.java") .addHeader("Accept", "application/json; q=0.5") .addHeader("Accept", "application/vnd.github.v3+json") .build(); Response response = client.newCall(request).execute(); if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); System.out.println("Server: " + response.header("Server")); System.out.println("Date: " + response.header("Date")); System.out.println("Vary: " + response.headers("Vary")); }
Gson是一個在JSON和Java對象之間轉(zhuǎn)換非常方便的api庫. 這里我們用Gson來解析Github API的JSON響應(yīng).
注意: ResponseBody.charStream()使用響應(yīng)頭Content-Type指定的字符集來解析響應(yīng)體. 默認(rèn)是UTF-8.
private final OkHttpClient client = new OkHttpClient(); private final Gson gson = new Gson();public void run() throws Exception { Request request = new Request.Builder() .url("https://api.github.com/gists/c2a7c39532239ff261be") .build(); Response response = client.newCall(request).execute(); if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);Gist gist = gson.fromJson(response.body().charStream(), Gist.class); for (Map.Entry<String, GistFile> entry : gist.files.entrySet()) { System.out.println(entry.getKey()); System.out.println(entry.getValue().content); } }static class Gist { Map<String, GistFile> files; }static class GistFile { String content; }
OKHTTP如果要設(shè)置緩存,首要的條件就是設(shè)置一個緩存文件夾,在Android中為了安全起見,一般設(shè)置為私密數(shù)據(jù)空間。通過getExternalCacheDir()獲取。
如然后通過調(diào)用OKHttpClient.Builder中的cache()方法。如下面代碼所示:
//緩存文件夾 File cacheFile = new File(getExternalCacheDir().toString(),"cache"); //緩存大小為10M int cacheSize = 10 * 1024 * 1024; //創(chuàng)建緩存對象 Cache cache = new Cache(cacheFile,cacheSize);OkHttpClient client = new OkHttpClient.Builder() .cache(cache) .build();
設(shè)置好Cache我們就可以正常訪問了。我們可以通過獲取到的Response對象拿到它正常的消息和緩存的消息。
Response的消息有兩種類型,CacheResponse和NetworkResponse。CacheResponse代表從緩存取到的消息,NetworkResponse代表直接從服務(wù)端返回的消息。
示例代碼如下:
private void testCache(){ //緩存文件夾 File cacheFile = new File(getExternalCacheDir().toString(),"cache"); //緩存大小為10M int cacheSize = 10 * 1024 * 1024; //創(chuàng)建緩存對象 final Cache cache = new Cache(cacheFile,cacheSize);new Thread(new Runnable() { @Override public void run() { OkHttpClient client = new OkHttpClient.Builder() .cache(cache) .build(); //官方的一個示例的url String url = "http://publicobject.com/helloworld.txt";Request request = new Request.Builder() .url(url) .build(); Call call1 = client.newCall(request); Response response1 = null; try { //第一次網(wǎng)絡(luò)請求 response1 = call1.execute(); Log.i(TAG, "testCache: response1 :"+response1.body().string()); Log.i(TAG, "testCache: response1 cache :"+response1.cacheResponse()); Log.i(TAG, "testCache: response1 network :"+response1.networkResponse()); response1.body().close(); } catch (IOException e) { e.printStackTrace(); }Call call12 = client.newCall(request);try { //第二次網(wǎng)絡(luò)請求 Response response2 = call12.execute(); Log.i(TAG, "testCache: response2 :"+response2.body().string()); Log.i(TAG, "testCache: response2 cache :"+response2.cacheResponse()); Log.i(TAG, "testCache: response2 network :"+response2.networkResponse()); Log.i(TAG, "testCache: response1 equals response2:"+response2.equals(response1)); response2.body().close(); } catch (IOException e) { e.printStackTrace(); } } }).start();}
我們在上面的代碼中,用同一個url地址分別進行了兩次網(wǎng)絡(luò)訪問,然后分別用Log打印它們的信息。打印的結(jié)果主要說明了一個現(xiàn)象,第一次訪問的時候,Response的消息是NetworkResponse消息,此時CacheResponse的值為Null.而第二次訪問的時候Response是CahceResponse,而此時NetworkResponse為空。也就說明了上面的示例代碼能夠進行網(wǎng)絡(luò)請求的緩存。
其實控制緩存的消息頭往往是服務(wù)端返回的信息中添加的如”Cache-Control:max-age=60”。所以,會有兩種情況。
第一種辦法當(dāng)然很好,只要服務(wù)器在返回消息的時候添加好Cache-Control相關(guān)的消息便好。
第二種情況,就很麻煩,你真的無法左右別人的行為。怎么辦呢?好在OKHTTP能夠很輕易地處理這種情況。那就是定義一個攔截器,
人為地添加Response中的消息頭,然后再傳遞給用戶,這樣用戶拿到的Response就有了我們理想當(dāng)中的消息頭Headers,從而達(dá)到控制緩存的意圖,正所謂移花接木。
因為攔截器可以拿到Request和Response,所以可以輕而易舉地加工這些東西。在這里我們?nèi)藶榈靥砑覥ache-Control消息頭。
class CacheInterceptor implements Interceptor{@Override public Response intercept(Chain chain) throws IOException {Response originResponse = chain.proceed(chain.request());//設(shè)置緩存時間為60秒,并移除了pragma消息頭,移除它的原因是因為pragma也是控制緩存的一個消息頭屬性 return originResponse.newBuilder().removeHeader("pragma") .header("Cache-Control","max-age=60").build(); } }
定義好攔截器中后,我們可以添加到OKHttpClient中了。
private void testCacheInterceptor(){ //緩存文件夾 File cacheFile = new File(getExternalCacheDir().toString(),"cache"); //緩存大小為10M int cacheSize = 10 * 1024 * 1024; //創(chuàng)建緩存對象 final Cache cache = new Cache(cacheFile,cacheSize);OkHttpClient client = new OkHttpClient.Builder() .addNetworkInterceptor(new CacheInterceptor()) .cache(cache) .build(); ....... }
代碼后面部分有省略。主要通過在OkHttpClient.Builder()中addNetworkInterceptor()中添加。而這樣也挺簡單的,就幾步完成了緩存代碼。
攔截器進行緩存的缺點
那么,問題來了。
因為OKHTTP開發(fā)建議是同一個APP,用同一個OKHTTPCLIENT對象這是為了只有一個緩存文件訪問入口。這個很容易理解,單例模式嘛。但是問題攔截器是在OKHttpClient.Builder當(dāng)中添加的。如果在攔截器中定義緩存的方法會導(dǎo)致圖片的緩存和新聞列表的緩存時間是一樣的,這顯然是不合理的,真實的情況不應(yīng)該是圖片請求有它的緩存時間,新聞列表請求有它的緩存時間,應(yīng)該是每一個Request有它的緩存時間。 那么,有解決的方案嗎? 有的,okhttp官方有建議的方法。
okhttp官方文檔建議緩存方法
okhttp中建議用CacheControl這個類來進行緩存策略的制定。
它內(nèi)部有兩個很重要的靜態(tài)實例。
/**強制使用網(wǎng)絡(luò)請求*/ public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();/*** 強制性使用本地緩存,如果本地緩存不滿足條件,則會返回code為504*/ public static final CacheControl FORCE_CACHE = new Builder() .onlyIfCached() .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS) .build();
我們看到FORCE_NETWORK常量用來強制使用網(wǎng)絡(luò)請求。FORCE_CACHE只取本地的緩存。它們本身都是CacheControl對象,由內(nèi)部的Buidler對象構(gòu)造。下面我們來看看CacheControl.Builder
CacheControl.Builder
它有如下方法:
noCache()
//不使用緩存,用網(wǎng)絡(luò)請求noStore()
//不使用緩存,也不存儲緩存onlyIfCached()
//只使用緩存noTransform()
//禁止轉(zhuǎn)碼maxAge(10, TimeUnit.MILLISECONDS)
//設(shè)置超時時間為10ms。maxStale(10, TimeUnit.SECONDS)
//超時之外的超時時間為10sminFresh(10, TimeUnit.SECONDS)
//超時時間為當(dāng)前時間加上10秒鐘。知道了CacheControl的相關(guān)信息,那么它怎么使用呢?不同于攔截器設(shè)置緩存,CacheControl是針對Request的,所以它可以針對每個請求設(shè)置不同的緩存策略。比如圖片和新聞列表。下面代碼展示如何用CacheControl設(shè)置一個60秒的超時時間。
private void testCacheControl(){ //緩存文件夾 File cacheFile = new File(getExternalCacheDir().toString(),"cache"); //緩存大小為10M int cacheSize = 10 * 1024 * 1024; //創(chuàng)建緩存對象 final Cache cache = new Cache(cacheFile,cacheSize);new Thread(new Runnable() { @Override public void run() { OkHttpClient client = new OkHttpClient.Builder() .cache(cache) .build(); //設(shè)置緩存時間為60秒 CacheControl cacheControl = new CacheControl.Builder() .maxAge(60, TimeUnit.SECONDS) .build(); Request request = new Request.Builder() .url("http://blog.csdn.net/briblue") .cacheControl(cacheControl) .build(); try { Response response = client.newCall(request).execute(); response.body().close(); } catch (IOException e) { e.printStackTrace(); } } }).start();}
強制使用緩存
前面有講CacheControl.FORCE_CACHE這個常量。
public static final CacheControl FORCE_CACHE = new Builder() .onlyIfCached() .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
它內(nèi)部其實就是調(diào)用onlyIfCached()和maxStale方法。
它的使用方法為
Request request = new Request.Builder() .url("http://blog.csdn.net/briblue") .cacheControl(Cache.FORCE_CACHE) .build();
但是如前面后提到的,如果緩存不符合條件會返回504.這個時候我們要根據(jù)情況再進行編碼,如緩存不行就再進行一次網(wǎng)絡(luò)請求。
Response forceCacheResponse = client.newCall(request).execute();if (forceCacheResponse.code() != 504) {// 資源已經(jīng)緩存了,可以直接使用} else {// 資源沒有緩存,或者是緩存不符合條件了。}
不使用緩存
前面也有講CacheControl.FORCE_NETWORK這個常量。
public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();
它的內(nèi)部其實是調(diào)用noCache()方法,也就是不緩存的意思。
它的使用方法為
Request request = new Request.Builder() .url("http://blog.csdn.net/briblue") .cacheControl(Cache.FORCE_NETWORK) .build();
還有一種情況將maxAge設(shè)置為0,也不會取緩存,直接走網(wǎng)絡(luò)。
Request request = new Request.Builder() .url("http://blog.csdn.net/briblue") .cacheControl(new CacheControl.Builder() .maxAge(0, TimeUnit.SECONDS)) .build();
使用Call.cancel()可以立即停止掉一個正在執(zhí)行的call. 如果一個線程正在寫請求或者讀響應(yīng), 將會引發(fā)IOException. 當(dāng)call沒有必要的時候, 使用這個api可以節(jié)約網(wǎng)絡(luò)資源. 例如當(dāng)用戶離開一個應(yīng)用時, 不管同步還是異步的call都可以取消.
你可以通過tags來同時取消多個請求. 當(dāng)你構(gòu)建一請求時, 使用RequestBuilder.tag(tag)來分配一個標(biāo)簽, 之后你就可以用OkHttpClient.cancel(tag)來取消所有帶有這個tag的call.
private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);private final OkHttpClient client = new OkHttpClient();public void run() throws Exception { Request request = new Request.Builder() .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay. .build();final long startNanos = System.nanoTime(); final Call call = client.newCall(request);// Schedule a job to cancel the call in 1 second. executor.schedule(new Runnable() { @Override public void run() { System.out.printf("%.2f Canceling call.%n", (System.nanoTime() - startNanos) / 1e9f); call.cancel(); System.out.printf("%.2f Canceled call.%n", (System.nanoTime() - startNanos) / 1e9f); } }, 1, TimeUnit.SECONDS);try { System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f); Response response = call.execute(); System.out.printf("%.2f Call was expected to fail, but completed: %s%n", (System.nanoTime() - startNanos) / 1e9f, response); } catch (IOException e) { System.out.printf("%.2f Call failed as expected: %s%n", (System.nanoTime() - startNanos) / 1e9f, e); } }
沒有響應(yīng)時使用超時結(jié)束call. 沒有響應(yīng)的原因可能是客戶點鏈接問題、服務(wù)器可用性問題或者這之間的其他東西. OkHttp支持連接超時, 讀取超時和寫入超時.
private final OkHttpClient client;public ConfigureTimeouts() throws Exception { client = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build(); }public void run() throws Exception { Request request = new Request.Builder() .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay. .build();Response response = client.newCall(request).execute(); System.out.println("Response completed: " + response); }
使用OkHttpClient, 所有的HTTP Client配置包括代理設(shè)置、超時設(shè)置、緩存設(shè)置. 當(dāng)你需要為單個call改變配置的時候, 調(diào)用OkHttpClient.newBuilder(). 這個api將會返回一個builder, 這個builder和原始的client共享相同的連接池, 分發(fā)器和配置.
下面的例子中,我們讓一個請求是500ms的超時、另一個是3000ms的超時。
private final OkHttpClient client = new OkHttpClient();public void run() throws Exception { Request request = new Request.Builder() .url("http://httpbin.org/delay/1") // This URL is served with a 1 second delay. .build();try { // Copy to customize OkHttp for this request. OkHttpClient copy = client.newBuilder() .readTimeout(500, TimeUnit.MILLISECONDS) .build();Response response = copy.newCall(request).execute(); System.out.println("Response 1 succeeded: " + response); } catch (IOException e) { System.out.println("Response 1 failed: " + e); }try { // Copy to customize OkHttp for this request. OkHttpClient copy = client.newBuilder() .readTimeout(3000, TimeUnit.MILLISECONDS) .build();Response response = copy.newCall(request).execute(); System.out.println("Response 2 succeeded: " + response); } catch (IOException e) { System.out.println("Response 2 failed: " + e); } }
這部分和HTTP AUTH有關(guān).
使用HTTP AUTH需要在server端配置http auth信息, 其過程如下:
OkHttp會自動重試未驗證的請求. 當(dāng)響應(yīng)是401 Not Authorized時,Authenticator會被要求提供證書. Authenticator的實現(xiàn)中需要建立一個新的包含證書的請求. 如果沒有證書可用, 返回null來跳過嘗試.
使用Response.challenges()來獲得任何authentication challenges的 schemes 和 realms. 當(dāng)完成一個Basic challenge, 使用Credentials.basic(username, password)來解碼請求頭.
private final OkHttpClient client;public Authenticate() { client = new OkHttpClient.Builder() .authenticator(new Authenticator() { @Override public Request authenticate(Route route, Response response) throws IOException { System.out.println("Authenticating for response: " + response); System.out.println("Challenges: " + response.challenges()); String credential = Credentials.basic("jesse", "password1"); return response.request().newBuilder() .header("Authorization", credential) .build(); } }) .build(); }public void run() throws Exception { Request request = new Request.Builder() .url("http://publicobject.com/secrets/hellosecret.txt") .build();Response response = client.newCall(request).execute(); if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);System.out.println(response.body().string()); }
當(dāng)認(rèn)證無法工作時, 為了避免多次重試, 你可以返回空來放棄認(rèn)證. 例如, 當(dāng)exact credentials已經(jīng)嘗試過, 你可能會直接想跳過認(rèn)證, 可以這樣做:
if (credential.equals(response.request().header("Authorization"))) { return null; // If we already failed with these credentials, don't retry.}
當(dāng)重試次數(shù)超過定義的次數(shù), 你若想跳過認(rèn)證, 可以這樣做:
if (responseCount(response) >= 3) { return null; // If we've failed 3 times, give up. }private int responseCount(Response response) { int result = 1; while ((response = response.priorResponse()) != null) { result++; } return result; }
OkHttp使用完全教程
日期:2018-04 瀏覽次數(shù):6774
日期:2017-02 瀏覽次數(shù):3448
日期:2017-09 瀏覽次數(shù):3674
日期:2017-12 瀏覽次數(shù):3542
日期:2018-12 瀏覽次數(shù):4839
日期:2016-12 瀏覽次數(shù):4592
日期:2017-07 瀏覽次數(shù):13658
日期:2017-12 瀏覽次數(shù):3522
日期:2018-06 瀏覽次數(shù):4278
日期:2018-05 瀏覽次數(shù):4454
日期:2017-12 瀏覽次數(shù):3568
日期:2017-06 瀏覽次數(shù):3993
日期:2018-01 瀏覽次數(shù):3957
日期:2016-12 瀏覽次數(shù):3922
日期:2018-08 瀏覽次數(shù):4439
日期:2017-12 瀏覽次數(shù):3726
日期:2016-09 瀏覽次數(shù):6443
日期:2018-07 瀏覽次數(shù):3220
日期:2016-12 瀏覽次數(shù):3240
日期:2018-10 瀏覽次數(shù):3392
日期:2018-10 瀏覽次數(shù):3501
日期:2018-09 瀏覽次數(shù):3591
日期:2018-02 瀏覽次數(shù):3609
日期:2015-05 瀏覽次數(shù):3536
日期:2018-09 瀏覽次數(shù):3316
日期:2018-06 瀏覽次數(shù):3444
日期:2017-02 瀏覽次數(shù):3882
日期:2018-02 瀏覽次數(shù):4346
日期:2018-02 瀏覽次數(shù):4190
日期:2016-12 瀏覽次數(shù):3586
Copyright ? 2013-2018 Tadeng NetWork Technology Co., LTD. All Rights Reserved.