前言
在實際Android應用的開發中,網路請求往往是必不可少的。現在有很多優秀的開源網路框架如Volley、Okhttp和Retrofit等,說到框架,很多童鞋信手拈來,反手一個Okhttp+etrofit+RxJava全家桶。不就是網路請求麼,so easy~
不過實際開發過程中,確實會出現各種各樣的問題,比如你上傳一張圖片,伺服器那邊接收不到,怎麼辦呢?你看了下自己這邊,完全按照標準api來寫的,講道理應該沒錯吧?這時候開啟debug,可是框架內的程式碼怎麼跟進?沒看過也不懂啊,所以可能有些童鞋會去閱讀原始碼,可是原始碼這種東西,不熟悉的話讀起來晦澀難懂,當然邊讀邊做原始碼分析寫下幾篇部落格也是不錯的選擇。
不過其實最本質的,就是對你框架的業務熟悉,比如網路請求框架,你就必須熟悉Http協議,才可以瞭解你的表單是怎麼封裝成資料,以什麼結果表示,怎麼發出去,接收到的內容又是什麼?瞭解了這些,我們完全可以參考優秀的原始碼,自己動手去實現一個簡易版的。
Http協議
談到網路框架,就不得不說到http協議了,網路框架必須嚴格按照http協議才能保證客戶端和伺服器雙方資料的正常傳輸。
Http請求
一個http請求主要包含以下幾個部分:請求行(request line)、請求頭(header)、空行和請求正文四個部分。
以一個http請求為例:
GET /form.html HTTP/1.1
Accept:image/gif,image/x-xbitmap,image/jpeg,application/x-shockwave-flash,application/vnd.ms-excel,application/vnd.ms-powerpoint,application/msword,*/* Accept-Language:zh-cn
Accept-Encoding:gzip,deflate
If-Modified-Since:Wed,05 Jan 2007 11:21:25 GMT
If-None-Match:W/"80b1a4c018f3c41:8317"
User-Agent:Mozilla/4.0(compatible;MSIE6.0;Windows NT 5.0)
Host:www.guet.edu.cn
Connection:Keep-Alive
複製程式碼
- 請求行:用來說明請求型別、要訪問的資源及使用的Http版本。
- 請求頭:從第二行起為頭部,用來說明伺服器要是用的附加資訊,一般以鍵值對的方式出現,中間以‘:’隔開,如 Accept-Language:zh-cn。常用的請求頭請看:Http請求頭大全,支援使用者自定義請求頭。
- 空行:在請求頭和請求正文中間會有一個空行用來分割,說明請求頭和正文的區別。即使後面的正文為空,這裡的空行也是必須的。
- 請求正文:即我們要傳送的資料,平時我們傳入的表單、檔案等,都會以正文的形式存在於請求正文中,如果是get、delete等請求,請求正文必須為空。
Http響應
HTTP響應也由四個部分組成,分別是:狀態行、響應頭、空行和響應正文。 這裡也以一段http響應為例:
HTTP/1.1 200 OK
Date: Fri, 22 May 2009 06:07:21 GMT
Content-Type: text/html; charset=UTF-8
<html>
<head></head>
<body>
<!--body goes here-->
</body>
</html>
複製程式碼
- 狀態行:由HTTP協議版本號, 狀態碼, 狀態訊息 三部分組成。
- 響應頭:用來說明客戶端要使用的一些附加資訊,也是和上面的請求頭相對應,以鍵值對的方式。
- 空行:和請求的空行一樣,這裡的空行也是必須的,不管後面的正文是否為空。
- 響應正文:伺服器返回給客戶端的文字資訊(二進位制形式)
技術選型
大概瞭解了Http,我們就得選擇一種具體的方式或者說一個比較底層的api來作為實現網路訪問的實際參與者。大概有三種選擇——Socket、HttpClient和HttpUrlConnection。如果是基於Socket那麼我們需要實現的內容比較多,當然目前的OkHttp是採用這種方式來的,畢竟socket進行操作自由度比較高,如內部socket連線池的分配,長連線短連線等都可以控制,自由度較高。而HttpClient在Android6.0以後官方已經移除了這個api,而HttpUrlConnection則是一個比較好的選擇,足夠輕量級,又實現了一些基本需求,因此以HttpUrlConnection作為實際網路請求的參與者。
構建方式
因為覺得Okhttp的構建方式很優雅,這裡我們的構建方式就以OkHttp的方式進行構建,根據上面的Http協議的分析,結合OkHttp的構建方式,對物件的抽象其實也就一目瞭然了。必然是支援同步和非同步的方式發起請求,所以我們的構建方式基本如下:
FormBody body = new FormBody.Builder()
.add("username", "浩哥")
.add("pwd", "abc")
.build();
Request request = new Request.Builder()
.url("http://192.168.31.34:8080/API/upkeep")
.post(body)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onResponse(Response response) {
if (response.code() == 200) {
String msg = response.body().string();
Logger.e("response msg = " + msg);
}
}
@Override
public void onFail(Request request, IOException e) {
e.printStackTrace();
}
});
複製程式碼
主要抽象出的物件包括:Request、RequestBody、Response、ResponseBody、 Call、CallBack等。
Request請求的構建
請求怎麼構建呢?結合上面對http協議的分析,請求包括起始行、請求頭、空行和請求正文。因為基於HttpUrlConnection,所以起始行和空行可以不用考慮,請求頭需要一個臨時的暫存空間,請求正文由於不同型別格式也不同,因此請求正文給一個抽象的基類。
RequestBody
requestBody主要負責對流的寫出和ContentType型別的構建,因為不同型別如表單和檔案的Content-Type內容是不一致的,伺服器那邊解析方式自然也是不一樣的。
public abstract class RequestBody {
/**
* body的型別
*
* @return
*/
abstract String contentType();
/**
* 將內容寫出去
*
* @param ous
*/
abstract void writeTo(OutputStream ous) throws IOException;
}
複製程式碼
Request
請求這塊儲存了url,和請求的方法型別,用ArrayMap來儲存請求頭,同時持有一個RequestBody的引用,均可以通過建造者模式構建進來。
public class Request {
final HttpMethod method;
final String url;
final Map<String, String> heads;
final RequestBody body;
public Request(Builder builder) {
this.method = builder.method;
this.url = builder.url;
this.heads = builder.heads;
this.body = builder.body;
}
public static final class Builder {
HttpMethod method;
String url;
Map<String, String> heads;
RequestBody body;
public Builder() {
this.method = HttpMethod.GET;
this.heads = new ArrayMap<>();
}
Builder(Request request) {
this.method = request.method;
this.url = request.url;
}
public Builder url(String url) {
this.url = url;
return this;
}
public Builder header(String name, String value) {
Util.checkMap(name, value);
heads.put(name, value);
return this;
}
public Builder get() {
method(HttpMethod.GET, null);
return this;
}
public Builder post(RequestBody body) {
method(HttpMethod.POST, body);
return this;
}
public Builder put(RequestBody body) {
method(HttpMethod.PUT, body);
return this;
}
public Builder delete(RequestBody body) {
method(HttpMethod.DELETE, body);
return this;
}
public Builder method(HttpMethod method, RequestBody body) {
Util.checkMethod(method, body);
this.method = method;
this.body = body;
return this;
}
public Request build() {
if (url == null) {
throw new IllegalStateException("訪問url不能為空");
}
if (body != null) {
if (!TextUtils.isEmpty(body.contentType())) {
heads.put("Content-Type", body.contentType());
}
}
heads.put("Connection", "Keep-Alive");
heads.put("Charset", "UTF-8");
return new Request(this);
}
}
public enum HttpMethod {
GET("GET"),
POST("POST"),
PUT("PUT"),
DELETE("DELETE");
public String methodValue = "";
HttpMethod(String methodValue) {
this.methodValue = methodValue;
}
public static boolean checkNeedBody(HttpMethod method) {
return POST.equals(method) || PUT.equals(method);
}
public static boolean checkNoBody(HttpMethod method) {
return GET.equals(method) || DELETE.equals(method);
}
}
}
複製程式碼
這樣整個請求塊也就構建完畢了。剩下的無非是對具體請求體的抽象的具體實現,我們再看看響應那邊怎麼實現的。
Response響應的構建
ResponseBody
響應體這塊主要儲存為位元組,可以轉換成String型別進行返回,不做更具體的解析,沒有直接提供流的原因是設計上回撥是在主執行緒中的,如果把流傳入有需要自己做非同步處理。
public class ResponseBody {
byte[] bytes;
public ResponseBody(byte[] bytes) {
this.bytes = bytes;
}
public byte[] bytes() {
return this.bytes;
}
public String string() {
try {
return new String(bytes(), "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return "";
}
}
複製程式碼
Response
response相對就比較簡單了,最關鍵的是服務端的返回碼和響應正文。
public class Response {
final ResponseBody body;
final String message;
final int code;
public Response(Builder builder) {
this.body = builder.body;
this.message = builder.message;
this.code = builder.code;
}
public ResponseBody body() {
return this.body;
}
public int code() {
return this.code;
}
public String message() {
return this.message;
}
static class Builder {
private ResponseBody body;
private String message;
private int code;
public Builder body(ResponseBody body) {
this.body = body;
return this;
}
public Builder message(String message) {
this.message = message;
return this;
}
public Builder code(int code) {
this.code = code;
return this;
}
public Response build() {
if (message == null) throw new NullPointerException("response message == null");
if (body == null) throw new NullPointerException("response body == null");
return new Response(this);
}
}
}
複製程式碼
這樣基本的請求和響應物件構建好了,中間需要向上面構建的方式進行呼叫,還需要引入Call和Callback作為請求的發起和回撥介面。
請求發起和結果回撥
Call
call支援同步和非同步方式的呼叫,同步直接返回Response,方法內部阻塞,非同步提供一個回撥介面回撥結果。
public interface Call {
/**
* 同步執行
*
* @return response
*/
Response execute();
/**
* 非同步執行
*
* @param callback 回撥介面
*/
void enqueue(Callback callback);
}
複製程式碼
Callback
Callback 作為回撥介面,提供成功和失敗的回撥,當訪問網路成功併成功拿到資料則進入成功的回撥,否則進入失敗的回撥。
public interface Callback {
/**
* 當成功拿到結果時返回
*
* @param response  返回結果
*/
void onResponse(Response response);
/**
* 當獲取結果失敗時
*
* @param request  請求
* @param e  Http請求過程中可能產生的異常
*/
void onFail(Request request, IOException e);
}
複製程式碼
還有一個關鍵的就是我們客戶端——CatHttp了。
CatHttpClient
CatHttpClient 主要配置了一些超時資訊之類的,主要是作為客戶端的抽象,作為Call(這一呼叫服務端連線動作的外部發起者)。
public class CatHttpClient {
private Config config;
public CatHttpClient(Builder builder) {
this.config = new Config(builder);
}
public Call newCall(Request request) {
return new HttpCall(config, request);
}
static class Config {
final int connTimeout;
final int readTimeout;
final int writeTimeout;
public Config(Builder builder) {
this.connTimeout = builder.connTimeout;
this.readTimeout = builder.connTimeout;
this.writeTimeout = builder.writeTimeout;
}
}
public static final class Builder {
private int connTimeout;
private int readTimeout;
private int writeTimeout;
public Builder() {
this.connTimeout = 10 * 1000;
this.readTimeout = 10 * 1000;
this.writeTimeout = 10 * 1000;
}
public Builder readTimeOut(int readTimeout) {
this.readTimeout = readTimeout;
return this;
}
public Builder connTimeOut(int connTimeout) {
this.connTimeout = connTimeout;
return this;
}
public Builder writeTimeOut(int writeTimeout) {
this.writeTimeout = writeTimeout;
return this;
}
public CatHttpClient build() {
return new CatHttpClient(this);
}
}
}
複製程式碼
這樣,請求和響應還有請求和回撥的介面都約定好了,關鍵的就在於任務的執行過程和任務的排程了,因為網路請求都是耗時的,所以必然需要非同步去處理網路請求才能最大的發揮框架的效能。我們需要構建一個具體的任務——Task。
Task的執行
HttpTask
可以看到,HttpTask實現了Runnable介面,內部實際訪問網路請求的操作交給了IRequestHandler來做,回撥交給了IResponseHandler來做,最終拿到了Response結果
public class HttpTask implements Runnable {
private HttpCall call;
private Callback callback;
private IRequestHandler requestHandler;
private IResponseHandler handler = IResponseHandler.RESPONSE_HANDLER;
public HttpTask(HttpCall call, Callback callback, IRequestHandler requestHandler) {
this.call = call;
this.callback = callback;
this.requestHandler = requestHandler;
}
@Override
public void run() {
try {
Response response = requestHandler.handlerRequest(call);
handler.handlerSuccess(callback, response);
} catch (IOException e) {
handler.handFail(callback, call.request, e);
e.printStackTrace();
}
}
}
複製程式碼
IRequestHandler
IRequestHandler是實際網路請求的發起者,因為是面向介面程式設計,外部不用管內部的實現細節,只要呼叫方法拿到結果就行了。
public interface IRequestHandler {
/**
* 處理請求
*
* @param call  一次請求發起
* @return 應答
* @throws IOException  網路連線或者其它異常
*/
Response handlerRequest(HttpCall call) throws IOException;
}
複製程式碼
IResponseHandler
看到這裡應該明白,這裡無非就是包裝了一層,實際內部是呼叫了handler的post(Runnable r)方法將結果回撥到主執行緒中,也就是Callback介面的回撥方法被我們切換到了主執行緒中執行。
public interface IResponseHandler {
/**
* 執行緒切換,http請求成功時的回撥
*
* @param callback  回撥介面
* @param response  返回結果
*/
void handlerSuccess(Callback callback, Response response);
/**
* 執行緒切換,http請求失敗時候的回撥
*
* @param callback  回撥介面
* @param request  請求
* @param e  可能產生的異常
*/
void handFail(Callback callback, Request request, IOException e);
IResponseHandler RESPONSE_HANDLER = new IResponseHandler() {
Handler HANDLER = new Handler(Looper.getMainLooper());
@Override
public void handlerSuccess(final Callback callback, final Response response) {
Runnable runnable = new Runnable() {
@Override
public void run() {
callback.onResponse(response);
}
};
execute(runnable);
}
@Override
public void handFail(final Callback callback, final Request request, final IOException e) {
Runnable runnable = new Runnable() {
@Override
public void run() {
callback.onFail(request, e);
}
};
execute(runnable);
}
/**
* 移除所有訊息
*/
public void removeAllMessage() {
HANDLER.removeCallbacksAndMessages(null);
}
/**
* 執行緒切換
* @param runnable
*/
private void execute(Runnable runnable) {
HANDLER.post(runnable);
}
};
}
複製程式碼
任務排程
可以看到,上面所有的內容,就差一點能夠全部連通,就在於任務的排程,也就是呼叫執行緒的執行,必然在Call實體類的enqueue和execute方法中通過任務排程來執行Runnable內部的邏輯的。
HttpThreadPool
可以看到,作為一個單例類,內部對外提供了同步執行和非同步執行task的介面,內部通過執行緒池來實現,採用生產者-消費者模式,所有客戶端提交的任務都會先進入到無界佇列BlockingQueue中,執行緒池滿的拒絕策略也是將當前無法被執行的任務放入BlockingQueue中,而在一開始就開了一個Runnable死迴圈從BlockingQueue中不斷取任務執行。
public class HttpThreadPool {
/**
* 執行緒核心數
*/
public static final int CORE_POOL_SIZE = Runtime.getRuntime().availableProcessors();
/**
* 最大存活時間
*/
public static final int LIVE_TIME = 10;
/**
* 單例物件
*/
private static volatile HttpThreadPool threadPool;
/**
* 無界佇列
*/
private BlockingQueue<Future<?>> queue = new LinkedBlockingQueue<>();
/**
* 執行緒池
*/
private ThreadPoolExecutor executor;
public static HttpThreadPool getInstance() {
if (threadPool == null) {
synchronized (HttpThreadPool.class) {
if (threadPool == null) {
threadPool = new HttpThreadPool();
}
}
}
return threadPool;
}
private HttpThreadPool() {
executor = new ThreadPoolExecutor(CORE_POOL_SIZE, CORE_POOL_SIZE+1, LIVE_TIME , TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(4), rejectHandler);
executor.execute(runnable);
}
/**
* 消費者
*/
Runnable runnable = new Runnable() {
@Override
public void run() {
while (true) {
FutureTask<?> task = null;
try {
task = (FutureTask<?>) queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (task != null) {
executor.execute(task);
}
}
}
};
/**
* 同步提交任務
*
* @param task  任務
* @return response物件
* @throws ExecutionException
* @throws InterruptedException
*/
public synchronized Response submit(Callable<Response> task) throws ExecutionException, InterruptedException {
if (task == null) throw new NullPointerException("task == null , 無法執行");
Future<Response> future = executor.submit(task);
return future.get();
}
/**
* 新增非同步任務
*
* @param task
*/
public void execute(FutureTask<?> task) {
if (task == null) throw new NullPointerException("task == null , 無法執行");
try {
queue.put(task);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 拒絕策略,如果當執行緒池中的阻塞佇列滿,則新增到link佇列中
*/
RejectedExecutionHandler rejectHandler = new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable runnable, ThreadPoolExecutor threadPoolExecutor) {
try {
queue.put(new FutureTask<>(runnable, null));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
}
複製程式碼
結語
可以看到,上面除了排程的HttpThreadPool類,其餘類基本都是抽象類或者介面,但是上面的這些介面和抽象類,相信看懂的童鞋應該明白,網路框架已經可以"執行"了。這裡的執行當然不是說能在編譯器或者具體的手機上執行,但是框架內部已經打通了任督二脈,可以完美的排程了。程式碼在github上——傳送門
剩下的具體的類和內容在這篇文章
手寫Android網路框架——CatHttp(二)