OkHttp阅读笔记(五)

2017-08-22  本文已影响104人  范锦浩

先上前几篇的地址
第一篇
第二篇
第三篇
第四篇
直接从上一篇开始,上一篇描述的是ConnectIntercept连接拦截器,其中提到了如果没有复用连接,那么需要新建一个连接。
首先要通过路由选择者来转换域名为IP,那么先看一下这个的实现

RouteSelector

路由选择者,顾名思义就是选择一个可用路由,一个请求要通过这个路由到达指定的主机,从而建立连接。
路由选择者就是为了选择一个合适的路径,接下来看一下细节

public Route next() throws IOException {
    // Compute the next route to attempt.
    // 判断当前是否有下一个可用的IP地址
    // 第一次进入的时候此处因为还未查找IP地址,则必然为false
    // 后续进入,比方说一开始查找IP地址的时候获得两个节点,然后第一个节点连接失败
    // 进行重连,此时会去使用第二个节点
    if (!hasNextInetSocketAddress()) {
      //要尝试通过代理去将域名转换为IP,并且存储起来
      //此处是查找下一个代理,默认只有NO_PROXY一个子项
      if (!hasNextProxy()) {
        //在获取节点的时候如果有的节点已经被标记过失败了,那么会优先跳过
        //但是跳过之后发现没有其它节点可用了,那么还是会使用这个节点
        //否则抛出无可用节点异常,这个会直接在onFailed回调
        if (!hasNextPostponed()) {
          throw new NoSuchElementException();
        }
        return nextPostponed();
      }
      //lastProxy则为NO_PROXY
      lastProxy = nextProxy();
      //在nextProxy中已经通过InetAddress通过Host获得对应IP和端口列表
    }
    //当前有可用的节点
    //这里一开始是获取InetAddress返回的第一个地址,但是计数会增长
    //后续进入相当于选择其它节点,前提是存在其他节点
    lastInetSocketAddress = nextInetSocketAddress();
    //构建一个路由
    Route route = new Route(address, lastProxy, lastInetSocketAddress);
    //当前查找的节点之前已经被标记失败了,一般这种都是在重连/重定向的场景
    //尽可能避免使用当前节点
    if (routeDatabase.shouldPostpone(route)) {
      postponedRoutes.add(route);//存储曾经被标记为失败的但是还是可以用的节点
      // We will only recurse in order to skip previously failed routes. They will be tried last.
      return next();
    }

    return route;
  }

这里其实体现了一个网络请求的基本域名转换原则:
1.首先查找可用的代理,默认是系统的DNS
2.通过当前代理来转换域名为IP,并且存储起来
3.使用下一个可用的IP构成路由
4.如果当前路由曾经请求失败了,那么最好先跳过当前路由,如果实在没有可以用的其他路由,那么再使用当前路由
接下来看一下域名转换的细节

private void resetNextInetSocketAddress(Proxy proxy) throws IOException {
    // Clear the addresses. Necessary if getAllByName() below throws!
    // 新建一个IP地址的列表
    inetSocketAddresses = new ArrayList<>();

    String socketHost;
    int socketPort;
    //NO_PROXY默认为DIRECT,则采用连接中的域名和端口
    if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.SOCKS) {
      socketHost = address.url().host();
      socketPort = address.url().port();
    } else {
      SocketAddress proxyAddress = proxy.address();
      if (!(proxyAddress instanceof InetSocketAddress)) {
        throw new IllegalArgumentException(
            "Proxy.address() is not an " + "InetSocketAddress: " + proxyAddress.getClass());
      }
      InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress;
      socketHost = getHostString(proxySocketAddress);
      socketPort = proxySocketAddress.getPort();
    }

    if (socketPort < 1 || socketPort > 65535) {
      throw new SocketException("No route to " + socketHost + ":" + socketPort
          + "; port is out of range");
    }

    if (proxy.type() == Proxy.Type.SOCKS) {
      inetSocketAddresses.add(InetSocketAddress.createUnresolved(socketHost, socketPort));
    } else {
      // Try each address for best behavior in mixed IPv4/IPv6 environments.
      // 这里的dns()实际上是可以在OkHttpClient.Builder中自己设置的,默认是DNS.SYSTEM
      // 内部会通过InetAddress.getAllByName(hostname)会返回IP和端口列表
      // 相当于默认的发起一个请求到DNS服务器,获取当前域名对应的IP地址
      List<InetAddress> addresses = address.dns().lookup(socketHost);
      //记录当前域名可能返回的所有IP地址和端口
      for (int i = 0, size = addresses.size(); i < size; i++) {
        InetAddress inetAddress = addresses.get(i);
        inetSocketAddresses.add(new InetSocketAddress(inetAddress, socketPort));
      }
    }
    //标记当前从第一个IP地址开始选择
    nextInetSocketAddressIndex = 0;
  }

