okhttp原理分析(持续更新),包含okio了解,拦截器以及断点下载的使用

1. 原理

1. okio

最基本的接口只有两个:Sink(writer)、Source(Read),大概相当于OutputStream和InputStream在原生接口中的地位。这两个接口中只定义了一些最基础的IO操作方法

1. 图解okio

okhttp原理分析(持续更新),包含okio了解,拦截器以及断点下载的使用_第1张图片
okio的骨架。下面是几个核心的类:
Okio:提供生成Sink和Source的方法
Sink : 接口类,功能上对应OutputStream
Source :接口类,功能上对应InputStream
BufferedSink:接口类继承自Sink,内部有一个Buffer的Sink
BufferedSource:接口类继承自Source,内部有一个Buffer的Source
Buffer:BufferedSink和BufferedSource的最终实现实现类, 实现缓存功能,内部有一个Segment链表
Segment:里面有个byte数组,通过pos,limit控制读写的位置(从byte[]哪里开始读,哪里开始写入),next, prev实现导航到前面或后面的Segment(实现Segment链表结构)

相关连接
  1. csdn

通过一段socket编程来演示:

package com.example.disignmode.myhttp.myokio;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.Charset;

import okio.BufferedSink;
import okio.BufferedSource;
import okio.Okio;

/**
 * describe :
 * date on 2019/5/2
 * author linghailong
 * email [email protected]
 */
