Http2.0 这个吧肯定是真香的,其中特别是二进制分帧和多路复用。
但是我一直有些疑惑,Http2.0为什么后端支持了前端就能直接访问2.0版本了,Okhttp如何开启的Http2.0呢?
帧是数据传输的最小单位,以二进制传输代替原本的明文传输,原本的报文消息被划分为更小的数据帧.
在一个 TCP 连接上,我们可以向对方不断发送帧,每帧的 stream identifier 的标明这一帧属于哪个流,然后在对方接收时,根据 stream identifier 拼接每个流的所有帧组成一整块数据。 把 HTTP/1.1 每个请求都当作一个流,那么多个请求变成多个流,请求响应数据分成多个帧,不同流中的帧交错地发送给对方,这就是 HTTP/2 中的多路复用。流的概念实现了单连接上多请求 - 响应并行,解决了线头阻塞的问题,减少了 TCP 连接数量和 TCP 连接慢启动造成的问题.http2 对于同一域名只需要创建一个连接,而不是像 http/1.1 那样创建 6~8 个连接。
浏览器发送一个请求,服务器主动向浏览器推送与这个请求相关的资源,这样浏览器就不用发起后续请求。
使用 HPACK 算法来压缩首部内容
IIS currently supports HTTP/2 only over TLS. When making an HTTPS connection to a web server running IIS on Windows 10, HTTP/2 is used if the client and server both support it. In IIS, we've implemented HTTP/2 as transparently as possible - you shouldn't need to change anything in your application for HTTP/2 to work. Certain HTTP/1.1 optimizations (domain sharding, inlining, etc.) are no longer recommended in HTTP/2, though, so you should plan to remove these in the future.
Http2.0必须建立在TLS的基础上,也就是必须是Https的请求。
Http2.0的前置条件是实现了https。而Https则是在Http的基础上增加了一层Tls。这个东西在大厂的面试中其实是一个高频考点了,简单的说Tls就是一个前后端约定好后续加密方式的过程。这篇文章写的很好,详细可以参考这个 传送门 ,而整体流程如下图。
不知道各位有没有思考过一个问题,为什么只要后端将接口升级到Http2.0的支持之后,客户端就能自动的把所有的请求切换到Http2.0上呢?还有2.0和Tls到底有什么关系呢?
ALPN (Application Layer Protocol Negotiation)是TLS的扩展,允许在安全连接的基础上进行应用层协议的协商。ALPN支持任意应用层协议的协商,目前应用最多是HTTP2的协商。当前主流浏览器,都只支持基于 HTTPS 部署的 HTTP/2,因为浏览器是基于ALPN协议来判断服务器是否支持HTTP2协议。
ALPN是TLS的扩展协议,而ALPN的作用就是告诉客户端,当前服务端支持的接口协议版本有哪些,当然这里会有很多种。所有上看的问题的答案基本呼之欲出,贴一张朋友吊打我的图。2.0必须使用TLS的原因就是因为这个ALPN的拓展协议。
Okhttp是如何实现的这整个流程呢,我画了个大概的流程图。
/** Opens a connection to the target server and proceeds to the next interceptor. */ public final class ConnectInterceptor implements Interceptor { public final OkHttpClient client; public ConnectInterceptor(OkHttpClient client) { this.client = client; } @Override public Response intercept(Chain chain) throws IOException { RealInterceptorChain realChain = (RealInterceptorChain) chain; Request request = realChain.request(); StreamAllocation streamAllocation = realChain.streamAllocation(); // We need the network to satisfy this request. Possibly for validating a conditional GET. boolean doExtensiveHealthChecks = !request.method().equals("GET"); HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks); RealConnection connection = streamAllocation.connection(); return realChain.proceed(request, streamAllocation, httpCodec, connection); } } 复制代码
这个拦截器的作用就是在发起实际请求之前构建好连接,然后使用这个连接发起访问,这里的核心就是调用了 streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
,获取一个连接对象。
我们主要说些connet方法,它是整个Http2.0的开启流程的关键。
public void connect(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled, Call call, EventListener eventListener) { 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")); } } else { if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) { throw new RouteException(new UnknownServiceException( "H2_PRIOR_KNOWLEDGE cannot be used with HTTPS")); } } while (true) { try { if (route.requiresTunnel()) { connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener); if (rawSocket == null) { // We were unable to connect the tunnel but properly closed down our resources. break; } } else { connectSocket(connectTimeout, readTimeout, call, eventListener); } establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener); eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol); break; } catch (IOException e) { closeQuietly(socket); closeQuietly(rawSocket); socket = null; rawSocket = null; source = null; sink = null; handshake = null; protocol = null; http2Connection = null; eventListener.connectFailed(call, route.socketAddress(), route.proxy(), null, e); if (routeException == null) { routeException = new RouteException(e); } else { routeException.addConnectException(e); } if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) { throw routeException; } } } if (route.requiresTunnel() && rawSocket == null) { ProtocolException exception = new ProtocolException("Too many tunnel connections attempted: " + MAX_TUNNEL_ATTEMPTS); throw new RouteException(exception); } if (http2Connection != null) { synchronized (connectionPool) { allocationLimit = http2Connection.maxConcurrentStreams(); } } } 复制代码
其中while true 循环内会去构建一个socket连接,当socket连接构建成功之后,会调用 establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
方法,这个就是整篇文章的主角了。
private void establishProtocol(ConnectionSpecSelector connectionSpecSelector, int pingIntervalMillis, Call call, EventListener eventListener) throws IOException { if (route.address().sslSocketFactory() == null) { if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) { socket = rawSocket; protocol = Protocol.H2_PRIOR_KNOWLEDGE; startHttp2(pingIntervalMillis); return; } socket = rawSocket; protocol = Protocol.HTTP_1_1; return; } eventListener.secureConnectStart(call); connectTls(connectionSpecSelector); eventListener.secureConnectEnd(call, handshake); if (protocol == Protocol.HTTP_2) { startHttp2(pingIntervalMillis); } } 复制代码
看到最后几行代码,其实已经能知道了。只要当前协议包含了HTTP_2,OKhttp就会开启Http2.0模式,否则则降级成1.1的代码。而如何去获取协议就是connectTls这个方法了,而且Tls完整流程都在方法内。
private void connectTls(ConnectionSpecSelector connectionSpecSelector) throws IOException { Address address = route.address(); SSLSocketFactory sslSocketFactory = address.sslSocketFactory(); boolean success = false; SSLSocket sslSocket = null; try { // Create the wrapper over the connected socket. sslSocket = (SSLSocket) sslSocketFactory.createSocket( rawSocket, address.url().host(), address.url().port(), true /* autoClose */); // Configure the socket's ciphers, TLS versions, and extensions. ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket); if (connectionSpec.supportsTlsExtensions()) { Platform.get().configureTlsExtensions( sslSocket, address.url().host(), address.protocols()); } // Force handshake. This can throw! sslSocket.startHandshake(); // block for session establishment SSLSession sslSocketSession = sslSocket.getSession(); // 获取HandShake 信息 Handshake unverifiedHandshake = Handshake.get(sslSocketSession); // Verify that the socket's certificates are acceptable for the target host. if (!address.hostnameVerifier().verify(address.url().host(), sslSocketSession)) { List<Certificate> peerCertificates = unverifiedHandshake.peerCertificates(); if (!peerCertificates.isEmpty()) { X509Certificate cert = (X509Certificate) 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)); } else { throw new SSLPeerUnverifiedException( "Hostname " + address.url().host() + " not verified (no certificates)"); } } // Check that the certificate pinner is satisfied by the certificates presented. address.certificatePinner().check(address.url().host(), unverifiedHandshake.peerCertificates()); // Success! Save the handshake and the ALPN protocol. // 成功之后,保存HandShake以及ALPN协议信息。 String maybeProtocol = connectionSpec.supportsTlsExtensions() ? Platform.get().getSelectedProtocol(sslSocket) : null; socket = sslSocket; source = Okio.buffer(Okio.source(socket)); sink = Okio.buffer(Okio.sink(socket)); handshake = unverifiedHandshake; protocol = maybeProtocol != null ? Protocol.get(maybeProtocol) : Protocol.HTTP_1_1; success = true; } catch (AssertionError e) { if (Util.isAndroidGetsocknameError(e)) throw new IOException(e); throw e; } finally { if (sslSocket != null) { Platform.get().afterHandshake(sslSocket); } if (!success) { closeQuietly(sslSocket); } } } 复制代码
这里要先引申出一个概念,Okhttp设计之初就是一个java平台通用的网络库,对于不同的java版本,还有安卓的底层适配逻辑是不同的。简单的说Okhttp就是抽象了下所有Tls,SSLSocket相关的代码,然后通过一个Platform,根据当前使用环境的不同,去反射调用不同的实现类,然后这个抽象的类去调用Platform的实现类代码,做到多平台的兼容。
其中Tls当生成好SSLSocket之后,就会开始进行client say hello 和server say hello的操作了,这部分完全和https定义的一模一样。Handshake则会把服务端支持的Tls版本,加密方式等都带回来,然后会把这个没有验证过的HandShake用X509Certificate去验证证书的有效性。然后会通过Platform去从SSLSocket去获取ALPN的协议支持信息,当后端支持的协议内包含Http2.0时,则就会把请求升级到Http2.0阶段。
学习过程中,最好是带着疑问去思考,然后再去做一部分源码追溯,这样事半功倍,同时也能把之前的一部分困惑消灭,同时加深记忆力。
之前Https的一系列问题,我都是靠博客之类的去学习的,基本上不超过两三天就会遗忘啊,同时对于2.0的开启也是一个不求甚解的过程,基本上我之前的后端同事说我们已经是2.0了,我就只能哦一句。
还有一点就是本文只介绍了前置操作,而关于Http2.0的分帧等操作你们可以看下这篇文章啊 传送门之HTTP 2.0与OkHttp 。
这几年https和Http2.0基本都是高频出现的面试题了,希望文章能对大家的认知有一定的帮助。最后能不能给我的gayhub的辣鸡项目点个赞。