OkHttpClient中有:dns = Dns.SYSTEM;

Dns SYSTEM = new Dns() {
    @Override public List<InetAddress> lookup(String hostname) throws UnknownHostException {
      if (hostname == null) throw new UnknownHostException("hostname == null");
      return Arrays.asList(InetAddress.getAllByName(hostname));//这里会返回IP、端口等数据的liebiao
    }
  };

实际上域名到IP的映射就是由DNS完成,默认的DNS就是平常说的DNS服务器,内部维持一张映射表这种概念,将域名转换为对应的IP地址。
实际使用中可能会存在有的DNS缓存导致个别节点失效的问题,这种的解决方案可以通过自定义dns类来添加映射方案,比方说在系统的请求失败之后重连的时候,使用自定的dns转换映射模式,这也是常说的HttpDNS方案。
到这里,路由已经确定了,那么接下来需要回到RealConnection中开始socket连接了

RealConnection

实际上在java中连接的建立都是通过socket,通过一个socket,指定服务器的IP地址和端口,这样就可以尝试去访问某一台主机了。看一下具体的细节

public void connect(
      int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled) {
    if (protocol != null) throw new IllegalStateException("already connected");

    RouteException routeException = null;
    //如果是Https协议,那么这里会涉及到一些加解密的套件,Http则为空
    //这里就是获取所支持的套件,可以通过在OkHttpClient中自定义
    List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
    //辅助进行加解密套件选择的选择器
    ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);

    if (route.address().sslSocketFactory() == null) {
      if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
        throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication not enabled for client"));
      }
      String host = route.address().url().host();
      if (!Platform.get().isCleartextTrafficPermitted(host)) {
        throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication to " + host + " not permitted by network security policy"));
      }
    }
    //尝试去建立socket连接,有的时候单次失败,可以继续尝试
    //除非在应用层指定了不可重连或者一些严重错误,重连没有意义
    while (true) {
      try {
        if (route.requiresTunnel()) {
          connectTunnel(connectTimeout, readTimeout, writeTimeout);
        } else {//一般来说都是NO_PROXY模式,则不会使用隧道,这里只是进行Scoket的连接
          connectSocket(connectTimeout, readTimeout);
          //至此TCP3次握手结束,并且初始化了双向流通道
        }
        //这里面主要是进行SSL握手,并且如果当前请求协议为Http2.0,则需要创建Http2Connection
        establishProtocol(connectionSpecSelector);
        break;
      } catch (IOException e) {
        //出现异常的时候先清理当前异常的连接数据
        closeQuietly(socket);
        closeQuietly(rawSocket);
        socket = null;
        rawSocket = null;
        source = null;
        sink = null;
        handshake = null;
        protocol = null;
        http2Connection = null;

        if (routeException == null) {
          routeException = new RouteException(e);
        } else {
          routeException.addConnectException(e);
        }
        // 如果在OkHttpClient中设置不允许重连,
        // 或者存在一些严重的错误,重连没有意义
        // 那么在一次连接失败之后就会结束
        if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
          throw routeException;
        }
      }
    }
    //如果创建的连接为HTTP2.0,不同于HTTP1相关的协议
    //HTTP2.0允许一个连接当中有多个流
    //Connection中默认最多StramAllocation为1
    //在Http2下可以为无限大
    if (http2Connection != null) {
      synchronized (connectionPool) {
        allocationLimit = http2Connection.maxConcurrentStreams();
      }
    }
  }

思路比较清晰
1.首先建立socket连接,首先是TCP的3次握手过程
2.然后如果当前请求协议为Https,那么要进行SSL的握手和校验流程
3.如果当前连接失败,在满足条件的情况下可以尝试重新进行连接
稍微看一下连接的实现细节:

private void connectSocket(int connectTimeout, int readTimeout) throws IOException {
    //这里一般说都是NO_PROXY
    Proxy proxy = route.proxy();
    Address address = route.address();
    //NO_PROXY模式为DIRECT,则需要通过Client中的SocketFactory来创建Socket
    //如果没有在Client中手动设置,默认使用DefaultSocketFactory
    //其实就是new socket()而已
    rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
        ? address.socketFactory().createSocket()
        : new Socket(proxy);
    //设置Socket中InputStream的读取最长时间,简单理解就是Server的响应时间
    rawSocket.setSoTimeout(readTimeout);
    try {
      //这里本质上就是socket.connect
      //内部实际上进行的就是传统的TCP3次握手,连接时长也就是握手成功最大时长
      //这里成功之后套接字连接就完成,接下来可以进行流的操作
      Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
    } catch (ConnectException e) {
      ConnectException ce = new ConnectException("Failed to connect to " + route.socketAddress());
      ce.initCause(e);
      throw ce;
    }
    //在TCP握手结束之后,连接已经建立,之后就是建立双向的流通道
    //这里通过Okio对于socket方来说建立输入和输出流
    //输入流用于接收服务端的数据,输出流用于向服务端写入数据
    source = Okio.buffer(Okio.source(rawSocket));
    sink = Okio.buffer(Okio.sink(rawSocket));
  }

@Override public void connectSocket(Socket socket, InetSocketAddress address,
      int connectTimeout) throws IOException {
    try {
      socket.connect(address, connectTimeout);
    } catch (AssertionError e) {
      if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
      throw e;
    } catch (SecurityException e) {
      // Before android 4.3, socket.connect could throw a SecurityException
      // if opening a socket resulted in an EACCES error.
      IOException ioException = new IOException("Exception in connect");
      ioException.initCause(e);
      throw ioException;
    }
  }

可以看到,在Android平台上,就是通过socket连接到之前选择的路由所对应的IP地址和端口的主机而已。
这里稍微注意一下几个时间的设置,这些都是可以在OkHttpClient中设置的属性

  //在ConnectInterceptor中调用,在建立连接之后来生成Codec
  public HttpCodec newCodec(
      OkHttpClient client, StreamAllocation streamAllocation) throws SocketException {
    //注意一个小区别,HTTP2在一次TCP连接中可能有多个流操作,对应的读取/写入超时时间应该从开始写入头部报文开始
    //此时一个流操作正式开始
    //对于HTTP1.1来说,一次TCP连接中虽然也可以有多次请求,但是流操作是在同一个流中进行的
    //那么完全可以在初始化的时候设置读取/写入超时时间
    if (http2Connection != null) {//HTTP2.0
      return new Http2Codec(client, streamAllocation, http2Connection);
    } else {
      socket.setSoTimeout(client.readTimeoutMillis());
      source.timeout().timeout(client.readTimeoutMillis(), MILLISECONDS);
      sink.timeout().timeout(client.writeTimeoutMillis(), MILLISECONDS);
      return new Http1Codec(client, streamAllocation, source, sink);
    }
  }

      //socket连接超时时间
      connectTimeout = 10_000;
      //从服务端读取响应报文的超时时间
      readTimeout = 10_000;
      //向服务端写入请求报文的超时时间
      writeTimeout = 10_000;

接下来看一下TLS的处理相关:

private void connectTls(ConnectionSpecSelector connectionSpecSelector) throws IOException {
    Address address = route.address();
    //获得SSLSocket的工厂,这个默认为DefaultSocketFactory
    //内部没有任何策略,只有创建socket而已
    SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
    boolean success = false;
    SSLSocket sslSocket = null;
    try {
      // Create the wrapper over the connected socket.
      // 注意此时的Socket已经完成握手过程,这里通过SSLSocketFactory的策略来构建对应的SSLSocket
      // 如果没有手动设置,默认可以看OkHttpClient中的DefaultSslSocketFactory
      sslSocket = (SSLSocket) sslSocketFactory.createSocket(
          rawSocket, address.url().host(), address.url().port(), true /* autoClose */);

      // Configure the socket's ciphers, TLS versions, and extensions.
      // ConnectionSpec的默认值可以看OkHttpClient中DEFAULT_CONNECTION_SPECS
      // 此处主要是查找满足SSLSocket的TLS版本和加解密套件,并且关联SSLSocket
      ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
      // 此处必然为true
      if (connectionSpec.supportsTlsExtensions()) {
        //注意这里的实现类是AndroidPlatform
        //这里面主要是通过反射,然后用SSLSocket调用一些方法,来实现一些设置
        //比方Session、Host和ALPN,细节不太清楚
        Platform.get().configureTlsExtensions(
            sslSocket, address.url().host(), address.protocols());
      }

      // Force handshake. This can throw!
      //开始TLS/SSL握手流程,
      sslSocket.startHandshake();
      //注意getSession会阻塞直到握手完成,此时获取握手的信息
      //这个Session包括服务端返回的TLS版本、Cipher组件、证书等信息
      Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession());

      // Verify that the socket's certificates are acceptable for the target host.
      // SSL握手完成之后,要尝试验证主机名,如果严重失败会抛出异常,则会一路回调到onFailure中
      if (!address.hostnameVerifier().verify(address.url().host(), sslSocket.getSession())) {
        X509Certificate cert = (X509Certificate) unverifiedHandshake.peerCertificates().get(0);
        throw new SSLPeerUnverifiedException("Hostname " + address.url().host() + " not verified:"
            + "\n    certificate: " + CertificatePinner.pin(cert)
            + "\n    DN: " + cert.getSubjectDN().getName()
            + "\n    subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
      }

      // Check that the certificate pinner is satisfied by the certificates presented.
      // CertificatePining证书锁定,用于校验证书的有效性,避免一些中间人攻击
      // 在完成主机名验证之后,此处进行的是证书认证
      // 具体可以看CertificatePinner的注释中的demo
      address.certificatePinner().check(address.url().host(),
          unverifiedHandshake.peerCertificates());

      // Success! Save the handshake and the ALPN protocol.
      // 尝试通过反射getAlpnSelectedProtocol获取ALPN协议并保存
      String maybeProtocol = connectionSpec.supportsTlsExtensions()
          ? Platform.get().getSelectedProtocol(sslSocket)
          : null;
      socket = sslSocket;
      source = Okio.buffer(Okio.source(socket));
      sink = Okio.buffer(Okio.sink(socket));
      handshake = unverifiedHandshake;
      protocol = maybeProtocol != null
          ? Protocol.get(maybeProtocol)
          : Protocol.HTTP_1_1;
      success = true;
    } catch (AssertionError e) {
      if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
      throw e;
    } finally {
      if (sslSocket != null) {
        Platform.get().afterHandshake(sslSocket);
      }
      if (!success) {
        closeQuietly(sslSocket);
      }
    }
  }

假设当前为Https请求:
1.进行SSL握手流程,这个主要就是生成随机密钥,证书校验这些流程,在握手完成之后,当前Https连接建立就完成了。
2.进行主机名校验,这个校验可以在OkHttpClient中自定义。默认的实现是校验证书中的域名或者IP
3.进行证书校验,这个校验可以在OkHttpClient中自定义。通过指定公钥来对应一个签名证书,使用的时候可能会导致服务端没有办法随意更换证书。
至此,ConnectInterceptor的工作就差不多了,说了那么多,其实就是获取连接这个工作,接着看下一个拦截器吧:

if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    //在已经建立的链接上进行参数发送和获取响应封装等操作
    interceptors.add(new CallServerInterceptor(forWebSocket));

NetworkInterceptors

这个其实也是自定义拦截器,在OkHttpClient.Builder中设置

public Builder addNetworkInterceptor(Interceptor interceptor) {
      networkInterceptors.add(interceptor);
      return this;
    }

其实之前也提到过一个自定义拦截器,但是那个拦截器的处理时机是整个请求的开始和结束,NetworkInterceptor则有所不同,在ConnectInterceptor之后执行,这意味着当前的连接已经建立,这个的意义个人觉得就是在可以第一时间在连接后和响应后进行数据的处理,比方说修改响应的结果,在请求之前修改请求头部报文之类的操作。
因为自定义拦截器在重试和跟随拦截器之前执行,那么不具有修改响应的功能。
接着看最后一个拦截器CallServerInterceptor吧:

CallServerInterceptor

调用服务端拦截器,其实就是和服务端进行通信,主要就是发送请求报文和接收响应报文这两个操作

