OkHttp 系列六:连接拦截器

510 阅读6分钟

连接拦截器

  • 本文概述:

    • 文章以OkHttp 中的连接拦截器为主题,讨论了其工作时机、工作内容、initExchange 干了什么、OkHttp 是怎么建立Socket连接、建立好Socket 连接后,我们怎么知道是发起HTTP1.X 还是HTTP2.0呢、补充了ALPN 协议、补充了代理连接;

什么时候工作?

  • 请求经过缓存拦截器处理后,不满足使用强缓存的条件(需要使用协商缓存或者其他缓存),此时就会将请求下发到连接拦截器;

主要干什么:

  • 帮助获取连接对象

源码长什么样子:ConnectInterceptor

 object ConnectInterceptor : Interceptor {
   @Throws(IOException::class)
   override fun intercept(chain: Interceptor.Chain): Response {
     val realChain = chain as RealInterceptorChain
     
     // 获取连接 Exchange对象:进行数据交换(可以看成一个连接)
     //利用了第一个拦截器( 重试重定向拦截器)在Call 身上创建的去获取连接的一个对象去完成连接的获取与查找 
     val exchange = realChain.call.initExchange(chain)
     
     //根据exchange 去创建一个新的链条对象,去执行最后一个拦截器
     val connectedChain = realChain.copy(exchange = exchange)
     
       return connectedChain.proceed(realChain.request)
   }
 }

