手写Android网络框架——CatHttp(一)
前言
手写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(); } });
主要抽象出的对象包括: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(二)