OkHttp源码解析——HTTP请求的逻辑流程

1 介绍

在我们所处的互联网世界中,HTTP协议算得上是使用最广泛的网络协议。
OKHttp是一款高效的HTTP客户端,支持同一地址的链接共享同一个socket,通过连接池来减小响应延迟,还有透明的GZIP压缩,请求缓存等优势。
如果您的服务器配置了多个IP地址,当第一个IP连接失败的时候,OkHttp会自动尝试下一个IP。OkHttp还处理了代理服务器问题和SSL握手失败问题。

值得一提的是:Android4.4原生的HttpUrlConnection底层已经替换成了okhttp实现了。

public final class URL implements Serializable {
...
    public URLConnection openConnection() throws IOException {
            return this.handler.openConnection(this);
        }
}

这个handler,在源码中判断到如果是HTTP协议,就会创建HtppHandler:

public final class HttpHandler extends URLStreamHandler {
    @Override protected URLConnection openConnection(URL url) throws IOException {
        // 调用了OKHttpClient()的方法
        return new OkHttpClient().open(url);
    }
    @Override protected URLConnection openConnection(URL url, Proxy proxy) throws IOException {
        if (url == null || proxy == null) {
            throw new IllegalArgumentException("url == null || proxy == null");
        }
        return new OkHttpClient().setProxy(proxy).open(url);
    }
    @Override protected int getDefaultPort() {
        return 80;
    }
}

2 基本使用方式

在OKHttp,每次网络请求就是一个Request,我们在Request里填写我们需要的url,header等其他参数,再通过Request构造出Call,Call内部去请求服务器,得到回复,并将结果告诉调用者。同时okhttp提供了同步异步两种方式进行网络操作。

2.1 同步

OkHttpClient client = new OkHttpClient();

String run(String url) throws IOException {
  Request request = new Request.Builder()
      .url(url)
      .build();

  Response response = client.newCall(request).execute();
  return response.body().string();
}

直接execute执行得到Response,通过Response可以得到code,message等信息。android本身是不允许在UI线程做网络请求操作,需要在子线程中执行。

2.2 异步

  Request request = new Request.Builder()
                .url("http://www.baidu.com")
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Request request, IOException e) {

            }

            @Override
            public void onResponse(Response response) throws IOException {
                //NOT UI Thread
                if(response.isSuccessful()){
                    System.out.println(response.code());
                    System.out.println(response.body().string());
                }
            }
        });

在同步的基础上讲execute改成enqueue,并且传入回调接口,但接口回调回来的代码是在非UI线程的,因此如果有更新UI的操作必须切到主线程。

3 整体结构

3.1 处理网络响应的拦截器机制

无论是同步的call.execute()还是异步的call.enqueue(),最后都是殊途同归地走到call.getResponseWithInterceptorChain(boolean forWebSocket)方法。

private 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 (!retryAndFollowUpInterceptor.isForWebSocket()) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(
        retryAndFollowUpInterceptor.isForWebSocket()));

    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null, null, null, 0, originalRequest);
    return chain.proceed(originalRequest);
  }

可以发现okhttp在处理网络响应时采用的是拦截器机制。okhttp用ArrayList对interceptors进行管理,interceptors将依次被调用。

okhttp_interceptors.png

如上图:

  1. 橙色框内是okhttp自带的Interceptors的实现类,它们都是在call.getResponseWithInterceptorChain()中被添加入 InterceptorChain中,实际上这几个Interceptor都是在okhttp3后才被引入,它们非常重要,负责了重连、组装请求头部、读/写缓存、建立socket连接、向服务器发送请求/接收响应的全部过程

  2. 在okhttp3之前,这些行为都封装在HttpEngine类中。okhttp3之后,HttpEngine已经被删去,取而代之的是这5个Interceptor,可以说一次网络请求中的细节被解耦放在不同的Interceptor中,不同Interceptor只负责自己的那一环节工作(对Request或者Response进行获取/处理),使得拦截器模式完全贯穿整个网络请求。

  3. 用户可以添加自定义的Interceptor,okhttp把拦截器分为应用拦截器和网络拦截器

    public class OkHttpClient implements Cloneable, Call.Factory {
     final List<Interceptor> interceptors;
     final List<Interceptor> networkInterceptors;
     ......
     }
    
    1. 调用OkHttpClient.Builder的addInterceptor()可以添加应用拦截器,只会被调用一次,可以处理网络请求回来的最终Response
    2. 调用addNetworkInterceptor()可以添加network拦截器,处理所有的网络响应(一次请求如果发生了redirect ,那么这个拦截器的逻辑可能会被调用两次)

Interceptor解析

