一起写一个最简单的Volley框架

一起写一个最简单的Volley框架

源码地址github

Request

首先定义请求类( Request 类 )

Request 类需要的属性有如下几个:

  • url
    请求的URL
  • method
    HTTP 请求的方法, GET、POST、HEAD或者是其他
  • 请求头
    HTTP 请求头,比如 Content-Type,Accept 等
  • 请求参数
    对于 POST 方法,想要往服务端传入请求的参数
  • 回调方法
    包括请求成功、失败后的回调方法

为什么用泛型 T 呢?因为对于网络请求来说,用户得到的请求结果格式是不确定的,可能是 JSON, 也可能是XML, String 等。但是 HTTP 的响应实体全部都是二进制流,因此,我们要在请求的基类中预留解析方法 abstract void parseResponse2(Response<T> response);, 该方法可以将 HTTP 响应实体中的二进制数据,解析成用户需要的具体类型。因此我们可以把Request作为泛型类,它的泛型类型就是它的返回数据类型,返回的类型是字符串,那么我们就用Request<String>

package com.fan.simplevolley.volley;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;

/**
 * @Description: 请求的基类封装
 * @Author: fan
 * @Date: 2020/3/5 23:34
 * @Modify:
 */
public abstract class Request<T> implements Comparable<Request<T>> {

    /**
     * 请求的URL
     */
    public String url;

    /**
     * 请求的方法
     */
    private Method method;

    /**
     * 默认的参数编码
     */
    private String DEFAULT_PARAMS_ENCODING = "UTF-8";

    /**
     * 请求的序列号,用于在请求队列中排序
     */
    private int sequenceNumber;

    /**
     * 请求的优先级,用于在请求队列中排序
     */
    private Priority priority;
    

    /**
     * 请求头
     */
    Map<String, String> headers = new HashMap<>();

    /**
     * 请求的参数
     */
    Map<String, String> params = new HashMap<>();

    /**
     * 请求结果回调
     */
    Response.RequestListener<T> listener;


    /**
     * constructor
     *
     * @param method
     * @param url
     * @param listener
     */
    public Request(Method method, String url, Response.RequestListener<T> listener) {
        this.method = method;
        this.url = url;
        this.listener = listener;
    }

    public Map<String, String> getHeaders() {
        return headers;
    }

    public Map<String, String> getParams() {
        return params;
    }

    public Method getHttpMethod() {
        return method;
    }

    protected String getParamsEncoding() {
        return DEFAULT_PARAMS_ENCODING;
    }

    public int getSequenceNumber() {
        return sequenceNumber;
    }

    public Priority getPriority() {
        return priority;
    }

    /**
     * 指定请求时的Content-Type
     *
     * @return
     */
    public String getBodyContentType() {
        return "application/x-www-form-urlencoded; charset=" + getParamsEncoding();
    }


    /**
     * 如果是GET请求, 参数拼接在URL后面, 并且没有请求实体
     * 如果是POST请求, 请求参数是放在请求实体里面
     * <p>
     * 该方法用于获取指定的请求参数, 并将这些参数按照指定格式编码, 生成字节数组。  对于POST 和 PUT请求, 该字节数组的内容将作为请求实体发送
     *
     * @return
     */
    public byte[] getBody() {
        // 这里会调用getParams()方法, 获取指定的参数
        // 多态, 向 RequestQueue 中 add 了Request的子类, 如果子类重写了该方法, 就调用子类的该方法
        Map<String, String> params = getParams();
        if (params != null && params.size() > 0) {
            return encodeParameters(params, getParamsEncoding());
        }
        return null;
    }

    /**
     * 将参数转换为Url编码的参数串, 也就是 key=value&key2=value2的形式, 但是要注意用URLEncoder编码一下
     * 如果请求以这种形式作为请求实体进行请求, 需要将 Content-Type 指定为 application/x-www-form-urlencoded
     */
    private byte[] encodeParameters(Map<String, String> params, String paramsEncoding) {
        StringBuilder encodedParams = new StringBuilder();
        try {
            for (Map.Entry<String, String> entry : params.entrySet()) {
                encodedParams.append(URLEncoder.encode(entry.getKey(), paramsEncoding));
                encodedParams.append('=');
                encodedParams.append(URLEncoder.encode(entry.getValue(), paramsEncoding));
                encodedParams.append('&');
            }
            return encodedParams.toString().getBytes(paramsEncoding);
        } catch (UnsupportedEncodingException uee) {
            throw new RuntimeException("Encoding not supported: " + paramsEncoding, uee);
        }
    }

