转载

深入OKHttp之网络连接

我们在进行http请求的时候,会有大致如下几个流程: DNS -> 建立Socket连接 -> 应用层进行 http 请求 (图片来源网络)

深入OKHttp之网络连接

那么 OKHttp 是怎么进行每一步的处理呢,今天我们就来一探究竟。

ConnectInterceptor

ConnectInterceptor 中,我们可以看到如下几行代码

StreamAllocation streamAllocation = realChain.streamAllocation();
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
复制代码

可以看到这里初始化了一个 StreamAllocation ,开启了一次新的 newStream ,最终返回了一个 RealConnection 来表示连接的对象。

我们一步一步具体分析

newStream 中,会调用 findHealthyConnection :

while (true) {
	RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
    pingIntervalMillis, 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;
}
复制代码

这里,会有一个循环,一直在寻找一个 "healthy" 的连接,如果不是全新的连接,则会释放掉,继续去建立连接。

查看 findConnection ,我留下了部分关键代码进行分析:

if (this.connection != null) {
	// We had an already-allocated connection and it's good.
	result = this.connection;
	releasedConnection = null;
}
复制代码

通过注释我们了解到,我们已经有了一个可用的连接,直接复用。

if (result == null) {
	// Attempt to get a connection from the pool.
	Internal.instance.get(connectionPool, address, this, null);
	if (connection != null) {
		foundPooledConnection = true;
		result = connection;
	} else {
		selectedRoute = route;
	}
}
复制代码

如果不存在连接,去一个叫 connectionPool 的对象中尝试去取。

if (result != null) {
	// If we found an already-allocated or pooled connection, we're done.
	return result;
}
复制代码

如果这里已经找到了连接,就会直接返回。

我们继续看下面的代码,当需要我们自己创建一个连接的时候,OKHttp 是怎么处理的:

boolean newRouteSelection = false;
if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
	newRouteSelection = true;
	routeSelection = routeSelector.next();
}
复制代码

如果这时候没有 selectedRoute , 我们就从 routeSelector.next() 中选出一个 "路由选择"。其中包含了一套路由,每个路由有自己的地址和代理。

在拥有这组 ip 地址后,会再次尝试从 Pool 中获取连接对象。如果仍然获取不到,就自己创建一个。并调用一下 acquire(RealConnection connection, boolean reportedAcquired) 方法。

这时候如果使用的是全新的 Connect, 那么,我们就要调用 connect 方法:

// Do TCP + TLS handshakes. This is a blocking operation.
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
routeDatabase().connected(result.route());
复制代码

并且,会把这个连接也 put 到 pool 里面:

// Pool the connection.
Internal.instance.put(connectionPool, result);
复制代码

连接池

从上面的代码中,我们可以一直看到 ConnectionPool 这个对象。这个对象代表的是一个 TCP 连接池。Http 协议需要先建立每个 TCP 连接。如果 TCP 连接在满足条件的时候进行复用,无疑会节省很多系统资源。并且加快 Http 的整个过程,也可以理解成,缩短了 Http 请求回来的时间。

ConnectionPool 内部维护了:

  • 线程池  executor
  • clean 任务的 cleanuoRunnable
  • 维护了 RealConnection  的队列
  • RouteDatabase

我们关注一下连接池的存取:

@Nullable RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, route)) {
        streamAllocation.acquire(connection, true);
        return connection;
      }
    }
    return null;
  }
复制代码

这里会在满足条件的时候,返回已经存在队列里面的 Connection 对象。 那么什么时候是满足条件的呢?我们直接看 isEligible 方法里面的注释:

  1. 接受新的 stream, 并且 address 的 host 字段都相同,满足
  2. 如果 hostname 不相同,我们仍然可以继续判断,这时候满足的条件就必须是 http2 了。具体 http2 的满足条件,我们后面再继续探究。

我们还可以发现:每次我们使用连接的时候,都会调用 StreamAllocationacquire 方法。我们瞥一眼这个方法:

connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
复制代码

原来在每个 Connection 中,维护了一个 StreamAllocation 的弱引用的数组,来表示这个连接被谁引用。这个是一个很典型的引用计数方式。如果连接没有被引用,则可以认为这个连接是可以被清理的。

取出连接看完了,我们再看看连接建立的时候,是怎么扔到连接池的:

void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) {
      cleanupRunning = true;
      executor.execute(cleanupRunnable);
    }
    connections.add(connection);
  }
复制代码

这里可以看到,每次连接放进连接池的时候,会触发一次清理操作:

