欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Okhttp解析(3.10.0)

程序员文章站 2022-07-13 10:58:24
...

Okhttp(3.10.0)


目录

请求体数据结构
  1. 表单形式
  2. 文件形式

简介

  1. SSL与TLS

    SSL — Secure Sockets Layer(安全套接层)

    TLS — Transport Layer Security(传输层安全协议)

    SSL 协议,为了解决HTTP协议是明文,存在很多缺点——比如传输内容会被偷窥(嗅探)和篡改的问题。到了1999年,SSL 因为应用广泛,已经成为互联网上的事实标准。IETF 就在那年把 SSL 标准化。标准化之后的名称改为 TLS

  2. URL

    URL — uniform resource locator

主要类

类图关系:
Okhttp解析(3.10.0)

  1. OkhttpClient

    Call的工厂类。应用中应该只使用一个OkhttpClient对象。

    当使用单例时,由于每个client拥有独立的连接池和线程池,Okhttp可以重用所有的HTTP请求。

    重用连接池和线程池可以减少延迟和节省内存。

    可以使用构造器实例化OkHttpClient client = new OkHttpClient(),或者使用Builder方式构建OkHttpClient client = new OkHttpClient.Builder().build()

    可以通过该类对象设置连接参数,如:读超时,写超时,重试,拦截器…

  2. Request

    请求封装实体,包含url,方法(post,get…),请求头,请求体等结构

  3. Response

    返回结果封装,包含请求,协议(http1.0,http1.1,http2,spdy3,h2c…),返回码(200,404…),返回体(字符串),握手信息(tls版本,证书信息),返回头,返回体(字节流),网络响应结果,缓存响应结果,预先响应结果,发送时间戳,接收时间戳

  4. RequestBody

    请求体封装抽象类,三个方法

    public abstract @Nullable MediaType contentType();
    public long contentLength() throws IOException {
        return -1;
    }
    //写入流中
    public abstract void writeTo(BufferedSink sink) throws IOException;

    其内置实现类有:

    1. FormBody – 表单

      MediaType为application/x-www-form-urlencoded,内部使用两个字符串集合记录表单的name和value:

      private final List<String> encodedNames;
      private final List<String> encodedValues;

      写入的实现

      private long writeOrCountBytes(@Nullable BufferedSink sink, boolean countBytes) {
          long byteCount = 0L;
      
          Buffer buffer;
          if (countBytes) {
              buffer = new Buffer();
          } else {
              buffer = sink.buffer();
          }
      
          for (int i = 0, size = encodedNames.size(); i < size; i++) {
          if (i > 0) buffer.writeByte('&');
              buffer.writeUtf8(encodedNames.get(i));
              buffer.writeByte('=');
              buffer.writeUtf8(encodedValues.get(i));
          }
      
          if (countBytes) {
              byteCount = buffer.size();
              buffer.clear();
          }
      
          return byteCount;
      }

      以utf-8的编码形式写入,最后的结构为

      name1=value1&name2=value2&name3=value3

    2. MultipartBody – 文件

      内部变量

      private static final byte[] COLONSPACE = {':', ' '};
      private static final byte[] CRLF = {'\r', '\n'};
      private static final byte[] DASHDASH = {'-', '-'};
      
      private final ByteString boundary;
      private final MediaType originalType;//
      private final MediaType contentType//请求的MediaType
      private final List<Part> parts;//内部Headers和RequestBody

      写入的实现:

      private long writeOrCountBytes(@Nullable BufferedSink sink, boolean countBytes) throws IOException {
          long byteCount = 0L;
      
          Buffer byteCountBuffer = null;
          if (countBytes) {
            sink = byteCountBuffer = new Buffer();
          }
      
          for (int p = 0, partCount = parts.size(); p < partCount; p++) {
            Part part = parts.get(p);
            Headers headers = part.headers;
            RequestBody body = part.body;
      
            sink.write(DASHDASH);
            sink.write(boundary);
            sink.write(CRLF);
      
            if (headers != null) {
              for (int h = 0, headerCount = headers.size(); h < headerCount; h++) {
                sink.writeUtf8(headers.name(h))
                    .write(COLONSPACE)
                    .writeUtf8(headers.value(h))
                    .write(CRLF);
              }
            }
      
            MediaType contentType = body.contentType();
            if (contentType != null) {
              sink.writeUtf8("Content-Type: ")
                  .writeUtf8(contentType.toString())
                  .write(CRLF);
            }
      
            long contentLength = body.contentLength();
            if (contentLength != -1) {
              sink.writeUtf8("Content-Length: ")
                  .writeDecimalLong(contentLength)
                  .write(CRLF);
            } else if (countBytes) {
              // We can't measure the body's size without the sizes of its components.
              byteCountBuffer.clear();
              return -1L;
            }
      
            sink.write(CRLF);
      
            if (countBytes) {
              byteCount += contentLength;
            } else {
              body.writeTo(sink);
            }
      
            sink.write(CRLF);
          }
      
          sink.write(DASHDASH);
          sink.write(boundary);
          sink.write(DASHDASH);
          sink.write(CRLF);
      
          if (countBytes) {
            byteCount += byteCountBuffer.size();
            byteCountBuffer.clear();
          }
      
          return byteCount;
      }

      如果是表单+文件形式,最后结构为:

      boundary的构建为UUID.randomUUID()

      --`boundary`
      \r\n
      part1HeaderName1: part1HeaderValue1
      \r\n
      part1HeaderName2: part1HeaderValue2
      \r\n
      Content-type: part1Body.contentType()
      \r\n
      Content-Length: part1Body.contentLenth()
      \r\n
      name1=value1&name2=value2&name3=value3        --- 写入body,此处以表单为例
      --`boundary`
      \r\n
      part2HeaderName1: part2HeaderValue1
      \r\n
      part2HeaderName2: part2HeaderValue2
      \r\n
      Content-type: part1Body.contentType()
      \r\n                                         ---此处contentLenth为-1,省略
      source = Okio.source(file);sink.writeAll(source); ---写入文件流
      \r\n
      --`boundary`--                               
      \r\n                                               ---结束
    3. 内部通过静态方法使用byte[],File或ByteString创建的匿名类

      将byte[],File,ByteString写入流中

  5. ResponseBody

    返回体封装,三个方法

    public abstract @Nullable MediaType contentType();
    public abstract long contentLength();
    public abstract BufferedSource source();

    实现类:

    1. CacheResponseBody
    2. RealResonseBody
    3. 内部通过静态方法使用BufferedSource创建的匿名类
  6. Call — interface

    请求操作,包括执行,加入队列,取消等

    实现类:RealCall

  7. Callback – interface

    异步结果回调

    // 由于取消,连接问题,超时等请求失败回调;由于网络原因,服务器可能接收了请求
    void onFailure(Call call, IOException e);
    // Http请求成功应答返回。传输层的成功不代表应用层的成功,仍可能会是404或500返回码
    void onResponse(Call call, Response response) throws IOException;
  8. EventListener

    Http请求事件监听,包括数量,大小和用时等

    3.10中由于主体API的修改,关于这个类的实现没有完成,会在3.11或3.12完善

    每个开始/连接/接收事件都会接收到匹配的事件回调方法,无论是成功或是失败,包含下列主要事件方法对

    事件 方法(对) 说明
    call callStart/End/Failed 每个Call只会回调一次start
    dns dnsStart/dnsEnd 多次回调(不同主机的重定向结果)或不回调(连接池复用的Call)
    connect connetctStart/End/Failed/Acquired/Released 连接事件
    secureConnect secureConnectStart/End 使用TLS时调用
    requestHeader requestHeaderStart/End 发送请求头前/后
    requestBody requestBodyStart/End 发送请求体前/后
    responseHeader responseHeaderStart/End 接收返回头前/后
    responseBody responseBodyStart/End 接收返回体前/后

    总的事件流程:

    call —> (dns –> connect –> secure connect) –> request

    请求事件流程:

    requestHeader —> requestBody —> responseHeaders —> responseBody

  9. Handshake

    记录TLS握手信息。

    内部成员:

    private final TlsVersion tlsVersion;
    // 加密方式
    private final CipherSuite cipherSuite;
    // 远程服务端证书
    private final List<Certificate> peerCertificates;
    // 本地客户端证书
    private final List<Certificate> localCertificates;

    枚举类TlsVersion的值

    版本 值(java name) 年份
    TLS_1_3 TLSv1.3 2016
    TLS_1_2 TLSv1.2 2008
    TLS_1_1 TLSv1.1 2006
    TLS_1_0 TLSv1.0 1999
    SSL_3_0 SSLv3 1996
  10. Dispatcher

    异步执行策略,内部包括一个线程池,几个请求队列(准备执行,正在执行-同步,正在执行-异步)

    其中:默认线程池大小为5,最大请求数64(同步执行队列大于该值时,会将请求放入准备队列中)

    每个OkHttpClient有独立的Dispatcher,每个Dispatcher有独立的线程池

  11. Headers

    Http请求和返回的头部信息,String[]数组结构

    {“headerName1”,”headerValue1”,”headerName2”,”headerValue2”}

  12. HttpUrl

    Url封装类,用于构建,分解网络地址

    google搜索

    HttpUrl url = new HttpUrl.Builder()
        .scheme("https")
        .host("www.google.com")
        .addPathSegment("search")
        .addQueryParameter("q", "polar bears")
        .build();
    // 打印
    // https://www.google.com/search?q=polar%20bears

    URL组成

    final String scheme;
    private final String username;
    private final String password;
    final String host;
    final int port;
    private final List<String> pathSegments;
    private final @Nullable List<String> queryNamesAndValues;
    private final @Nullable String fragment;
    private final String url;
    名称 含义
    Scheme 取决于协议,决定如何接收数据,多种mailto,file,ftp,此类只支持http/https
    username/password 成对出现或省略,http使用其他机制实现认证
    Host 如:square.com,localhost,IPv4 192.168.0.1,IPv6 ::1
    Port 默认http - 80,https - 443
    Path 资源位置,“/square/okhttp/issues/1486”会被解构成[“square”, “okhttp”,”issues”, “1486”]
    Query 可选,通常会是name-value参数集合
    Fragment 可选,并且不会发送给服务器,仅客户端私有
    Encoding 每个部分在放入URL之前要完成编码(UrlEndcode),如:cute #puppies –> cute%20%23puppies
    Percent encoding 使用UTF-8 16进制字节替换字符,适用于空格,Control,无ASCII字符和在上下文中其他含义的字符
    IDNA Mapping and Punycode encoding hostname使用不同的scheme编码

    注意:

    1. web服务使用多个特征:IP地址,域名,或localhost。每个服务的名字是不同的,无法交换的。
    2. Percent encoding会应用于除hostname以外的所有地方,但是每个部分对应的编码字符集不一样,如path部分需要去除所有的?字符来表示query的开始,但是在query和Fragment中的需要保留

      HttpUrl url = HttpUrl.parse("http://who-let-the-dogs.out").newBuilder()
          .addPathSegment("_Who?_")
          .query("_Who?_")
          .fragment("_Who?_")
          .build();
      // 打印
      // http://who-let-the-dogs.out/_Who%3F_?_Who?_#_Who?_
    3. 为了避免混淆和钓鱼攻击,”http://www.unicode.org/reports/tr46/#ToASCII“使用IDNA mapping变换名称来避免混淆字符
    4. 由于Java中的URL类使用IP地址判断两个url是否相同,如http://square.github.io/http://google.github.io/会被认为相同,使用equals方法会得到true;由此会引发一个问题:触发DNS过程降低效率,不同的Url由于托管方式而造成的相同引发的错误
    5. java中Uri对于http://host:80/http://host的结果是不相等的,
  13. Interceptor – interface

    请求拦截器。操作请求和结果,如添加,移除,变换请求/返回头

    每个请求,OkHttp会预制(在自定义之前)几个拦截器,包括缓存,重试,连接,解析的实现

    如:CacheInterceptor,ConnectInterceptor,CallServerInterceptor,RetryAndFollowUpInterceptor等

  14. ConnectionPool

    管理重用HTTP,HTTP/2中的连接来减少网络延时。使用相同地址的HTTP也会使用相同的Connection。该类决定了保留哪些连接并用于将来重用的策略

  15. Connection – interface

    HTTP,HTTP/2或者HTTP + HTTP/2 连接中的Socket和流。可用于多个HTTP的请求/相应。连接可以直接连接服务器或者通过代理

    方法:

    Route route();
    // HTTP -- Socket,HTTPS -- SSLSocket,如果是HTTP/2连接,可能会多个Call共享
    Socket socket();
    @Nullable Handshake handshake();
    Protocol protocol();

    实现类:RealConnection

  16. Protocol – enum

    支持一下协议:

    1. HTTP_1_0(“http/1.0”)
    2. HTTP_1_1(“http/1.1”)
    3. SPDY_3(“spdy/3.1”)
    4. HTTP_2(“h2”)
    5. H2C(“h2c”)
    6. QUIC(“quic”)
  17. WebSocket – interface

    非阻塞式web socket,通过WebSocket.Factory创建实例

    声明周期:

    1. Connection

      每个web socket的初始状态。可以添加消息,但是直到Open时才会发送

    2. Open

      web socket已被远程接受,可以执行所有操作。将会把消息加入队列

    3. Closing

      web socket任何一方终止了连接。web socket会继续提交队列中的消息,但会拒绝向队列中添加新的消息

    4. Closed

      web socket已经提交所有已存在消息,并接收所有消息

    5. Canceld

      web socket连接失败。加入队列中的消息可能不会被对方收到。

      可能由于HTTP升级问题,连接问题等

    注意:

    连接状态依赖于连接的双方。如果是优雅的关闭状态,标明发送了所以存在信息和接收了所有到来的信息,但不保证另一方接收了信息

    几个方法:

    // 初始化该web socket的请求实例
    Request request();
    long queueSize();
    // UTF-8编码 type 0x1
    boolean send(String text);
    // bytes type 0x2
    boolean send(ByteString bytes);
    // code --- http://tools.ietf.org/html/rfc6455#section-7.4
    boolean close(int code, @Nullable String reason);
    // 无视消息,直接断开
    void cancel();

