logo头像
Snippet 博客主题

再说Spring boot 2.0 与 HTTP/2

前面有文章简单介绍了HTTP/2协议和Spring boot2如何实现的过程,它就是《RESTful风格的微服务-spring boot&HTTP/2》,它主要介绍了HTTP/2协议相关知识,并介绍了一种Spring boot2实现http2协议服务的一种最简单的方法,其中还缺乏调用http2服务的客户端的实现,本文中将一一介绍。

00 前言

最近在做RESTful风格的微服务实践的过程中,由于种种原因,我们选择了比较艰难的一条路:spring boot2 + JDK8 + tomcat8

  • Spring boot2带来了开发过程的便利性,以及强大的辅助功能,比如监控等。这些功能是其他的java web框架所不能比的。
  • JDK8的选择主要处于稳定的原因,JDK8出生于2014年,距离现在差不多四年的时间了,已经很稳定了,并且已经大量使用在生产环境中。自从2017年下半年JDK9出现之后,相继JDK10也已经出现了,很快JDK11也要出世了,越来越看不懂oracle对JDK的规划了。一开始的时候,我们觉得制定一个未来一两年才实施的计划方案,应该选择的技术相对新一点,于是选择了JDK9。但是就在四月份oracle官网上JDK9不见了踪影,后来仔细查看才发现,JDK9版本已经走到头了,官网上已经不再升级JDK9的新功能了,已经建议使用JDK10了。我们仔细思考之后,觉得还是选择JDK8最为稳妥。
  • tomcat8的选择原因,首先就tomcat这个servlet容器来说,与undertow和jetty相比,它是我们最为熟悉的容器。在实现原理和性能调优方面,还有安全漏洞监测方面,我们都有一定方案和人才积累。其次就是选择的tomcat8这个版本,也是处于稳的目的,没选择当前最新的tomcat9.

01 Spring boot2实现HTTP/2服务的方案

在Spring boot2的框架中,实现HTTP/2协议的服务提供者有多种方案,主要是JDK版本和容器不同。下面就翻译一下官方文档的介绍吧。

在Spring boot应用中启用HTTP/2协议的支持,只需要在 application.properties
或者 application.yaml 中配置 server.http2.enabled 的值为 true,但是这需要依赖于你选择的web服务器和应用环境的支持,因为原生的JDK8是不支持HTTP/2协议的。

Spring boot 不支持h2c(HTTP/2协议的明文版本),因此你必须配置SSL。配置SSL的方式也是很简单的,这里就不翻译了,详细请参见官方文档介绍。

1. 使用Undertow实现HTTP/2

需要使用Undertow 1.4.0+的版本,然后使用JDK8,再也不需要其它的支持了。

最简单的实现方式,也是之前那篇文章中介绍的一个。

2. 使用Jetty实现HTTP/2

在Jetty 9.4.8 版本之后就开始支持HTTP/2协议了,其中一种方式就是使用 Conscrypt library来提供ALPN的实现。为了启用这个支持,还需要添加额外的依赖 org.eclipse.jetty:jetty-alpn-conscrypt-serverorg.eclipse.jetty.http2:http2-server

这个方案暂且还没实验,又挖了个坑

3. 使用Tomcat实现HTTP/2

Spring boot2默认使用的容器是 tomcat 8.5.x,在这个版本中要想支持HTTP/2协议,只有将libtcnative这个库和它的依赖安装到主机操作系统中。
如果JVM的库路径下没有的话,这个库文件夹必须是对JVM可访问的,你可以通过JVM参数来配置,例如-Djava.library.path=/usr/local/opt/tomcat-native/lib,详细的细节请参见tomcat官方文档
如果在使用tomcat8.5.x的时候启用了HTTP/2,而又没有native的支持的话,会输出如下错误日志:

ERROR 8787 --- [           main] o.a.coyote.http11.Http11NioProtocol      : The upgrade handler [org.apache.coyote.http2.Http2Protocol] for [h2] only supports upgrade via ALPN but has been configured for the ["https-jsse-nio-8443"] connector that does not support ALPN.

这个错误不影响应用的正常运行,还是会提供HTTP1.1的SSL支持。
如果你使用Tomcat 9.0.x 和 JDK9运行Spring boot应用,就不需要native的支持了。使用tomcat9,你需要在pom文件中的一个编译熟悉tomcat.version覆盖成你选择的版本即可。

