優雅設計封裝基於Okhttp3的網路框架(一):Http網路協議與Okhttp3解析

如今Android開發中Okhttp已成為主流網路框架,內建豐富、全面、強大的網路請求功能,也為開發者提供了api,但是在專案開發中的大量使用,會出現api的重複呼叫、程式碼冗雜等現象。應當封裝一個適用於專案的網路框架,便於使用。

此係列文章旨於:基於okhttp3原始框架來設計封裝一個滿足業務需求、擴充套件性強、耦合度低的網路框架。具體框架功能為:

  • 封裝基本的網路請求
  • 擴充套件其對資料庫的支援
  • 對多檔案上傳、多執行緒檔案下載的支援
  • 對Json資料解析等功能的支援

此係列文章將詳細記錄其封裝過程,從底層框架的選擇、基礎知識、設計理念到架構分析、封裝程式碼,自頂向下來完成網路框架的設計封裝,最後附上原始碼,下面開始!


一. 主流網路框架分析與選擇

1. 常用網路框架介紹

在Android網路框架並不成熟的時候,開發者在開發過程中往往會遇到幾大難題:如何去訪問網路資料?如何做本地儲存?如何做圖片快取等類似問題。隨著Android不斷髮展,繼而推出了以下幾大常用框架:

(1)Volley

谷歌研發的一款底層基礎網路框架,Volley在Android整個發展中是出現較早的,早期的專案使用的便是Volley框架,但是以當今的角度去分析它還是有些不足的地方,例如不支援文件、檔案的下載,更傾向於輕量級資料的請求。

優點:

  • 預設Android2.3及以上基於HttpURLConnection,2.3以下使用基於HttpClient。
  • 符合Http 快取語義 的快取機制(提供了預設的磁碟和記憶體等快取)。
  • 請求佇列的優先順序排序(網路框架的基本必備)。
  • 提供多樣的取消機制。
  • 提供簡便的圖片載入工具(其實圖片的載入才是我們最為看重的功能)。

缺點:

  • 不能下載檔案。

(2)Android-async-http

提供了Http請求的大部分功能,絕對滿足網路請求的各種需求,支援智慧重試、gzip壓縮等功能,是一個功能全面的網路框架,而且在github上start星數是Volley的兩倍。

優點:

  • 在匿名回撥中處理請求結果
  • 在UI執行緒外進行http請求
  • 檔案斷點上傳
  • 智慧重試
  • 預設gzip壓縮
  • 支援解析成Json格式
  • 可將Cookies持久化到SharedPreference

缺點:

  • 已停止維護更新,新功能需自己實現

(3)Afinal 和 XUtils

國人研發的框架,不僅僅提供網路請求的功能,還支援資料庫管理、圖片下載快取等功能。雖然功能豐富,但是沒有隻專於網路請求的框架功能細緻、全面,後期維護成本很高。此框架較適用於小型快速開發。

四大模組功能:

  • 資料庫模組: android中的orm框架,使用了執行緒池對sqlite進行操作。
  • 註解模組: android中的ioc框架,完全註解方式就可以進行UI繫結和事件繫結。無需findViewById和setClickListener等。
  • 網路模組:通過httpclient進行封裝http資料請求,支援ajax方式載入,支援下載、上傳檔案功能。
  • 圖片快取模組:通過FinalBitmap,imageview載入bitmap的時候無需考慮bitmap載入過程中出現的oom和android容器快速滑動時候出現的圖片錯位等現象。

(4)Okhttp

由square公司研發(開源界最著名的兩個公司:Square、Facebook),OkHttp是一款優秀的HTTP框架,大致功能為:

  • 支援get請求和post請求,支援基於Http的檔案上傳和下載
  • 支援載入快取圖片
  • 支援下載檔案透明的GZIP壓縮
  • 支援響應快取避免重複的網路請求
  • 支援使用連線池來降低響應延遲問題。

就目前而言,Okhttp網路請求框架已被廣泛應用成為主流框架,而且在Android6.0時已被整合到系統中預設底層協議,可以看出谷歌對Okhttp的滿意度。