流程分析

构建

Okhttp解析(3.10.0)

OkHttpClient的构建可以通过无参构建器(默认配置参数)或者使用OkHttpClient.Builder构建。

内部包括如下参数:

  1. 协议

    默认支持HTTP_2,HTTP_1_1,具体参见

  2. 对应的Dispatcher

    请求队列,线程池,并发数,线程数

  3. 连接配置 - ConnectionSpec

    默认为标准Tls和不加密、不认证模式,分别对应ConnectionSpec.MODERN_TLS和ConnectionSpec.CLEARTEXT

  4. 事件回调EventListener
  5. 代理设置
  6. Cookie设置
  7. SocketFactory
  8. 证书
  9. 连接池
  10. dns设置
  11. 重定向与重连
  12. 超时,心跳等

通过上述配置后,即可通过newCall()方法获取Call对应,并通过Call的execute()/enqueue()方法执行请求

请求

Okhttp解析(3.10.0)

上面创建的Call对象实际是RealCall类型,使用RealCall的静态方法newRealCall()创建。

这里以同步方法execute方法为例,进行分析,下面是RealCall的execute方法

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

这里可以看到EventListener的相关回调,不过这里的关键代码在于client.dispatcher().executed(this)Response result = getResponseWithInterceptorChain(),分别是将call加入到同步队列中,通过预制和用户自定的Intercepltor获取请求结果。重点看下Interceptor是怎么获取结果的,也就是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, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    return chain.proceed(originalRequest);
}

