| 注册
请输入搜索内容

热门搜索

Java Linux MySQL PHP JavaScript Hibernate jQuery Nginx
adora89l
8年前发布

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

   <h2><strong>1 介绍</strong></h2>    <p>在我们所处的互联网世界中,HTTP协议算得上是使用最广泛的网络协议。</p>    <p>OKHttp是一款高效的HTTP客户端,支持同一地址的链接共享同一个socket,通过连接池来减小响应延迟,还有透明的GZIP压缩,请求缓存等优势。</p>    <p>如果您的服务器配置了多个IP地址,当第一个IP连接失败的时候,OkHttp会自动尝试下一个IP。OkHttp还处理了代理服务器问题和SSL握手失败问题。</p>    <p>值得一提的是:Android4.4原生的HttpUrlConnection底层已经替换成了okhttp实现了。</p>    <pre>  <code class="language-java">public final class URL implements Serializable {  ...      public URLConnection openConnection() throws IOException {              return this.handler.openConnection(this);          }  }</code></pre>    <p>这个handler,在源码中判断到如果是HTTP协议,就会创建HtppHandler:</p>    <pre>  <code class="language-java">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;      }  }</code></pre>    <h2><strong>2 基本使用方式</strong></h2>    <p>在OKHttp,每次网络请求就是一个 Request ,我们在Request里填写我们需要的url,header等其他参数,再通过Request构造出 Call ,Call内部去请求服务器,得到回复,并将结果告诉调用者。同时okhttp提供了 <strong>同步</strong> 和 <strong>异步</strong> 两种方式进行网络操作。</p>    <h2><strong>2.1 同步</strong></h2>    <pre>  <code class="language-java">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();  }</code></pre>    <p>直接execute执行得到Response,通过Response可以得到code,message等信息。 android本身是不允许在UI线程做网络请求操作,需要在子线程中执行。</p>    <h2><strong>2.2 异步</strong></h2>    <pre>  <code class="language-java">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());                  }              }          });</code></pre>    <p>在同步的基础上讲execute改成enqueue,并且传入回调接口,但接口回调回来的代码是在非UI线程的,因此如果有更新UI的操作必须切到主线程。</p>    <h2><strong>3 整体结构</strong></h2>    <h3><strong>3.1 处理网络响应的拦截器机制</strong></h3>    <p>无论是同步的 call.execute() 还是异步的 call.enqueue() ,最后都是殊途同归地走到 call.getResponseWithInterceptorChain(boolean forWebSocket) 方法。</p>    <pre>  <code class="language-java">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);    }</code></pre>    <p>可以发现okhttp在处理网络响应时采用的是拦截器机制。okhttp用 <strong>ArrayList</strong> 对interceptors进行管理,interceptors将依次被调用。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/fdaed45f5ef6226316ede477f462f938.png"></p>    <p style="text-align:center">okhttp_interceptors.png</p>    <p>如上图:</p>    <ol>     <li>橙色框内是okhttp自带的Interceptors的实现类,它们都是在 call.getResponseWithInterceptorChain() 中被添加入 InterceptorChain中,实际上这几个Interceptor都是在okhttp3后才被引入,它们非常重要,负责了 重连、组装请求头部、读/写缓存、建立socket连接、向服务器发送请求/接收响应的全部过程 。</li>     <li>在okhttp3之前,这些行为都封装在HttpEngine类中。okhttp3之后,HttpEngine已经被删去,取而代之的是这5个Interceptor,可以说一次网络请求中的细节被解耦放在不同的Interceptor中,不同Interceptor只负责自己的那一环节工作(对Request或者Response进行获取/处理),使得拦截器模式完全贯穿整个网络请求。</li>     <li> <p>用户可以添加自定义的Interceptor,okhttp把拦截器分为应用拦截器和网络拦截器</p> <pre>  <code class="language-java">public class OkHttpClient implements Cloneable, Call.Factory {    final List<Interceptor> interceptors;    final List<Interceptor> networkInterceptors;    ......    }</code></pre>      <ol>       <li>调用OkHttpClient.Builder的 addInterceptor() 可以添加应用拦截器,只会被调用一次,可以处理网络请求回来的最终Response</li>       <li>调用 addNetworkInterceptor() 可以添加network拦截器,处理所有的网络响应(一次请求如果发生了redirect ,那么这个拦截器的逻辑可能会被调用两次)</li>      </ol> </li>    </ol>    <h3><strong>Interceptor解析</strong></h3>    <p>由上面的分析可以知道,okhttp框架内自带了5个Interceptor的实现:</p>    <ol>     <li><strong>RetryAndFollowUpInterceptor</strong> ,重试那些失败或者redirect的请求。</li>     <li><strong>BridgeInterceptor</strong> ,请求之前对响应头做了一些检查,并添加一些头,然后在请求之后对响应做一些处理(gzip解压or设置cookie)。</li>     <li><strong>CacheInterceptor</strong> ,根据用户是否有设置cache,如果有的话,则从用户的cache中获取当前请求的缓存。</li>     <li><strong>ConnectInterceptor</strong> ,复用连接池中的连接,如果没有就与服务器建立新的socket连接。</li>     <li><strong>CallServerInterceptor</strong> ,负责发送请求和获取响应。</li>    </ol>    <p>下图是在Interceptor Chain中的数据流:</p>    <p><img src="https://simg.open-open.com/show/03a2605608646d1177cd2b83e8057f79.png"></p>    <p style="text-align:center">Interceptor_flow.png</p>    <p>官方文档关于Interceptor的解释是:</p>    <p>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.</p>    <p>通过Interceptors可以 观察,修改或者拦截请求/响应。一般拦截器添加,删除或修改 请求/响应的header。</p>    <p>Interceptor是一个接口,里面只有一个方法:</p>    <pre>  <code class="language-java">public interface Interceptor {    Response intercept(Chain chain) throws IOException;  }</code></pre>    <p>实现Interceptor需要注意两点(包括源码内置的Interceptor也是严格遵循以下两点):</p>    <ol>     <li>通过intercept()方法里的 Chain 参数可以拿到request,这样子就可以 <strong>对request进行统一的修改</strong> (例如BridgeInterceptor对所有request的头部进行了设置),或者根据request去做一些事情。</li>     <li>在intercept()方法中通过 chain.proceed(request) 得到Response,从而 <strong>拦截了网络响应进行修改</strong> ,或者根据response去做一些事情。</li>    </ol>    <h2><strong>4 关键代码</strong></h2>    <p>以下是HTTP客户端向服务器发送报文的过程:</p>    <ol>     <li>从URL中解析出服务器的IP地址和端口号</li>     <li>在客户端和服务器之间建立一条TCP/IP连接</li>     <li>开始传输HTTP报文</li>    </ol>    <p>HTTP是个应用层协议。HTTP无需操心网络通信的具体细节;它把联网的细节都交给了通用、可靠的因特网传输协议TCP/IP。TCP/IP隐藏了各种网络和硬件的特点及弱点,使各种类型的计算机和网络都能够进行可靠的通信。</p>    <p>简单来说,HTTP协议位于TCP的上层。HTTP使用TCP来传输其报文数据。</p>    <p>如果你使用okhttp请求一个URL,具体的工作如下:</p>    <ol>     <li>框架使用URL和配置好的OkHttpClient创建一个address。此地址指定我们将如何连接到网络服务器。</li>     <li>框架通过address从连接池中取回一个连接。</li>     <li>如果没有在池中找到连接,ok会选择一个route尝试连接。这通常意味着使用一个DNS请求, 以获取服务器的IP地址。如果需要,ok还会选择一个TLS版本和代理服务器。</li>     <li>如果获取到一个新的route, <strong>它会与服务器建立一个直接的socket连接</strong> 、使用TLS安全通道(基于HTTP代理的HTTPS),或直接TLS连接。它的TLS握手是必要的。</li>     <li>开始发送HTTP请求并读取响应。</li>    </ol>    <p>如果有连接出现问题,OkHttp将选择另一条route,然后再试一次。这样的好处是当服务器地址的一个子集不可达时,OkHttp能够自动恢复。而且当连接池过期或者TLS版本不受支持时,这种方式非常有用。</p>    <p>一旦响应已经被接收到,该连接将被返回到池中,以便它可以在将来的请求中被重用。连接在池中闲置一段时间后,它会被赶出。</p>    <p>下面就说说这五个步骤的关键代码:</p>    <h3><strong>4.1 建立连接 —— ConnectInterceptor</strong></h3>    <p>上面所述前四个步骤都在ConnectInterceptor中。</p>    <p>HTTP是建立在TCP协议之上,HTTP协议的瓶颈及其优化技巧都是基于TCP协议本身的特性。比如TCP建立连接时也要在第三次握手时才能捎带 HTTP 请求报文,达到真正的建立连接,但是这些连接无法复用会导致每次请求都经历三次握手和慢启动。</p>    <p>正是由于TCP在建立连接的初期有 慢启动(slow start)的特性,所以连接的重用总是比新建连接性能要好 。</p>    <p>而okhttp的一大特点就是 <strong>通过连接池来减小响应延迟</strong> 。如果连接池中没有可用的连接,则会与服务器建立连接,并将socket的io封装到HttpStream(发送请求和接收response)中,这些都在ConnectInterceptor中完成。</p>    <p>具体在 StreamAllocation.findConnection() 方法中,下面是具体逻辑:</p>    <pre>  <code class="language-java">/**     * 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;    }</code></pre>    <p>下面具体说说每一步做了什么:</p>    <ol>     <li> <p>线程池中取得连接 RealConnection pooledConnection = pool.get(address, streamAllocation)</p> <pre>  <code class="language-java">//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;     }</code></pre> </li>     <li style="text-align:center"> <p>如果selectedRoute为空,则选择下一条路由Route selectedRoute = routeSelector.next();</p> <pre>  <code class="language-java">//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        ...       }     }</code></pre> <p>浏览器需要知道目标服务器的 IP地址和端口号 才能建立连接。将域名解析为 IP地址 的这个系统就是 DNS。</p> <img src="https://simg.open-open.com/show/55659dc521cac8377755d7c9c5e396db.png"> <p>debug_dns.png</p> </li>     <li> <p>以前面创建的route为参数新建一个RealConnection RealConnection newConnection = new RealConnection(selectedRoute);</p> <pre>  <code class="language-java">public RealConnection(Route route) {     this.route = route;     }</code></pre> </li>     <li> <p>添加到连接池</p> <pre>  <code class="language-java">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);         }   }</code></pre> </li>     <li> <p>调用RealConnection的connect()方法,实际上是 buildConnection() 构建连接。</p> <pre>  <code class="language-java">//RealConnection.java  private void buildConnection(int connectTimeout, int readTimeout, int writeTimeout,     ConnectionSpecSelector connectionSpecSelector) throws IOException {    connectSocket(connectTimeout, readTimeout);  //建立socket连接  establishProtocol(readTimeout, writeTimeout, connectionSpecSelector);    }</code></pre> <p>调用connectSocket连接socket。</p> <p>调用establishProtocol根据HTTP协议版本做一些不同的事情:SSL握手等等。</p> <p>重点来了! connectSocket(connectTimeout, readTimeout); 里的逻辑实际上是:</p> <pre>  <code class="language-java">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的输入和输出流     }</code></pre> <pre>  <code class="language-java">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);           }       }   }</code></pre> </li>     <li> <p>构建HttpStream</p> <pre>  <code class="language-java">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);</code></pre> <p>至此,HttpStream就构建好了,通过它可以发送请求和接收response。</p> </li>    </ol>    <h3><strong>4.2 发送request/接收Response —— CallServerInterceptor</strong></h3>    <p>CallServerInterceptor的 intercept() 方法里 负责发送请求和获取响应,实际上都是由 HttpStream 类去完成具体的工作。</p>    <p>Http1XStream</p>    <p>一个socket连接用来发送HTTP/1.1消息,这个类严格按照以下生命周期:</p>    <ol>     <li>writeRequestHeaders() 发送request header</li>     <li>打开一个 sink 来写request body,然后关闭sink</li>     <li>readResponseHeaders()读取response头部</li>     <li>打开一个 source 来读取response body,然后关闭source</li>    </ol>    <p><strong>4.2.1 writeRequest</strong></p>    <p>HTTP报文是由一行一行的简单字符串组成的,都是纯文本,不是二进制代码,可以很方便地进行读写。</p>    <pre>  <code class="language-java">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];    }  }</code></pre>    <p style="text-align:center"><img src="https://simg.open-open.com/show/044cd3f35190d4f3861f5dee87755426.png"></p>    <p style="text-align:center">debug_write_request.png</p>    <p><strong>4.2.2 readResponse</strong></p>    <pre>  <code class="language-java">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));    }  }</code></pre>    <ol>     <li style="text-align:center"> <p>解析HTTP报文,得到HTTP协议版本。</p> <pre style="text-align:left">  <code class="language-java">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.")) {         .......</code></pre> <img src="https://simg.open-open.com/show/e917762af9bbaed43b96fd9cc41de965.png"> <p>debug_status_line.png</p> </li>     <li style="text-align:center"> <p style="text-align:left">读取ResponseHeader</p> <pre style="text-align:left">  <code class="language-java">/** 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();     }</code></pre> <img src="https://simg.open-open.com/show/34ee6c024b2b2bdfb246c5b7ad2628ef.png"> <p>debug_read_response_header.png</p> </li>     <li> <p>读取ResponseBody,读取InputStream获得byte数组, <strong>至此就完全得到了客户端请求服务端接口 的响应内容。</strong></p> <pre>  <code class="language-java">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());     }</code></pre> </li>    </ol>    <p><img src="https://simg.open-open.com/show/157093b11942fbee7ba6dbecc2bef996.png"></p>    <p style="text-align:center">debug_result.png</p>    <h2><strong>5 总结</strong></h2>    <p>从上面关于okhttp发送网络请求及接受网络响应的过程的分析,可以发现 okhttp并不是Volley和Retrofit这种二次封装的网络框架,而是 <strong>基于最原始的java socket连接自己去实现了HTTP协议</strong> ,就连Android源码也将其收录在内,堪称网络编程的典范。结合HTTP协议相关书籍与okhttp的源码实践相结合进行学习,相信可以对HTTP协议有具体且深入的掌握。</p>    <p> </p>    <p>来自:http://www.jianshu.com/p/57c0b069452b</p>    <p> </p>    
 本文由用户 adora89l 自行上传分享,仅供网友学习交流。所有权归原作者,若您的权利被侵害,请联系管理员。
 转载本站原创文章,请注明出处,并保留原始链接、图片水印。
 本站是一个以用户分享为主的开源技术平台,欢迎各类分享!
 本文地址:https://www.open-open.com/lib/view/open1480559791642.html
HTTP OkHttp Android开发 移动开发