Android网络基础知识

网络体系

  • 主要关注TCP/IP模型,了解OSI模型即可


    image.png

TCP/IP协议族

TCP/IP概念层模型 功能 TCP/IP协议族
文件传输,电子邮件,文件服务 FTP、HTTP、DNS
应用层 数据格式化,代码格式化,数据加密
解除或建立与别的接点的联系
传输层 提供端对端的接口 TCP/UDP
网络层 为数据包选择路由 IP、ICMP、RIP
链路层 传输有地址的帧以及错误检测功能 PPP、ARP、RARP
以二进制数据形式在物理媒体上传输数据 ISO2110、IEEE802
image.png
  • TCP:面向连接的、可靠的流协议
  • UDP:面向无连接的通讯协议
  • IP:源地址和目的地址之间传送的数据包
  • ICMP:控制报文协议
  • IGMP:Internet组管理协议
  • ARP:地址解析协议
  • RARP:反向地址转化协议

端口号

端口号规定为16位,即允许一个IP主机有2的16次方65535个不同的端口。其中

  • 0~1023:分配给系统的端口号
  • 1024~49151:登记端口号,主要是让第三方应用使用
  • 49152~65535:短暂端口号,是留给客户进程选择暂时使用,一个进程使用完就可以供其他进程使用。

Socket使用时,可以用1024~65535的端口号

三次握手和四次挥手

三次握手

  • 为什么是三次握手
    TCP是面向连接的,所以需要双方都确认连接的建立。三次握手目的是交换tcp通信的初始序号,如果是两次握手,只能交换一方的tcp通信序号,四次又属于浪费


    image.png
  • 第一次握手
    客户端请求建立连接,发送SYN包,并进入SYN_SENT状态,等待服务器确认
  • 第二次握手
    服务端应答客户端,并请求建立连接,服务器进入SYN_RECV状态
  • 第三次握手
    客户端进入ESTABLISHED,并针对服务端请求确认应答,发送完毕,服务器进入ESTABLISHED

三次握手的漏洞
syn洪泛攻击

  • 定义
    通过网络服务所在的端口发送大量伪造原地址的攻击报文,发送到服务端,造成服务端上的半开连接队列被占满,从而阻止其他用户进行访问
  • 原理
    攻击者客户端利用伪造的IP地址向服务端发出请求(第一次握手),而服务端的响应(第二次握手)的报文将永远发送不到真实的客户端,服务端在等待客户端的第三次握手(永远都不会有的),服务端在等待这种半开的连接过程中消耗了资源,如果有成千上万的这种连接,主机资源将被耗尽,从而达到攻击的目的
  • 解决方案
    1.无效连接监控释放
    2.延缓TCB分配方法
    3.防火墙

四次挥手

  • 断开一个TCP连接时,需要客户端和服务端总共发送4个以确认连接的断开
  • 为什么需要四次挥手
    TCP是双全工(客户端和服务器端可以相互发送和接收请求),所以需要双方都确认关闭连接


    image.png
  • 第一次挥手:客户端发送关闭请求
  • 第二次挥手:服务器响应客户端关闭请求
  • 第三次挥手:服务端发送关闭请求
  • 第四次挥手:客户端发送关闭请求

TCP/IP中的数据包

image.png

滑动窗口

  • 发送方和接收方都会维护一个数据帧的序列,这个序列被称为窗口
  • 发送方的窗口大小由接收方确认

目的

  • 确保数据不丢失
    如果发送的数据丢失了可以重新发送
  • 控制发送速度
    防止接收方的缓存不够大导致溢出,同时控制流量也可以避免网络堵塞

HTTP协议

  • HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World Wide Web )服务器传输超文本到本地浏览器的传送协议
Http的工作方式

1、浏览器
用户输入地址后回车或者点击链接->浏览器拼装Http报文并发送请求给服务器->服务器处理请求后发送响应报文给浏览器->浏览器解析响应报文并使用渲染引擎显示到界面
2、手机App
用户点击或界面自动触发联网需求->android代码调用拼装HTTP报文并发送请求到服务器->服务器处理请求后发送响应报文给手机->Android代码处理响应报文并作出响应处理(如存储数据、加工数据、显示数据到界面)

URL

URL格式