while (true) {
	long waitNanos = cleanup(System.nanoTime());
	if (waitNanos == -1) return;
	if (waitNanos > 0) {
		long waitMillis = waitNanos / 1000000L;
		waitNanos -= (waitMillis * 1000000L);
		synchronized (ConnectionPool.this) {
			try {
				ConnectionPool.this.wait(waitMillis, (int) waitNanos);
            } catch (InterruptedException ignored) {
        	}
		}
	}
}
复制代码

这里的 cleanup 会返回纳秒为单位的下次清理时间的间隔。在时间到之前就阻塞进入冻结的状态。等待下一次清理。 cleanup 的具体逻辑不赘述。当连接的空闲时间比较长的时候,就会被清理释放。

路由选择

在获取连接的过程中,我们会调用 routeSelectornext 方法,来获取我们的路由。那么这个路由选择内部做了什么事情呢?

public Selection next() {
	List<Route> routes = new ArrayList<>();
    while (hasNextProxy()) {
        Proxy proxy = nextProxy();
        for (int i = 0, size = inetSocketAddresses.size(); i < size; i++) {
            Route route = new Route(address, proxy, inetSocketAddresses.get(i));
            if (routeDatabase.shouldPostpone(route)) {
                postponedRoutes.add(route);
            } else {
                 routes.add(route);
            }
        }
        if (!routes.isEmpty()) {
            break;
        }
    }
    return new Selection(routes);
}
复制代码

这里也有一个循环,会不断的获取 Proxy ,然后根据每一个 InetSocketAddress 创建 Route 对象。如果路由是通的,那么就直接返回。如果这些地址的路由在之前都存在 routeDatabase 中,说明都不是可用的,则继续下一个 Proxy

再看下 StreamAllocation 初始化 RouteSelector 的逻辑,会调用 resetNextProxy 方法:

List<Proxy> proxiesOrNull = address.proxySelector().select(url.uri());
      proxies = proxiesOrNull != null && !proxiesOrNull.isEmpty()
          ? Util.immutableList(proxiesOrNull)
          : Util.immutableList(Proxy.NO_PROXY);
复制代码

addressProxySelector , 则是在构造 OKHttpClient 的时候创建的:

proxySelector = ProxySelector.getDefault();
复制代码

它的实现类会去读取系统的代理。当然,我们也可以自己提供自定义的 Proxy 策略。绕过系统的代理。 这就是为什么有些时候我们给手机设置了 proxy,但是有些 APP 仍然不会走代理。

代理

现在我们来看看,获取 Proxy 的时候,OKHttp 究竟做了哪些事情:

Proxy result = proxies.get(nextProxyIndex++);
resetNextInetSocketAddress(result);
return result;
复制代码
private void resetNextInetSocketAddress(Proxy proxy) {
	String socketHost;
    int socketPort;
	if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.SOCKS) {
        socketHost = address.url().host();
        socketPort = address.url().port();
	} else {
    	SocketAddress proxyAddress = proxy.address();
        InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress;
        socketHost = getHostString(proxySocketAddress);
        socketPort = proxySocketAddress.getPort();
    }
    
    if (proxy.type() == Proxy.Type.SOCKS) {
         inetSocketAddresses.add(InetSocketAddress.createUnresolved(socketHost, socketPort));
    } else {
         List<InetAddress> addresses = address.dns().lookup(socketHost);
         for (int i = 0, size = addresses.size(); i < size; i++) {
             InetAddress inetAddress = addresses.get(i);
             inetSocketAddresses.add(new InetSocketAddress(inetAddress, socketPort));
         }
    }
}
复制代码

在代码中,Proxy 有三种模式:

  • http 代理
  • socks 代理
  • DIRECT 或者 没有代理

当直接连接或者是 socks 代理的时候,socket 的host 和 port 从 address 中获取, 当是http代理的时候,则从 proxy 的代理中获取 host 和 port。 如果是http代理,后续会继续走 DNS 去解析代理服务器的host。最终,这些host和port都会封装成 InetSocketAddress 对象放到 ip 列表中。

连接

介绍完连接池、路由和代理,我们来看发起 connect 这个操作的地方,即 RealConnectionconnect 方法: (这里我删除了不关键的错误处理代码)

public void connect(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled, Call call,
      EventListener eventListener) {
    
     while (true) {
         if (route.requiresTunnel()) {
            //1. 隧道连接 
			connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
            if (rawSocket == null) {
                break;
            }
         } else {
             // 2. 直接socket连接
         	 connectSocket(connectTimeout, readTimeout, call, eventListener);
         }
         // 3. 建立连接协议
         establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
     }
    
}
复制代码

套接字连接

我们先来看socket连接:

private void connectSocket(int connectTimeout, int readTimeout, Call call,
      EventListener eventListener) 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);
    
    Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
    
    source = Okio.buffer(Okio.source(rawSocket));
    sink = Okio.buffer(Okio.sink(rawSocket));
}
复制代码