public class OkioClient {
    public static void main(String[] args) {
        Socket socket = null;
        try {
            socket = new Socket("127.0.0.1", 8080);
            InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream();
            BufferedSink bufferedSink = Okio.buffer(Okio.sink(outputStream));
            BufferedSource bufferedSource = Okio.buffer(Okio.source(inputStream));
            writeMsg(bufferedSink,"hello");
            while (true){
                int length=bufferedSource.readInt();
                String message=bufferedSource.readString(length,Charset.forName("utf-8"));
                System.out.println("length is: "+length+" , message is : "+message); if ("error exit".equals(message)) {
                    break;
                }
                String respMsg = getResponseAccordMsg(message);
                writeMsg(bufferedSink, respMsg);
                if ("error exit".equals(respMsg)) {
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (socket != null) {
                    socket.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private static void writeMsg(BufferedSink sink, String msg) {
        try {
            int msgLength = msg.getBytes().length;
            sink.writeInt(msgLength);
            sink.writeString(msg, Charset.forName("utf-8"));
            sink.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private static String getResponseAccordMsg(String msg) {
        String result = "";
        if (msg != null && msg.length() > 0) {
            if (msg.equals("hello")) {
                result = "nice to meet you";
            } else if (msg.equals("nice to meet you too")) {
                result = "see you";
            }
        }
        if (result.length() == 0) {
            result = "error exit";
        }
        return result;
    }
}

Server端代码

package com.example.disignmode.myhttp.myokio;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.Charset;

import okio.BufferedSink;
import okio.BufferedSource;
import okio.Okio;

/**
 * describe :
 * date on 2019/5/2
 * author linghailong
 * email [email protected]
 */
public class OkioServer {
    public static void main(String[] args) {
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(8080);
            while (true) {
                Socket connection = null;
                try {
                    connection = serverSocket.accept();
                    handleClientSocket(connection);
                }catch (IOException ex){
                    ex.printStackTrace();
                }
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        } finally {
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private static void handleClientSocket(Socket socket) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while (true) {
                        BufferedSource source = Okio.buffer(Okio.source(socket));
                        BufferedSink sink = Okio.buffer(Okio.sink(socket));
                        int length = source.readInt();
                        String message = source.readString(length, Charset.forName("utf-8"));
                        System.out.println("length is: " + length + " , message is : " + message);
                        if ("error exit".equals(message)) {
                            break;
                        }
                        String responseMsg = getResponseAccordMsg(message);
                        if (responseMsg != null) {
                            int respLength = responseMsg.getBytes().length;
                            sink.writeInt(respLength);
                            sink.writeString(responseMsg, Charset.forName("utf-8"));
                            sink.flush();
                        }
                        if ("error exit".equals(responseMsg)) {
                            break;
                        }
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    if (socket != null) {
                        try {
                            socket.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }

            }
        });
        thread.start();
    }

    private static String getResponseAccordMsg(String msg) {
        String result = "";
        if (msg != null && msg.length() > 0) {
            if (msg.equals("hello")) {
                result = "hello";
            } else if (msg.equals("nice to meet you")) {
                result = "nice to meet you too";
            } else if (msg.equals("see you")) {
                result = "see you next time";
            }
        }
        if (result.length() == 0) {
            result = "error exit";
        }
        return result;
    }

}

2. 拦截器流程

|---RealCall
|   |--- Response getResponseWithInterceptorChain()

  1. getResponseWithInterceptorChain()
Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    // 客户端的所有拦截器
    // 使用了责任链的设计模式 每一个拦截器只处理与他相关的拦截器
    interceptors.addAll(client.interceptors());
    interceptors.add(new RetryAndFollowUpInterceptor(client));
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0,
        originalRequest, this, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    boolean calledNoMoreExchanges = false;
    try {
      Response response = chain.proceed(originalRequest);
      if (transmitter.isCanceled()) {
        closeQuietly(response);
        throw new IOException("Canceled");
      }
      return response;
    } catch (IOException e) {
      calledNoMoreExchanges = true;
      throw transmitter.noMoreExchanges(e);
    } finally {
      if (!calledNoMoreExchanges) {
        transmitter.noMoreExchanges(null);
      }
    }
  }
  1. 请求完成之后,通过getResponseWithInterceptorChain()方法把请求变成了一个响应。
  2. 分析几个拦截器的作用:
    • RetryAndFollowUpInterceptor: 处理重试的拦截器,首先会处理一些异常,只要不是一些致命的异常,它就会continue,重新发起一次请求(把request给下级),如果是致命的异常,抛给上一层。会处理一些重定向
    • BridgeInterceptor 设置一些通用的请求头:Content-Type,Content-Length,Cookie,做一些返回的处理:如果返回得数据被压缩了,那木采用ZipSource,保存Cookie
    • CacheInterceptor (重点) 在缓存可用的情况下,读取本地的缓存的数据,如果有首先判断有没有缓存陈列,然后判断有没有过期,如果没有过期直接拿缓存,如果过期了需要添加一些之前头部信息,如conditionName = "If-Modified-Since"
    • ConnectInterceptor
      findhealthyConnection()找一个连接,首先判断有没有健康的,没有就创建(建立Socket,握手连接,)连接缓存。OkHttp基于原生的Socket.RealConnection
    • CallServerInterceptor :从服务器读数据以及写数据。写头部信息,写body信息
  3. 重定向:
    • 返回码是307
    • 然后从response的Header中获取location
    • 重新请求
  4. Http缓存
    1. Cache-Control(缓存策略)public private no-cache maxage no-sotre
    2. Expires (缓存的过期策略)指明了缓存数据有效的绝对时间,告诉客户端到了这个时间点(比照客户端时间)后本地就作废了
  5. 连接三个核心类:连接复用
    • RealConnection
    • ConnectionPool
  6. no-cache, no-store以及max-age=0辨析
    • no-cache: 相当于是重载
    • no-store相当于是不缓存
    • max-age=0; 相当于是刷新
OkHttpClient okHttpClient = new OkHttpClient();
OkHttpClient newClient = okHttpClient.newBuilder()
               .cache(new Cache(mContext.getCacheDir(), 10240*1024))
               .connectTimeout(20, TimeUnit.SECONDS)
               .readTimeout(20, TimeUnit.SECONDS)
               .build();

缓存相关使用实例

/**
     * 一、无论有无网路都添加缓存。
     * 目前的情况是我们这个要addNetworkInterceptor
     * 这样才有效。经过本人测试(chan)测试有效.
     * 60S后如果没有网络将获取不到数据,显示连接失败
     */
    static Interceptor netInterceptor = new Interceptor() {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            Response response = chain.proceed(request);
          /*String cacheControl = request.header("Cache-Control");
            if (TextUtils.isEmpty(cacheControl)) {
                cacheControl = "public, max-age=60";
            }*/
            int maxAge = 60;
            return response.newBuilder()
                    .removeHeader("Pragma")// 清除头信息,因为服务器如果不支持,会返回一些干扰信息,不清除下面无法生效
                    .removeHeader("Cache-Control")
                    .header("Cache-Control", "public, max-age=" + maxAge)
                    .build();
        }
    };
    
 File cacheFile = new File(BaseApp.getInstance().getCacheDir(), "caheData");
        //设置缓存大小
        Cache cache = new Cache(cacheFile, DEFAULT_DIR_CACHE);//google建议放到这里
        OkHttpClient client = new OkHttpClient.Builder()
                .retryOnConnectionFailure(true)//连接失败后是否重新连接
                .connectTimeout(15, TimeUnit.SECONDS)//超时时间15S
                .addNetworkInterceptor(cacheInterceptor)//这里大家一定要注意了是addNetworkOnterceptor别搞错了啊。
                .cache(cache)
                .build();

拦截器的调用顺序

  1. 自己的
  2. retry
  3. Bridge
  4. Cache
  5. ConnectInterceptor
  6. CallServer
题目

自己写一个基于okhttp的缓存,有网络10s内可以读取缓存,没网每次请求读取缓存。

参考文献
  1. 简书上的分享

2. socket

文件上传进度监听

  1. 首先需要找到okhttp中是怎样进行上传的。

首先来看一段基础的代码

 private void uploadFile() {
        // 这个是 Okhttp 上传文件的用法
        String url = "https://api.baidu.com/api/upload";
        File file = new File(Environment.getExternalStorageDirectory(), "test.apk");
        OkHttpClient httpClient = new OkHttpClient();
        // 构建请求 Body , 这个我们之前自己动手写过
        MultipartBody.Builder builder = new MultipartBody.Builder()
                .setType(MultipartBody.FORM);
        builder.addFormDataPart("platform", "android");
        builder.addFormDataPart("file", file.getName(),
                RequestBody.create(MediaType.parse(guessMimeType(file.getAbsolutePath())), file));

        ExMultipartBody exMultipartBody = new ExMultipartBody(builder.build()
                ,new UploadProgressListener(){

            @Override
            public void onProgress(long total, long current) {
                showToast(total,current);
            }
        });

        // 怎么监听上传文件的进度?

        // 构建一个请求
        final Request request = new Request.Builder()
                .url(url)
                .post(exMultipartBody).build();
        // new RealCall 发起请求
        Call call = httpClient.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                e.printStackTrace();
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                Log.e("TAG", response.body().string());
            }
        });
    }

分析流程

|---RealCall
|   |---CallServerInterceptor
|   |--- MultipartBody
|   |   |---writeTo(BufferedSink sink)
  1. 由于MultipartBody 是final类型的方法,我们无法直接拿到这个方法,那木我们就可以考虑静态代理的形式,编写一个扩展类,传入代理。监听到contentlength以及currentLength这两个参数之后,通过编写一个借口回调,就可以拿到值,然后在主线程中进行一些操作就可以完成。
  2. ForwardingSink 这是Sink中的一个代理类。

断点下载

流程讲解:

okhttp原理分析(持续更新),包含okio了解,拦截器以及断点下载的使用_第2张图片
大概的流程图如上所示:

|---MainActivity()
|   |---DownloadFacade(Context context) //用于 管理
|   |   |---startDownload(String url,DownloadCallback callback)
|   |   |   |---DownloadDispatcher // okhttp执行请求的类
|   |   |   |   |---startDownload(final String url, final DownloadCallback callback)  //真正执行
|   |   |   |   |   |---DownloadTask // download 线程池的分发类
|   |   |   |   |   |   |---init() //实例化,开始计算哪个线程下载哪部分
|   |   |   |   |   |   |   |---DownloadRunnable(String url, int threadId, long start, long end, long progress, DownloadEntity downloadEntity,DownloadCallback callback) //执行下载的线程

代码流程分析

1. MainActivity 在这里仅仅做实例化以及传入需要下载的对象的链接。
2. DownloadFacade 做一些实例化的准备,以及下载入口
public class DownloadFacade {
    private static final DownloadFacade sFacade = new DownloadFacade();

    private DownloadFacade(){}

    public static DownloadFacade getFacade() {
        return sFacade;
    }

    public void init(Context context){
        FileManager.manager().init(context);
        DaoManagerHelper.getManager().init(context);
    }

    public void startDownload(String url,DownloadCallback callback){
        DownloadDispatcher.getDispatcher().startDownload(url,callback);
    }

    public void startDownload(String url){
        // DownloadDispatcher.getDispatcher().startDownload(url);
    }
}
DownloadDispatcher

okhttp的执行类,同时也是下载任务的管理类,在这里开始执行下载

package com.darren.architect_day28.download;

import com.darren.architect_day28.OkHttpManager;

import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Deque;

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Response;

final class DownloadDispatcher {

    private static final DownloadDispatcher sDispatcher = new DownloadDispatcher();

    private DownloadDispatcher(){

    }

    public static DownloadDispatcher getDispatcher() {
        return sDispatcher;
    }

    /** Ready async calls in the order they'll be run. */
    private final Deque<DownloadTask> readyTasks = new ArrayDeque<>();

    /** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
    private final Deque<DownloadTask> runningTasks = new ArrayDeque<>();

    /** Running synchronous calls. Includes canceled calls that haven't finished yet. */
    private final Deque<DownloadTask> stopTasks = new ArrayDeque<>();

    // 最大只能下载多少个 3 5

    public void startDownload(final String url, final DownloadCallback callback){
        // 获取文件的大小
        Call call = OkHttpManager.getManager().asyncCall(url);

        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                  callback.onFailure(e);
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                // 获取文件的大小
                long contentLength = response.body().contentLength();

                if(contentLength <= -1){
                    // 没有获取到文件的大小,
                    // 1. 跟后台商量
                    // 2. 只能采用单线程去下载
                    return;
                }
                // 计算每个线程负责哪一块?
                DownloadTask downloadTask = new DownloadTask(url,contentLength,callback);
                downloadTask.init();

                runningTasks.add(downloadTask);
            }
        });
    }

    public void recyclerTask(DownloadTask downloadTask) {
        runningTasks.remove(downloadTask);
        // 参考 OkHttp 的 Dispatcher 的源码,如果还有需要下载的开始下一个的下载
    }

    public void stopDownload(String url){
        // 这个停止的是不是正在下载的
    }

    // 开个单独的线程去执行 所有下载的回调

}