由上面的分析可以知道,okhttp框架内自带了5个Interceptor的实现:

  1. RetryAndFollowUpInterceptor,重试那些失败或者redirect的请求。
  2. BridgeInterceptor,请求之前对响应头做了一些检查,并添加一些头,然后在请求之后对响应做一些处理(gzip解压or设置cookie)。
  3. CacheInterceptor,根据用户是否有设置cache,如果有的话,则从用户的cache中获取当前请求的缓存。
  4. ConnectInterceptor,复用连接池中的连接,如果没有就与服务器建立新的socket连接。
  5. CallServerInterceptor,负责发送请求和获取响应。

下图是在Interceptor Chain中的数据流:


Interceptor_flow.png

官方文档关于Interceptor的解释是:

Observes, modifies, and potentially short-circuits requests going out and the corresponding responses coming back in. Typically interceptors add, remove, or transform headers on the request or response.
通过Interceptors可以 观察,修改或者拦截请求/响应。一般拦截器添加,删除或修改 请求/响应的header。

Interceptor是一个接口,里面只有一个方法:

public interface Interceptor {
  Response intercept(Chain chain) throws IOException;
}

实现Interceptor需要注意两点(包括源码内置的Interceptor也是严格遵循以下两点):

  1. 通过intercept()方法里的Chain参数可以拿到request,这样子就可以对request进行统一的修改(例如BridgeInterceptor对所有request的头部进行了设置),或者根据request去做一些事情。
  2. 在intercept()方法中通过chain.proceed(request)得到Response,从而拦截了网络响应进行修改,或者根据response去做一些事情。

4 关键代码

以下是HTTP客户端向服务器发送报文的过程:

  1. 从URL中解析出服务器的IP地址和端口号
  2. 在客户端和服务器之间建立一条TCP/IP连接
  3. 开始传输HTTP报文

HTTP是个应用层协议。HTTP无需操心网络通信的具体细节;它把联网的细节都交给了通用、可靠的因特网传输协议TCP/IP。TCP/IP隐藏了各种网络和硬件的特点及弱点,使各种类型的计算机和网络都能够进行可靠的通信。
简单来说,HTTP协议位于TCP的上层。HTTP使用TCP来传输其报文数据。

如果你使用okhttp请求一个URL,具体的工作如下:

  1. 框架使用URL和配置好的OkHttpClient创建一个address。此地址指定我们将如何连接到网络服务器。
  2. 框架通过address从连接池中取回一个连接。
  3. 如果没有在池中找到连接,ok会选择一个route尝试连接。这通常意味着使用一个DNS请求, 以获取服务器的IP地址。如果需要,ok还会选择一个TLS版本和代理服务器。
  4. 如果获取到一个新的route,它会与服务器建立一个直接的socket连接、使用TLS安全通道(基于HTTP代理的HTTPS),或直接TLS连接。它的TLS握手是必要的。
  5. 开始发送HTTP请求并读取响应。

如果有连接出现问题,OkHttp将选择另一条route,然后再试一次。这样的好处是当服务器地址的一个子集不可达时,OkHttp能够自动恢复。而且当连接池过期或者TLS版本不受支持时,这种方式非常有用。
一旦响应已经被接收到,该连接将被返回到池中,以便它可以在将来的请求中被重用。连接在池中闲置一段时间后,它会被赶出。

下面就说说这五个步骤的关键代码:

4.1 建立连接 —— ConnectInterceptor

上面所述前四个步骤都在ConnectInterceptor中。
HTTP是建立在TCP协议之上,HTTP协议的瓶颈及其优化技巧都是基于TCP协议本身的特性。比如TCP建立连接时也要在第三次握手时才能捎带 HTTP 请求报文,达到真正的建立连接,但是这些连接无法复用会导致每次请求都经历三次握手和慢启动。
正是由于TCP在建立连接的初期有慢启动(slow start)的特性,所以连接的重用总是比新建连接性能要好

