手写Android网络框架——CatHttp(二)

807 阅读6分钟

前言

上一篇文章已经对http协议和整体框架做了一个大致的介绍: 手写Android网络框架——CatHttp(一)

这篇文章我们主要就分析下具体子类是如何实现,以什么方式构建成可以被服务器识别并接受的数据类型提交上去的。所以这篇文章我们主要讨论正文的数据类型和格式。

这里写图片描述

Http正文

并不是每种请求方式都能携带正文,如post和put可以携带正文,而get和delete不能携带正文,有参数的话直接拼接在url后面。而不同的正文类型对应的一个请求头(Content-Type)是不同的,Http协议根据这个请求头按照对应的类型去解析正文。

正文类型

表单

如果正文传入的是表单,那么请求头声明的ContentType为:

application/x-www-form-urlencoded; charset=UTF-8

表单组织的结构格式为:

username=zhangsan&pwd=12345&date=2015

也就是当两者均满足该要求时,服务器可以正常解析表单里的数据。

文件/二进制

在最初的 http 协议中,没有上传文件方面的功能。 rfc1867为 http 协议添加了这个功能。

如果想传输文件或者一组二进制字节,则请求头声明的Content-Type声明为:

"multipart/from-data; boundary=" + boundary;

其中boundary是一个随机数,在下面的正文格式中会使用该boundary作为标识:

--AaB03x
Content-Disposition: form-data; name="field1"

Joe Blow
--AaB03x
Content-Disposition: form-data; name="pics"; filename="file1.txt"
Content-Type: text/plain

 ... contents of file1.txt ...
--AaB03x--

可以看到,其实Multipart的方式也同时支持传输键值对,只是构造的方式不一样,每一个(部分) ,姑且称为部分,都是以--boundary开始的,并且后面跟着类似请求头的键值对,用来说明数据类型,每部分的数据正文跟这种“请求头”分隔开。

了解了不同正文的格式,我们就可以实现我们的子类了。

这里写图片描述

针对于文件这种格式会比较复杂,但是观察会有一个共同点,也就是我们说的“部分”,这里用Part表示,下面先看具体的http任务是怎么执行的

Http任务的执行

HttpCall

可以看到,实际的Call实际不管是同步还是异步,都是调用了HttpThreadPool提供的结构来执行Task,从而调度任务的执行的。

public class HttpCall implements Call {

    final Request request;

    final CatHttpClient.Config config;

    private IRequestHandler requestHandler = new RequestHandler();


    public HttpCall(CatHttpClient.Config config, Request request) {
        this.config = config;
        this.request = request;
    }


    @Override
    public Response execute() {
        Callable<Response> task = new SyncTask();
        Response response;
        try {
            response = HttpThreadPool.getInstance().submit(task);
            return response;
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return new Response.Builder()
                .code(400)
                .message("线程异常中断")
                .body(new ResponseBody(null))
                .build();
    }

    @Override
    public void enqueue(Callback callback) {
        Runnable runnable = new HttpTask(this, callback, requestHandler);
        HttpThreadPool.getInstance().execute(new FutureTask<>(runnable, null));
    }

    /**
     * 同步提交Callable
     */
    class SyncTask implements Callable<Response> {
        @Override
        public Response call() throws Exception {
            Response response = requestHandler.handlerRequest(HttpCall.this);
            return response;
        }
    }
}

RequestHandler

RequestHandler 是网络请求的处理者,将请求的Request解析成标准的Http格式的请求提交给服务器并获取服务器返回的内容。

public class RequestHandler implements IRequestHandler {

    @Override
    public Response handlerRequest(HttpCall call) throws IOException {

        HttpURLConnection connection = mangeConfig(call);

        if (!call.request.heads.isEmpty()) addHeaders(connection, call.request);

        if (call.request.body != null) writeContent(connection, call.request.body);

        if (!connection.getDoOutput()) connection.connect();

        //解析返回内容
        int responseCode = connection.getResponseCode();
        if (responseCode >= 200 && responseCode < 400) {
            byte[] bytes = new byte[1024];
            int len;
            InputStream ins = connection.getInputStream();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            while ((len = ins.read(bytes)) != -1) {
                baos.write(bytes, 0, len);
            }
            Response response = new Response
                    .Builder()
                    .code(responseCode)
                    .message(connection.getResponseMessage())
                    .body(new ResponseBody(baos.toByteArray()))
                    .build();
            try {
                ins.close();
                connection.disconnect();
            } finally {
                if (ins != null) ins.close();
                if (connection != null) connection.disconnect();
            }
            return response;
        }
        throw new IOException(String.valueOf(connection.getResponseCode()));
    }

