網路一直是專案裡比較重要的一個模組,Android開源專案上出現過很多優秀的網路框架。從一開始只是一些對HttpClient
和HttpUrlConnection
簡易封裝使用的工具類,到後來Google開源的比較完善豐富的Volley
,再到如今比較流行的Okhttp
、Retrofit
。他們之間存在異同,這個系列主要想通過對網路基礎知識、Android網路框架的解析來理清他們的關係以及原理。
整個系列主要會分為以下幾個部分:
- 計算機網路的一些基礎
- HttpUrlConnection、Volley解析
- Okhttp解析(比較多,會分攔截器解析)
- Retrofit解析
- 總結
這是第一篇,主要介紹計算機網路的一些基礎,以及在Android開發中的一些使用及遇到的問題和解決,本篇主要分為以下幾部分:
- 計算機網路體系結構
- Http相關
- Tcp相關
- Socket
- 總結
對於計算機網路的一些基本知識,還是需要做一些瞭解,事實證明在Android的日常開發和原始碼閱讀中也會經常碰到相關知識,所以第一篇就寫一下一些計算機網路的基礎。
1.計算機網路體系結構
即經常看到的計算機網路體系的分層結構,理清這個還是有必要的,防止對Http和Tcp兩個根本不在同一層的協議糾纏不清。 根據不同的參考模型,分層結構有幾個不同的版本,如OSI模型以及TCP/IP模型,下面就以比較經常看到的的5層結構為例:
分層 |
---|
應用層 (HTTP、FTP、DNS、SMTP等等) |
運輸層 (TCP、UDP) |
網路層 (IP等) |
資料鏈路層(ARP等) |
物理層 |
五層的體系結構至上往下,最終可以實現端對端之間的資料傳輸與通訊,他們各自負責一些什麼,最終如何實現端對端之間的通訊?
1.應用層:如http協議,它實際上是定義瞭如何包裝和解析資料
,應用層是http協議的話,則會按照協議規定包裝資料,如按照請求行、請求頭、請求體包裝,包裝好資料後將資料傳至運輸層。
2.運輸層:運輸層有TCP和UDP兩種協議,分別對應可靠的運輸和不可靠的運輸,如TCP因為要提供可靠的傳輸,所以內部要解決如何建立連線、如何保證傳輸是可靠的不丟資料、如何調節流量控制和擁塞控制。關於這一層,我們平常一般都是和Socket
打交道,Socket是一組封裝的程式設計呼叫介面,通過它,我們就能操作TCP、UDP進行連線的建立等。我們平常使用Socket進行連線建立的時候,一般都要指定埠號
,所以這一層指定了把資料送到對應的埠號。
3.網路層:這一層IP協議,以及一些路由選擇協議等等,所以這一層的指定了資料要傳輸到哪個IP地址
。中間涉及到一些最優線路,路由選擇演算法等等。
4.資料鏈路層:印象比較深的就是ARP協議,負責把IP地址解析為MAC地址
,即硬體地址,這樣就找到了對應的唯一的機器。
5.物理層:這一層就是最底層了,提供二進位制流傳輸服務,也就是也就是真正開始通過傳輸介質(有線、無線)開始進行資料的傳輸了。
所以通過上面五層的各司其職,實現物理傳輸介質--MAC地址--IP地址--埠號--獲取到資料根據應用層協議解析資料
最終實現了網路通訊和資料傳輸。
下面會著重講一下HTTP和TCP相關的東西,關於其他層,畢業了這麼久也忘的很多,如果想更加細緻具體的瞭解像下面三層的如路由選擇演算法、ARP定址以及物理層等等還是要重新去看一下《計算機網路原理》~
2.HTTP相關
這裡主要講一些關於Http的基礎知識,以及在Android中的一些實際應用和碰到的問題和解決。
一、無連線與無狀態
Http是無連線無狀態的。
無連線
並不是說不需要連線,Http協議只是一個應用層協議,最終還是要靠運輸層的如TCP協議向上提供的服務進行連線。無連線的含義是http約定了每次連線只處理一個請求,一次請求完成後就斷開連線,這樣主要是為了緩解伺服器的壓力,減小連線對伺服器資源的佔用。我的理解是,建立連線實際上是運輸層的事,面向應用層的http來說的話,它就是無連線的,因為上層對下層無感知。
無狀態
的指每個請求之間都是獨立的,對於之前的請求事務沒有記憶的能力。所以就出現了像Cookie這種,用來儲存一些狀態的東西。
一、請求報文與響應報文
這裡主要簡單說一下請求報文和響應報文的格式:
請求報文:
名稱 | 組成 |
---|---|
請求行 | 請求方法post/get、請求路徑Url、協議版本等 |
請求頭 | 即header,裡面包含很多欄位 |
請求體 | 傳送的資料 |
響應報文:
名稱 | 組成 |
---|---|
狀態行 | 狀態碼如200、協議版本、等 |
響應頭 | 即返回的header |
響應體 | 響應正文資料 |
關於Get和Post: 我們都熟知的關於Get和Post的區別大致有以下幾點:
- Get會把請求引數都拼接在url後面,最終顯示在位址列,而Post則會把請求引數資料放進請求體中,不會再位址列顯示出來
- 傳遞引數的長度限制
問題:
對於第一點,如果是在瀏覽器裡把隱私資料暴露在位址列上確實不妥,但是如果是在App開發中呢,沒有位址列的概念,那麼這一點是不是還會成為選擇post還是get的制約條件。
對於第二點,長度的限制應該是瀏覽器的限制,跟get本身無關,如果是在App開發中,這一點是否也可以忽略。
二、HTTP的快取機制
之所以想介紹以下Http的快取機制,是因為Okhttp中對於網路請求快取這一塊就是利用了Http的的快取機制,而不是像Volley等框架那樣客戶端完全自己寫一套快取策略自己玩。
Http的快取主要利用header裡的兩個欄位來控制:
Cache-control
主要包含以及幾個欄位:
- private:則只有客戶端可以快取
- public:客戶端和代理伺服器都可以快取
- max-age:快取的過期時間
- no-cache:需要使用
對比快取
來驗證快取資料 - no-store:所有記憶體都不會進行快取
實際上就是在這裡面設定了一個快取策略,由服務端第一次通過header下發給客戶端,可以看到:
max-age
即快取過期的時間,則之後再次請求,如果沒有超過快取失效的時間則可以直接使用快取。
no-cache
:表示需要使用對比快取來驗證快取資料,如果這個欄位是開啟的,則就算max-age快取沒有失效,則還是需要發起一次請求向服務端確認一下資源是否有更新,是否需要重新請求資料,至於怎麼做對比快取,就是下面要說的Etag
的作用。如果服務端確認資源沒有更新,則返回304,取本地快取即可,如果有更新,則返回最新的資源。
no-store
:這個欄位開啟,則不會進行快取,也不會取快取。
2.ETag:
即用來進行對比快取,Etag是服務端資源的一個標識碼
當客戶端傳送第一次請求時服務端會下發當前請求資源的標識碼Etag,下次再請求時,客戶端則會通過header裡的If-None-Match
將這個標識碼Etag帶上,服務端將客戶端傳來的Etag與最新的資源Etag做對比,如果一樣,則表示資源沒有更新,返回304。
通過Cache-control
和Etag
的配合來實現Http的快取機制。
三、Cookie
上面說了Http協議是無狀態的,而Cookie就是用來在本地快取記住一些狀態的,一個Cookie一般都包含domin
(所屬域)、path
、Expires
(過期時間)等幾個屬性。服務端可以通過在響應頭裡的set-cookies來將狀態寫入客戶端的Cookie裡。下次客戶端發起請求時可以將Cookie帶上。
Android開發中遇到的問題及解決:
說起Cookie,一般如果平常只是做App開發,比較不經常遇到,但是如果是涉及到WebView的需求,則有可能會遇到,下面就說一下我在專案裡遇到過的一個關於WebView Cookie的揪心往事:
需求是這樣的,載入的WebView中的H5頁面需要是已登入狀態的,所以我們需要在原生頁面登入後,手動將ticket寫入WebView的Cookie,之後WebView里載入的H5頁面帶著Cookie裡的ticket給服務端驗證通過就好了。但是遇到一個問題:通過Chrome inspect
除錯WebView,手動寫的Cookie確實是已經寫進去了,但是發起請求的時候,Cookie就是沒有帶上,導致請求驗證失敗,之後通過排查,是WebView的屬性預設關閉引起,通過下面的程式碼設定開啟即可:
CookieManager cookieManager = CookieManager.getInstance();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cookieManager.setAcceptThirdPartyCookies(mWebView, true);
} else {
cookieManager.setAcceptCookie(true);
}
複製程式碼
四、Https
我們都知道Https保證了我們資料傳輸的安全,Https=Http+Ssl,之所以能保證安全主要的原理就是利用了非對稱加密演算法
,平常用的對稱加密演算法之所以不安全,是因為雙方是用統一的密匙進行加密解密的,只要雙方任意一方洩漏了密匙,那麼其他人就可以利用密匙解密資料。
而非對稱加密演算法
之所以能實現安全傳輸的核心精華
就是:
公鑰
加密的資訊只能用私鑰
解開,私鑰
加密的資訊只能被公鑰
解開
1.簡述非對稱加密演算法為什麼安全:
服務端申請CA機構頒發的證照,則獲取到了證照的公鑰和私鑰,私鑰只有伺服器端自己知道
,而公鑰可以告知其他人,如可以把公鑰傳給客戶端,這樣客戶端通過服務端傳來的公鑰來加密自己傳輸的資料,而服務端利用私鑰就可以解密這個資料了。由於客戶端這個用公鑰加密的資料只有私鑰能解密,而這個私鑰只有服務端有,所以資料傳輸就安全了。
上面只是簡單說了一下非對稱加密演算法是如何保證資料安全的,實際上Https的工作過程遠比這要複雜(篇幅限制這裡就不細說了,網上有很多相關文章):
-
一個是客戶端還需要驗證服務端傳來的CA證照的合法性、有效性,因為存在傳輸過程CA證照被人調包的風險,涉及到客戶端如何驗證伺服器證照的合法性的問題,保證通訊雙方的身份合法。
-
另一個是非對稱演算法雖然保證了資料的安全,但是效率相對於對稱演算法來說比較差,如何來優化,實現既保證了資料的安全,又提高了效率。
2.客戶端如何驗證證照的合法性:
首先CA證照一般包括以下內容:
- 證照的頒發機構以及版本
- 證照的使用者
- 證照的公鑰
- 證照有效時間
- 證照的數字簽名Hash值以及簽名Hash演算法(這個數字簽名Hash值是用證照的私鑰加密過的值)
- 等等
客戶端驗證服務端傳過來的證照的合法性是通過
:先利用獲取到的公鑰來解密證照中的數字簽名Hash值1(因為它是利用私鑰加密的嘛),然後在利用證照裡的簽名Hash演算法生成一個Hash值2,如果兩個值相等,則表示證照合法,伺服器端可以被信任。
Android開發中遇到的問題及解決:
順便說一個在專案開發中使用Android WebView載入公司測試伺服器上網頁證照過期導致網頁載入不出來白屏的問題:
解決方案就是測試環境下暫時忽略SSL的報錯,這樣就可以把網頁載入出來,當然在生產上不要這麼做,一個是會有安全問題,一個是google play應該稽核也不會通過。
重寫WebViewClient的onReceivedSslError():
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
if (ContextHolder.sDebug) {
handler.proceed();
return;
}
super.onReceivedSslError(view, handler, error);
}
複製程式碼
五、Http 2.0
Okhttp支援配置使用Http 2.0協議,Http2.0相對於Http1.x來說提升是巨大的,主要有以下幾點:
二進位制格式:
http1.x是文字協議,而http2.0是二進位制以幀為基本單位,是一個二進位制協議,一幀中除了包含資料外同時還包含該幀的標識:Stream Identifier,即標識了該幀屬於哪個request,使得網路傳輸變得十分靈活。多路複用:
一個很大的改進,原先http1.x一個連線一個請求的情況有比較大的侷限性,也引發了很多問題,如建立多個連線的消耗以及效率問題。
http1.x為了解決效率問題,可能會盡量多的發起併發的請求去載入資源,然而瀏覽器對於同一域名下的併發請求有限制,而優化的手段一般是將請求的資源放到不同的域名下來突破這種限制。
而http2.0支援的多路複用可以很好的解決這個問題,多個請求共用一個TCP連線,多個請求可以同時在這個TCP連線上併發
,一個是解決了建立多個TCP連線的消耗問題,一個也解決了效率的問題。那麼是什麼原理支撐多個請求可以在一個TCP連線上併發呢?基本原理就是上面的二進位制分幀,因為每一幀都有一個身份標識,所以多個請求的不同幀可以併發的無序傳送出去,在服務端會根據每一幀的身份標識,將其整理到對應的request中。
header頭部壓縮:
主要是通過壓縮header來減少請求的大小,減少流量消耗,提高效率。因為之前存在一個問題是,每次請求都要帶上header,而這個header中的資料通常是一層不變的。支援服務端推送
3.TCP相關
TCP面向連線,提供可靠的資料傳輸。在這一層,我們通常都是通過Socket Api來操作TCP,建立連線等等。
一、三次握手建立連線
第一次
:傳送SNY=1表示此次握手是請求建立連線的,然後seq生成一個客戶端的隨機數X
第二次
:傳送SNY=1,ACK=1表示是回覆請求建立連線的,然後ack=客戶端的seq+1(這樣客戶端收到後就能確認是之前想要連線的那個服務端),然後把服務端也生成一個代表自己的隨機數seq=Y發給客戶端。
第三次
:ACK=1。 seq=客戶端隨機數+1,ack=服務端隨機數+1(這樣服務端就知道是剛剛那個客戶端了)
為什麼建立連線需要三次握手?
首先非常明確的是兩次握手是最基本的,
第一次握手
,C端發了個連線請求訊息到S端,S端收到後S端就知道自己與C端是可以連線成功的,但是C端此時並不知道S端是否接收到這個訊息,所以S端接收到訊息後得應答,C端得到S端的回覆後,才能確定自己與S端是可以連線上的,這就是第二次握手
。C端只有確定了自己能與S端連線上才能開始發資料。所以兩次握手肯定是最基本的。
那麼為什麼需要第三次握手呢?
假設一下如果沒有第三次握手,而是兩次握手後我們就認為連線建立,那麼會發生什麼?
第三次握手是為了防止已經失效的連線請求報文段突然又傳到服務端,因而產生錯誤
具體情況就是:C端發出去的第一個網路連線請求由於某些原因在網路節點中滯留了,導致延遲,直到連線釋放的某個時間點才到達S端,這是一個早已失效的報文,但是此時S端仍然認為這是C端的建立連線請求第一次握手,於是S端迴應了C端,第二次握手。如果只有兩次握手,那麼到這裡,連線就建立了,但是此時C端並沒有任何資料要傳送,而S端就會傻傻的等待著,造成很大的資源浪費。所以需要第三次握手,只有C端再次迴應一下,就可以避免這種情況。
二、四次握手斷開連線
經過上面的建立連線圖的解析,這個圖應該不難看懂,這裡主要有一個問題:
為什麼比建立連線時多了一次握手?
可以看到這裡服務端的ACK(回覆客戶端)和FIN(終止)訊息並不是同時發出的,而是先ACK,然後再FIN,這也很好理解,當客戶端要求斷開連線時,此時服務端可能還有未傳送完的資料
,所以先ACK,然後等資料傳送完再FIN。這樣就變成了四次握手了。
上面講了TCP建立連線和斷開連線的過程,TCP最主要的特點就是提供可靠的傳輸,那麼他是如何保證資料傳輸是可靠的呢,這就是下面要講的滑動視窗協議
三、滑動視窗協議
滑動視窗協議是保證TCP的可靠傳輸的根本,因為傳送視窗只有收到確認幀才會向後移動視窗繼續傳送其他幀。
下面舉個例子:假如傳送視窗
是3幀
一開始傳送視窗在前3幀[1,2,3],則前3幀是可以傳送的,後面的則暫時不可以傳送,比如[1]幀傳送出去後,收到了來自接收方的確認訊息,則此時傳送視窗才可以往後移1幀,傳送視窗來到[2,3,4],同樣只有傳送視窗內的幀才可以被髮送,一次類推。
而接收視窗
接收到幀後將其放入對應的位置,然後移動接收視窗,介面視窗與傳送視窗一樣也有一個大小,如接收視窗是5幀,則落在接收視窗之外的幀會被丟棄。
傳送視窗和接收視窗大小的不同設定就延伸出了不同的協議:
協議 | 特點 |
---|---|
停止-等待協議 | 傳送視窗大小=1,接收視窗大小=1 |
後退N幀協議 | 傳送視窗大小>1,接收視窗大小=1, |
選擇重傳協議 | 傳送視窗大小>1,接收視窗大小>1 |
停止-等待協議
:每發一幀都要等到確認訊息才能傳送下一幀,缺點:
效率較差。
後退N幀協議
:採取累計確認的方式,接收方正確的接受到N幀後發一個累計確認訊息給傳送視窗,確認N幀已正確收到,如果傳送方規定時間內未收到確認訊息則認為超時或資料丟失,則會重新傳送確認幀之後的所有幀。缺點:
出錯序號後面的PDU已經傳送過了,但是還是要重新傳送,比較浪費。
選擇重傳協議
:若出現差錯,只重新傳輸出現差錯涉及需要的PDU,提高了傳輸效率,減少不必要的重傳。
到這裡還剩下最後一個問題:由於傳送視窗與接收視窗之間會存在傳送效率和接收效率不匹配的問題,就會導致擁塞,解決這個問題TCP有一套
流量控制和擁塞控制
的機制。
四、流量控制和擁塞控制
1.流量控制
流量控制是對一條通訊路徑上的流量進行控制,就是傳送方通過獲取接收方的回饋來動態調整傳送的速率,來達到控制流量的效果,其目的是保證傳送者的傳送速度不超過接收者的接收速度。
2.擁塞控制
擁塞控制是對整個通訊子網的流量進行控制,屬於全域性控制。
①慢開始
+擁塞避免
先來看一張經典的圖:
一開始使用慢啟動
,即擁塞視窗設為1,然後擁塞視窗指數增長到慢開始的門限值(ssthresh=16),則切換為擁塞避免
,即加法增長,這樣增長到一定程度,導致網路擁塞,則此時會把擁塞視窗重新降為1,即重新慢開始
,同時調整新的慢開始門限值為12,之後以此類推。
②快重傳
+快恢復
快重傳:
上面我們說的重傳機制都是等到超時還未收到接收方的回覆,才開始進行重傳。而快重傳的設計思路是:如果傳送方收到3個重複的接收方的ACK
,就可以判斷有報文段丟失,此時就可以立即重傳丟失的報文段,而不用等到設定的超時時間到了才開始重傳,提高了重傳的效率。
快恢復:
上面的擁塞控制會在網路擁塞時將擁塞視窗降為1,重新慢開始,這樣存在的一個問題就是網路無法很快恢復到正常狀態。快恢復就是來優化這個問題的,使用快恢復,則出現擁塞時,擁塞視窗只會降低到新的慢開始門閥值(即12),而不會降為1,然後直接開始進入擁塞避免加法增長,如下圖所示:
快重傳和快恢復是對擁塞控制的進一步改進。
4.Socket
Socket是一組操作TCP/UDP的API
,像HttpURLConnection和Okhttp這種涉及到比較底層的網路請求傳送的,最終當然也都是通過Socket來進行網路請求連線傳送,而像Volley、Retrofit則是更上層的封裝,最後是依靠HttpURLConnection或者Okhttp來進行最終的連線建立和請求傳送。
Socket的簡單使用的話應該都會,兩個端各建立一個Socket,服務端的叫ServerSocket,然後建立連線即可。
5.總結
當然這些只是我自己知道的並且認為挺重要的計算機網路基礎,還有非常多的網路基礎知識需要去深入瞭解去探索。寫了很多,算是對自己網路基礎的一個整理,可能也會有紕漏。接下來也會簡單解析一下HttpUrlConnection
以及Volley
,原始碼解析一下Okhttp
以及Retrofit
。