Spring boot 官方文档介绍的HTTP/2协议相关配置已经介绍完了,下面着重介绍一下我们选用的方案,JDK8 + Tomcat8.5版本组合。

02 JDK8 + Tomcat8.5实现HTTP/2服务

从上文中可以看到,在这个(JDK8 + Tomcat8.5)组合中要实现HTTP/2协议的服务,需要在操作系统中安装libtcnative库。下面首先介绍如何安装这个库,我是在Mac上实验的,其它的Linux操作系统也是相同方法,只是libtcnative包的后缀不太一样。

准备工作:

  • APR 1.2+ development headers (libapr1-dev package),下载地址
  • OpenSSL 1.0.2+ development headers (libssl-dev package),下载地址
  • JNI headers from Java compatible JDK 1.4+
  • GNU development environment (gcc, make)
  • Tomcat Native,在各自版本的tomcat的bin目录下,也可以通过Tomcat Native单独下载。

我选用的版本组合:apr-1.6.3 + openssl-1.0.2o + tomcat-native-1.2.16。

  1. apr的安装
    首先解压apr的包,查看README文档,看看里面有没有需要特别注意的,默认的安装路径/usr/local/apr,注意安装的时候需要管理员权限,记得使用sudo命令。具体命令如下:
    cd apr
    ./configure
    make
    make install
    
  2. 安装OpenSSL
    同样需要解压openssl的安装包,查看INSTALL文档中的安装方法,我采用的是默认的安装路径,同样需要管理员权限。具体安装命令:
    cd openssl
    ./config
    make
    make install
    
  3. 安装tomcat native
    解压tomcat native的压缩包,在/tomcat-native/native/里有BUIDING文档,注意查看。具体安装命令:
    cd tomcat-native/native/
    ./configure --with-apr=/usr/local/apr --with-ssl=/usr/local/openssl --with-java-home=${JAVA_HOME}
    make
    make install
    

以上三个步骤成功完成之后,在/usr/local/apr/lib下有libtcnative-1.0.dylib的库文件,这就是我们实现HTTP/2所需要的。然后我们只需要在启动Spring boot应用的时候加上参数:-Djava.library.path=/usr/local/apr/lib/使用。

然后,加上其它的application.yaml常规配置:

server:
  http2:
    enabled: true
  ssl:
    key-store: classpath:abcKeyStore.p12
    key-store-password: abc
    key-store-type: PKCS12

这样我们就实现了使用JDK8 + Tomcat8提供HTTP/2服务了。

03 如何后台调用HTTP/2服务

使用浏览器访问HTTP/2服务,目前大部分浏览器都已经支持,并且在前面的那篇文章中也已经介绍了使用和验证方式,这里介绍一下如何通过HTTP client 后台调用HTTP/2服务。

由于HTTP/2协议的服务首先肯定是https的,所以java客户端首先需要完成https的相关设置。关于https客户端有两种选择:

  1. 使用证书。
  2. 配置成信任所有证书。

使用证书的okhttp client:

/**
 *  获取安全的加密(Https)的HttpClient
 * @return
 */
public static OkHttpClient getTLSOKHttp() {
    InputStream trustStorePath = HttpClientUtils.class.getResourceAsStream("/galaxyKeyStore.p12");
    logger.info("包含授信公钥文件的路径:{}", trustStorePath);
    KeyStore keyStore = getKeyStore("galaxy", trustStorePath);
    TrustManagerFactory trustManagerFactory = null;
    try {
        trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        trustManagerFactory.init(keyStore);
    } catch (Exception e) {
        e.printStackTrace();
    }

    TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
    if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
        throw new IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers));
    }
    X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
    //DefaultTrustManager trustManager = new DefaultTrustManager();

    SSLContext sslContext = null;
    try {
        sslContext = SSLContext.getInstance("TLSv1.2");
        sslContext.init(null, new TrustManager[] { trustManager }, null);
    } catch (Exception e) {
        e.printStackTrace();
    }
    SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();

    //OkHttpClient okHttpClient = new OkHttpClient.Builder().sslSocketFactory(sslSocketFactory, trustManager).build();
    // 强行不验证hostName
    OkHttpClient okHttpClient = new OkHttpClient().newBuilder().protocols(Arrays.asList(Protocol.HTTP_1_1, Protocol.HTTP_2))
            .hostnameVerifier((String s, SSLSession sslSession) -> true)
            //.sslSocketFactory(sslSocketFactory).build();
            .sslSocketFactory(sslSocketFactory, trustManager).build();
    return okHttpClient;
}