    /**
     * 将 response 解析成需要的数据对象
     *
     * @param response
     * @return
     */
    abstract T parseResponse(Response<T> response);

    abstract void parseResponse2(Response<T> response);

    /**
     * 将解析后的数据对象投递到 UI 线程
     *
     * @param response
     */
    abstract void deliverResponse(T response);

    /**
     * 投递错误
     *
     * @param error
     */
    void deliverError(VolleyError error) {
        if (listener != null) {
            listener.onError(error);
        }
    }

    @Override
    public int compareTo(Request<T> another) {
        Priority myPriority = this.getPriority();
        Priority anotherPriority = another.getPriority();
        // 如果优先级相等,那么按照添加到队列的序列号顺序来执行
        return myPriority.equals(another) ? this.getSequenceNumber() - another.getSequenceNumber()
                : myPriority.ordinal() - anotherPriority.ordinal();
    }


    /**
     * 请求的方法枚举
     */
    public enum Method {
        GET("GET"),
        POST("POST");

        private String method = "";

        private Method(String method) {
            this.method = method;
        }

        @Override
        public String toString() {
            return method;
        }
    }

    /**
     * 请求的优先级枚举
     */
    public enum Priority {
        LOW,
        NORMAL,
        HIGH
    }

}

Response

根据 HTTP 协议,HTTP 响应报文结构如下:

HTTP 响应报文

org.apache.http 包下有个 BasicHttpResponse 类。这个类中帮我们维护了 HTTP 响应报文中的 ①报文协议及版本 ②状态码及状态描述 的相关内容。因此我们直接继承自它,再将 HTTP 响应报文中的③响应头④响应实体维护进去就可以了。因此我们的 Response 类这样声明 public class Response<T> extends BasicHttpResponse {}, 并在里面添加了如下两个变量:

  1. byte[] rawData; 响应实体中的原始二进制数据

  2. public T result; 将响应实体解析后的对象

tip1: 简单起见,响应头的处理我们给暂时忽略了。

tip2: Response 类为啥也做成泛型的呢?

因为每个请求都对应一个 Response,但这里的问题是这个 Response 的数据格式我们是不知道的。这个数据格式应该是跟 Request的 泛型 T 一致,因此我们将 Response 也写成泛型的。

package com.fan.simplevolley.volley;

import org.apache.http.ProtocolVersion;
import org.apache.http.StatusLine;
import org.apache.http.message.BasicHttpResponse;

/**
 * @Description: 响应的封装类
 * @Author: fan
 * @Date: 2020/3/5 23:34
 * @Modify:
 */
public class Response<T> extends BasicHttpResponse {

    /**
     * 响应实体数据
     */
    byte[] rawData;

    /**
     * 将响应实体解析后的对象
     */
    public T result;

    public Response(StatusLine statusline) {
        super(statusline);
    }

    public Response(ProtocolVersion ver, int code, String reason) {
        super(ver, code, reason);
    }

    public Response<T> success(T parsed) {
        result = parsed;
        return this;
    }

    public void setData(byte[] rowData) {
        this.rawData = rowData;
    }

    public void setResult(T result) {
        this.result = result;
    }

    public interface RequestListener<T> {

        void onSuccess(T result);

        void onError(VolleyError error);

    }
}

现在,有了 请求的基类 Request 和 响应的类 Response, 就可以开始具体工作流程了。

大体流程如下:

  1. 创建一个 Request 对象, 指定要请求所需的各种数据, url, 方法, 回调方法等。
  2. 将这个 Request 对象 add 到队列中。
  3. 由网络处理线程进行网络请求的处理, 并拿到数据返回。
  4. 交给用户再处理(比如更新UI)。

我们一起看看是如何完成的。

Request 有了,我们来看请求队列。

RequestQueue

