CustomNet:一个简单网络框架的设计与实现

Cover image

CustomeNet 的基本架构主要分为 4 个部分:

  • 最上面的部分是Request,包括了各种请求类型。比如:返回 JSON 数据格式的 JsonRequest ,返回字符串格式的 StringRequest 扥等
  • 第二个部分是消息队列,维护了提交给网络框架的请求列表,并且根据相应的规则进行排序。默认情况下,消息队列会按照优先级进入队列的顺序来执行,使用到的是线程安全的PriorityBlockingQueue<E>,因为他们的队列会被并发执行,需要保证队列访问的原子性。
  • 第三部分是NetworkExecutor,也就是网络的执行部分。该Executor是继承至Thread,在run()方法中会循环访问请求队列,从请求队列中获取并执行HTTP请求,请求完成之后再将结果传递给UI线程,实际上是一个线程类。
  • 第四部分是Response以及其传递类。因为第三部分的网络执行部分Executor实际上是一个线程类,但是 Android 中并不能在该线程中更新 UI。所以我们需要通过一种方式将请求结果传递给 UI 线程。ResponseDelivery封装了Response的投递,保证了Response执行在UI线程。

如下图所示:

Request 部分

首先,我们定义网络请求的方式。我们知道,常见的网络请求方式有GETPOSTPUTDELETE等。那么,我们就设置这四种最简单的网络请求方式。

我们可以通过设置枚举类的方式来定义网络请求方式:

public enum HttpMethod {
    GET("GET"),
    POST("POST"),
    PUT("PUT"),
    DELETE("DELETE");

    private String mHttpMethod = "";

    private HttpMethod(String method) {
        mHttpMethod = method;
    }

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

除此之外,我们上面提到过消息队列会具有优先级顺序来执行,而优先级其实是指请求Request的优先级。所以我们定义四种优先级:

public static enum Priority {
    LOW,
    NORMAL,
    HIGH,
    IMMEDIATE
}

因为对于网络请求来说,实际上返回的请求结果的格式是不确定的,有可能是 JSON 的数据格式,有可能是 XML 数据格式,有的甚至直接是字符串。所以需要让 Request 是抽象部分,而不是具体。一种方式是让 Request 作为泛型类,即Request<?>,那么如果返回的是字符串类型,就是Request<String>。以下就是 Request 类的核心代码:

public abstract class Request<T> implements Comparable<Request<T>> {

    /**
     * 默认的编码方式是 UTF-8
     */
    public static final String DEFAULT_PARAMS_ENCODING = "UTF-8";
    /**
     * 请求头类型
     */
    public static final String HEADER_CONTENT_TYPE = "";
    /**
     * 请求的序列号,默认从 0 开始
     */
    protected int mSerialNum = 0;
    /**
     * 优先级默认设置为 NORMAL
     */
    protected Priority mPriority = Priority.NORMAL;
    /**
     * 是否取消该请求,默认为 false
     */
    protected boolean isCancel = false;
    /**
     * 是否应该缓存
     */
    private boolean mShouldCache = true;
    /**
     * 请求 Listener
     */
    protected RequestListener<T> mRequestListener;
    /**
     * 请求的 URL
     */
    private String mUrl = "";
    /**
     * 请求方法,默认 GET,可更改
     */
    HttpMethod mHttpMethod = HttpMethod.GET;
    /**
     * 请求 header
     */
    private Map<String, String> mHeaders = new HashMap<>();
    /**
     * 请求参数
     */
    private Map<String, String> mBodyParams = new HashMap<>();

    /**
     * 携带请求方式、URL、回调接口的参数
     *
     * @param method
     * @param url
     * @param listener
     */
    public Request(HttpMethod method, String url, RequestListener<T> listener) {
        mHttpMethod = method;
        mUrl = url;
        mRequestListener = listener;
    }

    /**
     * 从原生的网络请求中解析结果,子类必须重写该抽象方法
     *
     * @param response
     * @return
     */
    public abstract T parseResponse(Response response);

    /**
     * 处理得到的结果 Response,该方法需要运行在 UI 主线程中
     */
    public final void deliveryResponse(Response response) {
        // 解析结果
        T result = parseResponse(response);
        if (mRequestListener != null) {
            int stCode = response != null ? response.getStatusCode() : -1;
            String msg = response != null ? response.getMessage() : "unknown error";
            // 调用 onComplete 回调接口
            mRequestListener.onComplete(stCode, result, msg);
        }
    }

