手寫Android網路框架——CatHttp(一)

panhaos發表於2018-01-23

前言

手寫Android網路框架——CatHttp(二)

在實際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();
            }
        });

複製程式碼

主要抽象出的物件包括:RequestRequestBodyResponseResponseBodyCallCallBack等。

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 &emsp;返回結果
     */
    void onResponse(Response response);


    /**
     * 當獲取結果失敗時
     *
     * @param request &emsp;請求
     * @param e       &emsp;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 &emsp;一次請求發起
     * @return 應答
     * @throws IOException &emsp;網路連線或者其它異常
     */
    Response handlerRequest(HttpCall call) throws IOException;

}
複製程式碼

IResponseHandler

看到這裡應該明白,這裡無非就是包裝了一層,實際內部是呼叫了handler的post(Runnable r)方法將結果回撥到主執行緒中,也就是Callback介面的回撥方法被我們切換到了主執行緒中執行。

public interface IResponseHandler {

    /**
     * 執行緒切換,http請求成功時的回撥
     *
     * @param callback &emsp;回撥介面
     * @param response &emsp;返回結果
     */
    void handlerSuccess(Callback callback, Response response);

    /**
     * 執行緒切換,http請求失敗時候的回撥
     *
     * @param callback &emsp;回撥介面
     * @param request  &emsp;請求
     * @param e        &emsp;可能產生的異常
     */
    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 &emsp;任務
     * @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(二)

這裡寫圖片描述

相關文章