我们在 RequestQueue 这个类中维护了一个阻塞队列,PriorityBlockingQueue<Request<?>> blockingQueue = new PriorityBlockingQueue<>();, 用于存放想要执行的请求。

在我们需要请求的时候, 就把构建好的 Request 对象丢到这个队列中。

然后网络处理线程会不断的从这个请求队列中取出 Request 对象,并去进行真正的网络请求。

package com.fan.simplevolley.volley;

import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
/**
 * @Description: 请求队列封装
 * @Author: fan
 * @Date: 2020/3/5 23:34
 * @Modify:
 */
public class RequestQueue {

    /**
     * 阻塞的请求队列
     */
    PriorityBlockingQueue<Request<?>> blockingQueue = new PriorityBlockingQueue<>();

    /**
     * 每一个请求的序列号生成器
     */
    AtomicInteger sequenceNumberGenerator = new AtomicInteger();

    /**
     * 默认执行网络请求的线程数
     */
    public static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;

    /**
     * 自己维护的网络请求线程
     */
    NetworkDispatcher[] networkDispatchers;

    /**
     * 真正执行网络请求的东东
     */
    HttpStack httpStack;

    /**
     * constructor
     *
     * @param threadPoolSize
     * @param httpStack
     */
    public RequestQueue(int threadPoolSize, HttpStack httpStack) {
        networkDispatchers = new NetworkDispatcher[DEFAULT_NETWORK_THREAD_POOL_SIZE];
        this.httpStack = httpStack != null ? httpStack : new UrlHttpStack();
    }


    /**
     * add的时候还要处理优先级等相关内容。这里没考虑。
     */
    public void add(Request<?> request) {
        if (!blockingQueue.contains(request)) {
            blockingQueue.add(request);
        } else {

            System.out.println("请求已经在队列中, 请不要重复add");
        }
    }

    public void start() {

        stop();

        for (int i = 0; i < DEFAULT_NETWORK_THREAD_POOL_SIZE; i++) {
            networkDispatchers[i] = new NetworkDispatcher(blockingQueue, httpStack);
            networkDispatchers[i].start();
        }

    }

    public void stop() {

        if (networkDispatchers!= null)
        for (int i = 0; i < networkDispatchers.length; i++) {
            if (networkDispatchers[i] != null) {
                networkDispatchers[i].quit();
            }
        }
    }
}

下面来看网络处理线程。

NetworkDispatcher

它的本质是一个 Thread,在该工作线程中(死循环),不断地从队列中取出 Request 对象并用 HttpStack去真正执行请求。

package com.fan.simplevolley.volley;

import java.util.concurrent.BlockingQueue;

/**
 * @Description: 网络请求线程, 其主要工作是:
 * 1. 从请求队列中取出Request(需要持有RequestQueue 中的 blockingQueue 的引用)
 * 2. 使用HttpStack执行网络请求并拿到响应结果 (需要持有HttpStack的引用)
 * 3. 将响应结果投递要UI线程进行处理 (需要一个投递者Delivery)
 * @Author: fan
 * @Date: 2020/3/5 23:34
 * @Modify:
 */
public class NetworkDispatcher extends Thread {

    /**
     * 是否退出
     */
    boolean quit;

    /**
     * 真正执行网络请求的
     */
    HttpStack httpStack;

    /**
     * 存放请求的队列
     */
    BlockingQueue<Request<?>> blockingQueue;

    /**
     * 将响应结果投递到UI线程的投递者
     */
    Delivery delivery = new Delivery();

    /**
     * constructor
     *
     * @param blockingQueue
     * @param httpStack
     */
    NetworkDispatcher(BlockingQueue<Request<?>> blockingQueue, HttpStack httpStack) {
        this.httpStack = httpStack;
        this.blockingQueue = blockingQueue;
    }


