在之前的博客中我们知道Okhttp在发起链接请求先从链接池中获取连接,如果链接池中没有链接则创建新的链接RealConnection对象,然后执行其connet方法打开SOCKET链接(详见《 OkHtp之ConnectInterceptor简单分析》):

rivate RealConnection findConnection(。。。){
     result = new RealConnection(connectionPool, selectedRoute);
    result.connect(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled);

}

看看RealConnection的connect方法都做些了什么:

public void connect(。。。) {
     //如果协议不等于null,抛出一个异常
    if (protocol != null) throw new IllegalStateException("already connected");

   。。 省略部分代码。。。。

    while (true) {//一个while循环
         //如果是https请求并且使用了http代理服务器
        if (route.requiresTunnel()) {
          connectTunnel(...);
        } else {//
            //直接打开socket链接
          connectSocket(connectTimeout, readTimeout);
        }
        //建立协议
        establishProtocol(connectionSpecSelector);
        break;//跳出while循环
        。。省略部分代码。。。
  }
 //当前route的请求是https并且使用了Proxy.Type.HTTP代理
 public boolean requiresTunnel() {
    return address.sslSocketFactory != null && proxy.type() == Proxy.Type.HTTP;
  }

简单来说上面connect主要做了如下的事儿:

1、如果当前route的请求是https并且使用了Proxy.Type.HTTP代理,就开启一个隧道链接。

2、如果1不成立,则调用connectSocket方法创建Socket链接

3、调用establishProtocol构建协议。

为了方便后续博文的说明,先简单说一下connectSocket方法都干了些什么:

  private void connectSocket(int connectTimeout, int readTimeout)  {

    Proxy proxy = route.proxy();
    Address address = route.address();
    //1、初始化socket
    rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
        ? address.socketFactory().createSocket()
        : new Socket(proxy);//使用SOCKS的代理服务器

    rawSocket.setSoTimeout(readTimeout);

      //2、打开socket链接
      Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
    //3、对输入和输出做处理
    source = Okio.buffer(Okio.source(rawSocket));
    sink = Okio.buffer(Okio.sink(rawSocket));
  }

1、如果没有使用代理或者使用了HTTP代理,那么通过Address对象的socketFactory来创建一个Socket:注意在构建Address对象时候如果在初始化OkhttpClient的时候没有build SocketFactory,那么把address对象的socketFactory引用初始化为 : SocketFactory.getDefault()。

2、如果使用了SOCKET代理,则初始化一个Socket

3、调用Platform.get().connectSocket实际就是调用socket的connect方法来打开一个连接

4、然后将socket的输入流inputStream交给 Okio.buffer(Okio.source(rawSocket))也就是Source对象,输出流outputStream对象交给 Okio.buffer(Okio.sink(rawSocket))即Sink对象。 由Okio对连接进行读写数据(关键Okio的这两种方法,会另开博文解释),此处只需要简单的理解为source对象为读取服务数据,sink为向服务器发送数据即可。

简单的来说connectSocket方法其作用就是打开了一条Socket链接

下面就来分析requiresTunnel方法,在分析该方法之前先回顾 HTTP的一些基础知识:

什么是隧道呢?隧道技术(Tunneling)是HTTP的用法之一,使用隧道传递的数据(或负载)可以是不同协议的数据帧或包,或者简单的来说隧道就是利用一种网络协议来传输另一种网络协议的数据。比如A主机和B主机的网络而类型完全相同都是IPv6的网,而链接A和B的是IPv4类型的网络,A和B为了通信,可以使用隧道技术,数据包经过Ipv4数据的多协议路由器时,将IPv6的数据包放入IPv4数据包;然后将包裹着IPv6数据包的IPv4数据包发送给B,当数据包到达B的路由器,原来的IPv6数据包被剥离出来发给B。

SSL隧道:SSL隧道的初衷是为了通过防火墙来传输加密的SSL数据,此时隧道的作用就是将非HTTP的流量(SSL流量)传过防火墙到达指定的服务器。

**怎么打开隧道?**HTTP提供了一个CONNECT方法 ,它是HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器,该方法就是用来建议一条web隧道。客户端发送一个CONNECT请求给隧道网关请求打开一条TCP链接,当隧道打通之后,客户端通过HTTP隧道发送的所有数据会转发给TCP链接,服务器响应的所有数据会通过隧道发给客户端。

(注:以来内容来源参考《计算机网络第五版》和《HTTP权威指南》第八章的有关内容,想深入了解的话可以查阅之。)

关于CONNECT在HTTP 的首部的内容格式,可以简单如下表示:

CONNECT hostname:port HTTP/1.1

准备工作完成之后,继续回到OKhttp的代码上来。

  private void connectTunnel(int connectTimeout, int readTimeout, int writeTimeout)
      throws IOException {
      //1、创建隧道请求对象
    Request tunnelRequest = createTunnelRequest();
    HttpUrl url = tunnelRequest.url();
    int attemptedConnections = 0;
    int maxAttempts = 21;
    //一个while循环
    while (true) {
       //尝试连接词说超过最大次数
      if (++attemptedConnections > maxAttempts) {
        throw new ProtocolException("Too many tunnel connections attempted: " + maxAttempts);
      }
      //2、打开socket链接
      connectSocket(connectTimeout, readTimeout);
     //3、请求开启隧道并返回tunnelRequest
      tunnelRequest = createTunnel(readTimeout, writeTimeout, tunnelRequest, url);

     //4、成功开启了隧道,跳出while循环
      if (tunnelRequest == null) break; /

      //隧道未开启成功,关闭相关资源,继续while循环
      //当然,循环次数超限后抛异常,退出wiile循环
      closeQuietly(rawSocket);
      rawSocket = null;
      sink = null;
      source = null;
    }
  }

上面的代码大致做了如下工作:

1、调用createTunnelRequest构建Request对象:

   private Request createTunnelRequest() {
    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())
        .build();

上面代码是是创建了一个Request对象,只不过这个对象增加了Host、Proxy-Connection、User-Agent首部。

Host首部:该首部主要是为了解决Http/1.0缺少主机信息(主机名和端口号)导致虚拟服务器不可用的问题(详细解释可参考《HTTP权威指南》第18章)。

Proxy-Connection:主要是解决(不支持Keep-alive首部的)代理服务器盲目转发Keep-alive给服务器,造成客户端挂起的问题(详细解释可参考《HTTP权威指南》第4章).

2、进入while循环,首先调用connectSocket打开TCP链接。而后调用createTunnel创建一个隧道,有意思的是这个方法放回一个Requset对象,当返回的Request对象不为null的时候说明隧道打通,退出while循环,否则就关闭当前循环的socket,继续循环直到隧道请求成功或者循环次数超出指定的值而异常退出。

所以看看这个createTunnel方法都做了些神马:

 private Request createTunnel(int readTimeout, int writeTimeout, Request tunnelRequest,
      HttpUrl url) throws IOException {
    // 拼接CONNECT命令
    String requestLine = "CONNECT " + Util.hostHeader(url, true) + " HTTP/1.1";
    while (true) {//又一个while循环
      //对应http/1.1 编码HTTP请求并解码HTTP响应
      Http1Codec tunnelConnection = new Http1Codec(null, null, source, sink);
       。。。
      //发送CONNECT,请求打开隧道链接,
      tunnelConnection.writeRequest(tunnelRequest.headers(), requestLine);
      //完成链接
      tunnelConnection.finishRequest();
      //构建response,操控的是inputStream流
      Response response = tunnelConnection.readResponseHeaders(false)
          .request(tunnelRequest)
          .build();

       。。。。。

      switch (response.code()) {
        case HTTP_OK:
          return null;
        case HTTP_PROXY_AUTH://表示服务器要求对客户端提供访问证书,进行代理认证
          //进行代理认证
          tunnelRequest = route.address().proxyAuthenticator().authenticate(route, response);
          //代理认证不通过
          if (tunnelRequest == null) throw new IOException("Failed to authenticate with proxy");

          //代理认证通过,但是响应要求close,则关闭TCP连接此时客户端无法再此连接上发送数据
          if ("close".equalsIgnoreCase(response.header("Connection"))) {
            return tunnelRequest;
          }
          break;

      }
    }
  }

上述方法作了如下几个工作:

1、拼接HTTP的 CONNECT信息

2、通过Http1Codec 的 writeRequest向TCP链接发起打开隧道请求。

3、构建服务器响应的Resopnse,如果状态码是200,说明CONNECT请求成功,如果是HTTP_PROXY_AUTH,说明需要进行代理认证,认证失败则抛异常,如果成功且服务器响应 了close,则返回tunnelRequest;如果返回其他状态码,则继续while循环。

到此为止,RealConnection打开隧道链接和打开Socket链接简单分析完毕。

至于最后的establishProtocol方法,目前还在研究中,后续会继续说明。

本篇博文有好几处“详细参考《HTTP权威指南》第xx章和《计算机网络》”的地方,本来也想详细写的,但是写多了觉得太啰里啰嗦,影响文章调理,所以有关网络的相关概念,还是请参考上面两本书,不一定仔细看完,大致翻翻即可。到此本篇博文简单介绍未必,如有不当之处,欢迎批评指正共同学习。

Okhttp之RealConnection建立链接简单分析的更多相关文章

  1. Okhttp对http2的支持简单分析

    在< Okhttp之RealConnection建立链接简单分析>一文中简单的分析了RealConnection的connect方法的作用:打开一个TCP链接或者打开一个隧道链接,在打开t ...

  2. Okhttp之连接池ConnectionPool简单分析(一)

    开篇声明:由于本篇博文用到的一些观点或者结论在之前的博文中都已经分析过,所以本篇博文直接拿来用,建议读此博文的Monkey们按照下面的顺序读一下博主以下博文,以便于对此篇博文的理解: <Okht ...

  3. Okhttp之CallServerInterceptor简单分析

    在Okhttp源码分析专栏的几篇博客分析了Okhttp几个拦截器的主要功能,还剩下最后一个拦截器CallServerInterceptor没有分析,本篇博客就简单分析下该拦截器的功能. 在Okhttp ...

  4. OkHttp之ConnectInterceptor简单分析

    在< Okhttp之CacheInterceptor简单分析 >这篇博客中简单的分析了下缓存拦截器的工作原理,通过此博客我们知道在执行完CacheInterceptor之后会执行下一个浏览 ...

  5. OkHttp之BridgeInterceptor简单分析

    在< Okhttp源码简单解析(一) >这篇博客简单分析了Okhttp请求的执行流程,通过该篇博客我们知道OkHttp的核心网络请求中内置"拦截器"发挥了重大作用:本篇 ...

  6. Okhttp之CacheInterceptor简单分析

    <OkHttp之BridgeInterceptor简单分析 >简单分析了BridgeInterceptor的工作原理,在Okhttp的拦截器链上BridgeInterceptor的下一个拦 ...

  7. python爬虫实践(二)——爬取张艺谋导演的电影《影》的豆瓣影评并进行简单分析

    学了爬虫之后,都只是爬取一些简单的小页面,觉得没意思,所以我现在准备爬取一下豆瓣上张艺谋导演的“影”的短评,存入数据库,并进行简单的分析和数据可视化,因为用到的只是比较多,所以写一篇博客当做笔记. 第 ...

  8. x264源代码简单分析:宏块编码(Encode)部分

    ===================================================== H.264源代码分析文章列表: [编码 - x264] x264源代码简单分析:概述 x26 ...

  9. FFmpeg源代码简单分析:configure

    ===================================================== FFmpeg的库函数源代码分析文章列表: [架构图] FFmpeg源代码结构图 - 解码 F ...

随机推荐

  1. 关于js中对事件绑定与普通事件的理解

    普通事件指的是可以用来注册的事件: 事件绑定是指把事件注册到具体的元素之上. 通俗点说: 普通事件:给html元素添加一个特定的属性(如:onclick): 事件绑定:js代码中通过标记(id  ta ...

  2. MySQL中变量的用法——LeetCode 178. Rank Scores

    Write a SQL query to rank scores. If there is a tie between two scores, both should have the same ra ...

  3. Spring MVC工作流程图

    图一   图二    Spring工作流程描述       1. 用户向服务器发送请求,请求被Spring 前端控制Servelt DispatcherServlet捕获:       2. Disp ...

  4. node实现rar格式压缩

    背景 由于公司的CMS系统里,只接受rar格式压缩的文件,所以没法直接使用nodejs里提供的zip压缩组件.只能从winRar软件入手了,但网上没有多少这方面相关的东西,所以下面也是自己尝试着在做. ...

  5. HTML代码转义(JAVA)

    String org.apache.commons.lang.StringEscapeUtils.escapeHtml(String str)     测试 System.out.println(St ...

  6. JAVA多线程本质分析

    多线程是Java开发中的重中之重,其重要性和难度,可见一斑.掌握并精通多线程开发,是每一个程序员的必修之课.哪怕中间的过程很痛苦,只要坚持了,并最终豁然开朗了,都是一种升华. 多线程的优化:合理利用C ...

  7. LeetCode——Range Sum Query - Immutable

    Question Given an integer array nums, find the sum of the elements between indices i and j (i ≤ j), ...

  8. 【Discriminative Localization】Learning Deep Features for Discriminative Localization 论文解析(转)

    文章翻译: 翻译 以下文章来源: 链接

  9. RabbitMQ入门(6)——远程过程调用(RPC)

    在RabbitMQ入门(2)--工作队列中,我们学习了如何使用工作队列处理在多个工作者之间分配耗时任务.如果我们需要运行远程主机上的某个方法并等待结果怎么办呢?这种模式就是常说的远程过程调用(Remo ...

  10. linux一键安装nginx脚本

    #!/bin/sh echo "----------------------------------start install nginx ------------------------- ...