    /**
     * 获得编码的方式
     *
     * @return
     */
    protected String getParamsEncoding() {
        return DEFAULT_PARAMS_ENCODING;
    }

    /**
     * 获得 Body 的内容类型
     * 格式为:application/x-www-form-urlencoded; charset=UTF-8
     *
     * @return
     */
    public String getBodyContentType() {
        return "application/x-www-form-urlencoded; charset=" + getParamsEncoding();
    }

    /**
     * 返回 POST 或者 PUT 请求时的 Body 参数字节数组
     *
     * @return
     */
    public byte[] getBody() {
        Map<String, String> params = getParams();
        if (params != null && params.size() > 0) {
            return encodeParameters(params, getParamsEncoding());
        }
        return null;
    }

    /**
     * 获得请求头
     *
     * @return
     */
    public Map<String, String> getHeaders() {
        return mHeaders;
    }

    /**
     * 获得请求的参数
     *
     * @return
     */
    public Map<String, String> getParams() {
        return mBodyParams;
    }

    /**
     * 获得请求的 URL 链接
     *
     * @return
     */
    public String getUrl() {
        return mUrl;
    }

    /**
     * 获得请求方式
     *
     * @return
     */
    public HttpMethod getHttpMethod() {
        return mHttpMethod;
    }

    /**
     * 将参数转换为 URL 编码的参数串,格式为 key1=value&key2=value2
     *
     * @param params
     * @param paramsEncoding
     * @return
     */
    private byte[] encodeParameters(Map<String, String> params, String paramsEncoding) {
        StringBuilder encodedParams = new StringBuilder();
        try {
            for (Map.Entry<String, String> entry : params.entrySet()) {
                // key 值
                encodedParams.append(URLEncoder.encode(entry.getKey(), paramsEncoding));
                encodedParams.append("=");
                // value 值
                encodedParams.append(URLEncoder.encode(entry.getValue(), paramsEncoding));
                encodedParams.append("&");
            }
            return encodedParams.toString().getBytes(paramsEncoding);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("Encoding not supported: " + paramsEncoding);
        }
    }

    /**
     * 用于对请求的排序处理,根据优先级和加入到队列的序号进行排序
     * 之所以要实现 Comparable 的接口,就是为了重写 CompareTo 方法,以实现优先级的比较
     *
     * @param another
     * @return
     */
    @Override
    public int compareTo(Request<T> another) {
        Priority myPriority = this.getPriority();
        Priority anotherPriority = another.getPriority();
        // 如果优先级相等,那么按照添加到队列的序列号顺序来执行
        return myPriority.equals(another) ? this.getSerialNumber() - another.getSerialNumber() :
                myPriority.ordinal() - anotherPriority.ordinal();
    }

    /**
     * 获得优先级
     *
     * @return
     */
    public Priority getPriority() {
        return mPriority;
    }

    /**
     * 获得序列号
     *
     * @return
     */
    public int getSerialNumber() {
        return mSerialNum;
    }

    /**
     * 设置序列号
     *
     * @param serialNumber
     */
    public void setSerialNumber(int number) {
        this.mSerialNum = serialNumber;
    }

    /**
     * 网络请求 Listener 会被执行在 UI 线程
     *
     * @param <T>
     */
    public static interface RequestListener<T> {
        /**
         * 请求完成的回调
         *
         * @param stCode
         * @param response
         * @param errMsg
         */
        public void onComplete(int stCode, T response, String errMsg);
    }
}

上述代码就是Request<T>的抽象类的实现,T是该请求Response的数据格式。因为不同场景会有不同的需求,返回的数据格式也是不一样的。

每个 Request 会有一个序列号,该序列号由请求队列生成,标识该请求在队列中的序号,该序号和优先级决定了该请求在队列中的排序,即它在请求队列的执行顺序。

在上面这个抽象类中,封装了通用的方法,即parseResponse这个方法。继承Request的子类只需要重写这个方法,对对应的数据格式进行解析即可。例如,解析 JSON 数据,就可以通过JsonRequest里重写这个方法进行解析;解析 Bitmap 数据,就可以创建一个ImageRequest对图片进行转化,将 Response 的数据转化为 Bitmap 即可。

消息队列部分

网络请求队列就是在内部封装了一个优先级队列,让网络请求器NetworkExecutor从请求队列中获取、执行请求。这样就可以保证优先级高的请求得到尽快的处理;如果优先级一致,就按照 FIFO 的策略执行;

RequestQueue

public final class RequestQueue {

