深刻OKHttp之网络链接

从链接提及

咱们在进行http请求的时候,会有大体以下几个流程: DNS -> 创建Socket链接 -> 应用层进行 http 请求 (图片来源网络)java

image.png

那么 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" 的链接,若是不是全新的链接,则会释放掉,继续去创建链接。socket

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

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

经过注释咱们了解到,咱们已经有了一个可用的链接,直接复用。post

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 的知足条件,咱们后面再继续探究。

咱们还能够发现:每次咱们使用链接的时候,都会调用 StreamAllocation 的 acquire 方法。咱们瞥一眼这个方法:

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 的具体逻辑不赘述。当链接的空闲时间比较长的时候,就会被清理释放。

路由选择

在获取链接的过程当中,咱们会调用 routeSelector 的 next 方法,来获取咱们的路由。那么这个路由选择内部作了什么事情呢?

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);
复制代码

address 的 ProxySelector , 则是在构造 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 这个操做的地方,即 RealConnection 的 connect 方法: (这里我删除了不关键的错误处理代码)

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 中是在 AndroidPlatform 的 connectSocket 中进行的:

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

这时候, RealConnection 中的 source 和 sink 就分别表明了 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;
复制代码

这里会经过反射调用 OpenSSLSocketImpl 的 getAlpnSelectedProtocol 方法,最终经过 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 来执行网络流的读取,参考ReaderRunnableexecute` 方法:

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

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

image.png

这里对 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 在何时用到呢?在 OKHttp 的 CallServerInteceptor 里构造 ResonseBody 的时候,若是是 HTTP2 的请求,会从这个 buffer 里面读取数据。

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

image.png

具体 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 须要知足这些条件能够进行链接复用:

  • 路由共享 ip 地址,这要求咱们为两个 host 都有一个dns地址,代理除外。
  • 此链接的证书必须覆盖在新的 host 之上
  • 证书的 pinning 必须和 host 匹配

思考

经过源码分析,咱们也能够获得以下结论:

  • 一个APP中应该尽量使用一个 OKHttpClient ,由于链接池不是多个 client 共享
  • 咱们能够自定义 ProxySelector 来自定义咱们在代理下的行为,例如:有代理也不走
  • 咱们能够自定义 DNS ,在里面作咱们本身的 DNS 解析逻辑

总结

如今,咱们了解了 OKHTTP 对 HTTP 请求进行的链接, UML 图能够清晰的展现每一个类的关系:

image.png

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

请关注个人微信公众号 【半行代码】

相关文章
相关标签/搜索