image.png

  • schema协议: http/https/ftp.
  • host: web服务器的ip地址或者域名
  • port: 服务端端口, http默认访问的端口是80
  • path: 资源访问路径
  • query-string: 查询参数
    public static void main(String[] args) throws MalformedURLException {
        URL url = new URL("https://www.baidu.com:443");
        URL relativeUrl = new URL(url, "/s?wd=peakmain&rsv_spt=1#name=peakmain1");
        System.out.println("协议:" + relativeUrl.getProtocol());
        System.out.println("主机:" + relativeUrl.getHost());
        System.out.println("端口:" + relativeUrl.getPort());
        System.out.println("文件路径:" + relativeUrl.getPath());
        System.out.println("相对路径:" + relativeUrl.getRef());
        System.out.println("查询的字符串:" + relativeUrl.getQuery());
    }
HTTP请求的传输过程
image.png
一次完整http请求过程

首先进行DNS域名解析

  • 三次握手建立TCP连接
  • 客户端向服务器发送请求命令:get/www.baidu.com/http/1.1
  • 客户端发送请求头信息
  • 服务器应答Http/1.1 200 ok
  • 返回响应头信息
  • 服务器向客户端发送数据
  • 服务器关闭TCP连接
报文格式

请求报文


image.png

响应报文


image.png
请求方式

1、GET

  • 用于获取资源
  • 对服务器数据不进行修改
  • 不发送body,
GET  /article/list/0/json HTTP/1.1
Host:www.wanandroid.com

对应的retortfit

    @GET("/article/list/{page}/json")
    fun getArticleList(@Path("page") page: Int): Observable<BaseEntity<Any>>

2、POST

  • 用于增加或修改资源
  • 发送给服务器内容写在body
POST  /user/login
Host: www.wanandroid.com
content-type: application/json;charset=UTF-8
name=peakmain&password=123456

对应的retorfit

    @POST("/user/login")
    @FormUrlEncoded
    fun login(
        @Field("username") username: String?,
        @Field("password") password: String?
    ): Observable<BaseEntity<Any>>

3、PUT

  • 用于修改资源
  • 发送服务器的内容写在body内容
PUT   /user/login
Host: www.wanandroid.com
content-type: application/json;charset=UTF-8
name=peakmain&password=123456

对应的retorfit

    @PUT("/user/login")
    @FormUrlEncoded
    fun login(
        @Field("username") username: String?,
        @Field("password") password: String?
    ): Observable<BaseEntity<Any>>

4、DELETE

  • 用于删除资源
  • 不发送body

5、HEAD

  • 和GET使用方法完全相同
  • 和GET唯一区别在于,返回的响应中没有body

这里需要注意,只有post请求不是幂等,其他请求都是幂等

Status Code状态码

三位数字,⽤于对响应结果做出类型化描述(如「获取成功」「内容未找到」)

  • 1xx:临时性消息。如:100 (继续发送)、101(正在切换协议)
  • 2xx:成功。最典型的是 200(OK)、201(创建成功)
  • 3xx:重定向。如 301(永久移动)、302(暂时移动)、304(内容未改变)
  • 4xx:客户端错误。如 400(客户端请求错误)、401(认证失败)、403(被禁⽌)、404(找不到内容)
  • 5xx:服务器错误。如 500(服务器内部错误)
Headers
  • Cache-Control:指定请求和响应遵循缓存机制

    • max-age:缓存的内容将在xx秒后失效
    • private:客户端可以缓存
    • public:客户端和代理服务器都可缓存
    • no-cache:指示请求不能缓存
  • Connection:表示是否需要持久连接,Http 1.1默认是持久连接

  • Accept-Encoding: 客户端接受的压缩编码类型。如 gzip

  • Range:可以请求实体的一个或者多个子范围

    • 表示头500个字节:bytes=0-499
    • 表示第二个500字节:bytes=500-999
    • 表示最后500个字节:bytes=-500
    • 表示500字节以后的范围:bytes=500-
    • 第一个字节和最后一个字节:bytes=0-0,-1
  • Accept-Charset: 客户端接受的字符集。如 utf-8

  • Content-Encoding:压缩类型。如 gzip

  • Content-Type:指定Body的类型。主要类型有四类

    • text/html
    • x-www-form-urlencoded
    • multitype/form-data
    • application/json,image/jpeg,application/zip...
  • Content-Length:指定body的长度字节