    @Override
    public void run() {
        // 死循环,  不断从阻塞队列中取出Request 去执行
        while (true) {
            Request<?> request;
            try {
                // Take a request from the queue, 如果没有的话就阻塞了.
                request = blockingQueue.take();
            } catch (InterruptedException e) {
                // We may have been interrupted because it was time to quit.
                if (quit) {
                    return;
                }
                continue;
            }

            Response response = null;
            try {

                // 调用 httpStack 去执行真正的网络请求, 此时, response 中的rawData已经存入了响应的实体
                response = httpStack.performRequest(request);

                // 方式1:
                //Object o = request.parseResponse(response);
                //response.setResult(o);

                // 方式2:
                request.parseResponse2(response);

                delivery.postResponse(request, response);
            } catch (Exception e) {
                e.printStackTrace();
                delivery.postError(request, new VolleyError(e));
            }
        }
    }

    public void quit() {
        quit = true;
        interrupt();
    }
}

HttpStack

package com.fan.simplevolley.volley;

/**
 * @Description: 执行网络请求的接口, 其子类需要实现performRequest()方法, 去真正的进行网络请求
 * @Author: fan
 * @Date: 2020/3/5 23:34
 * @Modify:
 */
public interface HttpStack {
    public Response<?> performRequest(Request<?> request) throws Exception;
}

HttpStack 是个接口, 我们构造了一个它的实现类 UrlHttpStack

UrlHttpStack

UrlHttpStackperformRequest 方法内部, 先从 Request 对象中读取用户设置的关于请求的各种参数,包括url, 是GET方法还是POST方法,请求头,请求的参数参数。获取到这些参数之后,将这些参数设置到 HttpUrlConnection 对象中,然后由 HttpUrlConnection 对象真正的发起请求,获取数据。

请求返回后, 我们可以拿到相应的状态码、状态描述以及响应实体等相关信息,我们用这些信息去构建一个Response 对象。此时, Response对象中的 状态码、状态描述、响应实体 这些数据已经赋值,只有那个存放解析后的对象 public T result; 还没赋值。此时我们调用 RequestparseResponse2() 方法,将响应实体中的二进制数据解析成需要的类型,并存入 T result; 中。

package com.fan.simplevolley.volley;

import org.apache.http.ProtocolVersion;
import org.apache.http.StatusLine;
import org.apache.http.message.BasicStatusLine;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Set;

/**
 * @Description: 使用 URLConnection 进行网络请求的工具
 * @Author: fan
 * @Date: 2020/3/5 23:34
 * @Modify:
 */
public class UrlHttpStack implements HttpStack {
    @Override
    public Response<?> performRequest(Request<?> request) throws Exception {
        URL newURL = new URL(request.url);
        HttpURLConnection connection = (HttpURLConnection) newURL.openConnection();
        connection.setDoInput(true);
        connection.setUseCaches(false);

        Set<String> headersKeys = request.getHeaders().keySet();
        for (String headerName : headersKeys) {
            connection.addRequestProperty(headerName, request.getHeaders().get(headerName));
        }

        //request.getParams();

        Request.Method method = request.getHttpMethod();
        connection.setRequestMethod(method.toString());
        // add params
        byte[] body = request.getBody();
        if (body != null) {
            // enable output
            connection.setDoOutput(true);
            // set content type
            connection
                    .addRequestProperty("Content-Type", request.getBodyContentType());
            // write params data to connection
            DataOutputStream dataOutputStream = new DataOutputStream(connection.getOutputStream());
            dataOutputStream.write(body);
            dataOutputStream.close();
        }

//        connection.connect();

        ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1);
        int responseCode = connection.getResponseCode();
        if (responseCode == -1) {
            throw new IOException("Could not retrieve response code from HttpUrlConnection.");
        }
        // 状态行数据
        StatusLine responseStatus = new BasicStatusLine(protocolVersion,
                connection.getResponseCode(), connection.getResponseMessage());
        // 构建response
        Response<?> response = new Response(responseStatus);
        // 设置response数据
        //BasicHttpEntity entity = new BasicHttpEntity();
        InputStream inputStream = null;
        try {
            inputStream = connection.getInputStream();
        } catch (IOException e) {
            e.printStackTrace();
            inputStream = connection.getErrorStream();
        }

        int len = -1;
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] n = new byte[1024];
        while ((len = inputStream.read(n)) != -1) {
            baos.write(n, 0, len);
        }
        baos.flush();

        response.setData(baos.toByteArray());
        return response;


    }
}

这样一次请求就完成了, 下面由 Deliver派发到UI线程

Delivery

