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

前言

手寫Android網絡框架——CatHttp(二)html

在實際Android應用的開發中,網絡請求每每是必不可少的。如今有不少優秀的開源網絡框架如Volley、Okhttp和Retrofit等,說到框架,不少童鞋信手拈來,反手一個Okhttp+etrofit+RxJava全家桶。不就是網絡請求麼,so easy~git

不過實際開發過程當中,確實會出現各類各樣的問題,好比你上傳一張圖片,服務器那邊接收不到,怎麼辦呢?你看了下本身這邊,徹底按照標準api來寫的,講道理應該沒錯吧?這時候打開debug,但是框架內的代碼怎麼跟進?沒看過也不懂啊,因此可能有些童鞋會去閱讀源碼,但是源碼這種東西,不熟悉的話讀起來晦澀難懂,固然邊讀邊作源碼分析寫下幾篇博客也是不錯的選擇。github

不過其實最本質的,就是對你框架的業務熟悉,好比網絡請求框架,你就必須熟悉Http協議,才能夠了解你的表單是怎麼封裝成數據,以什麼結果表示,怎麼發出去,接收到的內容又是什麼?瞭解了這些,咱們徹底能夠參考優秀的源碼,本身動手去實現一個簡易版的。編程

這裏寫圖片描述

Http協議

談到網絡框架,就不得不說到http協議了,網絡框架必須嚴格按照http協議才能保證客戶端和服務器雙方數據的正常傳輸。api

Http請求

一個http請求主要包含如下幾個部分:請求行(request line)、請求頭(header)、空行請求正文四個部分。bash

以一個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做爲實際網絡請求的參與者。app

構建方式

由於以爲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(二)

這裏寫圖片描述
相關文章
相關標籤/搜索