具体连接操作在不同的平台上不一样,在 Android 中是在 AndroidPlatformconnectSocket 中进行的:

socket.connect(address, connectTimeout);
复制代码

这时候, RealConnection 中的 sourcesink 就分别代表了 socket 网络流的读入和写入。

隧道连接

隧道连接的逻辑在 connectTunnel 中:

private void connectTunnel(int connectTimeout, int readTimeout, int writeTimeout, Call call,
      EventListener eventListener) throws IOException {
	Request tunnelRequest = createTunnelRequest();
    HttpUrl url = tunnelRequest.url();
    for (int i = 0; i < MAX_TUNNEL_ATTEMPTS; i++) {
        connectSocket(connectTimeout, readTimeout, call, eventListener);
        tunnelRequest = createTunnel(readTimeout, writeTimeout, tunnelRequest, url);
        if (tunnelRequest == null) break; // Tunnel successfully created.
    }
 }
复制代码

这里我们可以看到,隧道连接会先进行socket连接,然后创建隧道。如果创建不成功,会连续尝试 21 次。

private Request createTunnel(int readTimeout, int writeTimeout, Request tunnelRequest,
      HttpUrl url) throws IOException {
	String requestLine = "CONNECT " + Util.hostHeader(url, true) + " HTTP/1.1";
    while (true) {
        Http1Codec tunnelConnection = new Http1Codec(null, null, source, sink);
		tunnelConnection.writeRequest(tunnelRequest.headers(), requestLine);
        tunnelConnection.finishRequest();
        
        Response response = tunnelConnection.readResponseHeaders(false)
          .request(tunnelRequest)
          .build();

		Source body = tunnelConnection.newFixedLengthSource(contentLength);
		Util.skipAll(body, Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
		body.close();
        
        switch (response.code()) {
			case HTTP_OK:
				if (!source.buffer().exhausted() || !sink.buffer().exhausted()) {
					throw new IOException("TLS tunnel buffered too many bytes!");
          		}
          		return null;
            case HTTP_PROXY_AUTH:
                tunnelRequest = route.address().proxyAuthenticator().authenticate(route, response);
          		if (tunnelRequest == null) throw new IOException("Failed to authenticate with proxy");

          		if ("close".equalsIgnoreCase(response.header("Connection"))) {
            		return tunnelRequest;
          		}
          		break;
			default:
          		throw new IOException("Unexpected response code for CONNECT: " + response.code());
        }
    }
}
复制代码

确认协议

在隧道或者socket连接建立完成后,会进行应用层的协议选择。查看 establishProtocol :

if (route.address().sslSocketFactory() == null) {
    // 不是 ssl 连接,确认为 http 1.1
	protocol = Protocol.HTTP_1_1;
    return;
}
// ssl 连接
connectTls(connectionSpecSelector);
// http 2
if (protocol == Protocol.HTTP_2) {
	http2Connection = new Http2Connection.Builder(true)
          .socket(socket, route.address().url().host(), source, sink)
          .listener(this)
          .pingIntervalMillis(pingIntervalMillis)
          .build();
	http2Connection.start();
}
复制代码

这里可以看到,如果 http 连接不支持 ssl 的话,就认为他是 http 1.1, 虽然理论上 http2 也可以是非 ssl 的,但是一般在使用中,http2 是必须支持 https 的。

如果设置了 SSLSocketFactory , 那么先进行 SSL 的连接。

查看 connectTls

Address address = route.address();
SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
SSLSocket sslSocket = null;

// ssl socket
sslSocket = (SSLSocket) sslSocketFactory.createSocket(rawSocket, address.url().host(), address.url().port(), true /* autoClose */);
// configure the socket's clphers, TLS versions, adn extensions
ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);

if (connectionSpec.supportsTlsExtensions()) {
    // 配置 TLS 扩展
    Platform.get().configureTlsExtensions(sslSocket, address.url().host(), address.protocols());
}

// ssl 握手
sslSocket.startHandshake();
// 校验证书
Handshake unverifiedHandshake = Handshake.get(sslSocketSession);
if (!address.hostnameVerifier().verify(address.url().host(), sslSocketSession)) {
	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));
}
address.certificatePinner().check(address.url().host(),unverifiedHandshake.peerCertificates());

// 校验成功,判断具体的协议
String maybeProtocol = connectionSpec.supportsTlsExtensions()
          ? Platform.get().getSelectedProtocol(sslSocket)
          : null;
protocol = maybeProtocol != null
          ? Protocol.get(maybeProtocol)
          : Protocol.HTTP_1_1;
success = true;
复制代码

查看 Platform.get().getSelectedProtocol(sslSocket)

