preface

The last article provided an overview of the HTTP protocol and the overall framework: The Handwritten Android Web Framework — CatHttp (1)

In this article, we will analyze how specific subclasses are implemented and how they are constructed to submit data types that can be recognized and accepted by the server. So in this article we will focus on the data types and formats of the text.

The Http body

Not all request methods can carry the body. For example, POST and PUT can carry the body. Get and DELETE cannot carry the body. Different body types correspond to different content-Types. The Http protocol parses the body according to the content-Type of the request header.

The body type

The form

If the body is passing in a form, the request header declares ContentType as follows:

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

The form organization structure format is:

username=zhangsan&pwd=12345&date=2015

When both requirements are met, the server can parse the data in the form.

File/binary

In the original HTTP protocol, there was no function for uploading files. Rfc1867 adds this functionality to the HTTP protocol.

If you want to transfer a file or a set of binary bytes, the content-Type of the request header declaration is:

“multipart/from-data; boundary=” + boundary;

Where boundary is a random number, which will be used as an identifier in the following text format:

--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--
Copy the code

As you can see, in fact Multipart way also support the transmission of key-value pairs at the same time, just a way of structure is different, each (part), can call part, are – a boundary, and followed by a similar request header key-value pairs, used to indicate the data type, data each part of the body with this “request”.

Knowing the format of the different bodies, we can implement our subclasses.

It’s a bit more complicated for files, but there’s one thing that observations have in common, and that’s what we call “parts,” which is represented by parts. Now let’s look at how specific HTTP tasks are performed. Okay

Http task execution

HttpCall

As you can see, the actual Call, whether synchronous or asynchronous, calls the structure provided by HttpThreadPool to execute the Task, thus scheduling the execution of the 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("Thread abnormal interrupt") .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)); } /** * synchronously submit Callable */ class SyncTask implements Callable<Response> {@override public Response Call () throws Exception  { Response response = requestHandler.handlerRequest(HttpCall.this);returnresponse; }}}Copy the code

RequestHandler

A RequestHandler is a network Request handler that parses the requested Request into a standard Http Request and submits it to the server to retrieve the content returned by the server.

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(); / / parsing return content 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(); }returnresponse; } 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 basic parameter configuration ** @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);
        }
        returnconnection; } private void addHeaders(HttpURLConnection connection, HttpURLConnection) Request request) { Set<String> keys = request.heads.keySet();for(String key : keys) { connection.addRequestProperty(key, request.heads.get(key)); }}}Copy the code

Utility class 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" + method + "No request body.");
        if (body == null && Request.HttpMethod.checkNeedBody(method))
            throw new IllegalStateException("Method" + method + "There must be a request body"); } /** * convert to the header of 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");

        returnsb.toString(); } /** * convert to form ** @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 linereturn sb.toString();
    }

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

RequestBody body

FormBody

Since the form format is relatively simple, let’s build the form first, and as you can see, we can pass in the key-value pairs or the map directly by the builder, use an ArrayMap to store the key-value pairs internally, and then write out the key-value pairs in the map as a form.

Public class FormBody extends RequestBody {public static final int MAX_FROM = 1000;  final Map<String, String> map; public FormBody(Builder builder) { this.map = builder.map; } @Override public StringcontentType() {
        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(); }}} /** * Concatenate request parameters ** @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("Too many request parameters");
            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("Too many request parameters");
            this.map = map;
            return this;
        }

        public FormBody build() {
            returnnew FormBody(this); }}}Copy the code

Part

Part, as an abstract class, provides two static methods for creating different objects, one for key-value pairs and the other for files. Among them, the method of writing the text comes in accordance with the standard format.

public abstract class Part {

    private Part() { } public abstract String contentType(); public abstract String heads(); public abstract void write(OutputStream ous) throws IOException; /** * Create the part that builds the form ** @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 Stringtype, final String key, final File file) {
        if (file == null) throw new NullPointerException("The file is empty");
        if(! file.exists()) throw new IllegalStateException("File does not exist");

        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(); } /** * write out the file * @paramous&emsp; Output streams * @param file&emsp; File */ 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(); }}}}; }}Copy the code

MultipartBody

MultipartBody stores a set of Part objects and provides two interfaces — passing in key-value pairs and passing in files — while writing out the contents of the body store in the same format as Multipart.

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 = {The '-'.The '-'};

    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; publicBuilder() {
            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");
            returnnew MultipartBody(this); }}}Copy the code

conclusion

This is CatHttp. The source code is now available on Github – Portal. If there are any shortcomings, please feel free to correct them