OKHTTP使用以及源码分析

OKHTTP相关文章虽然对,作为学习笔记使用,有问题希望能够尽情提出,共同交流
蒋八九

设计模式:

主要使用了建造者模式、责任链模式

简单使用

OkHttpClient okHttpClient = new OkHttpClient();                  //创建client对象
Request request = RequestBody.create(new MediaType(),jsonString);//post消息体
Request request = new Request.Builder().url(url).get().build();  //get请求
Request request = new Request.Builder().url(url).post(RequestBody.create(new MediaType(),jsonString)).build();                                //post请求
Call call = okHttpClient.newCall(request);                       //获取call,实体是realCall
Response response = call.execute();                              //同步阻塞
call.enqueue(new Callback()}                                     //异步提交

类的介绍

OkhttpClient:
主要封装dispatch调度器、自定义拦截器、代理、协议、连接池、读写超时时间等;
Dispatch:
client构造中创建的。其中有一个线程池(cacheSchesualThreadPool线程池)、一个运行中的同步队列runningAsyncCalls、一个运行中异步队列runningSyncCalls、一个等待异步队列readyAsyncCalls,最大运行中异步任务数:64个 ;同一个host访问上限数5个。

public final class Dispatcher {
  private int maxRequests = 64;                       同时提交的请求任务数量上限
  private int maxRequestsPerHost = 5;                 同时请求同一个服务器数量
  private @Nullable Runnable idleCallback;             
  private @Nullable ExecutorService executorService;  相当于cacheThreadPool线程池
  private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>(); 异步等待双向队列
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();异步运行双向队列
  private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();  同步运行双向队列

Request:
主要封装请求方式、请求头、请求地址
Call call = okHttpClient.newCall(request)——>return RealCall.newRealCall(this, request, false)
其实就是将client和request封装到realcall对象中

static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket){
  //realcall中同时创建了RetryAndFollowUpInterceptor对象
RealCall call = new RealCall(client, originalRequest, forWebSocket); 
  call.eventListener = client.eventListenerFactory().create(call);   这个是http请求的监听器
  return call;
}

提交任务

  1. 提交同步任务
Response response = call.execute();
client.dispatcher().executed(this);
Response result = getResponseWithInterceptorChain();
  1. 提交异步任务
call.enqueue(new Callback()}
1、client.dispatcher().enqueue(new AsyncCall(responseCallback));
2synchronized void enqueue(AsyncCall call) {
  if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost){
    runningAsyncCalls.add(call);  当前运行任务数小于64,服务器请求数小于5 入队
    executorService().execute(call); 交给cacheThreadPoll线程池进行子线程操作
  } else {
    readyAsyncCalls.add(call);   入异步等待队列
  }
}
3、最终在AyncCall类中的execute方法中执行拦截器方法
Response result = getResponseWithInterceptorChain();

Interceptor拦截器:

一共有6个拦截器1、retryAndFollowUpInterceptor;2、BridgeInterceptor;3、CacheInterceptor;4、ConnectInterceptor;5、networkInterceptors;6、CallServerInterceptor;

一、RetryAndFollowUpInterceptor重定向拦截器作用:

首先创建一个StreamAllocation对象,然后将执行response = realChain.proceed(request, streamAllocation, null, null);将责任链+1然后分发到抛向下一个BridgeInterceptor

二、BridgeInterceptor组装请求头拦截器:

如果有自定义的请求头head或者是mediaType会将其添加进去,如果没有的话,会自动补充完成,如(“Host”, “www.baidu.com”)(“Connection”, “Keep-Alive”)(“Location”, “重定向地址”)(“Accept-Encoding”, “gzip”)(“User-Agent”, userAgent)

三、CacheInterceptor缓存拦截器:

客户端缓存就是为了下次请求时节省请求时间,可以更快的展示数据

实现原理:

  1. Last-Modified配合If-Modified-Since或者If-Unmodified-since使用:对比上次修改时间来验证是 否使用缓存。步骤是:client第一次对资源发起请求时,server会设置Last-Modified的头信息,当浏览器再次对相同的资源发起请求 时,浏览器会设置request的头部(chrome可以使用disable cache来禁止浏览器自动设置,这样每次都会去源服务器获取),设置If-Modified-Since或者If-Unmodified-since的 值为之前接收到的Last-Modified的值,这样,server每次取得头部信息If-Modified-Since或者If- Unmodified-since,与文件的修改时间进行对比,如果相同,就返回304Not Modified,client使用缓存,如果不相同,就返回更新后的资源。
  2. Etag 数字签名配合If-Match或者If-None-Match使用:一般的数字签名可以对资源的内容进行hash计算,client第一次请求资源时,在 server中设置头部信息Etag,值为hash值,当浏览器再次对相同的资源发起请求时,浏览器会设置request的头部,设置If-Match或 者If-None-Match的值为之前接收到的Etag的值,这样,server每次取得头部信息If-Match或者If-None-match,与 重新计算的hash值进行对比,如果相同,就返回304Not Modified,client使用缓存,如果不相同,就返回更新后的资源。

使用方法:

//配置缓存的路径,和缓存空间的大小;这里用的是diskLruCache缓存机制
Cache cache = new Cache(new File("/Users/zeal/Desktop/temp"),10*10*1024);
Cache(File directory, long maxSize, FileSystem fileSystem) {
  this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
}
从这可以看出底层用的是DiskLruCache缓存机制;
OkHttpClient okHttpClient = new OkHttpClient.Builder() .cache(cache).build();
CacheControl.Builder builder = new CacheControl.Builder();
builder.noCache();//不使用缓存,全部走网络
builder.noStore();//不使用缓存,也不存储缓存
builder.onlyIfCached();//只使用缓存
builder.noTransform();//禁止转码
builder.maxAge(10, TimeUnit.MILLISECONDS);//指示客户机可以接收生存期不大于指定时间的响应。
builder.maxStale(10, TimeUnit.SECONDS);//指示客户机可以接收超出超时期间的响应消息
builder.minFresh(10, TimeUnit.SECONDS);//指示客户机可以接收响应时间小于当前时间加上指定时间的响应。
CacheControl cache = builder.build();//cacheControl
Request request = new Request.Builder().url(url).cacheControl(builder).build();
自定义拦截器应用:
        OkHttpClient.Builder newBuilder = mOkHttpClient.newBuilder();
         newBuilder.addInterceptor(new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                Request request = chain.request();
                boolean connected = NetworkUtil.isConnected(context);
                if (!connected) {
                    request = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).build();
                }
                Response response = chain.proceed(request);
                return response;
            }
        });

四、ConnectInterceptor连接拦截器:

连接拦截器主要工作就是找到一个可用的连接去连接服务器。主要要求对socket、http1、http2、https的理解
实现流程如下:
在拦截器中主要做了两件事:

HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();

在newStream中主要做了三件事:

  1. 查找是否有健康的连接,仅在http2.0有用,如果没有将会实例化一个;
    何为健康?
    1.socket没有关闭
    2.输入流没有关闭
    3.输出流没有关闭
    4.http2时连接没有关闭
  2. 实例化httpcodec,如果是http2.0实例化Http2Codec否则实例化Http1Codec
  3. 移除不健康的连接
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
    writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);

noNewStreams();
在findHealthyConnection的流程:
1.上一个连接是否完好,是,直接使用
2.缓存中有没有可以用的,有,使用
3.自己创建
4.握手、连接

releasedConnection = this.connection;    //先复用上一次连接
  if (this.connection != null) {
    result = this.connection;
}
  if (result == null) {                 如果本地没有,那么就连接池connectionPool中获取一个
    Internal.instance.get(connectionPool, address, this, null);
    if (connection != null) {
      result = connection;
    } else {selectedRoute = route;} } }
..........
if (result != null) {
  return result;                               获取到了就return
}
if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
  routeSelection = routeSelector.next();      如果连接池中也没有,就从路由选择器中获取
}
synchronized (connectionPool) {
    List<Route> routes = routeSelection.getAll();
    遍历routes
      Internal.instance.get(connectionPool, address, this, route);
      if (connection != null) {
        result = connection;                    如果获取到了就返回
} 
  if (!foundPooledConnection) {                 到最后都没有找到的话就自己new一个
    result = new RealConnection(connectionPool, selectedRoute);   
    acquire(result, false);
  }}
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,  
    connectionRetryEnabled, call, eventListener);  连接并握手
routeDatabase().connected(result.route());
synchronized (connectionPool) {
  Internal.instance.put(connectionPool, result);  最后将result添加到连接池中,方便下次复用
}
上面的result.connect()while (true) {
    if (route.requiresTunnel()) {                 如果是通道,则建立通道
      connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
    } else {                                      否正建立socket,就是最简单的那中建立
      connectSocket(connectTimeout, readTimeout, call, eventListener);
    }
建立http连接
    establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
    break;
}

五、CallServerInterceptor访问服务器拦截器:

Http发送网络请求前两个步骤是:

  1. 建立TCP链接:
  2. 客户端向web服务器发送请求命令:形如GET /login/login.jsp?username=android&password=123 HTTP/1.1的信息
    这一步在connectInterceptor中完成;当建立socket连接时,同时生成BufferSource读和BufferSink 写输入和输出
httpCodec.writeRequestHeaders(request); 首先向web服务端发送请求命令;通过sink写
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
 if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {  对Expect请求头做了处理
  }
  if (responseBuilder == null) {                         如果是Expect请求头,把body写出去
    BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
    request.body().writeTo(bufferedRequestBody);}}
客户端向服务端发送的部分就结束了,剩下的就是送服务端读取了
responseBuilder = httpCodec.readResponseHeaders(false);  通过readResponseHeaders读取响应头
Response response = responseBuilder                      构建responseBuilder对象
    .request(request)
    .handshake(streamAllocation.connection().handshake())
    .sentRequestAtMillis(sentRequestMillis)
    .receivedResponseAtMillis(System.currentTimeMillis())
    .build();
  response = response.newBuilder()                        
      .body(httpCodec.openResponseBody(response))        
      .build();
最后通过openResponseBody方法将Socket的输入流InputStream对象交给OkIo的Source对象BufferedSource中,最后封装到RealResponseBody中返回
return new RealResponseBody(contentType, -1L, Okio.buffer(source));
最后在ResponseBody中提供了string()方法读取字符串
response.body().string()
public final String string() throws IOException {
  BufferedSource source = source();                           获取source输入流
  Charset charset = Util.bomAwareCharset(source, charset());  获取字符编码utf-8
  return source.readString(charset);                          将输入流转utf-8字符串输出
}

猜你喜欢

转载自blog.csdn.net/massonJ/article/details/107956806