各位大佬,能不能随便给我的项目或者之前的文章点个star,苦兮兮。github.com/ 掘金文章
简介
Http2.0 这个吧肯定是真香的,其中特别是二进制分帧和多路复用。
但是我一直有些疑惑,Http2.0为什么后端支持了前端就能直接访问2.0版本了,Okhttp如何开启的Http2.0呢?
简单说下Http2.0
二进制分帧层 (Binary Framing Layer)
帧是数据传输的最小单位,以二进制传输代替原本的明文传输,原本的报文消息被划分为更小的数据帧.
多路复用 (MultiPlexing)
在一个 TCP 连接上,我们可以向对方不断发送帧,每帧的 stream identifier 的标明这一帧属于哪个流,然后在对方接收时,根据 stream identifier 拼接每个流的所有帧组成一整块数据。 把 HTTP/1.1 每个请求都当作一个流,那么多个请求变成多个流,请求响应数据分成多个帧,不同流中的帧交错地发送给对方,这就是 HTTP/2 中的多路复用。流的概念实现了单连接上多请求 - 响应并行,解决了线头阻塞的问题,减少了 TCP 连接数量和 TCP 连接慢启动造成的问题.http2 对于同一域名只需要创建一个连接,而不是像 http/1.1 那样创建 6~8 个连接。
服务端推送 (Server Push)
浏览器发送一个请求,服务器主动向浏览器推送与这个请求相关的资源,这样浏览器就不用发起后续请求。
Header 压缩 (HPACK)
使用 HPACK 算法来压缩首部内容
Http2.0 你必须知道的小秘密
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的请求。
TLS
Http2.0的前置条件是实现了https。而Https则是在Http的基础上增加了一层Tls。这个东西在大厂的面试中其实是一个高频考点了,简单的说Tls就是一个前后端约定好后续加密方式的过程。这篇文章写的很好,详细可以参考这个传送门,而整体流程如下图。
- client 发起第一次client hello过程,请求 Https 连接,发送可用的 TLS 版本和可用的密码套件。
- server 发起第一次server hello过程,返回证书,密码套件和 TLS 版本等信息。
- 生成随机对称密钥,使用证书中的服务端公钥加密,发送给服务端
- 服务端使用私钥解密获取对称密钥
不知道各位有没有思考过一个问题,为什么只要后端将接口升级到Http2.0的支持之后,客户端就能自动的把所有的请求切换到Http2.0上呢?还有2.0和Tls到底有什么关系呢?
ALPN((Application Layer Protocol Negotiation)协议
ALPN (Application Layer Protocol Negotiation)是TLS的扩展,允许在安全连接的基础上进行应用层协议的协商。ALPN支持任意应用层协议的协商,目前应用最多是HTTP2的协商。当前主流浏览器,都只支持基于 HTTPS 部署的 HTTP/2,因为浏览器是基于ALPN协议来判断服务器是否支持HTTP2协议。
ALPN是TLS的扩展协议,而ALPN的作用就是告诉客户端,当前服务端支持的接口协议版本有哪些,当然这里会有很多种。所有上看的问题的答案基本呼之欲出,贴一张朋友吊打我的图。2.0必须使用TLS的原因就是因为这个ALPN的拓展协议。
OkHttp Connection 分析
Okhttp是如何实现的这整个流程呢,我画了个大概的流程图。
ConnectInterceptor
代码语言:javascript复制/** 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);
}
}
复制代码
其实这里还有个小知识点:一个 TCP 连接可以对应几个 HTTP 请求?
从拦截器实现可以发现,Okhttp实现了一个连接池,当ConnectionInterceptor被调用的时候,先是判断连接池内有没有空闲并且健康的可用连接,然后再使用连接去调度下一个拦截器,那么也就是一个tcp连接的存活时间是大于Http请求的,所以一个Tcp可以对应多个Http请求。
这个拦截器的作用就是在发起实际请求之前构建好连接,然后使用这个连接发起访问,这里的核心就是调用了streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
,获取一个连接对象。
RealConnection
我们主要说些connet方法,它是整个Http2.0的开启流程的关键。
代码语言:javascript复制 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 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完整流程都在方法内。
代码语言:javascript复制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 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的辣鸡项目点个赞。