    /**
     * 线程安全的优先级请求队列
     */
    private PriorityBlockingQueue<Request<?>> mRequestQueue = new PriorityBlockingQueue<Request<?>>();
    /**
     * 请求的序列化生成器
     */
    private AtomicInteger mSerialNumGenerator = new AtomicInteger(0);
    /**
     * 默认的核心数为 CPU 数加 1
     */
    public static int DEFAULT_CORE_NUMS = Runtime.getRuntime().availableProcessors() + 1;
    /**
     * CPU 核心数 + 1 个分发线程数
     */
    private int mDispatcherNums = DEFAULT_CORE_NUMS;
    /**
     * 执行网络请求的线程
     */
    private NetworkExecutor[] mDispatchers = null;
    /**
     * Http 请求的真正执行者
     */
    private HttpStack mHttpStack;

    protected RequestQueue(int coreNums, HttpStack httpStack) {
        mDispatcherNums = coreNums;
        mHttpStack = httpStack != null ? httpStack : HttpStackFactory.createHttpStack();
    }

    /**
     * 启动网络执行器
     */
    private final void startNetworkExecutor() {
        mDispatchers = new NetworkExecutor[mDispatcherNums];
        for (int i = 0; i < mDispatcherNums; i++) {
            mDispatchers[i] = new NetworkExecutor(mRequestQueue, mHttpStack);
            mDispatchers[i].start();
        }
    }

    /**
     * 启动网络请求的线程
     */
    public void start() {
        // 先停止已经启动的线程
        stop();
        startNetworkExecutor();
    }

    /**
     * 停止已经启动的线程
     */
    public void stop() {
        if (mDispatchers != null && mDispatchers.length > 0) {
            for (int i = 0; i < mDispatchers.length; i++) {
                mDispatchers[i].quit();
            }
        }
    }

    /**
     * 往消息队列中添加请求
     *
     * @param request
     */
    public void addRequest(Request<?> request) {
        if (!mRequestQueue.contains(request)) {
            // 为请求设置序列号
            request.setSerialNumber(this.generateSerialNumber());
            mRequestQueue.add(request);
        } else {
            Log.d("", "请求队列中已经含有");
        }
    }

    /**
     * 生成序列号
     *
     * @return
     */
    private int generateSerialNumber() {
        return mSerialNumGenerator.incrementAndGet();
    }
}

RequestQueue中的两个核心是请求队列和网络执行器。消息队列是负责管理请求,网络请求器负责在后台执行。NetworkExecutor 实际上是通过HttpStack接口,接口中定义了执行网络请求的对象。

HttpStack

这是一个接口,里面定义了发起请求的方法performRequest。我们知道,原生库中发起网络请求的方式有HttpClientHttpURLConnection的方法,所以我们可以定义不同的子类继承HttpStack,分别对应的是HttpClientStackHttpUrlConnStack类。

HttpUrlConnStack类举例,其中应该包括 HTTP 请求,构建请求、设置 header、设置请求参数、解析 Response 等操作。其实HttpClientStack也是这个逻辑操作,但是实现的模式不同。

public class HttpUrlConnStack implements HttpStack {

    @Override
    public Response performRequest(Request<?> request) {
        HttpURLConnection urlConnection = null;
        try {
            // 构建 HttpURLConnection
            urlConnection = createUrlConnection(request.getUrl());
            // 设置 headers
            setRequestHeaders(urlConnection, request);
            // 设置 body 参数
            setRequestParams(urlConnection, request);
            return fetchResponse(urlConnection);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (urlConnection != null) {
                // 结束之前一定要关闭连接
                urlConnection.disconnect();
            }
        }
        return null;
    }

    private HttpURLConnection createUrlConnection(String url) throws IOException {
        URL newURL = new URL(url);
        URLConnection urlConnection = newURL.openConnection();
        urlConnection.setConnectTimeout(mConfig.connTimeOut);
        urlConnection.setReadTimeout(mConfig.soTimeOut);
        urlConnection.setDoInput(true);
        urlConnection.setUseCaches(false);
        return (HttpURLConnection) urlConnection;
    }

    private void setRequestHeaders(HttpURLConnection connection, Request<?> request) {
        Set<String> headerKeys = request.getHeaders().keySet();
        for (String headerName : headerKeys) {
            connection.addRequestProperty(headerName, request.getHeaders().get(headerName));
        }
    }