@Override public Response intercept(Chain chain) throws IOException {
    //获得在RealInterceptorChain中的基本参数
    HttpCodec httpCodec = ((RealInterceptorChain) chain).httpStream();
    StreamAllocation streamAllocation = ((RealInterceptorChain) chain).streamAllocation();
    Request request = chain.request();

    long sentRequestMillis = System.currentTimeMillis();
    //到这里之前socket连接已经建立,流通道也已经准备完成
    // 开始将请求报文的一部分写入流中
    // 起始行
    // 头部报文
    httpCodec.writeRequestHeaders(request);

    Response.Builder responseBuilder = null;
    //验证头部请求method合法且具有请求体内容,比方此时是POST,那么需要一个正文体
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      // If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
      // Continue" response before transmitting the request body. If we don't get that, return what
      // we did get (such as a 4xx response) without ever transmitting the request body.
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        httpCodec.flushRequest();
        responseBuilder = httpCodec.readResponseHeaders(true);
      }

      // Write the request body, unless an "Expect: 100-continue" expectation failed.
      if (responseBuilder == null) {//正常情况下,将requestBody写进输出流,这里一般就是往服务端传POST的参数
        //构建一个可以用于承载一定数量字节的流
        Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
        //使用缓冲流,简单理解就是类似于BufferedStream
        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
        //将request中的body写入sink中
        request.body().writeTo(bufferedRequestBody);
        //关闭流
        bufferedRequestBody.close();
      }
    }
    //关闭输出流,这里可以认为一次请求的发送已经完成,剩下的就是等待服务端的返回响应
    httpCodec.finishRequest();

    if (responseBuilder == null) {
      //此处是获得响应报文的起始行,method code reason
      //然后再读取头部报文
      responseBuilder = httpCodec.readResponseHeaders(false);
    }
    //到此处如果responseBuilder不为空,说明已经正常接收到起始行和头部报文
    //可以设置握手结果、发送时间和接收到响应的时间
    Response response = responseBuilder
        .request(request)
        .handshake(streamAllocation.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

    int code = response.code();
    if (forWebSocket && code == 101) {
      // Connection is upgrading, but we need to ensure interceptors see a non-null response body.
      response = response.newBuilder()
          .body(Util.EMPTY_RESPONSE)
          .build();
    } else {
      //设置响应中的正文体,注意此处只是关联了source供应流而已
      //大致也可以理解OkHttp的回调默认是在子线程中,从该线程中获取实际的body
      //比方说String类型是要经过IO操作的,所以说回调是合理的
      response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))
          .build();
      //至此整个网络请求流程已经完成
    }

    if ("close".equalsIgnoreCase(response.request().header("Connection"))
        || "close".equalsIgnoreCase(response.header("Connection"))) {
      //如果当前请求或者响应报文中指定当前为短连接,那么当前连接不应该进入连接池去复用
      streamAllocation.noNewStreams();
    }
    //204和205是无内容状态码,所以此时不应该有正文体
    if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
      throw new ProtocolException(
          "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
    }

    return response;
    //到这里一次请求以及响应的数据设置就全部完成,剩下的会按照调用链向上返回
  }

可以看到,报文的写入细节都委托到Codec里面执行了,而对于Http1.1来说,实现类为Http1Codec,首先看一下起始行和头部报文的写入格式

@Override public void writeRequestHeaders(Request request) throws IOException {
    //拼接请求报文中起始行数据
    String requestLine = RequestLine.get(
        request, streamAllocation.connection().route().proxy().type());
    //写入起始行和头部报文数据
    writeRequest(request.headers(), requestLine);
  }

public static String get(Request request, Proxy.Type proxyType) {
    //此处是拼接请求报文起始行
    StringBuilder result = new StringBuilder();
    //POST
    result.append(request.method());
    result.append(' ');
    //默认是NO_PROXY
    if (includeAuthorityInRequestLine(request, proxyType)) {
      result.append(request.url());
    } else {
      //一般是此处,Host+参数的格式
      result.append(requestPath(request.url()));
    }
    //最后拼接协议,注意有空格
    result.append(" HTTP/1.1");
    //总结一下请求报文的起始行格式,比方说请求http://www.hh.com?a=1的一个POST无代理HTTP1.1协议请求
    //此处注意空格,这个是协议的一部分
    //POST www.hh.com?a=1 HTTP1.1
    return result.toString();
  }