(5)Retrofit
本質上就是在Okhttp上做了相應的封裝,網路底層互動還是Okhttp,封裝如下:

  • 支援okhttp
  • 註解處理,簡化程式碼
  • 支援上傳和下載檔案
  • 支援自己更換解析方式
  • 支援多種http請求庫


2. 網路框架選擇標準

參考標準:

  • 學習成本
  • 文件是否齊全
  • github 星數量
  • 現在是否有人維護
  • 流行程度
  • 程式碼設計是否有借鑑性
  • 程式碼體積

選擇

綜合以上考慮,本次封裝的網路框架是基於Okhttp3,第二點從架構設計的層面來分析本次封裝的網路框架,此點較為重要!




二. 架構設計分析 ★ ★ ★ ★ ★

在Android開發當中往往會涉及到設計模式的採用,例如MVC、MVP,MVVM等,但是在這個網路框架設計並未採用任何設計模式,主要是按照分層次的劃分來實現解耦的目的,方便於此框架以後拓展修改

這裡寫圖片描述

如上圖所示,此框架可以分為三個層次

  • 第一層:便於框架擴充套件,第一層即最底層是Http InterfaceAbstact,例如Http中的Headers、Request、Response等通用的原生介面。

  • 第二層:有了第一層請求介面定義,便於第二層對介面的實現,此框架採用兩種方式對介面進行實現,分別是Okhttp和原生的HttpURLConnection。通過這兩個相關的API去實現整個Http請求和響應的過程,若還想要做相應的拓展,採用別的第三方http請求庫,在此處可增加。(已經預先在第一層定義了足夠多的介面實現網路請求的回撥,第一層可無需修改)對於整個上層業務來說,無需直接接觸到底層Okhttp、HttpURLConnection具體實現,所以提供二次封裝的 HttpProvider ,暴露介面給上層呼叫。(具體底層是呼叫Http還是HttpURLConnection取決於配置,首先判斷Okhttp依賴在專案中是否存在,若有則主要採用Okhttp來進行網路請求,否則採用HttpURLConnection)

  • 第三層:即最上層由 WorkstationConvert組成。Workstation 的中文意思是工作站,用來處理一些執行緒的排程分發和任務的佇列,之所以將它設計在最上層,因為整個多執行緒、佇列機制是與業務層緊密相關的。Convert是為上層開發者提供了更好的介面封裝,用於介面返回型別轉換、資料解析,例如json、xml等。





三. Http協議基礎內容

1. 什麼是Http協議?

HTTP(Hypertext Transfer Protocol),即超文字傳輸協議。是WWW瀏覽器和WWW伺服器之間的應用層通訊協議。HTTP協議是基於TCP/IP之上的協議,它不僅保證正確傳輸超文字文件,還確定傳輸文件中的哪一部分,以及哪一部分內容首先顯示(如文字先於圖形)。


2. Http 版本區別

這裡寫圖片描述


3. Http的請求方式總結

方式名稱含義
GET請求獲取Request-URI所標識的資源
POST在Request-URI所標識的資源後附加新的資料
HEAD請求獲取由Request-URI所標識的資源的響應資訊報頭
PUT請求伺服器儲存一個資源,並用Request-URI作為其標識
DELETE請求伺服器刪除Request-URI所標識的資源
TRACE請求伺服器回送收到的請求資訊,主要用於測試或診斷
CONNECT保留將來使用
OPTIONS請求查詢伺服器的效能,或者查詢與資源相關的選項

4. Http協議的特點

  • 支援客戶/伺服器模式。
  • 簡單快速:客戶向伺服器請求服務時,只需傳送請求方法和路徑。請求方法常用的有GET、 HEAD、POST。每種方法規定了客戶與伺服器聯絡的型別不同。 由於HTTP協議簡單,使得HTTP伺服器的程式規模小,因而通訊速度很快。
  • 靈活: HTTP允許傳輸任意型別的資料物件。正在傳輸的型別由Content-Type加以標記。
  • 無連線: 無連線的含義是限制每次連線只處理一個請求。伺服器處理完客戶的請求, 並收到客戶的應答後,即斷開連線。採用這種方式可以節省傳輸時間。
  • 無狀態: HTTP協議是無狀態協議。無狀態是指協議對於事務處理沒有記憶能力。 缺少狀態意味著如果後續處理需要前面的資訊,則它必須重傳,這樣可能導致每 次連線傳送的資料量增大。另一方面,在伺服器不需要先前資訊時它的應答就較快。