http缓存机制和原理
image.png
  • 对比缓存:缓存标识(Last-Modified/If-Modified-Since)发送到服务器进行比较,如果缓存一样,返回304,如果变了就返回数据

    • 服务器返回数据的时候,会返回Last-Modified,下次客户端请求,服务器通过If-Modified-Since进行时间对比(If-Modified-Since是否大于Last-Modified,大于等于则表示资源修改过,没有则返回304)
  • Etag 主要为了解决 Last-Modified 无法解决的一些问题。当前资源在服务器的唯一标识UUID(生成规则由服务器规定)

  • 客户端第一次请求的时候,服务器会返回Etag,下次请求客户端会发送一个if-None-Match,服务器会比较Etag和If-None-Match,如果两个相同,则返回304,否则返回200

  • 强制缓存:

    • Expires:服务器返回给客户端的到期时间,Http1.0
    • Cache-Control:Http1.1 弥补Expires的缺陷而设计的

Http/Https

Http Https
数据都是未加密的,也就是明文 对Http协议传输的数据进行加密(SSL),保证会话过程中的安全性
TCP端口默认为:80 TCP端口默认为443
SSL协议加密方式

SSL协议用到对称加密和非对称加密(公钥加密),在建立传输链路时,SSL首先对对称加密的秘钥使用公钥进行非对称加密,链路建立好之后,SSL对传输内容使用对称加密

对称加密 非对称加密
使⽤公钥对数据进⾏加密得到密⽂;使⽤私钥对数据进⾏解密得到原数据 通信双⽅使⽤同⼀个密钥,使⽤加密算法配合上密钥来加密,解密时使⽤加密过程的完全逆过程配合密钥来进⾏解
速度快,可加密内容较大 加密速度慢,能提供更好的身份认证技术
DES、AES RSA、背包算法
窃听风险/篡改风险/冒充风险
Https单向认证
image.png
Https双向认证
image.png

OKHttp

优点

  • 支持HTTP/2并允许对同一主机的所有请求共享一个套接字
  • 通过连接池,减少了请求延迟
  • 默认通过GZip压缩数据
  • 响应缓存,避免了重复请求的网络
  • 请求失败自动重试主机的其他ip,自动重定向
调用流程
image.png
  • 分发器:内部维护队列与线程池,完成请求调配;
  • 拦截器:五大默认拦截器完成整个请求过程
分发器

异步请求工作流程

image.png

什么条件加入到runningAsyncCalls

  void enqueue(AsyncCall call) {
    synchronized (this) {
      readyAsyncCalls.add(call);
    }
    promoteAndExecute();
  }
  private boolean promoteAndExecute() {
    assert (!Thread.holdsLock(this));

    List<AsyncCall> executableCalls = new ArrayList<>();
    boolean isRunning;
    synchronized (this) {
      for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
        AsyncCall asyncCall = i.next();

        if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
        if (runningCallsForHost(asyncCall) >= maxRequestsPerHost) continue; // Host max capacity.

        i.remove();
        executableCalls.add(asyncCall);
        runningAsyncCalls.add(asyncCall);
      }
      isRunning = runningCallsCount() > 0;
    }

    for (int i = 0, size = executableCalls.size(); i < size; i++) {
      AsyncCall asyncCall = executableCalls.get(i);
      asyncCall.executeOn(executorService());
    }

    return isRunning;
  }
  • 首先会将AsyncCall加入到readyAsyncCalls中
  • 遍历所有readyAsyncCalls,当runingAsyncCalls大于64或者统一域名正在请求的个数大于5就直接返回
  • 当满足条件,将AsyncTask加入到runningAsyncCalls并加入executableCalls集合中
  • 将任务放到线程池里面进行执行

Ready什么条件加入running

    @Override protected void execute() {
      boolean signalledCallback = false;
      timeout.enter();
      try {
        Response response = getResponseWithInterceptorChain();
      } finally {
        client.dispatcher().finished(this);
      }
    }
  }
  void finished(AsyncCall call) {
    finished(runningAsyncCalls, call);
  }
  private <T> void finished(Deque<T> calls, T call) {
    Runnable idleCallback;
    synchronized (this) {
      if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
      idleCallback = this.idleCallback;
    }

    boolean isRunning = promoteAndExecute();

    if (!isRunning && idleCallback != null) {
      idleCallback.run();
    }
  }

  • 首先从runningAsyncCalls中移除AsyncCall
  • 之后又走到promoteAndExecute方法,继续从readyAsyncCalls取出数据进行处理
线程池的execute
  • 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务
  • 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务加入队列
  • 如果队列满了,而且正在运行的线程数量小于maximumPoolSize,那么还要创建线程运行这个任务
  • 如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会抛出异常