public void writeRequest(Headers headers, String requestLine) throws IOException {
    //稍微注意一下这个状态标记是用于处理当前报文的发送流程的,在HTTP1.1下请求报文要满足先协议报文后正文
    if (state != STATE_IDLE) throw new IllegalStateException("state: " + state);
    //起始行结束需要换行操作
    sink.writeUtf8(requestLine).writeUtf8("\r\n");
    //换行之后要开始写入头部报文数据
    for (int i = 0, size = headers.size(); i < size; i++) {
      //基本格式:
      //name: value
      //name1: value1
      //...
      sink.writeUtf8(headers.name(i))
          .writeUtf8(": ")
          .writeUtf8(headers.value(i))
          .writeUtf8("\r\n");
    }
    //最后与正文体之间的识别符是一个空行
    sink.writeUtf8("\r\n");
    //标记状态允许写入请求体
    state = STATE_OPEN_REQUEST_BODY;
  }

用一个例子总结:
POST www.bai.com?a=1 http1.1
Connection: Keep-Alive
Cache-Control: no-cache; no-store

这个就是起始行的协议格式。
接着看正文体的处理

@Override public Sink createRequestBody(Request request, long contentLength) {
    if ("chunked".equalsIgnoreCase(request.header("Transfer-Encoding"))) {
      // 此处可以回想一下BridgeInterceptor中的情况,Transfer-Encoding: chunked
      // 相当于在RequestBody中的Content-Length为-1,简单理解就是无法预测长度
      // Stream a request body of unknown length.
      // 会根据需要一直读
      return newChunkedSink();
    }

    if (contentLength != -1) {
      // 只能读取指定的字节数
      // Stream a request body of a known length.
      return newFixedLengthSink(contentLength);
    }

    throw new IllegalStateException(
        "Cannot stream a request body without chunked encoding or a known content length!");
  }

@Override public void write(Buffer source, long byteCount) throws IOException {
      if (closed) throw new IllegalStateException("closed");
      checkOffsetAndCount(source.size(), 0, byteCount);
      if (byteCount > bytesRemaining) {
        throw new ProtocolException("expected " + bytesRemaining
            + " bytes but received " + byteCount);
      }
      sink.write(source, byteCount);
      bytesRemaining -= byteCount;
    }

这里其实也没什么注意的,总之就是通过sink来写数据,注意到上面的全局变量sink其实就是当初在建立连接的时候创建的,也就是对应socket建立后的OutputStream,可以用于向建立连接后的服务端写入数据。
接着看接收响应报文的操作

@Override public Response.Builder readResponseHeaders(boolean expectContinue) throws IOException {
    //正常来说此时的状态都是STATE_OPEN_REQUEST_BODY
    if (state != STATE_OPEN_REQUEST_BODY && state != STATE_READ_RESPONSE_HEADERS) {
      throw new IllegalStateException("state: " + state);
    }
    //毫无疑问,此处是等待阻塞性操作
    try {
      //读取响应的起始行
      //parse内部是解析协议、状态码等参数,然后设置到StatusLine中,具体解析可以看parse函数
      StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict());

      Response.Builder responseBuilder = new Response.Builder()
          .protocol(statusLine.protocol)
          .code(statusLine.code)
          .message(statusLine.message)
          .headers(readHeaders());//此处是读取并解析响应的头部报文

      if (expectContinue && statusLine.code == HTTP_CONTINUE) {
        return null;
      }

      state = STATE_OPEN_RESPONSE_BODY;
      return responseBuilder;
    } catch (EOFException e) {
      // Provide more context if the server ends the stream before sending a response.
      IOException exception = new IOException("unexpected end of stream on " + streamAllocation);
      exception.initCause(e);
      throw exception;
    }
  }

