在之前的博客中我们知道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. [Linux 005]——IO重定向

    通常在 Shell 中执行命令的时候,我们会在输入命令的下方看到执行结果,操作系统默认将命令的执行结果输出到显示器上.当然,我们也可以手动的指定输出路径,或者输入路径,这就是 I/O 重定向. 1.标 ...

  2. 彻底的卸载干净oracle 11g

    1.关闭oracle所有的服务.可以在windows的服务管理器中关闭:   2.打开注册表:regedit 打开路径: <找注册表 :开始->运行->regedit>   H ...

  3. logstash在Windows2008简单配置实例

    Windows2008 安装java1.8,配置系统环境变量: 官方下载并安装略...然后配置 logstash的配置文件 注意PATH路径名称不支持中文 input { file { type =& ...

  4. 解题报告:hdu1008 Elvator

    2017-09-07 19:30:22 writer:pprp 比较顺利,最近生活出现了各种问题, 发生了很多矛盾,我要耐下心来,最重要的不是努力不努力,而是选择 希望我能处理好人际关系还有学业上的压 ...

  5. mysql 索引相关问题

    mysql中key .primary key .unique key 与index区别 https://blog.csdn.net/nanamasuda/article/details/5254317 ...

  6. Unity与Web结合

    偶然在论坛上看到了一篇文章,觉的挺有意思,转载一下,之前做游戏,现在做前端,这篇文章不错..转载 Unity WebPlayer 写在前面 最近在做unity与web之间通讯的项目,在网上搜索了一些资 ...

  7. spring mvc:内部资源视图解析器(注解实现)@Controller/@RequestMapping

    spring mvc:内部资源视图解析器(注解实现)@Controller/@RequestMapping 项目访问地址: http://localhost:8080/guga2/hello/prin ...

  8. 关于.net4.0中的Action委托

    在使用委托时,若封装的方法无返回值,并且参数在0-7个,可考虑使用.Net4.0中的Action委托,建议使用系统自带的,减少自定义 public delegate void Action<in ...

  9. JavaScript深拷贝—我遇到的应用场景

    简述 深拷贝即拷贝实例,其作用是为了不影响拷贝后的数组对起原数组造成影响.这时我们就需要进行深拷贝.(JavaScript的继承) 我遇到的应用场景 我是在用vue的element-ui做项目的时候遇 ...

  10. are only available on JDK 1.5 and higher

    根本原因是项目中的一些配置包括jar包什么的根当前jdk版本(我刚开始用的是1.8的,好像是不支持低版本的springjar包),反正正确的思路是更改jdk版本是最合理的,叫我去把所有spring版本 ...