NetworkDispatch 对象持有一个默认的 Delivey,请求结束后,由 Delivery中的 Handler(Handler handler = new Handler(Looper.getMainLooper());)将请求结果 Response 对象派到到UI线程。

所谓派发,就是在UI线程去调用Request的回调方法。就完了。

package com.fan.simplevolley.volley;

import android.os.Handler;
import android.os.Looper;

/**
 * @Description: 将响应数据从工作线程投递到UI线程的投递者。使用 Android Handler来实现
 * @Author: fan
 * @Date: 2020/3/5 23:34
 * @Modify:
 */
public class Delivery {

    Handler handler = new Handler(Looper.getMainLooper());

    public void postResponse(final Request request, final Response response) {
        handler.post(new Runnable() {
            @Override
            public void run() {

                // Deliver a normal response or error, depending.
                // 成功, 就去调用request设置好的成功回调
                // 这里deliverResponse 只是在  request.callback上又包了一层, 无其他
                request.deliverResponse(response.result);
            }
        });
    }

    public void postError(final Request request, final VolleyError error) {
        handler.post(new Runnable() {
            @Override
            public void run() {
                request.deliverError(error);
            }
        });
    }
}

SimpleVolley

网络请求的主体流程完了,最后我们要想办法把 构建好的 Request 加到 请求队列, 参照Volley

在SimpleVolley中, 创建一个单例的RequestQueue。

package com.fan.simplevolley.volley;

/**
 * @Description: 这是一个简单的 volley 网络请求框架的实现。 我称它为 simple-volley。
 * @Author: fan
 * @Date: 2020/3/5 23:34
 * @Modify:
 */
public class SimpleVolley {

    /**
     * 创建一个 RequestQueue, 类似 Volley.newRequestQueue();
     * @return
     */
    public static RequestQueue newRequestQueue() {
        return newRequestQueue(RequestQueue.DEFAULT_NETWORK_THREAD_POOL_SIZE);
    }

    /**
     * 创建一个请求队列,NetworkExecutor数量为coreNums
     *
     * @param coreNums
     * @return
     */
    public static RequestQueue newRequestQueue(int coreNums) {
        return newRequestQueue(coreNums, null);
    }

    public static RequestQueue newRequestQueue(int coreNum, HttpStack httpStack) {

        RequestQueue requestQueue = new RequestQueue(coreNum, httpStack);
        requestQueue.start();
        return requestQueue;
    }
}

具体使用:

    // 创建一个RequestQueue, 对应 Volley.newRequestQueue();
    RequestQueue requestQueue = SimpleVolley.newRequestQueue();

    StringRequest request = new StringRequest(Request.Method.GET, "http://www.baidu.com", new Response.RequestListener<String>() {
                    @Override
                    public void onSuccess(String result) {
                        System.out.println(result);
                        resultTextView.setText(result);
                    }

                    @Override
                    public void onError(VolleyError error) {
                        System.out.println(error.getMessage());
                        resultTextView.setText(error.getMessage());
                    }
                });
                requestQueue.add(request);

源码地址github

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,820评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,648评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,324评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,714评论 1 297
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,724评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,328评论 1 310
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,897评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,804评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,345评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,431评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,561评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,238评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,928评论 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,417评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,528评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,983评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,573评论 2 359

推荐阅读更多精彩内容

  • 我的博客: Volley 源码分析 Volley 的使用流程分析 官网示例 创建一个请求队列 RequestQue...
    realxz阅读 2,042评论 1 11
  • 注:本文转自http://codekk.com/open-source-project-analysis/deta...
    Ten_Minutes阅读 1,301评论 1 16
  • 我们再来看看volley是怎么工作的。首先还是要带着重点去看源码,我们要关注的地方除了最核心的工作流程之外,还有一...
    反复横跳的龙套阅读 495评论 0 1
  • Volley简介 Volley 是 Google I/O 2013上发布的网络通信库,使网络通信更快、更简单、更健...
    shenhuniurou阅读 932评论 0 3
  • 睿睿妈践行日志18 记录公婆对我的好,深深感恩! 1、最近一上班,我都是第一个起来,不上班,我就会睡到自然醒,然后...
    睿妈_刘敏阅读 74评论 0 0