在拦截器这个集合中,除开用户添加的(client.interceptors()),OkHttp如添加了几项:

  1. RetryAndFollowUpInterceptor – 重试
  2. BridgeInterceptor – 对OkHttp中的Request和Response与网络代码的转换
  3. CacheInterceptor – 缓存读取和相应的写入(DiskLruCache)
  4. ConnectInterceptor – 创建连接
  5. networkInterceptors –
  6. CallServerInterceptor – 与服务端交互

通过流程,一一进行解析。首先进入RealInterceptroChain的proceed()方法中,内部直接调用重载的proceed()方法,直接看重载的方法

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,RealConnection connection) throws IOException {
    if (index >= interceptors.size()) throw new AssertionError();

    calls++;

    // If we already have a stream, confirm that the incoming request will use it.
    if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must retain the same host and port");
    }

    // If we already have a stream, confirm that this is the only call to chain.proceed().
    if (this.httpCodec != null && calls > 1) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must call proceed() exactly once");
    }

    // Call the next interceptor in the chain.
    RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
        connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
        writeTimeout);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);

    // Confirm that the next interceptor made its required call to chain.proceed().
    if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
      throw new IllegalStateException("network interceptor " + interceptor
          + " must call proceed() exactly once");
    }

    // Confirm that the intercepted response isn't null.
    if (response == null) {
      throw new NullPointerException("interceptor " + interceptor + " returned null");
    }

    if (response.body() == null) {
      throw new IllegalStateException(
          "interceptor " + interceptor + " returned a response with no body");
    }

    return response;
}

