Okhttp解析(四)网络连接的建立

Okhttp作为一款底层网络访问框架,它和Volley等上层网络框架不一样的地方在于,Okhttp自己实现了与服务端的TCP连接,并在此连接上根据HTTP协议的规范与服务端进行HTTP协议及内容的请求和响应。Okhttp将请求内容通过修正,填充等方式封装成符合HTTP规范的HTTP请求内容,通过TCP连接,将内容以流的方式输出给服务端,并从服务端返回的响应流中读取出响应内容,根据HTTP协议解析并作出相应的响应。

Okhttp连接建立的情况区分


Okhttp支持HTTP 1.0/1.1, HTTP2,SPDY 三种HTTP协议,同时支持加密传输HTTPS,以及SOCKS代理和HTTP代理。那么在连接建立时就需要处理它们组合时的处理操作了。这里的连接根据代理类型做主要区分。

无代理

  1. 无代理的HTTP请求, 与服务器建立TCP连接。
  2. 无代理的HTTPS加密请求, 与服务器建立TCP连接,然后建立TLS加密连接。
  3. 无代理的HTTP2/SPDY请求, 与服务器建立TCP连接,然后建立TLS加密连接,然后创建帧连接。

SOCKS代理

SOCKS代理服务器会将请求信息转发给目标HTTP服务器,目标HTTP服务器返回信息之后,SOCKS代理服务器再将响应信息返回给客户端。SOCKS代理服务器只会对信息进行转发,而不会解析修改。

  1. SOCKS代理下的HTTP请求, 与SOCKS代理服务器建立TCP连接。
  2. SOCKS代理下的HTTPS加密请求, 通过SOCKS代理服务器与HTTP服务器建立连接,然后建立TLS加密连接。
  3. SOCKS代理下的HTTP2/SPDY请求, 通过SOCKS代理服务器与HTTP服务器建立连接,然后建立TLS加密连接,然后创建帧连接。

HTTP代理

  1. HTTP代理下的HTTP请求, 与HTTP代理服务器建立TCP连接。HTTP代理服务器会解析请求的内容信息,并根据这些信息去请求目标HTTP服务器,目标HTTP2服务器返回信息之后,再解析响应内容,然后再将这些响应信息返回给客户端。HTTP服务器会解析和修改请求和响应的内容。
  2. HTTP代理下的HTTPS加密请求, 与目标服务器建立通过HTTP代理的隧道连接,然后建立TLS加密连接。因为是加密连接,HTTP代理服务器不再解析请求和响应内容,而只是转发数据。
  3. HTTP代理下的HTTP2/SPDY请求, 与目标服务器建立通过HTTP代理的隧道连接,然后建立TLS加密连接,然后创建帧连接。因为是加密连接,HTTP代理服务器不再解析请求和响应内容,而只是转发数据。

虽然情况分为很多种,但是重要的部分只是以下几个阶段。

  1. 根据是否需要代理,创建Socket连接。
  2. 进入Socket连接阶段,判断是否需要TLS连接,进入TLS连接处理。
  3. 进入TLS连接处理阶段,判断是否需要建立隧道连接。
  4. 完成TLS握手,判断是否需要帧连接。

关于SOCKS代理和HTTP代理区别

Socks代理

