美文网首页
okhttp3.6.0源码分析3——建立socket连接

okhttp3.6.0源码分析3——建立socket连接

作者: hello_小丁同学 | 来源:发表于2017-11-08 18:19 被阅读41次

    okhttp3.6.0源码分析系列文章整体内容如下:

    前言

    在开始本章之前有三个概念要明确一个:
    1.connection: socket连接,具体实现类是RealConnection
    2.stream: okhttp里面的stream指的是在connection上层的http请求/响应数据流。它对应的类是HttpCodec
    3.StreamAllocation:负责提供一个connection(如果有就复用,没有就会创建),和一个stream(根据Call组装成一个httpCodec对象),并让stream通过该connection进行传输。

    正文

    到拦截器为止,都是在介绍okhttp如何构造请求报文和处理响应报文,都是对http协议的具体实现。本章关注的内容则是数据是怎么调用socket进行传输的。

    http协议是无状态的,要进行一个http请求,就要先进行tcp握手,然后传输数据,最后释放。


    本文主要是分析RealConnection是如何被使用的。
    RealConnection是对数据传输连接的一个抽象。
    通过跟踪第一次建立socket连接的流程,来分析RealConnection 的使用。
    在执行ConnectInterceptor的intercept的时候有两个非常重要的步骤:

        HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
        RealConnection connection = streamAllocation.connection();
    
    1. 获取httpCodec实例,如果没有socket连接则创建一个
    2. 获取一个RealConnection,仅仅是保存一个RealConnection 的引用,开启连接的工作已经在新建HttpCodec的时候完成了。
      具体看下streamAllocation.newStream的方法:
    public HttpCodec newStream(OkHttpClient client, boolean doExtensiveHealthChecks) {
        int connectTimeout = client.connectTimeoutMillis();
        int readTimeout = client.readTimeoutMillis();
        int writeTimeout = client.writeTimeoutMillis();
        boolean connectionRetryEnabled = client.retryOnConnectionFailure();
    
        try {
          ①RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
              writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);
          ②HttpCodec resultCodec = resultConnection.newCodec(client, this);
    
          synchronized (connectionPool) {
            codec = resultCodec;
            return resultCodec;
          }
        } catch (IOException e) {
          throw new RouteException(e);
        }
      }
    

    可以看到HttpCodec对象是通过RealConnection实例来构建的。
    深入到①方法里面,看下它是怎么获取一个realConnection对象的:

    private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
          int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)
          throws IOException {
        while (true) {
         ③ RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
              connectionRetryEnabled);
    
          // If this is a brand new connection, we can skip the extensive health checks.
          synchronized (connectionPool) {
            if (candidate.successCount == 0) {
              return candidate;
            }
          }
    
          // Do a (potentially slow) check to confirm that the pooled connection is still good. If it
          // isn't, take it out of the pool and start again.
          if (!candidate.isHealthy(doExtensiveHealthChecks)) {
            noNewStreams();
            continue;
          }
    
          return candidate;
        }
      }
    

    具体工作是由③完成的,进入findConnection方法内部:

     private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
          boolean connectionRetryEnabled) throws IOException {
        Route selectedRoute;
        synchronized (connectionPool) {
          if (released) throw new IllegalStateException("released");
          if (codec != null) throw new IllegalStateException("codec != null");
          if (canceled) throw new IOException("Canceled");
    
          // Attempt to use an already-allocated connection.
          RealConnection allocatedConnection = this.connection;
          if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
            return allocatedConnection;
          }
    
          // Attempt to get a connection from the pool.
          Internal.instance.get(connectionPool, address, this, null);
          if (connection != null) {
            return connection;
          }
    
          selectedRoute = route;
        }
    
        // If we need a route, make one. This is a blocking operation.
        if (selectedRoute == null) {
          selectedRoute = routeSelector.next();
        }
    
        RealConnection result;
        synchronized (connectionPool) {
          if (canceled) throw new IOException("Canceled");
    
          // Now that we have an IP address, make another attempt at getting a connection from the pool.
          // This could match due to connection coalescing.
          Internal.instance.get(connectionPool, address, this, selectedRoute);
          if (connection != null) return connection;
    
          // Create a connection and assign it to this allocation immediately. This makes it possible
          // for an asynchronous cancel() to interrupt the handshake we're about to do.
          route = selectedRoute;
          refusedStreamCount = 0;
          result = new RealConnection(connectionPool, selectedRoute);
          acquire(result);
        }
    
        // Do TCP + TLS handshakes. This is a blocking operation.
        result.connect(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled);
        routeDatabase().connected(result.route());
    
        Socket socket = null;
        synchronized (connectionPool) {
          // Pool the connection.
          Internal.instance.put(connectionPool, result);
    
          // If another multiplexed connection to the same address was created concurrently, then
          // release this connection and acquire that one.
          if (result.isMultiplexed()) {
            socket = Internal.instance.deduplicate(connectionPool, address, this);
            result = connection;
          }
        }
        closeQuietly(socket);
    
        return result;
      }
    

    findConnection里面主要有六步:

    1. 尝试返回已经和这个allocatedConnection对象关联的连接
    2. 如果没有,尝试从连接池里面取出一个
    3. 如果还没有,设置一个新的路由,然后再次尝试从连接池里取一个
    4. 如果依旧没有,则创建一个RealConnection,然后让他被当前的streamAllocation引用,并记录引用次数
    5. 建立socket传输连接
    6. 将新建的RealConnection对象放入连接池里面

    到这里我们就有一个连接了,然后构建HttpCodec对象,具体方法:

      public HttpCodec newCodec(
          OkHttpClient client, StreamAllocation streamAllocation) throws SocketException {
        if (http2Connection != null) {
          return new Http2Codec(client, streamAllocation, http2Connection);
        } else {
          socket.setSoTimeout(client.readTimeoutMillis());
          source.timeout().timeout(client.readTimeoutMillis(), MILLISECONDS);
          sink.timeout().timeout(client.writeTimeoutMillis(), MILLISECONDS);
          return new Http1Codec(client, streamAllocation, source, sink);
        }
      }
    

    该方法有三步:

    1. 设置socket超时时间
    2. 数据读取超时时间
    3. 数据写入超时时间

    现在ConnectInterceptor的工作已经完成了。

    我们再回头看一下RealConnection的socket传输是怎样创建的。
    如果是https,还会先对传输的内容进行加密/解密

    public void connect(
          int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled) {
        if (protocol != null) throw new IllegalStateException("already connected");
    
        RouteException routeException = null;
        List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
        ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
    
        if (route.address().sslSocketFactory() == null) {
          if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
            throw new RouteException(new UnknownServiceException(
                "CLEARTEXT communication not enabled for client"));
          }
          String host = route.address().url().host();
          if (!Platform.get().isCleartextTrafficPermitted(host)) {
            throw new RouteException(new UnknownServiceException(
                "CLEARTEXT communication to " + host + " not permitted by network security policy"));
          }
        }
    
        while (true) {
          try {
            if (route.requiresTunnel()) {
              connectTunnel(connectTimeout, readTimeout, writeTimeout);
            } else {
              //建立socket连接
              connectSocket(connectTimeout, readTimeout);
            }
            establishProtocol(connectionSpecSelector);
            break;
          } catch (IOException e) {
            closeQuietly(socket);
            closeQuietly(rawSocket);
            socket = null;
            rawSocket = null;
            source = null;
            sink = null;
            handshake = null;
            protocol = null;
            http2Connection = null;
    
            if (routeException == null) {
              routeException = new RouteException(e);
            } else {
              routeException.addConnectException(e);
            }
    
            if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
              throw routeException;
            }
          }
        }
    
        if (http2Connection != null) {
          synchronized (connectionPool) {
            allocationLimit = http2Connection.maxConcurrentStreams();
          }
        }
      }
    

    如果用的是http协议,那么该方法只做一件事就是调用connectSocket(connectTimeout, readTimeout);建立一个socket连接。
    进入connectSocket内部看下:

    private void connectSocket(int connectTimeout, int readTimeout) throws IOException {
        Proxy proxy = route.proxy();
        Address address = route.address();
    
        rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
            ? address.socketFactory().createSocket()
            : new Socket(proxy);
    
        rawSocket.setSoTimeout(readTimeout);
        try {
          Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
        } catch (ConnectException e) {
          ConnectException ce = new ConnectException("Failed to connect to " + route.socketAddress());
          ce.initCause(e);
          throw ce;
        }
    
        // The following try/catch block is a pseudo hacky way to get around a crash on Android 7.0
        // More details:
        // https://github.com/square/okhttp/issues/3245
        // https://android-review.googlesource.com/#/c/271775/
        try {
          source = Okio.buffer(Okio.source(rawSocket));
          sink = Okio.buffer(Okio.sink(rawSocket));
        } catch (NullPointerException npe) {
          if (NPE_THROW_WITH_NULL.equals(npe.getMessage())) {
            throw new IOException(npe);
          }
        }
      }
    

    调用connectSocket建立socket连接的步骤:

    1. 获取socket实例,我们使用的是http协议,那就会从调用socket的工厂方法创建一个socket。
    2. 尝试和服务端建立socket连接
    3. 设置可以往数据传输流里读写数据的对象

    (完)

    参考:

    相关文章

      网友评论

          本文标题:okhttp3.6.0源码分析3——建立socket连接

          本文链接:https://www.haomeiwen.com/subject/bwrsmxtx.html