    protected void setRequestParams(HttpURLConnection connection, Request<?> request) throws ProtocolException, IOException {
        HttpMethod method = request.getHttpMethod();
        connection.setRequestMethod(method.toString());
        // 添加参数
        byte[] body = request.getBody();
        if (body != null) {
            connection.setDoOutput(true);
            // 设置内容类型
            connection.addRequestProperty(Request.HEADER_CONTENT_TYPE, request.getBodyContentType());
            DataOutputStream dataOutputStream = new DataOutputStream(connection.getOutputStream());
            dataOutputStream.write(body);
            dataOutputStream.close();
        }
    }

    /**
     * 解析 response
     *
     * @param connection
     * @return
     * @throws IOException
     */
    private Response fetchResponse(HttpURLConnection connection) throws IOException {
        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 数据
        response.setEntity(entityFromURLConnection(connection));
        addHeadersToResponse(response, connection);
        return response;
    }

    /**
     * 执行 HTTP 请求之后获取到其数据流,即返回请求结果的流
     *
     * @param connection
     * @return
     */
    private HttpEntity entityFromURLConnection(HttpURLConnection connection) {
        BasicHttpEntity entity = new BasicHttpEntity();
        InputStream inputStream = null;
        try {
            inputStream = connection.getInputStream();
        } catch (IOException e) {
            e.printStackTrace();
            inputStream = connection.getErrorStream();
        }
        // TODO: GZIP
        entity.setContent(inputStream);
        entity.setContentLength(connection.getContentLength());
        entity.setContentEncoding(connection.getContentEncoding());
        entity.setContentType(connection.getContentType());
        return entity;
    }

    /**
     * 添加 header 到 response 中
     *
     * @param response
     * @param connection
     */
    private void addHeadersToResponse(BasicHttpResponse response, HttpURLConnection connection) {
        for (Map.Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
            // 将 header 依次加入 response 中
            if (header.getKey() != null) {
                Header h = new BasicHeader(header.getKey(), header.getValue().get(0));
                response.addHeader(h);
            }
        }
    }
}

以上操作就是通过构建HttpURLConnection,通过其对象设置请求 Header 参数,发起请求,请求完成之后再解析结果,最后返回给Response

除此之外,因为我们知道,在 Android 6.0 之后的版本中,HttpClient已废弃,都推荐使用HttpUrlConnection的方式发起网络请求,因此我们创建HttpFactory工具类,可以根据 Android API 的版本,来判断调用哪种方式来发起请求。

public final class HttpStackFactory {
    /**
     * 定义 Android 版本
     */
    private static final int GINGERBREAD_SDK_NUM = 9;

    public static HttpStack createHttpStack() {
        int runtimeSDKApi = Build.VERSION.SDK_INT;
        // 如果版本号大于这个版本
        if (runtimeSDKApi >= GINGERBREAD_SDK_NUM) {
            // 使用 HttpURLConnection() 来创建连接
            return new HttpUrlConnStack();
        }
        // 使用 HttpClient() 来创建连接
        return new HttpClientStack();
    }
}

NetworkExecutor 网络执行部分

之前提到过,NetworkExecutor 网络执行器实际上是一个继承至Thread的线程类。用户需要创建并启动一个请求队列之后,指定个数的NetworkExecutor来随之启动。多个Executor共享一个消息队列,然后每个启动器中的run()方法会循环地提取请求队列中的请求,拿到请求之后就交给HttpStack的具体实现子类来真正地执行请求。它的核心类代码如下:

public final class NetworkExecutor extends Thread {

    /**
     * 网络请求队列
     */
    private BlockingQueue<Request<?>> mRequestQueue;
    /**
     * 网络请求栈
     */
    private HttpStack mHttpStack;
    /**
     * 结果分发器,将结果投递到主线程
     */
    private static ResponseDelivery mResponseDelivery = new ResponseDelivery();
    /**
     * 请求缓存
     */
    private static Cache<String, Response> mReqCache = new LruMemCache();

    /**
     * 是否停止
     */
    private boolean isStop = false;

    public NetworkExecutor(BlockingQueue<Request<?>> queue, HttpStack httpStack) {
        mRequestQueue = queue;
        mHttpStack = httpStack;
    }

    @Override
    public void run() {
        try {
            while (!isStop) {
                final Request<?> request = mRequestQueue.take();
                if (request.isCanceled()) {
                    Log.d("###", "取消执行了");
                    continue;
                }
                Response response = null;
                if (isUseCache(request)) {
                    // 从缓存中去读
                    response = mReqCache.get(request.getUrl());
                } else {
                    response = mHttpStack.performRequest(request);
                    if (request.shouldCache() && isSuccess(response)) {
                        mReqCache.put(request.getUrl(), response);
                    }
                }

                // 分发请求结果
                mResponseDelivery.deliveryResponse(request, response);
            }
        } catch (InterruptedException e) {
            Log.i("", "请求分发器退出");
        }
    }