是全能代理,就像有很多跳线的转接板,它只是简单地将一端的系统连接到另外一端。支持多种协议,包括http、ftp请求及其它类型的请求。它分socks 4 和socks 5两种类型,socks 4只支持TCP协议而socks 5支持TCP/UDP协议,还支持各种身份验证机制等协议。其标准端口为1080。socks代理相应的采用socks协议的代理服务器就是SOCKS服务器,是一种通用的代理服务器。Socks是个电路级的底层网关,是DavidKoblas在1990年开发的,此后就一直作为Internet RFC标准的开放标准。Socks不要求应用程序遵循特定的操作系统平台,Socks 代理与应用层代理、 HTTP 层代理不同,Socks代理只是简单地传递数据包,而不必关心是何种应用协议(比如FTP、HTTP和NNTP请求)。所以,Socks代理比其他应用层代理要快得多。它通常绑定在代理服务器的1080端口上。如果您在企业网或校园网上,需要透过防火墙或通过代理服务器访问Internet就可能需要使用SOCKS。一般情况下,对于拨号上网用户都不需要使用它。注意,浏览网页时常用的代理服务器通常是专门的http代理,它和SOCKS是不同的。因此,您能浏览网页不等于您一定可以通过SOCKS访问Internet。 常用的防火墙,或代理软件都支持SOCKS,但需要其管理员打开这一功能。如果您不确信您是否需要SOCKS或是否有SOCKS可用,请与您的网络管理员联系。


HTTP代理

www对于每一个上网的人都再熟悉不过了,www连接请求就是采用的http协议,所以我们在浏览网页,下载数据(也可采用ftp协议)是就是用http代理。它通常绑定在代理服务器的80、3128、8080等端口上。

详见

http://blog.csdn.net/clh604/article/details/9235597

http://blog.csdn.net/extraordinarylife/article/details/52512860

Okhttp连接建立的流程

有了以上对HTTP连接建立的大概了解后,我们接下来分析代码就会更容易理解了。我们从RealConnection的connect方法开始

public void connect(int connectTimeout, int readTimeout, int writeTimeout,
      List<ConnectionSpec> connectionSpecs, boolean connectionRetryEnabled) throws RouteException {
    if (protocol != null) throw new IllegalStateException("already connected");

    RouteException routeException = null;
    //根据连接配置信息创建连接配置选择器,用于后面自动重试路由地址建立可用TCP连接
    ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
    Proxy proxy = route.proxy();
    Address address = route.address();
    //对于普通HTTP连接,连接配置信息中必须包含CLEARTEXT,也就是明文传输
    if (route.address().sslSocketFactory() == null
        && !connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
      throw new RouteException(new UnknownServiceException(
          "CLEARTEXT communication not supported: " + connectionSpecs));
    }
    //进入while循环,创建连接,直到连接成功
    while (protocol == null) {
      try {
        //创建Socket,如果是无代理或HTTP代理,交给SocketFactory创建Socket,如果是SOCKS代理,则手动创建一个代理Socket
        rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
            ? address.socketFactory().createSocket()
            : new Socket(proxy);
        //连接Socket
        connectSocket(connectTimeout, readTimeout, writeTimeout, connectionSpecSelector);
      } catch (IOException e) {
        closeQuietly(socket);
        closeQuietly(rawSocket);
        socket = null;
        rawSocket = null;
        source = null;
        sink = null;
        handshake = null;
        protocol = null;

        if (routeException == null) {
          routeException = new RouteException(e);
        } else {
          routeException.addConnectException(e);
        }

        if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
          throw routeException;
        }
      }
    }
}

可以看到上面很简单,首先创建一个底层Socket,然后进入Socket连接阶段。我们继续看connectSocket

/** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
  private void connectSocket(int connectTimeout, int readTimeout, int writeTimeout,
      ConnectionSpecSelector connectionSpecSelector) throws IOException {
    //设置读取超时时间
    rawSocket.setSoTimeout(readTimeout);
    分android和java两种平台进行Socket的连接,其实也就是调用socket的connect
    try {
      Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
    } catch (ConnectException e) {
      throw new ConnectException("Failed to connect to " + route.socketAddress());
    }
    //根据socket取得封装后的输入和输出流,类似InputStream和OutputStream
    source = Okio.buffer(Okio.source(rawSocket));
    sink = Okio.buffer(Okio.sink(rawSocket));

    if (route.address().sslSocketFactory() != null) {
      //建立TLS连接,看看哪些情况需要建立TLS连接
      connectTls(readTimeout, writeTimeout, connectionSpecSelector);
    } else {
      //不需要建立TLS连接的,统一当做HTTP 1.1协议
      protocol = Protocol.HTTP_1_1;
      socket = rawSocket;
    }

    if (protocol == Protocol.SPDY_3 || protocol == Protocol.HTTP_2) {
      socket.setSoTimeout(0); // Framed connection timeouts are set per-stream.
      //如果是SPDY或者HTTP2协议连接,则需要创建帧连接
      FramedConnection framedConnection = new FramedConnection.Builder(true)
          .socket(socket, route.address().url().host(), source, sink)
          .protocol(protocol)
          .listener(this)
          .build();
      //发送帧连接相关的信息
      framedConnection.sendConnectionPreface();

      //获取一个帧连接支持的最大并发流的数量
      // Only assign the framed connection once the preface has been sent successfully.
      this.allocationLimit = framedConnection.maxConcurrentStreams();
      this.framedConnection = framedConnection;
    } else {
      //不支持帧连接的,最大并发流为1,也是不支持流并发
      this.allocationLimit = 1;
    }
  }

可以看到这里分为3个步骤。

  1. 建立了底层Socket的连接,并获得输入和输出流。
  2. 判断是否进行TLS握手连接。HTTPS,HTTP2,SPDY三种情况下需要建立TLS加密连接。
  3. 判断是否需要创建帧连接,并进行帧连接握手。HTTP2,SPDY情况下需要建立帧连接。

接着我们进入建立TLS连接的过程,

private void connectTls(int readTimeout, int writeTimeout,
      ConnectionSpecSelector connectionSpecSelector) throws IOException {
    //在HTTP代理下,HTTPS,HTTP2,SPDY情况下,要建立隧道连接
    if (route.requiresTunnel()) {
      createTunnel(readTimeout, writeTimeout);
    }

    Address address = route.address();
    SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
    boolean success = false;
    SSLSocket sslSocket = null;
    try {
      //基于之前的TCP连接,创建加密的SSLSocket连接
      // Create the wrapper over the connected socket.
      sslSocket = (SSLSocket) sslSocketFactory.createSocket(
          rawSocket, address.url().host(), address.url().port(), true /* autoClose */);

      //配置加密连接需要的秘钥,TLS版本等信息
      // Configure the socket's ciphers, TLS versions, and extensions.
      ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
      if (connectionSpec.supportsTlsExtensions()) {
        Platform.get().configureTlsExtensions(
            sslSocket, address.url().host(), address.protocols());
      }
      //开始TLS握手
      // Force handshake. This can throw!
      sslSocket.startHandshake();
      //获取TLS握手后的结果
      Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession());
      //验证收到的证书的地址和服务端地址是否相同,防止证书被篡改。
      // Verify that the socket's certificates are acceptable for the target host.
      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.
      address.certificatePinner().check(address.url().host(),
          unverifiedHandshake.peerCertificates());

      //建立了TLS连接,获取协商之后的HTTP协议
      // Success! Save the handshake and the ALPN protocol.
      String maybeProtocol = connectionSpec.supportsTlsExtensions()
          ? Platform.get().getSelectedProtocol(sslSocket)
          : null;
      //指定默认的socket连接,输入和输出流
      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 {
      //进行TLS握手结束阶段的资源清理
      if (sslSocket != null) {
        Platform.get().afterHandshake(sslSocket);
      }
      //TLS连接不成功的话,关闭TLS连接
      if (!success) {
        closeQuietly(sslSocket);
      }
    }
  }

可以看到TLS连接这个阶段主要做了以下操作。

  1. 判断是否需要建立隧道连接。在HTTP代理下,HTTPS,HTTP2,SPDY情况下,要建立隧道连接
  2. 创建SSLSocket这个TLS连接,并开始握手。
  3. 获取握手后的返回信息,例如连接协议的商定,版本信息,加密算法,证书的验证等。
  4. TLS成功建立后,更新socket,HTTP协议,输入输出流等信息。
  5. 释放资源,TLS连接成功后的资源清理和失败后的关闭操作。