public static StatusLine parse(String statusLine) throws IOException {
    //这个没什么说的,参见下面的例子就好
    // H T T P / 1 . 1   2 0 0   T e m p o r a r y   R e d i r e c t
    // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0

    // Parse protocol like "HTTP/1.1" followed by a space.
    int codeStart;
    Protocol protocol;
    if (statusLine.startsWith("HTTP/1.")) {
      if (statusLine.length() < 9 || statusLine.charAt(8) != ' ') {
        throw new ProtocolException("Unexpected status line: " + statusLine);
      }
      int httpMinorVersion = statusLine.charAt(7) - '0';
      codeStart = 9;
      if (httpMinorVersion == 0) {
        protocol = Protocol.HTTP_1_0;
      } else if (httpMinorVersion == 1) {
        protocol = Protocol.HTTP_1_1;
      } else {
        throw new ProtocolException("Unexpected status line: " + statusLine);
      }
    } else if (statusLine.startsWith("ICY ")) {
      // Shoutcast uses ICY instead of "HTTP/1.0".
      protocol = Protocol.HTTP_1_0;
      codeStart = 4;
    } else {
      throw new ProtocolException("Unexpected status line: " + statusLine);
    }

    // Parse response code like "200". Always 3 digits.
    if (statusLine.length() < codeStart + 3) {
      throw new ProtocolException("Unexpected status line: " + statusLine);
    }
    int code;
    try {
      code = Integer.parseInt(statusLine.substring(codeStart, codeStart + 3));
    } catch (NumberFormatException e) {
      throw new ProtocolException("Unexpected status line: " + statusLine);
    }

    // Parse an optional response message like "OK" or "Not Modified". If it
    // exists, it is separated from the response code by a space.
    String message = "";
    if (statusLine.length() > codeStart + 3) {
      if (statusLine.charAt(codeStart + 3) != ' ') {
        throw new ProtocolException("Unexpected status line: " + statusLine);
      }
      message = statusLine.substring(codeStart + 4);
    }

    return new StatusLine(protocol, code, message);
  }

可以看到,首先也是读取了响应的起始行和头部报文,这里也举一个例子来理解:
http1.1 200 OK
Connection: close
Transfer-Encoding: chunked

类似这种格式,接下来看响应报文的正文体处理

@Override public ResponseBody openResponseBody(Response response) throws IOException {
    Source source = getTransferStream(response);
    return new RealResponseBody(response.headers(), Okio.buffer(source));
  }

private Source getTransferStream(Response response) throws IOException {
    if (!HttpHeaders.hasBody(response)) {
      return newFixedLengthSource(0);
    }
    //分块传输模式,一般就是不确定当前数据大小的时候使用
    if ("chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
      return newChunkedSource(response.request().url());
    }
    //固定大小,会指定在头部报文的Content-Length中
    long contentLength = HttpHeaders.contentLength(response);
    if (contentLength != -1) {
      return newFixedLengthSource(contentLength);
    }

    // Wrap the input stream from the connection (rather than just returning
    // "socketIn" directly here), so that we can control its use after the
    // reference escapes.
    return newUnknownLengthSource();
  }

实际上就是封装了一个ResponseBody

public final class RealResponseBody extends ResponseBody {
  private final Headers headers;
  private final BufferedSource source;

  public RealResponseBody(Headers headers, BufferedSource source) {
    this.headers = headers;
    this.source = source;
  }

  @Override public MediaType contentType() {
    String contentType = headers.get("Content-Type");
    return contentType != null ? MediaType.parse(contentType) : null;
  }

  @Override public long contentLength() {
    return HttpHeaders.contentLength(headers);
  }

  @Override public BufferedSource source() {
    return source;
  }
}

也没做什么,重点是source,可以看到这里并没有去读取,稍微看一下newFixedLengthSource的实现:

@Override public long read(Buffer sink, long byteCount) throws IOException {
      if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
      if (closed) throw new IllegalStateException("closed");
      if (bytesRemaining == 0) return -1;

      long read = source.read(sink, Math.min(bytesRemaining, byteCount));
      if (read == -1) {
        endOfInput(false); // The server didn't supply the promised content length.
        throw new ProtocolException("unexpected end of stream");
      }

      bytesRemaining -= read;
      if (bytesRemaining == 0) {//当前数据读完了
        //实际上关注这里就好了,一旦数据读取完成,这里就会去处理连接完成
        //true的话表示连接可以复用
        //会释放连接的引用,标记连接池中当前连接可以复用了
        endOfInput(true);
      }
      return read;
    }

实际上这个read完成了才算是一个连接真正完成,然后标记当前连接可以复用。
上面的内容主要都是一些协议报文的内容,可以看基础这篇文章。
上述流程关心一下就好了,平时的重点可能都在RequestBody和ResponseBody的封装,其实从这里可以看到,这两个实际上就是封装Content-Type、Content-Length和如何写入数据,通过实现这两个类,可以做到不同的请求正文体和响应正文体。

总结

拦截器的部分就结束了,其实按着当前拦截器一路走下来,一个完整的请求也就完成了,对于整个框架的理解也提高了一些。
OkHttp这个框架可以说非常优秀,比方说很方便的封装了dns、通过Okio简化了传输数据流程的监听、进行了连接复用优化、重连机制等,这些都可以成为选择这个框架的理由。

上一篇下一篇

猜你喜欢

热点阅读