logo头像
Snippet 博客主题

如何让Spring boot 2.0 支持h2c协议

前面有文章介绍了Spring boot 2.0 中配置实现HTTP/2协议的各种情形,但是其中介绍都是h2协议。HTTP/2协议有两个版本:h2h2ch2ch2 的明文版本,没有建立在TLS基础上,没有安全保障。正是因为没有TLS层的加解密相关步骤,比较适合用在后端服务之间通信,gRPC就是同时支持这两个版本的HTTP/2协议。

00 前言

HTTP/2连接是建立在TCP连接之上的应用层协议,客户端是TCP连接的发起者。

HTTP/2使用和HTTP/1.1一样的 URI schemes:”http” 和 “https”,并且还是共享同样的默认端口:http的80,https的443。这意味着,对于”http” 和 “https”确定其是否支持HTTP/2协议的方式是不同的。

官方文档中,为HTTP/2协议定义了两个版本:h2h2c

  • h2版本的协议是建立在TLS层之上的HTTP/2协议,这个标志被用在TLS应用层协议协商(TLS-ALPN)域和任何其它的TLS之上的HTTP/2协议。
  • h2c版本是建立在明文的TCP之上的HTTP/2协议,这个标志被用在HTTP/1.1的升级协议头域和其它任何直接在TCP层之上的HTTP/2协议。

既然在HTTP/2协议的官方介绍中有两个版本,我们之前的文章介绍的是Spring boot 2.0如何实现h2,本文我们会介绍如何让Spring boot 2.0支持h2c协议。

01 Spring boot 2.0 h2c协议服务端

由于h2c协议相对于h2来说简单些,应该要实现也不难,但是从Spring boot 2.0的官方文档上看,明确写明了“Spring boot 不支持h2c —— HTTP/2协议的明文版本”,于是就想其它办法来支持h2c。

首先,想的是通过外置的tomcat配置来支持h2c,因为tomcat8.5中的server.xml配置中,有HTTP/2协议相关的配置:

<Connector port="8443" protocol="org.apache.coyote.http11.Http11AprProtocol"
           maxThreads="150" SSLEnabled="true" >
    <UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol" />
    <SSLHostConfig>
        <Certificate certificateKeyFile="conf/localhost-rsa-key.pem"
                     certificateFile="conf/localhost-rsa-cert.pem"
                     certificateChainFile="conf/localhost-rsa-chain.pem"
                     type="RSA" />
    </SSLHostConfig>
</Connector>

这里显示要添加证书,明显支持的是基于TLS层之上的h2版本的HTTP/2协议,但是从配置上可以禁用SSL,于是就尝试了一下,果然成功了。配置成如下示例,就支持h2c了:

<Connector port="5080" protocol="HTTP/1.1" connectionTimeout="20000">
    <UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol" />
</Connector>

tomcat容器自身是支持多个connector的配置的,想到这里,就想在Spring boot 2.0 中是否支持同时配置多个Connector,查看Spring boot的官方文档发现如下配置:

Spring boot 2.0 tomcat Connector

文档中示例是通过 java configure 的方式配置一个https的connector,在application.yaml中不支持配置多个connector。

因此我就模仿这外置tomcat配置h2c的方式,在Spring boot 2.0 的内置tomcat中通过java configure的方式配置h2c协议,具体代码如下:

@Bean
public ServletWebServerFactory servletContainer() {
    TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
    tomcat.addAdditionalTomcatConnectors(createH2cConnector());
    return tomcat;
}

private Connector createH2cConnector() {
    Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
    Http2Protocol upgradeProtocol = new Http2Protocol();
    connector.addUpgradeProtocol(upgradeProtocol);
    //connector.setScheme("http");
    connector.setPort(5080);

    return connector;
}

这时启动我们的Spring boot应用,会发现最后的启动日志,我们同时启动三个端口,也即是三个Connector:

Tomcat started on port(s): 8080 (http) 8443 (https) 5080 (http) with context path '/demo-h2c'

02 Curl 工具验证h2c协议服务端

首先需要检查你的curl工具是否支持HTTP/2协议,验证方式:

➜  ~ curl -V
curl 7.54.0 (x86_64-apple-darwin17.0) libcurl/7.54.0 LibreSSL/2.0.20 zlib/1.2.11 nghttp2/1.24.0
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtsp smb smbs smtp smtps telnet tftp
Features: AsynchDNS IPv6 Largefile GSS-API Kerberos SPNEGO NTLM NTLM_WB SSL libz HTTP2 UnixSockets HTTPS-proxy

从上面的 Features 信息发现,我的curl工具是支持HTTP/2协议的。

下面开始验证我Spring boot 的服务在5080端口是否是h2c,具体如下:

➜  ~ curl -v --http2 http://127.0.0.1:5080/demo-h2c/h2c/hello\?name\=guoyankui
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 5080 (#0)
> GET /demo-h2c/h2c/hello?name=guoyankui HTTP/1.1
> Host: 127.0.0.1:5080
> User-Agent: curl/7.54.0
> Accept: */*
> Connection: Upgrade, HTTP2-Settings
> Upgrade: h2c
> HTTP2-Settings: AAMAAABkAARAAAAAAAIAAAAA
>
< HTTP/1.1 101
< Connection: Upgrade
< Upgrade: h2c
< Date: Sun, 06 May 2018 13:12:37 GMT
* Received 101
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Connection state changed (MAX_CONCURRENT_STREAMS updated)!
< HTTP/2 200
< content-type: text/plain;charset=UTF-8
< date: Sun, 06 May 2018 13:12:37 GMT
<
* Connection #0 to host 127.0.0.1 left intact
Hello h2c: null%

OK, 验证通过了。

03 支持h2c协议的java客户端

在上一节中使用Curl工具验证了Spring boot 2.0 的服务的h2c协议,但是在java语言中,还需要有支持h2c的Java客户端。目前一般的 java 客户端都只是支持h2,有的还不支持HTTP/2协议,而且浏览器一般也不支持h2c。

现在找到一个支持h2c的java客户端也不容易,目前找到okhttp3的最新版本3.10.0也不支持h2c,但是发现在最新的github代码中3.11.0-SNAPSHOT版本已经支持了h2c,代码示例:

public static void main(String[] args) throws Exception {
    testH2C();
}

public static void testH2C() throws Exception {
    OkHttpClient okHttpClient = new OkHttpClient.Builder()
            .protocols(Arrays.asList(Protocol.H2C))
            .build();

    Request request = new Request.Builder()
            .url("http://127.0.0.1:5080/demo-h2c/h2c/hello?name=guoyankui")
            .build();
    Response response = okHttpClient.newCall(request).execute();
    System.out.println(response.protocol());
    System.out.println(response.body().string());
}

执行这个main函数之后,输出的结果:

h2c
Hello h2c: guoyankui

从输出的结果来看,验证了h2c协议的客户端和服务端。

04 结束

目前,找到了okhttp3的3.11.0版本支持h2c,有支持h2c的Java 客户端了,我们也在Spring boot 2.0中实现了h2c的服务端,因此也算有了支持h2c协议的完整方案。

上一篇