5 . 請求頭資訊

請求頭說明
User-Agent中文名為使用者代理,是Http協議中的一部分,它是一個特殊字串頭,是一種向訪問網站提供你所使用的瀏覽器型別及版本、作業系統及版本、瀏覽器核心、等資訊的標識
Referer先前網頁的地址,當前請求網頁緊隨其後,即來路
Cache-Control指定請求和響應遵循的快取機制
Connection表示是否需要持久連線。(HTTP 1.1預設進行持久連線)
If-Match只有請求內容與實體相匹配才有效
If-Modified-Since如果請求的部分在指定時間之後被修改則請求成功,未被修改則返回304程式碼
If-None-Match如果內容未改變返回304程式碼,引數為伺服器先前傳送的Etag,與伺服器迴應的Etag比較判斷是否改變

這裡只介紹舉例部分重要的請求響應頭,關於更詳細資訊可看:
http://tools.jb51.net/table/http_header


6 . 響應頭資訊

應答頭說明
Content-Encoding文件的編碼(Encode)方法。只有在解碼之後才可以得到Content-Type頭指定的內容型別。
Content-Type表示後面的文件屬於什麼MIME型別。
Date當前的GMT時間。你可以用setDateHeader來設定這個頭以避免轉換時間格式的麻煩。
Expires過期時間
Last-Modified檔的最後改動時間。客戶可以通過If-Modified-Since請求頭提供一個日期,該請求將被視為一個條件GET,只有改動時間遲於指定時間的文件才會返回,否則返回一個304(Not Modified)狀態。

7 . 狀態碼資訊

HTTP狀態碼分類

分類描述
1**資訊,伺服器收到請求,需要請求者繼續執行操作
2**成功,操作被成功接收並處理
3**重定向,需要進一步的操作以完成請求
4**客戶端錯誤,請求包含語法錯誤或無法完成請求
5**伺服器錯誤,伺服器在處理請求的過程中發生了錯誤

較常用狀態碼

  • 200 – 請求成功
  • 301 – 資源(網頁等)被永久轉移到其它URL
  • 404 – 請求的資源(網頁等)不存在
  • 500 – 內部伺服器錯誤




四. Okhttp使用解析

以上基礎HTTP網路知識在後續的網路框架編碼中會涉及到,所以需要稍作了解。在學習以上知識點後,下面舉例介紹使用Okhttp3框架請求網路常用的方式,這裡只介紹後續封裝使用到的請求方式。(預設讀者有基本使用Okhttp3基礎,網上使用教程很多,可自行搜查)

1. 同步請求和非同步請求

大多數情況非同步請求較於同步請求更為廣泛,因為同步請求的過程中容易阻塞到執行緒,而非同步請求通過內部佇列維護完美避免此問題,並且在回撥中處理請求結果更符合需求。典型的使用方法如下:

public class AsyncHttp {
/*
*   同步請求(會阻塞到執行緒)
* */
public static void sendRequest(String url) {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(url).build();
try {
Response response = client.newCall(request).execute();
if (response.isSuccessful()) {
System.out.println(response.body().string());
}
} catch (IOException e) {
e.printStackTrace();
}
}
/*
*   非同步請求
* */
public static void sendAsyncRequest(String url) {
System.out.println(Thread.currentThread().getId());
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(url).build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
System.out.println(Thread.currentThread().getId());
}
}
});
}
public static void main(String args[]) {
System.out.println(0/100.0);
sendRequest("http://www.baidu.com");
//        sendAsyncRequest("http://www.baidu.com");
}
}

2. http請求頭與響應頭請求

在第二節中介紹了Http協議及請求頭、響應頭等相關知識,在使用Okhttp3框架請求網路時可以顯式呼叫、設定、獲取相關資訊,典型使用如下:

public class HeadHttp {
public static void main(String args[]) {
String str = "1234";
System.out.println(str.substring(0,str.length() - 3));
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().
url("http://www.imooc.com/static/sea-modules/seajs/seajs/2.1.1/sea.js").
addHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.71 Safari/537.36").
addHeader("Range", "bytes=2-").
addHeader("Accept-Encoding", "identity").
build();
try {
Response response = client.newCall(request).execute();
//            System.out.println(response.body().string());
System.out.println("size="   response.body().contentLength());
System.out.println("type="   response.body().contentType());
if (response.isSuccessful()) {
Headers headers = response.headers();
for (int i = 0; i < headers.size(); i  ) {
System.out.println(headers.name(i)   " : "   headers.value(i));
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

3. get請求之新增引數

get請求為Http的請求方式中最常用的一種,訪問URL獲取資源,大多數情況url通過引數組成而來,以下程式碼則介紹通過新增引數的方式來獲取最終url進行get請求,典型使用如下:

public class QueryHttp {
public static void main(String args[]) {
OkHttpClient client = new OkHttpClient();
HttpUrl httpUrl = HttpUrl.parse("https://api.heweather.com/x3/weather").
newBuilder().
addQueryParameter("city", "beijing").
addQueryParameter("key", "d17ce22ec5404ed883e1cfcaca0ecaa7").
build();
String url = httpUrl.toString();
System.out.println(httpUrl.toString());
Request request = new Request.Builder().url(url).build();
try {
Response response = client.newCall(request).execute();
if (response.isSuccessful()) {
System.out.println(response.body().string());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

這裡寫圖片描述


4. post請求之新增引數

上一點介紹完get請求後,必然聯想到post請求,get請求中的新增的引數資訊全部顯示在url上,根本上決定了get請求只適用於資源的資源獲取顯示,需要傳送重要資料給伺服器時需要使用post請求方式,不僅對傳送引數大小限制少,而且更為安全,典型的應用有登入註冊。使用方式如下:

public class PostHttp {
public static void main(String args[]) {
new Thread() {
@Override
public void run() {
OkHttpClient client = new OkHttpClient();
FormBody body = new FormBody.Builder().add("username", "nate")
.add("userage", "99").build();
Request request = new Request.Builder().url("http://localhost:8080/web/HelloServlet").post(body).build();
try {
Response response = client.newCall(request).execute();
if (response.isSuccessful()) {
System.out.println(response.body().string());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}
}

5. martipart 上傳檔案(MP4)

上一點傳送給伺服器的資料僅限於簡單的欄位,但是需求中常涉及到傳輸檔案,所以需要使用martipart 來使用類似需求,典型使用方式如下:

public class MultipartHttp {
public static void main(String args[]) {
RequestBody imageBody = RequestBody.create(MediaType.parse("image/jpeg"), new File("/Users/nate/girl.jpg"));
MultipartBody body = new MultipartBody.Builder().
setType(MultipartBody.FORM).
addFormDataPart("name", "girl").
addFormDataPart("filename", "girl.jpg", imageBody).build();
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().
url("http://192.168.1.6:8080/web/UploadServlet").post(body).build();
try {
Response response = client.newCall(request).execute();
if (response.isSuccessful()) {
System.out.println(response.body().string());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

另外,有興趣瞭解Okhttp3原始碼圖解分析快取機制的讀者可看此篇部落格,是由此係列分離出來的知識點,瞭解學習對後續封裝框架頗有幫助:

以 Okhttp3原始碼 為例 —— 圖解 快取機制 的原理和實現(上)

以 Okhttp3原始碼 為例 —— 圖解 快取機制 的原理和實現(下)





四. 小結

此篇文章的第二大點,也就是架構設計分析 最為重要,這是將要設計的網路框架的精髓支撐。要設計一個適用、合理的框架,解耦與拓展是相當重要的,不同分層的功能的實現相輔相成,應當被注重!

以上內容只是此係列的一個開始,Http網路協議的基礎瞭解和Okhttp3基本方法的解析,做好鋪墊之後,下一篇文章開始解析多執行緒功能的設計和實現。



期待下篇文章出爐 ~

希望對你們有幫助 :)