前端外刊评论 2018-01-23
手写Android网络框架——CatHttp(二)
在实际Android应用的开发中,网络请求往往是必不可少的。现在有很多优秀的开源网络框架如Volley、Okhttp和Retrofit等,说到框架,很多童鞋信手拈来,反手一个Okhttp+etrofit+RxJava全家桶。不就是网络请求么,so easy~
不过实际开发过程中,确实会出现各种各样的问题,比如你上传一张图片,服务器那边接收不到,怎么办呢?你看了下自己这边,完全按照标准api来写的,讲道理应该没错吧?这时候打开debug,可是框架内的代码怎么跟进?没看过也不懂啊,所以可能有些童鞋会去阅读源码,可是源码这种东西,不熟悉的话读起来晦涩难懂,当然边读边做源码分析写下几篇博客也是不错的选择。
不过其实最本质的,就是对你框架的业务熟悉,比如网络请求框架,你就必须熟悉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响应也由四个部分组成,分别是:状态行、响应头、空行和响应正文。这里也以一段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,我们就得选择一种具体的方式或者说一个比较底层的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等。
请求怎么构建呢?结合上面对http协议的分析,请求包括起始行、请求头、空行和请求正文。因为基于HttpUrlConnection,所以起始行和空行可以不用考虑,请求头需要一个临时的暂存空间,请求正文由于不同类型格式也不同,因此请求正文给一个抽象的基类。
requestBody主要负责对流的写出和ContentType类型的构建,因为不同类型如表单和文件的Content-Type内容是不一致的,服务器那边解析方式自然也是不一样的。
public abstract class RequestBody {
    /**
     * body的类型
     *
     * @return
     */
    abstract String contentType();
    /**
     * 将内容写出去
     *
     * @param ous
     */
    abstract void writeTo(OutputStream ous) throws IOException;
}请求这块存储了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);
        }
    }
}这样整个请求块也就构建完毕了。剩下的无非是对具体请求体的抽象的具体实现,我们再看看响应那边怎么实现的。
响应体这块主要存储为字节,可以转换成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相对就比较简单了,最关键的是服务端的返回码和响应正文。
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支持同步和异步方式的调用,同步直接返回Response,方法内部阻塞,异步提供一个回调接口回调结果。
public interface Call {
    /**
     * 同步执行
     *
     * @return response
     */
    Response execute();
    /**
     * 异步执行
     *
     * @param callback 回调接口
     */
    void enqueue(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 主要配置了一些超时信息之类的,主要是作为客户端的抽象,作为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。
可以看到,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是实际网络请求的发起者,因为是面向接口编程,外部不用管内部的实现细节,只要调用方法拿到结果就行了。
public interface IRequestHandler {
    /**
     * 处理请求
     *
     * @param call  一次请求发起
     * @return 应答
     * @throws IOException  网络连接或者其它异常
     */
    Response handlerRequest(HttpCall call) throws IOException;
}看到这里应该明白,这里无非就是包装了一层,实际内部是调用了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内部的逻辑的。
可以看到,作为一个单例类,内部对外提供了同步执行和异步执行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(二)