OkHttp的线程池
  public synchronized ExecutorService executorService() {
    if (executorService == null) {
      executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
          new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
    }
    return executorService;
  }
  • 核心线程数为0,最大核心线程数为Integer.MAX_VALUE,代表永不会缓存线程
  • 60, TimeUnit.SECONDS代表当一个线程无事可做,超过一定的时间(60s),线程会判断,如果当前运行的线程数大于corePoolSiz,那么这个线程就会被停掉,所以线程池所有的任务完成后,它最终会收缩到corePoolSize的大小
  • Okhttp的线程池工作行为:无等待,最大并发
队列 特点
ArrayBlockingQueue 基于数组的阻塞队列,初始化需要指定固定的大小
LinkedBlockingQueue 基于链表实现的阻塞队列,初始化可以指定大小,也可以不指定
SynchronousQueue 无容量队列
五大拦截器
  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, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    return chain.proceed(originalRequest);
  }

执行流程

image.png

  • 重试拦截器判断用户是否取消了请求,在获得了结果之后,会根据响应码判断是否需要重定向,如果满足条件那么就会重启执行所有拦截器
  • 桥接拦截器负责将HTTP协议必备的请求头加入其中并添加一些默认的行为(如:GZIP压缩),在获得结果后,调用保存cookie接口并解析GZIP数据
  • 缓存拦截器,读取并判断是否使用缓存,获得结果后判断是否缓存
  • 连接拦截器负责找到或者新建一个连接,并获得对应的socket流,在获得结果后不进行额外的处理
  • 请求服务器拦截器进行真正的服务器的通信,向服务器发送数据,解析读取的响应数据

一、重定向与重试拦截器
1、重试的判定

  @Override public Response intercept(Chain chain) throws IOException {
          try {
          //请求出现了异常,那么releaseConnection 依然为true
        response = realChain.proceed(request, streamAllocation, null, null);
        releaseConnection = false;
      } catch (RouteException e) {
       //连接未成功,请求还没有发出去
        if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
          throw e.getFirstConnectException();
        }
        releaseConnection = false;
        continue;
      } catch (IOException e) {
        //请求发出去了,但是和服务器通信失败了(socket流正在读写数据的时候断开连接)
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
        releaseConnection = false;
        continue;
      } finally {
        // 
        if (releaseConnection) {
          streamAllocation.streamFailed(null);
          streamAllocation.release();
        }
      }
   }
  private boolean recover(IOException e, StreamAllocation streamAllocation,
      boolean requestSendStarted, Request userRequest) {
    streamAllocation.streamFailed(e);

    // 1、在配置OkHttpClient时设置了不允许重试(默认允许),则一旦发送请求失败不再重试
    if (!client.retryOnConnectionFailure()) return false;

    // requestSendStarted 只存在于HTTP2,暂不考虑
    if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;

    // 2、判断是不是属于重试的异常
    if (!isRecoverable(e, requestSendStarted)) return false;

    // 3、是否存在更多的路由,比如设置了代理,DNS解析可能存在多个IP
    if (!streamAllocation.hasMoreRoutes()) return false;
    // For failure recovery, use the same route selector with a new connection.
    return true;
  }
  private boolean isRecoverable(IOException e, boolean requestSendStarted) {
    //1、是不是协议的异常
    if (e instanceof ProtocolException) {
      return false;
    }

    // 2、是不是Socket超时异常
    if (e instanceof InterruptedIOException) {
      return e instanceof SocketTimeoutException && !requestSendStarted;
    }

    // 3、是不是SSL格式异常
    if (e instanceof SSLHandshakeException) {
      if (e.getCause() instanceof CertificateException) {
        return false;
      }
    }
  //4、是不是SSL证书异常
    if (e instanceof SSLPeerUnverifiedException) {
      return false;
    }
    return true;
  }
image.png

2、重定向的判定

 @Override public Response intercept(Chain chain) throws IOException {
   try {
       followUp = followUpRequest(response, streamAllocation.route());
     } catch (IOException e) {
       streamAllocation.release();
       throw e;
     }
  }