/**
 * 获得keyStore
 * @param password
 * @param keyStorePath
 * @return
 */
public static KeyStore getKeyStore(String password, InputStream keyStorePath) {
    KeyStore ks = null;
    //FileInputStream is = null;
    try {
        // 实例化密钥库 KeyStore用于存放证书,创建对象时 指定交换数字证书的加密标准
        // 指定交换数字证书的加密标准
        ks = KeyStore.getInstance("PKCS12");
        // 获得密钥库文件流
        //is = new FileInputStream(keyStorePath);
        // 加载密钥库
        ks.load(keyStorePath, password.toCharArray());
    } catch (Exception e){
        e.printStackTrace();
    } finally {
        try {
            // 关闭密钥库文件流
            keyStorePath.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    return ks;
}

信任所有证书的okhttp client:

/**
 *  获取安全的加密(Https)的HttpClient
 * @return
 */
public static OkHttpClient getTLSOKHttp() {
    DefaultTrustManager trustManager = new DefaultTrustManager();

    SSLContext sslContext = null;
    try {
        sslContext = SSLContext.getInstance("TLSv1.2");
        sslContext.init(null, new TrustManager[] { trustManager }, null);
    } catch (Exception e) {
        e.printStackTrace();
    }
    SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();

    //OkHttpClient okHttpClient = new OkHttpClient.Builder().sslSocketFactory(sslSocketFactory, trustManager).build();
    // 强行不验证hostName
    OkHttpClient okHttpClient = new OkHttpClient().newBuilder().protocols(Arrays.asList(Protocol.HTTP_1_1, Protocol.HTTP_2))
            .hostnameVerifier((String s, SSLSession sslSession) -> true)
            .sslSocketFactory(sslSocketFactory).build();
            //.sslSocketFactory(sslSocketFactory, trustManager).build();
    return okHttpClient;
}

private static class DefaultTrustManager implements X509TrustManager {
    @Override
    public void checkClientTrusted(java.security.cert.X509Certificate[] x509Certificates, String s) throws java.security.cert.CertificateException {

    }

    @Override
    public void checkServerTrusted(java.security.cert.X509Certificate[] x509Certificates, String s) throws java.security.cert.CertificateException {

    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return new X509Certificate[0];
    }

}

另外还有一个需要特别注意:
由于JDK8以及以前的jdk版本原生不支持http2,要想okhttp client能真正地走h2协议(如果客户端和服务端一端不支持h2,会自动降级为http1.1),需要添加alpn-boot的支持。

具体操作方法:

  1. 下载地址下载对应jdk版本的alpn-boot,虽然其中只提到了openJDK,但是使用oracle JDK也是可以的;
  2. 启动JVM的时候:java -Xbootclasspath/p:<path_to_alpn_boot_jar> ...,例如:-Xbootclasspath/p:/Users/uname/soft/alpn-boot/alpn-boot-8.1.12.v20180117.jar

这样配置之后,我们使用如下代码调用http/2的服务:

public static void main(String[] args) {
    String url = baseUrlProperties.getSerialNumber() + "/serial/number?systemNo=123456";
    Request request = new Request.Builder().url(url).build();
    OkHttpClient client = HttpClientUtils.getTLSOKHttp();
    return sendRequest(client, request);
}

private static String sendRequest(OkHttpClient client, Request request) {
    String result = null;
    String protocolName = null;
    try {
        Response response = client.newCall(request).execute();
        if (response != null) {
            protocolName = response.protocol().name();
            result = response.body().string();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    logger.info("测试app的http协议:{}", protocolName);
    return result + ": " + protocolName;
}

这样我们就能明确看到日志的打印结果:测试app的http协议:HTTP_2,证明成功的完成了HTTP/2协议的服务端和客户端通信。

04 结束

本文介绍了在Spring boot项目中实现HTTP/2协议的最复杂的情形,客户端和服务端都需要外力的支持。关于HTTP/2相关知识还需要细细体会,欢迎一起交流。

上一篇