byte[] alpnResult = (byte[]) getAlpnSelectedProtocol.invokeWithoutCheckedException(socket);
return alpnResult != null ? new String(alpnResult, Util.UTF_8) : null;
复制代码

这里会通过反射调用 OpenSSLSocketImplgetAlpnSelectedProtocol 方法,最终通过 jni 层调用 NativeCrypto.cpp 去获取确定的应用层协议。可能获取到的值目前有

  • http/1.0
  • http/1.1
  • spdy/3.1
  • h2
  • quic

HTTP2

如果这时候支持的是 HTTP2 协议,那么我们关注点就要放到 Http2Connection 这个类上来。查看它的 start 方法:

void start(boolean sendConnectionPreface) throws IOException {
    if (sendConnectionPreface) {
      // 连接引导
      writer.connectionPreface();
      // 写 settings
      writer.settings(okHttpSettings);
      // 获取窗口大小
      int windowSize = okHttpSettings.getInitialWindowSize();
      if (windowSize != Settings.DEFAULT_INITIAL_WINDOW_SIZE) {
        writer.windowUpdate(0, windowSize - Settings.DEFAULT_INITIAL_WINDOW_SIZE);
      }
    }
    // 读取服务端的响应数据
    new Thread(readerRunnable).start(); // Not a daemon thread.
  }
复制代码

首先,在 sendConnectionPreface 中,客户端会发送  "PRI * HTTP/2.0/r/n/r/nSM/r/n/r/n" 到服务端。发送完 Connection Preface 之后,会继续发送一个 setting 帧。

Http2Connection`` 中通过 readerRunnable 来执行网络流的读取,参考 ReaderRunnable execute` 方法:

reader.readConnectionPreface(this);
while (reader.nextFrame(false, this)) {}
复制代码

首先,会读取 connection preface 的内容,即服务端返回的 settings 帧。如果顺利,后面会在循环中不断的读取下一帧,查看 nextFrame

深入OKHttp之网络连接

这里对 HTTP2 不同类型的帧进行了处理。我们挑一个 data 帧查看,会继续走到 data 方法:

// 去掉了不关键代码
Http2Stream dataStream = getStream(streamId); // 获取抽象的流对象
dataStream.receiveData(source, length);  // 把 datastream 读取到 source
if (inFinished) {
	dataStream.receiveFin(); // 读取结束
}
复制代码

继续查看 receiveData :

void receiveData(BufferedSource in, int length) {
  this.source.receive(in, length);
 }
复制代码

这里调用的是一个类型为 FramingSource 的 Source 对象。最终会调用 long read = in.read(receiveBuffer, byteCount); 方法。会把网络的 source 内容写到 receiveBuffer 中。然后把 receiveBuffer 的内容写到 readBuffer 中。这里的读写全部都是使用的 OKIO 框架。

那么 FramingSource 里面的的 readBuffer 在什么时候用到呢?在 OKHttpCallServerInteceptor 里构造 ResonseBody 的时候,如果是 HTTP2 的请求,会从这个 buffer 里面读取数据。

从这里对 HTTP2 的帧处理,我们可以看到 HTTP2 的特性和 HTTP1.1 有很大的不一样,HTTP2 把数据分割成了很多的二进制帧。配合多路复用的特性,每个连接可以发送很多这样的内容较小的帧,整体上提升了 HTTP 的传输性能。每个 frame 的格式如下:

深入OKHttp之网络连接

具体 HTTP2 二进制分帧的原理,我们以后再做单独探究。

HTTP2 连接复用

现在回头看看连接池内对 HTTP2 的连接复用:

if (route == null) return false;
if (route.proxy().type() != Proxy.Type.DIRECT) return false;
if (this.route.proxy().type() != Proxy.Type.DIRECT) return false;
if (!this.route.socketAddress().equals(route.socketAddress())) return false;

if (route.address().hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
if (!supportsUrl(address.url())) return false;

address.certificatePinner().check(address.url().host(), handshake().peerCertificates());
复制代码

可以看到 HTTP2 需要满足这些条件可以进行连接复用:

pinning

思考

通过源码分析,我们也可以得到如下结论:

OKHttpClient
ProxySelector
DNS

总结

现在,我们了解了 OKHTTP 对 HTTP 请求进行的连接, UML 图可以清晰的展示每个类的关系:

深入OKHttp之网络连接

我们也可以对 隧道代理,SSL,HTTP2具体的帧格式等特性,进行进一步的网络知识的深入学习和分析。来寻找一些网络优化的突破点和思路。

请关注我的微信公众号 【半行代码】

深入OKHttp之网络连接
原文  https://juejin.im/post/5d96294f5188254cf76c6327
正文到此结束
Loading...