initExchange 干了什么?

  • 获取编解码器对象:将请求封装为Http 报文,将响应解析为OkHttp 中的Response 对象

     //ExchangeCodec: 编解码器 find:查找连接Realconnection
     val codec = exchangeFinder.find(client, chain)
    
    • find 方法是怎么去查到并获取一个连接,又是怎么确定编解码器使用的是Http 1 还是Http 2?
  • 封装数据交换器对象:initExchange 最后就会返回出这个对象

     // Exchange:数据交换器 包含了exchangecodec与Realconnection
     val result = Exchange(this, eventListener, exchangeFinder, codec)
    
  • find 方法干了什么?

    • 内部存在findHealthyConnection ,在其中就完成了连接的查找与获取

       val resultConnection = findHealthyConnection(
           connectTimeout = chain.connectTimeoutMillis,
           readTimeout = chain.readTimeoutMillis,
           writeTimeout = chain.writeTimeoutMillis,
           pingIntervalMillis = client.pingIntervalMillis,
           connectionRetryEnabled = client.retryOnConnectionFailure,
           doExtensiveHealthChecks = chain.request.method != "GET"
       )
      
      • findHealthyConnection 干了什么?

        • 执行findConnection :去获取连接

        • 判断连接是否健康:判断Sokect 是否关闭等

          • 因为OkHttp 使用的是Http 协议,Http 协议使用的是TCP/IP,而底层是使用Socket 协议;
           if (rawSocket.isClosed || socket.isClosed || socket.isInputShutdown ||
               socket.isOutputShutdown) {
               return false
           }
          
      • findConnection 是怎么去找到连接的?

        • 首先判断连接是否被取消

           if (call.isCanceled()) throw IOException("Canceled")
          
        • 拿到连接

           //这个Call 是AsyncCall 的一个对象,connection 为其类属性代表一个连接 
           val callConnection = call.connection
          
        • 判断连接的非空性?

          • 第一次请求肯定是null ,后面进行重定向后可能就不为null
        • 当连接为空:从连接池中找有没有现成的与本次请求目标服务器相同的连接,有就直接拿来用

         //连接池 查找 连接
         if (connectionPool.callAcquirePooledConnection(address, call, null, false)) {
             val result = call.connection!!
             eventListener.connectionAcquired(call, result)
             return result
         }
        
        • 没有:创建RealConnection 对象
         val newConnection = RealConnection(connectionPool, route)
        
        • 调用connect 建立连接:Socket

           try {
               newConnection.connect(
                   ……
               )
          
      • findHealthyConnection 与 findConnection 有什么区别?

        • 后者获取的连接不一定是健康的,也就是说这个连接可能与服务器已经断开了 ;连接健康(能传数据)
    • 利用连接(resultConnection) 去创建ExchangeCodec

       return resultConnection.newCodec(client, chain)
      
      • newCodec 干了什么?

        • 根据RealCall 连接对象做了协议的判断
         return if (http2Connection != null) {
             Http2ExchangeCodec(client, this, chain, http2Connection)
         } 
        

OkHttp 是怎么建立Socket连接的:调用RealCall 对象的connect

  • 在while (true) 中

    • 建立隧道代理

       if (route.requiresTunnel()) {
      
      1. connectSocket:最终调用socket.connect
      2. 先发connect 请求 建立隧道代理
       val requestLine = "CONNECT ${url.toHostHeader(includeDefaultPort = true)} HTTP/1.1"
      
    • 建立HTTP 普通/Socket 代理

       else{
      

建立好Socket 连接后,我们怎么知道是发起HTTP1.X 还是HTTP2.0呢?

  • OkHttp 在完成连接后会记录所使用的HTTP 版本信息

     connectTls(connectionSpecSelector)
    
  • connectTls:SSL 握手(在这个地方就会确认所使用的HTTP 版本信息)

     //maybeProtocol 的非空性决定所使用的HTTP 协议版本
           // Success! Save the handshake and the ALPN protocol.
     val maybeProtocol = if (connectionSpec.supportsTlsExtensions) {
         Platform.get().getSelectedProtocol(sslSocket)
     } 
     //默认使用HTTP 1.1
     protocol = if (maybeProtocol != null) Protocol.get(maybeProtocol) else Protocol.HTTP_1_1
    
  • 关键代码:getSelectedProtocol

    • 从sslSocket 中拿到当前所使用的HTTP 协议版本信息

      • 在握手的时候已经确认了所使用的HTTP 版本信息

      • 底层:Android 的实现

        • 反射调用sslSocket 中的一个方法,得到返回值alpnResult (这个里面就有所使用的HTTP 版本信息)

补充ALPN 协议:应用层协议协商扩展(确定使用的HTTP 版本信息)

  • 是TLS (SSL)的扩展协议,SSL 握手的时候会去确认所使用的加密方式,协议版本号,证书校验等;
  • SSL 握手第一步:客户端发送Client Hello(包含了ALPN ---> 里面就有所支持的HTTP 版本信息),服务器在回传响应(Server Hello)的时候,里面包含ALPN ---> 指定了所使用的HTTP 协议版本信息

代理连接相关补充:

  • 概述:在建立Socket 连接的时候就可以配置代理

    • Socket 代理连接

      • 代理的是Sokect(TCP/IP ),可以完成TCP/IP 协议所有的代理(RTMP,RTSP)
      • 代理服务器连接的是真实服务器
    • HTTP 代理连接

      • 后者代理的是(HTTP ),只能代理(HTTP 代理)

      • HTTP普通代理:

        • connect 的是HTTP代理服务器并不是真实的服务器

        • 发起请求 (get 请求) 时,需要在请求行的Path 中附带域名(这样代理服务端才知道要请求谁)

          • 原始请求行:

            • 请求方法,Path,协议版本号
      • HTTP隧道代理:建立连接部分跟普通HTTP 代理相同

        • 在真正进行数据请求前,需要先发connect 请求(包含真实服务端的地址)
  • 示意图:

    image-20220730225105484

  • 代理是怎么工作的:C <---> S

    • 起到一个中间人的作用,C 端将请求交给代理,代理再交给S 端;S 端将响应交给代理,代理再将响应交给C 端
  • 细节:如果使用的是HTTPS,那么此时使用的是HTTP 隧道代理

  • 隧道代理与普通代理有什么区别?

    • 普通代理:充当中间人

      • 在拿到C 端请求后,可以对其做一定处理,然后交给S 端
    • 隧道代理:不再充当中间人

      • 不能修改客户端请求,无脑转发给服务端;且其需要先发起Http CONNECT 请求(没有请求体,仅提供给代理服务器使用,并且不会传给服务端)
  • OkHttp 是可以允许配置HTTP 代理的

    • 基本上找不到免费的HTTP 代理服务器
     var okHttpClient = OkHttpClient.Builder()
     //配置Http 代理
     .proxy(Proxy.Type.HTTP,InetSocketAddress("HTTP 代理服务器域名","端口号"))
    
    • 测试
     //okhttp的用法,还可以
     //Socket 指定代理服务器
     Socket socket = new Socket(new Proxy(
         Proxy.Type.HTTP, new InetSocketAddress(
             "124.205.155.148",9090)));
     //Socket 指定真实服务器
     socket.connect(new InetSocketAddress("restapi.amap.com", 80));
     System.out.println("连接完成!");
     ​
     //照常请求:
     StringBuilder sb = new StringBuilder();
     sb.append("GET /v3/weather/weatherInfo?city=长沙&key" +
               "=13cb58f5884f9749287abbead9c658f2 HTTP/1.1\r\n");
     ​
     但是,如果你是直接用Socket对象connect代理服务器,那么在后续请求中,需要在Path处加上域名
     如果是HTTPS ,需要在真正请求之前先发起一次connect 请求,当服务端响应(表示连接建立成功)才能去发起真正请求
    
  • OkHttp 同样可以配置Socket 代理

    • 启动Socket 代理服务端

    • 创建Socket 对象:需要指定代理服务器IP 与 端口

       Socket socket = new Socket(new Proxy(
           Proxy.Type.SOCKS, new InetSocketAddress(
               "localhost",808)));
      
    • 调用connect:传入真正目标服务器的IP 与 端口

       //Socket 指定真实服务器
       socket.connect(new InetSocketAddress("restapi.amap.com", 80));
      
    • 然后就正常地使用就行了

    • Socket 代理服务器

      • 可以拿到用户的请求并解析,还可以拿到服务器的响应进行处理;甚至还可以处理指定的握手轮次