DownloadTask

真正执行断点下载的管理类,为执行断点下载的线程分发任务。这个task执行完毕后从downloaddispatcher中的list中移除任务。


public class DownloadTask {
    private String mUrl;
    private long mContentLength;
    private List<DownloadRunnable> mRunnables;
    // OkHttp 为什么搞一个能被回收的线程池?
    OkHttpClient client = new OkHttpClient();
    /**
     * Executes calls. Created lazily.
     */
    private
    @Nullable
    ExecutorService executorService;
    private volatile int mSucceedNumber;

    private DownloadCallback mCallback;
    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        public Thread newThread(Runnable r) {
            return new Thread(r, "DownThread #" + mCount.getAndIncrement());
        }
    };


    public synchronized ExecutorService executorService() {
        if (executorService == null) {
            executorService = new ThreadPoolExecutor(0, THREAD_SIZE, 30, TimeUnit.SECONDS,
                    new SynchronousQueue<Runnable>(), new ThreadFactory() {
                @Override
                public Thread newThread(@NonNull Runnable r) {
                    Thread thread = new Thread(r, "DownloadTask");
                    thread.setDaemon(false);
                    return thread;
                }
            });
        }
        return executorService;
    }

    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int THREAD_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));

    public DownloadTask(String url, long contentLength, DownloadCallback callback) {
        this.mUrl = url;
        this.mContentLength = contentLength;
        mRunnables = new ArrayList<>();
        this.mCallback = callback;
    }

    /**
     * 初始化
     */
    public void init() {
        for (int i = 0; i < THREAD_SIZE; i++) {
            // 计算出每个线程要下载的内容
            long threadSize = mContentLength / THREAD_SIZE;

            // 初始化的时候 这里要去读取数据库


            long start = i * threadSize;
            long end = (i + threadSize) - 1;

            if (i == THREAD_SIZE - 1) {
                end = mContentLength - 1;
            }

            List<DownloadEntity> entities = DaoManagerHelper.getManager().queryAll(mUrl);
            DownloadEntity downloadEntity = getEntity(i, entities);
            if (downloadEntity == null) {
                downloadEntity = new DownloadEntity(start, end, mUrl, i, 0, mContentLength);
            }

            DownloadRunnable downloadRunnable = new DownloadRunnable(mUrl, i, start, end,
                    downloadEntity.getProgress(), downloadEntity, new DownloadCallback() {

                @Override
                public void onFailure(IOException e) {
                    // 一个apk 下载里面有一个线程异常了,处理异常,把其他线程停止掉
                    mCallback.onFailure(e);
                }

                @Override
                public void onSucceed(File file) {
                    // 线程同步一下,
                    synchronized (DownloadTask.this) {
                        mSucceedNumber += 1;
                        if (mSucceedNumber == THREAD_SIZE) {
                            mCallback.onSucceed(file);
                            DownloadDispatcher.getDispatcher().recyclerTask(DownloadTask.this);
                            // 清楚数据库的这个文件下载存储
                        }
                    }
                }
            });
            // 通过线程池去执行
            executorService().execute(downloadRunnable);
        }
    }

    private DownloadEntity getEntity(int threadId, List<DownloadEntity> entities) {
        for (DownloadEntity entity : entities) {
            if (threadId == entity.getThreadId()) {
                return entity;
            }
        }
        return null;
    }

    public void stop() {
        for (DownloadRunnable runnable : mRunnables) {
            runnable.stop();
        }
    }
