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