網絡通訊一直是Android項目裏比較重要的一個模塊,Android開源項目上出現過不少優秀的網絡框架,從一開始只是一些對HttpClient和HttpUrlConnection簡易封裝使用的工具類,到後來Google開源的比較完善豐富的Volley,再到現在比較流行的Okhttp、Retrofit。php
要想理解他們之間存在的異同(或者具體點說,要想更深刻地掌握Android開發中的網絡通訊技術),必須對網絡基礎知識、Android網絡框架的基本原理等作到心中有數、信手拈來,關鍵時刻才能找到適合您APP的最佳網絡通訊技術實踐。html
事實證實在Android的平常開發和源碼閱讀中也會常常碰到相關知識,掌握這些網絡基礎知識,也是Android程序員真正邁向高階的過程當中必備的一些基本技術素質之一。git
有鑑於此,本文將主要介紹計算機網絡的一些基礎,以及在Android開發中的一些使用及遇到的問題和解決。程序員
本篇主要分爲如下幾部分:github
1)計算機網絡體系結構;算法
2)Http相關;編程
3)Tcp相關;瀏覽器
4)Socket。緩存
學習交流:安全
- 即時通信開發交流3羣:185926912[推薦]
- 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM》
(本文同步發佈於:http://www.52im.net/thread-1963-1-1.html)
舒大飛:攜程網Android開發工程師,做者博客:https://www.jianshu.com/u/ae379336190b。
注:在收錄本文時,爲了更易於理解,對內容作了更爲細緻的修訂。
計算機網絡體系結構,即常常看到的計算機網絡體系的分層結構,理清這個仍是有必要的,防止對Http和Tcp兩個根本不在同一層的協議糾纏不清。 根據不一樣的參考模型,分層結構有幾個不一樣的版本,如OSI模型以及TCP/IP模型。
下面就以比較常常看到的的5層結構爲例:
(更爲清晰完整的圖,請見《計算機網絡通信協議關係圖(中文珍藏版)[附件下載] 》)
如上圖所示,五層的體系結構至上往下,最終能夠實現端對端之間的數據傳輸與通訊,他們各自負責一些什麼,最終如何實現端對端之間的通訊?
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尋址以及物理層等等仍是要從新去看一下《TCP/IP詳解 卷1:協議》~
本節主要講一些關於Http的基礎知識,以及在Android中的一些實際應用和碰到的問題和解決。
限於篇幅緣由,本文在一些知識點上只作簡要性的概述,若是想要全面深刻地掌握HTTP協議,請閱讀如下文章:
《從HTTP/0.9到HTTP/2:一文讀懂HTTP協議的歷史演變和設計思路》
《腦殘式網絡編程入門(三):HTTP協議必知必會的一些知識》
Http是無鏈接無狀態的。
無鏈接並非說不須要鏈接,Http協議只是一個應用層協議,最終仍是要靠運輸層的如TCP協議向上提供的服務進行鏈接。
無鏈接的含義是http約定了每次鏈接只處理一個請求,一次請求完成後就斷開鏈接,這樣主要是爲了緩解服務器的壓力,減少鏈接對服務器資源的佔用。個人理解是,創建鏈接其實是運輸層的事,面向應用層的http來講的話,它就是無鏈接的,由於上層對下層無感知。
無狀態的指每一個請求之間都是獨立的,對於以前的請求事務沒有記憶的能力。因此就出現了像Cookie這種,用來保存一些狀態的東西。
這裏主要簡單說一下HTTP請求報文和響應報文的格式方面的基礎知識。
請求報文:
響應報文:
關於Get和Post,咱們都熟知的關於Get和Post的區別大體有如下幾點:
1)Get會把請求參數都拼接在url後面,最終顯示在地址欄,而Post則會把請求參數數據放進請求體中,不會再地址欄顯示出來;
2)傳遞參數的長度限制。
問題:
對於第1)點,若是是在瀏覽器裏把隱私數據暴露在地址欄上確實不妥,可是若是是在App開發中呢,沒有地址欄的概念,那麼這一點是否是還會成爲選擇post仍是get的制約條件;
對於第2)點,長度的限制應該是瀏覽器的限制,跟get自己無關,若是是在App開發中,這一點是否也能夠忽略。
之因此想介紹如下Http的緩存機制,是由於Okhttp中對於網絡請求緩存這一塊就是利用了Http的的緩存機制,而不是像Volley等框架那樣客戶端徹底本身寫一套緩存策略本身玩。
Http的緩存主要利用header裏的兩個字段來控制:即Cache-control和ETag,下面將分別來介紹。
1)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。
3)小結:
經過Cache-control和Etag的配合來實現Http的緩存機制。更多有關HTTP緩存方面的的知識,在文章《腦殘式網絡編程入門(三):HTTP協議必知必會的一些知識》的相關章節作了詳細的讀解,能夠參閱之。
上面說了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=Http+Ssl,之因此能保證安全主要的原理就是利用了非對稱加密算法,日常用的對稱加密算法之因此不安全,是由於雙方是用統一的密匙進行加密解密的,只要雙方任意一方泄漏了密匙,那麼其餘人就能夠利用密匙解密數據。
而非對稱加密算法之因此能實現安全傳輸的核心精華就是:公鑰加密的信息只能用私鑰解開,私鑰加密的信息只能被公鑰解開。
1)簡述非對稱加密算法爲何安全:
服務端申請CA機構頒發的證書,則獲取到了證書的公鑰和私鑰,私鑰只有服務器端本身知道,而公鑰能夠告知其餘人,如能夠把公鑰傳給客戶端,這樣客戶端經過服務端傳來的公鑰來加密本身傳輸的數據,而服務端利用私鑰就能夠解密這個數據了。因爲客戶端這個用公鑰加密的數據只有私鑰能解密,而這個私鑰只有服務端有,因此數據傳輸就安全了。
上面只是簡單說了一下非對稱加密算法是如何保證數據安全的,實際上Https的工做過程遠比這要複雜(篇幅限制這裏就不細說了,網上有不少相關文章):
一個是客戶端還須要驗證服務端傳來的CA證書的合法性、有效性,由於存在傳輸過程CA證書被人調包的風險,涉及到客戶端如何驗證服務器證書的合法性的問題,保證通訊雙方的身份合法;
另外一個是非對稱算法雖然保證了數據的安全,可是效率相對於對稱算法來講比較差,如何來優化,實現既保證了數據的安全,又提升了效率。
2)客戶端如何驗證證書的合法性:
首先CA證書通常包括如下內容:
證書的頒發機構以及版本;
證書的使用者;
證書的公鑰;
證書有效時間;
證書的數字簽名Hash值以及簽名Hash算法(這個數字簽名Hash值是用證書的私鑰加密過的值);
等等。
客戶端驗證服務端傳過來的證書的合法性是經過:先利用獲取到的公鑰來解密證書中的數字簽名Hash值1(由於它是利用私鑰加密的嘛),而後在利用證書裏的簽名Hash算法生成一個Hash值2,若是兩個值相等,則表示證書合法,服務器端能夠被信任。
3)Android開發中遇到的問題及解決:
順便說一個在項目開發中使用Android WebView加載公司測試服務器上網頁證書過時致使網頁加載不出來白屏的問題。
解決方案就是測試環境下暫時忽略SSL的報錯,這樣就能夠把網頁加載出來,固然在生產上不要這麼作,一個是會有安全問題,一個是google play應該審覈也不會經過。
最佳辦法是重寫WebViewClient的onReceivedSslError():
publicvoidonReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
if(ContextHolder.sDebug) {
handler.proceed();
return;
}
super.onReceivedSslError(view, handler, error);
}
最後:有關HTTPS更爲詳細全面的知識,請深刻閱讀《即時通信安全篇(七):若是這樣來理解HTTPS原理,一篇就夠了》。
Okhttp支持配置使用Http 2.0協議,Http2.0相對於Http1.x來講提高是巨大的,主要有如下幾點。
1)二進制格式:http1.x是文本協議,而http2.0是二進制以幀爲基本單位,是一個二進制協議,一幀中除了包含數據外同時還包含該幀的標識:Stream Identifier,即標識了該幀屬於哪一個request,使得網絡傳輸變得十分靈活;
2)多路複用:一個很大的改進,原先http1.x一個鏈接一個請求的狀況有比較大的侷限性,也引起了不少問題,如創建多個鏈接的消耗以及效率問題。
http1.x爲了解決效率問題,可能會盡可能多的發起併發的請求去加載資源,然而瀏覽器對於同一域名下的併發請求有限制,而優化的手段通常是將請求的資源放到不一樣的域名下來突破這種限制。
而http2.0支持的多路複用能夠很好的解決這個問題,多個請求共用一個TCP鏈接,多個請求能夠同時在這個TCP鏈接上併發,一個是解決了創建多個TCP鏈接的消耗問題,一個也解決了效率的問題。
那麼是什麼原理支撐多個請求能夠在一個TCP鏈接上併發呢?基本原理就是上面的二進制分幀,由於每一幀都有一個身份標識,因此多個請求的不一樣幀能夠併發的無序發送出去,在服務端會根據每一幀的身份標識,將其整理到對應的request中。
3)header頭部壓縮:主要是經過壓縮header來減小請求的大小,減小流量消耗,提升效率。由於以前存在一個問題是,每次請求都要帶上header,而這個header中的數據一般是一層不變的。
4)支持服務端推送。
有關HTTP2的更多知識,請閱讀《從HTTP/0.9到HTTP/2:一文讀懂HTTP協議的歷史演變和設計思路》。
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端再次迴應一下,就能夠避免這種狀況。
要想深入理解TCP三次握手,請不要錯過如下文章:
《理論聯繫實際:Wireshark抓包分析TCP 3次握手、4次揮手過程》
《腦殘式網絡編程入門(一):跟着動畫來學TCP三次握手和四次揮手》
通過上面的創建鏈接圖的解析,這個圖應該不難看懂。
這裏主要有一個問題:爲何比創建鏈接時多了一次揮手?
能夠看到這裏服務端的ACK(回覆客戶端)和FIN(終止)消息並非同時發出的,而是先ACK,而後再FIN,這也很好理解,當客戶端要求斷開鏈接時,此時服務端可能還有未發送完的數據,因此先ACK,而後等數據發送完再FIN。這樣就變成了四次握手了。
上面講了TCP創建鏈接和斷開鏈接的過程,TCP最主要的特色就是提供可靠的傳輸,那麼他是如何保證數據傳輸是可靠的呢,這就是下面要講的滑動窗口協議。
相關知識請深刻閱讀:
《理論聯繫實際:Wireshark抓包分析TCP 3次握手、4次揮手過程》
《腦殘式網絡編程入門(一):跟着動畫來學TCP三次握手和四次揮手》
滑動窗口協議是保證TCP的可靠傳輸的根本,由於發送窗口只有收到確認幀纔會向後移動窗口繼續發送其餘幀。
下面舉個例子:假如發送窗口是3幀
一開始發送窗口在前3幀[1,2,3],則前3幀是能夠發送的,後面的則暫時不能夠發送,好比[1]幀發送出去後,收到了來自接收方的確認消息,則此時發送窗口才能夠日後移1幀,發送窗口來到[2,3,4],一樣只有發送窗口內的幀才能夠被髮送,一次類推。
而接收窗口接收到幀後將其放入對應的位置,而後移動接收窗口,接口窗口與發送窗口同樣也有一個大小,如接收窗口是5幀,則落在接收窗口以外的幀會被丟棄。
發送窗口和接收窗口大小的不一樣設定就延伸出了不一樣的協議:
中止-等待協議:每發一幀都要等到確認消息才能發送下一幀,缺點:效率較差。
後退N幀協議:採起累計確認的方式,接收方正確的接受到N幀後發一個累計確認消息給發送窗口,確認N幀已正確收到,若是發送方規定時間內未收到確認消息則認爲超時或數據丟失,則會從新發送確認幀以後的全部幀。缺點:出錯序號後面的PDU已經發送過了,可是仍是要從新發送,比較浪費。
選擇重傳協議:若出現差錯,只從新傳輸出現差錯涉及須要的PDU,提升了傳輸效率,減小沒必要要的重傳。
到這裏還剩下最後一個問題:因爲發送窗口與接收窗口之間會存在發送效率和接收效率不匹配的問題,就會致使擁塞,解決這個問題TCP有一套流量控制和擁塞控制的機制。
1)流量控制:
流量控制是對一條通訊路徑上的流量進行控制,就是發送方經過獲取接收方的回饋來動態調整發送的速率,來達到控制流量的效果,其目的是保證發送者的發送速度不超過接收者的接收速度。
2)擁塞控制:
擁塞控制是對整個通訊子網的流量進行控制,屬於全局控制。
① 慢開始+擁塞避免
先來看一張經典的圖:
一開始使用慢啓動,即擁塞窗口設爲1,而後擁塞窗口指數增加到慢開始的門限值(ssthresh=16),則切換爲擁塞避免,即加法增加,這樣增加到必定程度,致使網絡擁塞,則此時會把擁塞窗口從新降爲1,即從新慢開始,同時調整新的慢開始門限值爲12,以後以此類推。
② 快重傳+快恢復
快重傳:上面咱們說的重傳機制都是等到超時還未收到接收方的回覆,纔開始進行重傳。而快重傳的設計思路是:若是發送方收到3個重複的接收方的ACK,就能夠判斷有報文段丟失,此時就能夠當即重傳丟失的報文段,而不用等到設置的超時時間到了纔開始重傳,提升了重傳的效率。
快恢復:上面的擁塞控制會在網絡擁塞時將擁塞窗口降爲1,從新慢開始,這樣存在的一個問題就是網絡沒法很快恢復到正常狀態。快恢復就是來優化這個問題的,使用快恢復,則出現擁塞時,擁塞窗口只會下降到新的慢開始門閥值(即12),而不會降爲1,而後直接開始進入擁塞避免加法增加,以下圖所示:
快重傳和快恢復是對擁塞控制的進一步改進。
要更深刻地理解本小節問題,請詳讀:《TCP/IP詳解 - 第21章·TCP的超時與重傳》、《通俗易懂-深刻理解TCP協議(下):RTT、滑動窗口、擁塞處理》。
Socket是一組操做TCP/UDP的API,像HttpURLConnection和Okhttp這種涉及到比較底層的網絡請求發送的,最終固然也都是經過Socket來進行網絡請求鏈接發送,而像Volley、Retrofit則是更上層的封裝,最後是依靠HttpURLConnection或者Okhttp來進行最終的鏈接創建和請求發送。
Socket的簡單使用的話應該都會,兩個端各創建一個Socket,服務端的叫ServerSocket,而後創建鏈接便可。
相關資料,請閱讀:
《網絡編程懶人入門(八):手把手教你寫基於TCP的Socket長鏈接》
《腦殘式網絡編程入門(二):咱們在讀寫Socket時,究竟在讀寫什麼?》
固然,以上這些內容只是我本身知道的而且認爲挺重要的計算機網絡基礎,還有很是多的網絡基礎知識須要去深刻了解去探索。寫了不少,算是對本身網絡基礎的一個整理,可能也會有紕漏,權當拋磚引玉,還請各位大牛不吝賜教。
《技術往事:改變世界的TCP/IP協議(珍貴多圖、手機慎點)》
《通俗易懂-深刻理解TCP協議(下):RTT、滑動窗口、擁塞處理》
《理論聯繫實際:Wireshark抓包分析TCP 3次握手、4次揮手過程》
《P2P技術詳解(一):NAT詳解——詳細原理、P2P簡介》
《P2P技術詳解(二):P2P中的NAT穿越(打洞)方案詳解》
《P2P技術詳解(三):P2P技術之STUN、TURN、ICE詳解》
《高性能網絡編程(一):單臺服務器併發TCP鏈接數到底能夠有多少》
《高性能網絡編程(二):上一個10年,著名的C10K併發鏈接問題》
《高性能網絡編程(三):下一個10年,是時候考慮C10M併發問題了》
《高性能網絡編程(四):從C10K到C10M高性能網絡應用的理論探索》
《高性能網絡編程(五):一文讀懂高性能網絡編程中的I/O模型》
《高性能網絡編程(六):一文讀懂高性能網絡編程中的線程模型》
《鮮爲人知的網絡編程(一):淺析TCP協議中的疑難雜症(上篇)》
《鮮爲人知的網絡編程(二):淺析TCP協議中的疑難雜症(下篇)》
《鮮爲人知的網絡編程(三):關閉TCP鏈接時爲何會TIME_WAIT、CLOSE_WAIT》
《鮮爲人知的網絡編程(七):如何讓不可靠的UDP變的可靠?》
《網絡編程懶人入門(五):快速理解爲何說UDP有時比TCP更有優點》
《網絡編程懶人入門(六):史上最通俗的集線器、交換機、路由器功能原理入門》
《網絡編程懶人入門(八):手把手教你寫基於TCP的Socket長鏈接》
《技術掃盲:新一代基於UDP的低延時網絡傳輸層協議——QUIC詳解》
《現代移動端網絡短鏈接的優化手段總結:請求速度、弱網適應、安全保障》
《移動端IM開發者必讀(一):通俗易懂,理解移動網絡的「弱」和「慢」》
《移動端IM開發者必讀(二):史上最全移動弱網絡優化方法總結》
《從HTTP/0.9到HTTP/2:一文讀懂HTTP協議的歷史演變和設計思路》
《腦殘式網絡編程入門(一):跟着動畫來學TCP三次握手和四次揮手》
《腦殘式網絡編程入門(二):咱們在讀寫Socket時,究竟在讀寫什麼?》
《腦殘式網絡編程入門(三):HTTP協議必知必會的一些知識》
《腦殘式網絡編程入門(四):快速理解HTTP/2的服務器推送(Server Push)》
《以網遊服務端的網絡接入層設計爲例,理解實時通訊的技術挑戰》
>> 更多同類文章 ……
(本文同步發佈於:http://www.52im.net/thread-1963-1-1.html)