    /**
     * 执行是否成功
     *
     * @param response
     * @return
     */
    private boolean isSuccess(Response response) {
        return response != null && response.getStatusCode() == 200;
    }

    /**
     * 是否从缓存中读取
     *
     * @param request
     * @return
     */
    private boolean isUseCache(Request<?> request) {
        return request.shouldCache() && mReqCache.get(request.getUrl()) != null;
    }

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

网络执行器的部分就是指定数量的NetworkExecutor,构造执行器时将请求队列以及HttpStack注入。让run()函数的循环中不断从请求队列中取出请求,并且交给HttpStack执行,期间需要判断是否需要缓存、是否已经有缓存等等。如果使用缓存,并且已经含有缓存,就使用缓存的结果。

Response回调部分

Response类

每个 Request 都对应一个 Response,存储了请求的状态码、请求结果等内容。但框架中不应该使用太具体的内容,而是能够让开发者能够自由地、简单地扩展内容。因为我们知道,HTTP 基于 TCP 协议,TCP 协议基于 Socket,Socket 实际上操作的是输入输出流。输出流是向服务器写数据,输入流是从服务器读取数据。所以在 Response 类中,我们应该使用InputStream存储结果或者用字节数组来存储结果。

public class Response extends BasicHttpResponse {

    /**
     * 原始的 Response 主体数据
     */
    public byte[] rawData = new byte[0];

    public Response(StatusLine statusline, ReasonPhraseCatalog catalog, Locale locale) {
        super(statusline, catalog, locale);
    }

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

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

    @Override
    public void setEntity(HttpEntity entity) {
        super.setEntity(entity);
        rawData = entityToBytes(getEntity());
    }

    public int getStatusCode() {

    }

    @Override
    public void setStatusCode(int code) {

    }

    public String getMessage() {

    }

    public void setMessage(String msg) {

    }

    /**
     * 获得原始的数据
     *
     * @return
     */
    public byte[] getRawData() {
        return rawData;
    }

    /**
     * 将 entity 转化为 bytes
     *
     * @param entity
     * @return
     */
    private byte[] entityToBytes(HttpEntity entity) {
        try {
            return EntityUtils.toByteArray(entity);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new byte[0];
    }
}

ResponseDelivery 类

这个类的作用是将请求的回调执行到 UI 线程,以便用户可以更新 UI 等操作。

public class ResponseDelivery implements Executor {

    /**
     * 关联主线程消息队列的 Handler
     */
    Handler mResponseHandler = new Handler(Looper.getMainLooper());

    /**
     * 处理请求结果,在 UI 主线程中执行
     *
     * @param request
     * @param response
     */
    public void deliveryResponse(final Request<?> request, final Response response) {
        Runnable respRunnable = new Runnable() {
            @Override
            public void run() {
                request.deliveryResponse(response);
            }
        };

        execute(respRunnable);
    }


    @Override
    public void execute(Runnable command) {
        mResponseHandler.post(command);
    }
}

我们也可以看到,ResponseDelivery 类内部其实也是封装了关联UI线程消息队列的 Handler,在deliveryResponse函数中将request执行在UI线程中,然后在子线程中再执行requestdeliveryResponse方法,这个方法里会解析数据,并且将结果通过回调的方式传递给了 UI 线程。

使用

通过以上步骤,一个精彩简陋的网络框架就搭建好了。我们就可以直接建立请求如下:

// 创建请求队列,使用默认的请求核心数,并根据 Android 版本判断使用何种网络连接方式
RequestQueue mQueue = new RequestQueue(RequestQueue mQueue = new RequestQueue(Runtime.getRuntime().availableProcessors() + 1, HttpStackFactory.createHttpStack());

// 例如,如下是一个返回字符串的请求
StringRequest request = new StringRequest(HttpMethod.GET, "http://www.baidu.com", new RequestListener<String>() {
    @Override
    public void onComplete(int stCode, String response, String errMsg) {
        // 处理结果,例如将 response 显示在界面中
        mTv.setText(response);
    }
});

// 将这个请求 request 添加到请求队列中去
mQueue.addRequest(request);
◀ 着手实现一个优秀的 ImageLoader基于 WanAndroid 开放 API 编写的安卓应用 CodeHub 源码分析 ▶