而okhttp的一大特点就是通过连接池来减小响应延迟。如果连接池中没有可用的连接,则会与服务器建立连接,并将socket的io封装到HttpStream(发送请求和接收response)中,这些都在ConnectInterceptor中完成。
具体在StreamAllocation.findConnection()方法中,下面是具体逻辑:

  /**
   * Returns a connection to host a new stream. This prefers the existing connection if it exists,
   * then the pool, finally building a new connection.
   */
  private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      boolean connectionRetryEnabled) throws IOException {
    Route selectedRoute;
    synchronized (connectionPool) {
      ......
      // Attempt to get a connection from the pool.
      RealConnection pooledConnection = 
          Internal.instance.get(connectionPool, address, this);// 1
      ......
    if (selectedRoute == null) {
      selectedRoute = routeSelector.next();//2
      ......
    }

    RealConnection newConnection = new RealConnection(selectedRoute);//3
    ......
    synchronized (connectionPool) {//4
      Internal.instance.put(connectionPool, newConnection);
      this.connection = newConnection;
      if (canceled) throw new IOException("Canceled");
    }

    newConnection.connect(connectTimeout, readTimeout, writeTimeout, address.connectionSpecs(),
        connectionRetryEnabled);//5

    return newConnection;
  }

下面具体说说每一步做了什么:

  1. 线程池中取得连接RealConnection pooledConnection = pool.get(address, streamAllocation)

    //StreamAllocation.java
      RealConnection get(Address address, StreamAllocation streamAllocation) {3
        for (RealConnection connection : connections) {
          if (connection.allocations.size() < connection.allocationLimit
              && address.equals(connection.route().address)//根据url来命中connection
              && !connection.noNewStreams) {
            streamAllocation.acquire(connection);//将可用的连接放入
            return connection;
          }
        }
        return null;
      }
    
  2. 如果selectedRoute为空,则选择下一条路由Route selectedRoute = routeSelector.next();

    //RouteSelector.java
    public final class RouteSelector {
        public Route next() throws IOException {  
           // Compute the next route to attempt.  
           if (!hasNextInetSocketAddress()) {  
             if (!hasNextProxy()) {  
               if (!hasNextPostponed()) {  
                 throw new NoSuchElementException();  
               }  
               return nextPostponed();  
             }  
             lastProxy = nextProxy();  
           }  
           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;  
           }  
    
        private Proxy nextProxy() throws IOException {  
            if (!hasNextProxy()) {  
              throw new SocketException("No route to " + address.url().host()  
                  + "; exhausted proxy configurations: " + proxies);  
            }  
            Proxy result = proxies.get(nextProxyIndex++);  
            resetNextInetSocketAddress(result);  
            return result;  
        }  
    
        private void resetNextInetSocketAddress(Proxy proxy) throws IOException {  
        ......
        List<InetAddress> addresses = address.dns().lookup(socketHost); //调用dns查询域名对应的ip 
        ...
        }
    }
    

    浏览器需要知道目标服务器的 IP地址和端口号 才能建立连接。将域名解析为 IP地址 的这个系统就是 DNS。

    debug_dns.png
  3. 以前面创建的route为参数新建一个RealConnectionRealConnection newConnection = new RealConnection(selectedRoute);

    public RealConnection(Route route) {  
    this.route = route;  
    }  
    
  4. 添加到连接池

public final class ConnectionPool {
      void put(RealConnection connection) {  
      assert (Thread.holdsLock(this));  
      if (!cleanupRunning) {  
        cleanupRunning = true;  
        executor.execute(cleanupRunnable); 
     //这里很重要,把闲置超过keepAliveDurationNs时间的connection从连接池中移除。
    //具体细节看ConnectionPool 的cleanupRunnable里的run()逻辑
      }  
      connections.add(connection);  
      } 
}
  1. 调用RealConnection的connect()方法,实际上是buildConnection()构建连接。
//RealConnection.java
private void buildConnection(int connectTimeout, int readTimeout, int writeTimeout,  
  ConnectionSpecSelector connectionSpecSelector) throws IOException {  
connectSocket(connectTimeout, readTimeout);  //建立socket连接
establishProtocol(readTimeout, writeTimeout, connectionSpecSelector);  
}

调用connectSocket连接socket。
调用establishProtocol根据HTTP协议版本做一些不同的事情:SSL握手等等。

重点来了! connectSocket(connectTimeout, readTimeout); 里的逻辑实际上是:

public final class RealConnection extends FramedConnection.Listener implements Connection {

    public void connectSocket(Socket socket, InetSocketAddress address,  
        int connectTimeout) throws IOException {  
      socket.connect(address, connectTimeout);  //Http是基于TCP的,自然底层也是建立了socket连接
      ...
      source = Okio.buffer(Okio.source(rawSocket));  
      sink = Okio.buffer(Okio.sink(rawSocket));  //用Okio封装了socket的输入和输出流
    }  
  public final class Okio {

      public static Source source(Socket socket) throws IOException {
          if(socket == null) {
              throw new IllegalArgumentException("socket == null");
          } else {
              AsyncTimeout timeout = timeout(socket);
              Source source = source((InputStream)socket.getInputStream(), (Timeout)timeout);
              return timeout.source(source);
          }
      }

      public static Sink sink(Socket socket) throws IOException {
          if(socket == null) {
              throw new IllegalArgumentException("socket == null");
          } else {
              AsyncTimeout timeout = timeout(socket);
              Sink sink = sink((OutputStream)socket.getOutputStream(), (Timeout)timeout);
              return timeout.sink(sink);
          }
      }
  }
  1. 构建HttpStream
resultConnection.socket().setSoTimeout(readTimeout);  
     resultConnection.source.timeout().timeout(readTimeout, MILLISECONDS);  
     resultConnection.sink.timeout().timeout(writeTimeout, MILLISECONDS);  
     resultStream = new Http1xStream(  
         client, this, resultConnection.source, resultConnection.sink);  

至此,HttpStream就构建好了,通过它可以发送请求和接收response。

4.2 发送request/接收Response —— CallServerInterceptor

CallServerInterceptor的intercept()方法里 负责发送请求和获取响应,实际上都是由HttpStream类去完成具体的工作。

Http1XStream

一个socket连接用来发送HTTP/1.1消息,这个类严格按照以下生命周期:

  1. writeRequestHeaders()发送request header
  2. 打开一个sink来写request body,然后关闭sink
  3. readResponseHeaders()读取response头部
  4. 打开一个source来读取response body,然后关闭source

4.2.1 writeRequest

HTTP报文是由一行一行的简单字符串组成的,都是纯文本,不是二进制代码,可以很方便地进行读写。

public final class Http1xStream implements HttpStream {
  /** Returns bytes of a request header for sending on an HTTP transport. */
  public void writeRequest(Headers headers, String requestLine) throws IOException {
    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++) {
      sink.writeUtf8(headers.name(i))
          .writeUtf8(": ")
          .writeUtf8(headers.value(i))
          .writeUtf8("\r\n");
    }
    sink.writeUtf8("\r\n");
    state = STATE_OPEN_REQUEST_BODY;
  }
}


public final class Headers {
  private final String[] namesAndValues;

    /** Returns the field at {@code position}. */
  public String name(int index) {
    return namesAndValues[index * 2];
  }

  /** Returns the value at {@code index}. */
  public String value(int index) {
    return namesAndValues[index * 2 + 1];
  }
}
debug_write_request.png

4.2.2 readResponse

public final class Http1xStream implements HttpStream {

//读取Response Header
  public Response.Builder readResponse() throws IOException {
  ......
   while (true) {
        StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict());//1 从InputStream上读入一行数据

        Response.Builder responseBuilder = new Response.Builder()
            .protocol(statusLine.protocol)
            .code(statusLine.code)
            .message(statusLine.message)
            .headers(readHeaders());

        if (statusLine.code != HTTP_CONTINUE) {
          state = STATE_OPEN_RESPONSE_BODY;
          return responseBuilder;
        }
      }
    }

//读取Response Body,获得
    @Override public ResponseBody openResponseBody(Response response) throws IOException {
    Source source = getTransferStream(response);
    return new RealResponseBody(response.headers(), Okio.buffer(source));
  }
}
  1. 解析HTTP报文,得到HTTP协议版本。

    public final class StatusLine {
    
      public static StatusLine parse(String statusLine/*HTTP/1.1 200 OK*/) 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.")) {
          .......
    
debug_status_line.png
  1. 读取ResponseHeader

    /** Reads headers or trailers. */
    public Headers readHeaders() throws IOException {
        Headers.Builder headers = new Headers.Builder();
        // parse the result headers until the first blank line
        for (String line; (line = source.readUtf8LineStrict()).length() != 0; ) {
          Internal.instance.addLenient(headers, line);
        }
        return headers.build();
    }
    
debug_read_response_header.png
  1. 读取ResponseBody,读取InputStream获得byte数组,至此就完全得到了客户端请求服务端接口 的响应内容。

    public abstract class ResponseBody implements Closeable {
      public final byte[] bytes() throws IOException {
      ......
        try {
          bytes = source.readByteArray();
        } finally {
          Util.closeQuietly(source);
        }
    ......
        return bytes;
    }
    
      /**
       * Returns the response as a string decoded with the charset of the Content-Type header. If that
       * header is either absent or lacks a charset, this will attempt to decode the response body as
       * UTF-8.
       */
      public final String string() throws IOException {
        return new String(bytes(), charset().name());
      }
    
debug_result.png

5 总结

从上面关于okhttp发送网络请求及接受网络响应的过程的分析,可以发现 okhttp并不是Volley和Retrofit这种二次封装的网络框架,而是基于最原始的java socket连接自己去实现了HTTP协议,就连Android源码也将其收录在内,堪称网络编程的典范。结合HTTP协议相关书籍与okhttp的源码实践相结合进行学习,相信可以对HTTP协议有具体且深入的掌握。

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

推荐阅读更多精彩内容