OkHttp源码读后感

来源:互联网 发布:通信网络集成资质取消 编辑:程序博客网 时间:2024/05/22 05:28

这次是第二次看OkHttp的源码了,比起上一次,这次总算是理清了其中的脉络,这或许是随着工作经验的增加而发生的改变,说到底还是单身的锅,单身狗的周末只能玩代码消磨时间…….我是用SourceInsight作为工具的(之前装好的),有说用JetBrains的IDEA效果更好,无奈何用的是长城宽带,坑的一比….下半天没下好…

从平时使用Api的顺序来源码是个不错的选择,下面是OkHttp的一般用法:

OkHttpClient client = new OkHttpClient.Builder().build();Request request = new Request.Builder()                .url(url)                .post(body)                .build();Response response = client.newCall(request).execute();    String result = response.body().string();    response.close();

上面是同步的请求,但是异步请求才是OkHttp的精髓所在,异步请求是这样的:

OkHttpClient okHttpClient = new OkHttpClient();Request request = new Request.Builder()        .url("http://www.qq.com")        .build();Call call = okHttpClient.newCall(request);//1.异步请求,通过接口回调告知用户 http 的异步执行结果call.enqueue(new Callback() {    @Override    public void onFailure(Call call, IOException e) {        System.out.println(e.getMessage());    }    @Override    public void onResponse(Call call, Response response) throws IOException {        if (response.isSuccessful()) {            System.out.println(response.body().string());        }    }});

异步请求使用了队列进行并发任务的分发(Dispatch)与回调,下面就先来看看OkHttpClient里面的newCall方法:

@Override public Call newCall(Request request) {    return RealCall.newRealCall(this, request, false /* for web socket */);  }

看来OkHttpClient只是一层皮,想知道newCall的具体实现还得去RealCall里面找才行:

static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {    // Safely publish the Call instance to the EventListener.    RealCall call = new RealCall(client, originalRequest, forWebSocket);    call.eventListener = client.eventListenerFactory().create(call);    return call;  }

这里实例化了一个RealCall对象,并且给这个RealCall添加一个EventListener,监听RealCall,这样就可以知道每次的网络请求具体进行到哪一步。

说了一大串好戏终于要开始了!在上面的代码中,不难发现execute()也是在RealCall里面的:

@Override public Response execute() throws IOException {    synchronized (this) {      if (executed) throw new IllegalStateException("Already Executed");      executed = true;    }    captureCallStackTrace();    try {      client.dispatcher().executed(this);      Response result = getResponseWithInterceptorChain();      if (result == null) throw new IOException("Canceled");      return result;    } finally {      client.dispatcher().finished(this);    }  }

事不宜迟,马上来看看getResponseWithInterceptorChain():

Response getResponseWithInterceptorChain() throws IOException {    // Build a full stack of interceptors.    List<Interceptor> interceptors = new ArrayList<>();    interceptors.addAll(client.interceptors());    interceptors.add(retryAndFollowUpInterceptor);    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, null, null, null, 0, originalRequest, this, eventListener);    return chain.proceed(originalRequest);  }}

嗯,OkHttp的主要精髓之一就在这里了!这里有一个类型为Interceptor的List,这个List里面,有负责失败重试以及重定向的 RetryAndFollowUpInterceptor;负责把用户构造的请求转换为发送到服务器的请求、把服务器返回的响应转换为用户友好的响应的 BridgeInterceptor;负责读取缓存直接返回、更新缓存的 CacheInterceptor;负责和服务器建立连接的 ConnectInterceptor;配置 OkHttpClient 时设置的 networkInterceptors;负责向服务器发送请求数据、从服务器读取响应数据的 CallServerInterceptor。

OkHttp把所有这些负责不同工作的拦截,用责任链的模式把它们串起来,让它们各自完成各自的工作,这样就很好的实现了类似Http协议的分层的思想,每个拦截都实现了Interceptor接口,重写intercept()方法,返回各自对应的Response。

到这里,要理清OkHttp的各种策略只需要去看对应的Interceptor就可以了,我只看了其中的复用连接池策略、缓存策略这两种….下面就逐一的来看看是怎样实现的吧。

复用连接池:

创建一个TCP连接需要3次握手,而释放连接则需要2次或4次握手,如果每个请求都重新走一遍创建和销毁的流程,那是很费时和很费资源的事情,所以OkHttp通过复用连接池的方式实现了Socket连接的重用。说到这,还得补充一下,OkHttp还支持SPDY黑科技,什么是SPDY呢?引述《图解HTTP》的原文:

使用SPDY后,HTTP协议额外获得以下功能:多路复用流通过单一的TCP连接,可以无限制处理多个HTTP请求。所有请求的处理都在一条TCP连接上完成,因此TCP的处理效率得到提高。赋予请求优先级SPDY不仅可以无限制地并发处理请求,还可以给请求逐个分配优先级顺序。这样主要是为了在发生多个请求时,解决因带宽低而导致响应变慢的问题。压缩HTTP首部压缩HTTP请求和响应的首部,这样一来,通信产生的数据包数量和发送的字节数就更少了。推送功能支持服务器主动向客户端推送数据的功能。这样,服务器可直接发送数据,而不必等待客户端的请求。服务器提示功能服务器可以主动提示客户端请求所需的资源。由于在客户端发现资源之前就可以获知资源的存在,因此在资源已缓存的情况下,可以避免发送不必要的请求。

还是说回复用连接池的实现,具体的实现是在ConnectionPool中:

public final class ConnectionPool {  /**   * Background threads are used to cleanup expired connections. There will be at most a single   * thread running per connection pool. The thread pool executor permits the pool itself to be   * garbage collected.   */   //这个executor就是复用连接池(其实就是线程池)  private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,      Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,      new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));/**这个Runnable就是专门用来淘汰末位的socket,当满足以下条件时,就会进行末位淘汰,非常像GC1. 并发socket空闲连接超过5个2. 某个socket的keepalive时间大于5分钟**/ private final Runnable cleanupRunnable = new Runnable() {    @Override public void run() {      while (true) {        long waitNanos = cleanup(System.nanoTime());        if (waitNanos == -1) return;        if (waitNanos > 0) {          long waitMillis = waitNanos / 1000000L;          waitNanos -= (waitMillis * 1000000L);          synchronized (ConnectionPool.this) {            try {              ConnectionPool.this.wait(waitMillis, (int) waitNanos);            } catch (InterruptedException ignored) {            }          }        }      }    }  };

可以看到具体的实现是在cleanup()里面:

long cleanup(long now) {    int inUseConnectionCount = 0;    int idleConnectionCount = 0;    RealConnection longestIdleConnection = null;    long longestIdleDurationNs = Long.MIN_VALUE;    // Find either a connection to evict, or the time that the next eviction is due.    synchronized (this) {      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {        RealConnection connection = i.next();         //找到每个连接的引用数        // If the connection is in use, keep searching.        if (pruneAndGetAllocationCount(connection, now) > 0) {          inUseConnectionCount++;          continue;        }        idleConnectionCount++;        // If the connection is ready to be evicted, we're done.        long idleDurationNs = now - connection.idleAtNanos;        if (idleDurationNs > longestIdleDurationNs) {          longestIdleDurationNs = idleDurationNs;          longestIdleConnection = connection;        }      }//最大并发连接大于5,或者keepalive时间超过5分钟      if (longestIdleDurationNs >= this.keepAliveDurationNs          || idleConnectionCount > this.maxIdleConnections) {        // We've found a connection to evict. Remove it from the list, then close it below (outside        // of the synchronized block).        connections.remove(longestIdleConnection);      } else if (idleConnectionCount > 0) {        // A connection will be ready to evict soon.        return keepAliveDurationNs - longestIdleDurationNs;      } else if (inUseConnectionCount > 0) {        // All connections are in use. It'll be at least the keep alive duration 'til we run again.        return keepAliveDurationNs;      } else {        // No connections, idle or in use.        cleanupRunning = false;        return -1;      }    }    closeQuietly(longestIdleConnection.socket());    // Cleanup again immediately.    return 0;  }

复用连接池对于Socket的清理就类似于Java的引用计数法。

缓存策略:

先来说一下HTTP协议中的Cache-Control,服务器跟客户端的缓存都是通过这个HTTP的首部字段Cache-Control来实现的,下面引述《图解HTTP》原文:

举个栗子-> Cache-Control:private,max-age=0,no-cache这是Cache-Control的一般用法,就是通过在Cache-Control中设定一些参数达到控制缓存的效果.上面三个参数代表的意思分别是:private:响应只以特定的用户作为对象,缓存服务器会对该特定用户提供资源缓存的服务,对于其他用户发过来的请求,代理服务器则不会返回缓存;public则反之。max-age:当客户端发送的请求中包含max-age指令时,如果判定缓存资源的缓存时间数值没有比指定时间的数值更小,那么客户端就接收缓存的资源,否则代理服务器向源服务器发送该请求;当max-age的值为0时,那么缓存服务器通常需要将请求转发给源服务器。no-cache:使用no-cache指令的目的是为了防止从缓存中返回过期的资源。除了上面三个还有下面几个常用的指令.......no-store:与no-cache容易混淆,当使用no-store指令时,暗示请求或响应中包含机密信息,所以该指令规定缓存不能在本地存储请求或响应的任一部分,no-store才是真正的不进行缓存。max-stale:表示即使缓存过期也照常接收,max-stale后面是具体数值单位为秒min-fresh:指令要求缓存服务器返回至少还未过指定时间的缓存资源。比如min-fresh=60(单位:秒),在这60秒以内如果有超过有效期的资源都无法作为响应返回了.....还有几个指令就不介绍了,感兴趣的同学可以买本《图解HTTP》看看哦

好了,说了那么多,回到OkHttp的缓存实现中来,OkHttp的缓存是通过CacheControl来实现的:

 private CacheControl(boolean noCache, boolean noStore, int maxAgeSeconds, int sMaxAgeSeconds,      boolean isPrivate, boolean isPublic, boolean mustRevalidate, int maxStaleSeconds,      int minFreshSeconds, boolean onlyIfCached, boolean noTransform, boolean immutable,      @Nullable String headerValue) {    this.noCache = noCache;    this.noStore = noStore;    this.maxAgeSeconds = maxAgeSeconds;    this.sMaxAgeSeconds = sMaxAgeSeconds;    this.isPrivate = isPrivate;    this.isPublic = isPublic;    this.mustRevalidate = mustRevalidate;    this.maxStaleSeconds = maxStaleSeconds;    this.minFreshSeconds = minFreshSeconds;    this.onlyIfCached = onlyIfCached;    this.noTransform = noTransform;    this.immutable = immutable;    this.headerValue = headerValue;  }  CacheControl(Builder builder) {    this.noCache = builder.noCache;    this.noStore = builder.noStore;    this.maxAgeSeconds = builder.maxAgeSeconds;    this.sMaxAgeSeconds = -1;    this.isPrivate = false;    this.isPublic = false;    this.mustRevalidate = false;    this.maxStaleSeconds = builder.maxStaleSeconds;    this.minFreshSeconds = builder.minFreshSeconds;    this.onlyIfCached = builder.onlyIfCached;    this.noTransform = builder.noTransform;    this.immutable = builder.immutable;  }

在CacheControl的构造方法里,可以看到刚才上面介绍的几个cache-control指令的身影。通过构建一个CacheControl的Builder,然后再把这个CacheControl加入CacheInterceptor拦截中,就可以轻松的对缓存进行控制(至于具体怎么添加拦截就请自行百度或google了,细心的同学会发现CacheInterceptor也就是责任链的其中一环,所以在构建Request的时候加上自定义的CacheControl就可以了。 )

其实一般情况下我们都不会去给Request加自定义的CacheControl,但是如果服务器端是外包给别人的,而你却又找不到那些服务端开发的人,那么这时候就只能靠自己了……

其实只要抓住责任链这一主要的设计,看OkHttp的源码也就轻松很多了,因为每个Interceptor拦截都对应着一个Control,Control是具体实现这些拦截的地方,而每个Interceptor则是拦截各自负责的请求并返回处理结果,读后感就写这么多了。

参考文章:
《图解HTTP》这是一本好书!
感谢这位大神
感谢另一位大神

原创粉丝点击