private Request followUpRequest(Response userResponse, Route route) throws IOException {
   switch (responseCode) {
     case HTTP_PROXY_AUTH:
       //407
       Proxy selectedProxy = route != null
           ? route.proxy()
           : client.proxy();
       if (selectedProxy.type() != Proxy.Type.HTTP) {
         throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
       }
      //加入身份验证的Request
       return client.proxyAuthenticator().authenticate(route, userResponse);
     case HTTP_UNAUTHORIZED: 
        //401
       return client.authenticator().authenticate(route, userResponse);
     case HTTP_PERM_REDIRECT:
     case HTTP_TEMP_REDIRECT:
         //307或者308,不是GET而且不是HEAD直接返回null
       if (!method.equals("GET") && !method.equals("HEAD")) {
         return null;
       }
     case HTTP_MULT_CHOICE:
     case HTTP_MOVED_PERM:
     case HTTP_MOVED_TEMP:
     case HTTP_SEE_OTHER:
       // 300、301、302、303
       if (!client.followRedirects()) return null;
     //获取Location
       String location = userResponse.header("Location");
       if (location == null) return null;
       HttpUrl url = userResponse.request().url().resolve(location);
       if (url == null) return null;
       if (!sameScheme && !client.followSslRedirects()) return null;
       return requestBuilder.url(url).build();

     case HTTP_CLIENT_TIMEOUT:
       //408是否请求超时
       //设置允许自动重试
       if (!client.retryOnConnectionFailure()) {
         return null;
       }
    //是不是响应408的重试结果
       if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
         return null;
       }
    //未响应Retry-After
       if (userResponse.priorResponse() != null
           && userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
         return null;
       }
         //Retry-After是否大于0
       if (retryAfter(userResponse, 0) > 0) {
         return null;
       }
       return userResponse.request();

     case HTTP_UNAVAILABLE:
      //503
        //是不是响应503的重试结果
       if (userResponse.priorResponse() != null
           && userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
         return null;
       }
       //retryAfter是否等于0
       if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
         return userResponse.request();
       }
       return null;
     default:
       return null;
   }
 }
响应码 说明 重定向条件
407 代理需要授权,如付费代理,需要验证身份 通过proxyAuthenticator获取到Request,例: 添加 Proxy-Authorization 请求头
401 服务器需要授权(不安全)
300、301、302、303、307、308 重定向响应 307和308必须要为GET/HEAD请求再继续判断
1、用户允许自动重定向(默认允许)
2、能够获得Location响应头,并且值为有效url
3、如果重定向需要http和https间切换,需要允许(默认允许)
408 请求超时 1、用户允许自动重试
2、本次请求的结果不是响应408的重试结果
3、服务器未响应Retry-After(稍后重试)或者响应Retry-After:0
503 服务不可用 1、本次请求的结果不是响应503的重试结果
2、服务器明确响应Retry-After:0,立即重试

二、桥接拦截器
补全请求头

请求头 说明
Content-Type 请求体类型,如:application/x-www-form-urlencoded
Content-Length/Transfer-Encoding 请求体解析方式
Host 请求的主机站点
Connection:Keep-Alive 默认保持长连接
Accept-Encoding:gzip 接收机响应体使用gzip压缩
Cookie Cookie身体识别
User-Agent 用户信息,如操作系统,浏览器等

得到响应:

  • 读取Set-Cookie响应头并调用接口告知用户,在下次请求则会读取对应的数据设置进入请求头,默认CookieJar无法实现
  • 响应头Content-Encoding为gzip,使用GzipSource包装便于解析

三、缓存拦截器
拦截器通过CacheStrategy判断使用缓存或发起网络请求。此对象中的networkRequest与cacheResponse分别代表需要发起请求或者直接使用缓存