    /**
     * 用
     *
     * @param connection
     * @param body
     * @throws IOException
     */
    private void writeContent(HttpURLConnection connection, RequestBody body) throws IOException {
        OutputStream ous = connection.getOutputStream();
        body.writeTo(ous);
    }

    /**
     * HttpUrlConnection基本参数的配置
     *
     * @param call
     * @return
     * @throws IOException
     */
    private HttpURLConnection mangeConfig(HttpCall call) throws IOException {
        URL url = new URL(call.request.url);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setConnectTimeout(call.config.connTimeout);
        connection.setReadTimeout(call.config.readTimeout);
        connection.setDoInput(true);
        if (call.request.body != null && Request.HttpMethod.checkNeedBody(call.request.method)) {
            connection.setDoOutput(true);
        }
        return connection;
    }

    /**
     * 给对象添加请求头
     *
     * @param connection
     * @param request
     */
    private void addHeaders(HttpURLConnection connection, Request request) {
        Set<String> keys = request.heads.keySet();
        for (String key : keys) {
            connection.addRequestProperty(key, request.heads.get(key));
        }
    }
}

工具类Util

public class Util {

    public static void checkMap(String key, String value) {
        if (key == null) throw new NullPointerException("key == null");
        if (key.isEmpty()) throw new NullPointerException("key is empty");
        if (value == null) throw new NullPointerException("value == null");
        if (value.isEmpty()) throw new NullPointerException("value is empty");
    }

    public static void checkMethod(Request.HttpMethod method, RequestBody body) {
        if (method == null)
            throw new NullPointerException("method == null");
        if (body != null && Request.HttpMethod.checkNoBody(method))
            throw new IllegalStateException("方法" + method + "不能有请求体");
        if (body == null && Request.HttpMethod.checkNeedBody(method))
            throw new IllegalStateException("方法" + method + "必须有请求体");
    }


    /**
     * 转换成file的头
     *
     * @param key
     * @param fileName
     * @return
     */
    public static String trans2FileHead(String key, String fileName) {
        StringBuilder sb = new StringBuilder();
        sb.append(MultipartBody.disposition)
                .append("name=")//name=

                .append("\"").append(key).append("\"").append(";").append(" ")//"key";

                .append("filename=")//filename

                .append("\"").append(fileName).append("\"")//"filename"

                .append("\r\n");

        return sb.toString();
    }

    /**
     * 转换成表单形式
     *
     * @param key
     * @return
     */
    public static String trans2FormHead(String key) {
        StringBuilder sb = new StringBuilder();
        sb.append(MultipartBody.disposition)
                .append("name=")//name=

                .append("\"").append(key).append("\"") //"key"

                .append("\r\n");//next line

        return sb.toString();
    }

    public static byte[] getUTF8Bytes(String str) throws UnsupportedEncodingException {
        return str.getBytes("UTF-8");
    }


}

RequestBody正文

FormBody

既然表单这种格式比较简单,我们就先构建表单,可以看到,我们可以通过建造者的方式可以直接传入键值对或者map,内部用一个ArrayMap来存储键值对,写出的时候将map里的键值对按照表单的方式构建好再写出去。

public class FormBody extends RequestBody {

    // 限制参数不要过多(ArrayMap效率,而且很少需要破k的参数)
    public static final int MAX_FROM = 1000;

    final Map<String, String> map;

    public FormBody(Builder builder) {
        this.map = builder.map;
    }

    @Override
    public String contentType() {
        return "application/x-www-form-urlencoded; charset=UTF-8";
    }

    @Override
    public void writeTo(OutputStream ous) throws IOException {
        try {
            ous.write(transToString(map).getBytes("UTF-8"));
            ous.flush();
        } finally {
            if (ous != null) {
                ous.close();
            }
        }
    }

    /**
     * 拼接请求参数
     *
     * @param map
     * @return
     */
    private String transToString(Map<String, String> map) throws UnsupportedEncodingException {
        StringBuilder sb = new StringBuilder();
        Set<String> keys = map.keySet();
        for (String key : keys) {
            if (!TextUtils.isEmpty(sb)) {
                sb.append("&");
            }
            sb.append(URLEncoder.encode(key, "UTF-8"));
            sb.append("=");
            sb.append(URLEncoder.encode(map.get(key), "UTF-8"));
        }
        return sb.toString();
    }


    public static class Builder {
        private Map<String, String> map;

        public Builder() {
            map = new ArrayMap<>();
        }

        public Builder add(String key, String value) {
            if (map.size() > MAX_FROM) throw new IndexOutOfBoundsException(" 请求参数过多");
            Util.checkMap(key, value);
            map.put(key, value);
            return this;
        }