DownloadRunnable 执行断点下载的线程
public class DownloadRunnable implements Runnable{
    private static final int STATUS_DOWNLOADING = 1;
    private static final int STATUS_STOP = 2;
    private final long start;
    private final long end;
    private final int threadId;
    private final String url;
    private final DownloadCallback mCallback;
    private int mStatus = STATUS_DOWNLOADING;
    private long mProgress = 0;
    private DownloadEntity mDownloadEntity;

    public DownloadRunnable(String url, int threadId, long start, long end, long progress, DownloadEntity downloadEntity,DownloadCallback callback) {
        this.threadId = threadId;
        this.url = url;
        this.start = start + progress;// 1M-2M 0.5M  1.5M - 2M
        this.end = end;
        mCallback = callback;
        this.mProgress = progress;
        this.mDownloadEntity = downloadEntity;
    }

    @Override
    public void run() {
        // 只读写我自己的内容,Range
        RandomAccessFile accessFile = null;
        InputStream inputStream = null;
        try {
            Response response = OkHttpManager.getManager().syncResponse(url,start,end);
            Log.e("TAG",this.toString());

           inputStream = response.body().byteStream();
            // 写数据
            File file = FileManager.manager().getFile(url);
            // 从这里开始
            accessFile.seek(start);

            int len = 0;
            byte[] buffer = new byte[1024*10];

            while ((len = inputStream.read(buffer))!=-1){
                if(mStatus == STATUS_STOP)
                    break;
                // 保存进度,做断点 , 100kb
                mProgress += len;
                accessFile.write(buffer,0,len);
            }

            mCallback.onSucceed(file);
        } catch (IOException e) {
            mCallback.onFailure(e);
        }finally {
            Utils.close(inputStream);
            Utils.close(accessFile);

            // 存到数据库,数据库怎么存?
            mDownloadEntity.setProgress(mProgress);
            DaoManagerHelper.getManager().addEntity(mDownloadEntity);
        }
    }

    @Override
    public String toString() {
        return "DownloadRunnable{" +
                "start=" + start +
                ", end=" + end +
                ", threadId=" + threadId +
                ", url='" + url + '\'' +
                '}';
    }

    public void stop() {
        mStatus = STATUS_STOP;
    }

参考文献

  1. hongyang
  2. okhttp的使用教程

你可能感兴趣的:(android)