其实上述代码中开头和结尾是一些校验工作,可以直接看中间部分代码:

  1. 使用同一套参数创建了新的RealInterceptorChain对象,为其指定了新的Interceptor
  2. 每个Interceptor的intercept方法传入的是下一个Chain对象,并限制其调用proceed()方法,完成整个链式调用

共用的参数为RealInterceptorChain的所有成员变量(不包括calls,用于判断是否调用proceed方法),介绍几个:

  1. HttpCodec

    用于HTTP请求的编码和返回的解码,实现类有Http1Codec和HTTP2Codec

  2. RealConnection

    TODO

    用于连接和数据接收,内部包含socket(应用层,可能为SSLSocket或rawScoket),rawSocket(tcp),握手信息,输入输出流

  3. StreamAllocation

    该类协调了三个类型的关系

    1. Connections:连接远程服务的物理Socket。由于建立过程比较慢,需要能够断开已经建立连接的连接
    2. Streams:HTTP 请求/响应应该成对出现在连接中。对于每个连接可携有的并发流,每个连接有独立的分配限制。HTTP/1.x只能携带一个流,而HTTP/2可以携带多个
    3. Calls:流的顺序由请求决定。希望将相同连接中的所有流放在同一个Call中。

    代表call,在一个或多个连接上使用一个或多个流,并提供API来释放资源

    支持同步取消。如果HTTP/2流存活,会取消对应的流而不会取消共享这个连接的其他流。但是如果正在进行TLS握手,取消会破坏整个连接