        public Builder map(Map<String, String> map) {
            if (map.size() > MAX_FROM) throw new IndexOutOfBoundsException(" 请求参数过多");
            this.map = map;
            return this;
        }

        public FormBody build() {
            return new FormBody(this);
        }

    }
}

Part

Part作为一个抽象类提供了两个静态方法用来创建不同的对象,一种是键值对,另一种是文件。其中写出正文的方法按照标准格式来就行了。

public abstract class Part {

    private Part() {
    }

    public abstract String contentType();

    public abstract String heads();

    public abstract void write(OutputStream ous) throws IOException;


    /**
     * 创建构建form的part
     *
     * @param key
     * @param value
     * @return
     */
    public static Part create(final String key, final String value) {

        return new Part() {
            @Override
            public String contentType() {
                return null;
            }

            @Override
            public String heads() {
                return Util.trans2FormHead(key);
            }

            @Override
            public void write(OutputStream ous) throws IOException {
                ous.write(heads().getBytes("UTF-8"));
                ous.write(END_LINE);
                ous.write(value.getBytes("UTF-8"));
                ous.write(END_LINE);
            }
        };
    }


    public static Part create(final String type, final String key, final File file) {
        if (file == null) throw new NullPointerException("file 为空");
        if (!file.exists()) throw new IllegalStateException("file 不存在");

        return new Part() {
            @Override
            public String contentType() {
                return type;
            }

            @Override
            public String heads() {
                return Util.trans2FileHead(key, file.getName());
            }

            @Override
            public void write(OutputStream ous) throws IOException {
                ous.write(heads().getBytes());
                ous.write("Content-Type: ".getBytes());
                ous.write(Util.getUTF8Bytes(contentType()));
                ous.write(END_LINE);
                ous.write(END_LINE);
                writeFile(ous, file);
                ous.write(END_LINE);
                ous.flush();
            }

            /**
             * 写出文件
             * @param ous&emsp;输出流
             * @param file&emsp;文件
             */
            private void writeFile(OutputStream ous, File file) throws IOException {
                FileInputStream ins = null;
                try {
                    ins = new FileInputStream(file);
                    int len;
                    byte[] bytes = new byte[2048];
                    while ((len = ins.read(bytes)) != -1) {
                        ous.write(bytes, 0, len);
                    }
                } finally {
                    if (ins != null) {
                        ins.close();
                    }
                }
            }
        };

    }
}

MultipartBody

MultipartBody 存储了一组Part对象,对外提供了两个接口——传入键值对和传入文件,同时按照上面Multipart的格式写出body存储的所有内容。

public class MultipartBody extends RequestBody {

    public static final String disposition = "content-disposition: form-data; ";
    public static final byte[] END_LINE = {'\r', '\n'};
    public static final byte[] PREFIX = {'-', '-'};

    final List<Part> parts;
    final String boundary;

    public MultipartBody(Builder builder) {
        this.parts = builder.parts;
        this.boundary = builder.boundary;
    }

    @Override
    public String contentType() {
        return "multipart/from-data; boundary=" + boundary;
    }

    @Override
    public void writeTo(OutputStream ous) throws IOException {
        try {
            for (Part part : parts) {
                ous.write(PREFIX);
                ous.write(boundary.getBytes("UTF-8"));
                ous.write(END_LINE);
                part.write(ous);
            }
            ous.write(PREFIX);
            ous.write(boundary.getBytes("UTF-8"));
            ous.write(PREFIX);
            ous.write(END_LINE);
            ous.flush();
        } finally {
            if (ous != null) {
                ous.close();
            }
        }
    }

    public static class Builder {

        private String boundary;
        private List<Part> parts;

        public Builder() {
            this(UUID.randomUUID().toString());
        }

        private Builder(String boundary) {
            this.parts = new ArrayList<>();
            this.boundary = boundary;
        }

        public Builder addPart(String type, String key, File file) {
            if (key == null) throw new NullPointerException("part name == null");
            parts.add(Part.create(type, key, file));
            return this;
        }

        public Builder addForm(String key, String value) {
            if (key == null) throw new NullPointerException("part name == null");
            parts.add(Part.create(key, value));
            return this;
        }

        public MultipartBody build() {
            if (parts.isEmpty()) throw new NullPointerException("part list == null");
            return new MultipartBody(this);
        }
    }
}

结语

具体实现类基本如上,这两篇就是CatHttp的全部内容了,源码已经放在github上——传送门,如果有什么不足之处,欢迎大家指正,如果觉得我写的还不错,就关注我吧~

这里写图片描述