networkRequest cacheResponse 说明
NULL Not NULL 直接使用缓存
Not NULL NULL 向服务器发送请求
NULL NULL 返回504
Not NULL Not NULL 发起请求,若得到304无修改,更新缓存响应并返回
  @Override public Response intercept(Chain chain) throws IOException {
     //1、通过url的md5数据,从文件缓存查找(get请求才有缓存)
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();
    //缓存策略:根据各种条件组成请求头
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
     //网络请求的Request
    Request networkRequest = strategy.networkRequest;
    //缓存的Response 
    Response cacheResponse = strategy.cacheResponse;

    if (cache != null) {
      cache.trackResponse(strategy);
    }

    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }
      //直接返回504
    if (networkRequest == null && cacheResponse == null) {
      return new Response.Builder()
          .request(chain.request())
          .protocol(Protocol.HTTP_1_1)
          .code(504)
          .message("Unsatisfiable Request (only-if-cached)")
          .body(Util.EMPTY_RESPONSE)
          .sentRequestAtMillis(-1L)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();
    }

    // 没有网络,直接使用缓存
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

    Response networkResponse = null;
    try {
      networkResponse = chain.proceed(networkRequest);
    } finally {
      if (networkResponse == null && cacheCandidate != null) {
        closeQuietly(cacheCandidate.body());
      }
    }
    if (cacheResponse != null) {
    //networkRequest和cacheResponse 都不为空
        //如果networkResponse等于304则更新缓存并返回
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        Response response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
            .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
            .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
        networkResponse.body().close();

        cache.trackConditionalCacheHit();
        cache.update(cacheResponse, response);
        return response;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }

    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    if (cache != null) {
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }

    return response;
  }

   private CacheStrategy getCandidate() {
      // 1、没有缓存,进行网络请求
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }

      // 2、Https请求,但是没有握手信息,进行网络请求
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }

      // 3、主要通过响应码以及头部缓存控制字段判断响应能不能缓存,不能缓存就进行网络请求
      if (!isCacheable(cacheResponse, request)) {
        return new CacheStrategy(request, null);
      }

      CacheControl requestCaching = request.cacheControl();
      //4、如果请求包含cacheControl:no-cache,需要与服务器验证缓存有效性
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }
      //5、如果缓存响应中Cache-Control:immutable,响应内容一直不会改变,可以使用缓存
      CacheControl responseCaching = cacheResponse.cacheControl();
      //6、根据缓存响应的控制缓存的响应头,判断是否允许使用缓存
      //6.1、获得缓存响应从创建到现在的时间
      long ageMillis = cacheResponseAge();
      //6.2获取这个响应有效缓存的时长
      long freshMillis = computeFreshnessLifetime();

      if (requestCaching.maxAgeSeconds() != -1) {
          //如果请求指定了max-age表示指定了资源缓存的最大时间
        freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
      }

      long minFreshMillis = 0;
      //6.3请求包含Cache-Control:min-fresh表示用户认为这个缓存有效的时长,假设本身缓存的新鲜度为:100ms,而缓存的最小新鲜度为10ms,那么缓存真正有效时间为90ms
      if (requestCaching.minFreshSeconds() != -1) {
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
      }

      long maxStaleMillis = 0;
      //6.4判断缓存的响应包含Cache-Control:must-revalidate(不可用过期资源),获得用户请求头包含Cache-Control:max-stal=[秒]缓存过期后仍有效的时长
      if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
      }
      //6.5判断换粗是否有效
      if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
        Response.Builder builder = cacheResponse.newBuilder();
        if (ageMillis + minFreshMillis >= freshMillis) {
          builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
        }
        long oneDayMillis = 24 * 60 * 60 * 1000L;
        if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
          builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
        }
        return new CacheStrategy(null, builder.build());
      }
     //7、缓存过期处理
      String conditionName;
      String conditionValue;
      if (etag != null) {
        conditionName = "If-None-Match";
        conditionValue = etag;
      } else if (lastModified != null) {
        conditionName = "If-Modified-Since";
        conditionValue = lastModifiedString;
      } else if (servedDate != null) {
        conditionName = "If-Modified-Since";
        conditionValue = servedDateString;
      } else {
        return new CacheStrategy(request, null); // No condition! Make a regular request.
      }

      Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
      Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);

      Request conditionalRequest = request.newBuilder()
          .headers(conditionalRequestHeaders.build())
          .build();
      return new CacheStrategy(conditionalRequest, cacheResponse);
    }

networkReuqest存在则优先发起网络请求,否则使用cacheResponse缓存,若都为NULL则请求失败

image.png

四、连接拦截器

image.png

  void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) {
      cleanupRunning = true;
      executor.execute(cleanupRunnable);
    }
    connections.add(connection);
  }
  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) {
            }
          }
        }
      }
    }
  };
  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;
        }
      }

      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;
  }
  • 新建连接,放入连接池
  • 启动清理任务,开始定时清理任务
  • 清除无用连接

五、请求服务拦截器
一般出现上传大容量请求体或者需要验证,代表需要先询问服务器是否接受发送请求体数据
Okhttp的做法

  • 如果服务器允许则返回100,客户端继续发送请求体
  • 如果服务器不允许则直接返回给用户
  • 同时服务器也可能会忽略此请求头,一直无法读取应答,此时抛出超时异常
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容