整理下RealConnection和Interceptor的关系

每个RealConnection对应一个Interceptor,方法调用顺序为 RealConnection.proceed() --> Interceptor.intercept(nextRealConnection) --> nextRealConnection.proceed(),最后一个CallServerInterceptor为调用proceed()方法,并返回结果

拦截器
  1. RetryAndFollowUpInterceptor – 重试与跟进

    使用client的connectionPool初始化StreamAllocation,并传递给后续的Interceptor

  2. BridgeInterceptor – 请求解析

    1. 补充请求头信息,如Host,Accept-Encoding,Cookie等;
    2. 保存cookie信息
    3. 处理gzip模式的返回头
  3. ConnectInterceptor – 连接

    建立连接,并将流交由HttpCodec,实现后续的数据传输与解析

    @Override 
    public Response intercept(Chain chain) throws IOException {
        RealInterceptorChain realChain = (RealInterceptorChain) chain;
        Request request = realChain.request();
        StreamAllocation streamAllocation = realChain.streamAllocation();
    
        // We need the network to satisfy this request. Possibly for validating a conditional GET.
        boolean doExtensiveHealthChecks = !request.method().equals("GET");
        HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
        RealConnection connection = streamAllocation.connection();
    
        return realChain.proceed(request, streamAllocation, httpCodec, connection);
    }
    1. 通过StreamAllocation创建(复用)RealConnection – 并完成连接状态

      1. 如果复用,直接返回connection
      2. 如果创建,需先进行TCP + TLS握手(阻塞)
    2. 通过StreamAllocation创建HttpCodec

      这里分为Http1Codec和Http2Codec

      1. Http1Codec – 通过输入输出流创建
      2. Http2Codec – 通过RealConnection创建
    3. 将创建好的RealConnection,HttpCodec传递给后续的Interceptor
  4. CallServerInterceptor – 解析

    由ConnectInterceptor建立连接后,开始http请求的写入与结果读取,截取部分代码

    // 写入请求头信息
    httpCodec.writeRequestHeaders(request);
    // 如果不是GET,或者HEAD,写入请求体
    CountingSink requestBodyOut = new CountingSink(httpCodec.createRequestBody(request, contentLength));
    BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
    request.body().writeTo(bufferedRequestBody);
    // 完成请求写入
    httpCodec.finishRequest();
    // 读取相应头信息,相应码,协议,响应体(message)
    responseBuilder = httpCodec.readResponseHeaders(false);
    // 赋值body属性
    response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))
          .build();      
    // 返回结果
    return response;
相关标签: okhttp