接下来我们看看是如何建立隧道连接的。

/**
   * To make an HTTPS connection over an HTTP proxy, send an unencrypted CONNECT request to create
   * the proxy connection. This may need to be retried if the proxy requires authorization.
   */
  private void createTunnel(int readTimeout, int writeTimeout) throws IOException {
    //创建隧道连接的请求
    // Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
    Request tunnelRequest = createTunnelRequest();
    HttpUrl url = tunnelRequest.url();
    //隧道连接的请求行
    String requestLine = "CONNECT " + Util.hostHeader(url, true) + " HTTP/1.1";
    //while循环中,不断重试建立隧道连接,直到建立成功
    while (true) {
      //创建隧道连接
      Http1xStream tunnelConnection = new Http1xStream(null, source, sink);
      source.timeout().timeout(readTimeout, MILLISECONDS);
      sink.timeout().timeout(writeTimeout, MILLISECONDS);
      //输出隧道连接的请求头和请求行数据,请求建立隧道连接
      tunnelConnection.writeRequest(tunnelRequest.headers(), requestLine);
      tunnelConnection.finishRequest();
      //读取响应信息
      Response response = tunnelConnection.readResponse().request(tunnelRequest).build();
      // The response body from a CONNECT should be empty, but if it is not then we should consume
      // it before proceeding.
      long contentLength = OkHeaders.contentLength(response);
      if (contentLength == -1L) {
        contentLength = 0L;
      }
      //忽略隧道连接请求过程中响应的数据
      Source body = tunnelConnection.newFixedLengthSource(contentLength);
      Util.skipAll(body, Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
      body.close();

      switch (response.code()) {
        case HTTP_OK:
          //响应码返回HTTP_OK,说明隧道连接建立成功
          // Assume the server won't send a TLS ServerHello until we send a TLS ClientHello. If
          // that happens, then we will have buffered bytes that are needed by the SSLSocket!
          // This check is imperfect: it doesn't tell us whether a handshake will succeed, just
          // that it will almost certainly fail because the proxy has sent unexpected data.
          if (!source.buffer().exhausted() || !sink.buffer().exhausted()) {
            throw new IOException("TLS tunnel buffered too many bytes!");
          }
          return;

        case HTTP_PROXY_AUTH:
          //响应码返回HTTP_PROXY_AUTH,说明还需要进行证书验证,while循环再次请求
          tunnelRequest = route.address().proxyAuthenticator().authenticate(route, response);
          if (tunnelRequest != null) continue;
          throw new IOException("Failed to authenticate with proxy");

        default:
          throw new IOException(
              "Unexpected response code for CONNECT: " + response.code());
      }
    }
  }
  
  /**
   * Returns a request that creates a TLS tunnel via an HTTP proxy, or null if no tunnel is
   * necessary. Everything in the tunnel request is sent unencrypted to the proxy server, so tunnels
   * include only the minimum set of headers. This avoids sending potentially sensitive data like
   * HTTP cookies to the proxy unencrypted.
   */
  private Request createTunnelRequest() throws IOException {
    //创建隧道连接的请求,就是指定请求头的一些字段信息
    return new Request.Builder()
        .url(route.address().url())
        .header("Host", Util.hostHeader(route.address().url(), true))
        .header("Proxy-Connection", "Keep-Alive")
        .header("User-Agent", Version.userAgent()) // For HTTP/1.0 proxies like Squid.
        .build();
  }

可见隧道连接的建立过程,是通过封装的Http1xStream流对象,输出建立隧道连接所需要的请求头和请求行信息,去请求建立隧道连接,这个过程中,可能需要证书的验证,因此,在while循环中,根据返回的响应信息重新创建请求,并提交请求,知道返回隧道创建成功或抛出异常。

到这里的话,Okhttp网络连接的建立过程就讲解完成了。

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

推荐阅读更多精彩内容