探索http/2: 初试http/2
目前支持http/2的服务器端与客户端实现已有不少,的第二篇就分别以jetty和curl作为服务器端和客户端,描述了http/2测试环境的搭建过程。本文还将使用这个测试环境去展示jetty在实现http/2时的一个局限和一个bug。(2016.09.22最后更新)1. http/2的实现 目前已经有众多的服务器端和客户端实现了对http/2的支持。在服务器端,著名的apache httpd从2.4.17版,nginx从1.9.5版,开始支持http/2。在客户端,主流的浏览器,如chrome,firefox和ie,的最新版均支持http/2,但它们都只支持运行在tls上的http/2(即h2)。使用java语言实现的,则有jetty和netty,它们都实现了服务器端和客户端。此处有一份http/2实现的列表: 另外,还有一些工具支持对http/2的分析与调试,如curl和wireshark。这里也有一份此类工具的列表:2. 服务器端 作为java程序员,选用一款使用java语言编写的开源http/2服务器端实现似乎是很自然的结果。实际上,在日后的研究中,我们也需要查看服务器端的源代码。这对于深入地理解http/2,并发现实现中可能的问题,具有现实意义。 本文选择jetty的最新版本9.3.11作为服务器端。jetty是一个成熟的servlet容器,这为开发web应用程序提供了极大便利。而本文第1节中提到的netty是一个传输层框架,它专注于网络程序。可以使用netty去开发一个servlet容器,但这显然不如直接使用jetty方便。 安装和配置jetty是一件很容易的事情,具体过程如下所示。 假设此时已经下载并解压好了jetty 9.3.11的压缩文件,目录名为jetty-9.3.11。在其中创建一个test-base子目录,作为将要创建的jetty base的目录。$ cd jetty-9.3.11
$ mkdir test-base
$ cd test-base
在创建base时,加入支持http,https,http2(h2),http2c(h2c)和deploy的模块。$ java -jar ../start.jar --add-to-startd=http,https,http2,http2c,deploy
alert: there are enabled module(s) with licenses.
the following 1 module(s):
contains software not provided by the eclipse foundation!
contains software not covered by the eclipse public license!
has not been audited for compliance with its license
module: alpn
alpn is a hosted at github under the gpl v2 with classpath exception.
alpn replaces/modifies openjdk classes in the java.sun.security.ssl package.
http://github.com/jetty-project/jetty-alpn
http://openjdk.java.net/legal/gplv2 ce.html
proceed (y/n)? y
info: server initialised (transitively) in ${jetty.base}\start.d\server.ini
info: http initialised in ${jetty.base}\start.d\http.ini
info: ssl initialised (transitively) in ${jetty.base}\start.d\ssl.ini
info: alpn initialised (transitively) in ${jetty.base}\start.d\alpn.ini
info: http2c initialised in ${jetty.base}\start.d\http2c.ini
info: https initialised in ${jetty.base}\start.d\https.ini
info: deploy initialised in ${jetty.base}\start.d\deploy.ini
info: http2 initialised in ${jetty.base}\start.d\http2.ini
download: http://central.maven.org/maven2/org/mortbay/jetty/alpn/alpn-boot/8.1.5.v20150921/alpn-boot-8.1.5.v20150921.jar to ${jetty.base}\lib\alpn\alpn-boot-8.1.5.v20150921.jar
download: https://raw.githubusercontent.com/eclipse/jetty.project/master/jetty-server/src/test/config/etc/keystore?id=master to ${jetty.base}\etc\keystore
mkdir: ${jetty.base}\webapps
info: base directory was modified
注意,在上述过程中,会根据当前环境变量中使用的java版本(此处为1.8.0_60)去下载一个对应的tls-alpn实现jar文件(此处为alpn-boot-8.1.5.v20150921.jar),该jar会用于对h2的支持。当启动jetty时,该jar会被java的bootstrap class loader加载到类路径中。 创建一个最简单的web应用,使它在根目录下包含一个文本文件index,内容为"http/2 test"。再包含一个简单的servlet,代码如下:package test;
import java.io.ioexception;
import javax.servlet.servletexception;
import javax.servlet.http.httpservlet;
import javax.servlet.http.httpservletrequest;
import javax.servlet.http.httpservletresponse;
public class testservlet extends httpservlet {
private static final long serialversionuid = 5222793251610509039l;
@override
public void doget(httpservletrequest request, httpservletresponse response)
throws servletexception, ioexception {
response.getwriter().println("test");
}
@override
public void dopost(httpservletrequest request, httpservletresponse response)
throws servletexception, ioexception {
doget(request, response);
}
}
web.xml主要是定义了一个servlet,具体内容如下:xml version="1.0" encoding="utf-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/xmlschema-instance"
xsi:schemalocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
metadata-complete="false" version="3.1">
<welcome-file-list>
<welcome-file>indexwelcome-file>
welcome-file-list>
<servlet>
<servlet-name>testservlet-name>
<servlet-class>test.testservletservlet-class>
servlet>
<servlet-mapping>
<servlet-name>testservlet-name>
<url-pattern>/test/*url-pattern>
servlet-mapping>
web-app>
该应用的部署路径为jetty-9.3.11/test-base/webapps/test.war。在该war文件所在的目录下,创建一个test.xml,其内容如下所示:xml version="1.0" encoding="iso-8859-1"?>
doctype configure public "-//jetty//configure//en" "http://www.eclipse.org/jetty/configure_9_0.dtd">
<configure class="org.eclipse.jetty.webapp.webappcontext">
<set name="contextpath">/set>
<set name="war"><systemproperty name="jetty.base" default="."/>/webapps/test.warset>
configure>
启动jetty服务器,使用默认的http和https端口,分别为8080和8443。$ java -jar ../start.jar
2016-09-15 21:15:51.190:info:oejs.server:main: jetty-9.3.11.v20160721
2016-09-15 21:15:51.237:info:oejdp.scanningappprovider:main: deployment monitor [file:///d:/http2/jetty/jetty-9.3.11/test-base/webapps/] at interval 1
2016-09-15 21:15:52.251:info:oejw.standarddescriptorprocessor:main: no jsp support for /test.war, did not find org.eclipse.jetty.jsp.jettyjspservlet
2016-09-15 21:15:52.313:info:oejsh.contexthandler:main: started o.e.j.w.webappcontext@4520ebad{/test.war,file:///d:/http2/jetty/jetty-9.3.11/test-base/webapps/test.war/,available}{d:\http2\jetty\jetty-9.3.11\test-base\webapps\test.war}
2016-09-15 21:15:52.391:info:oejw.standarddescriptorprocessor:main: no jsp support for /, did not find org.eclipse.jetty.jsp.jettyjspservlet
2016-09-15 21:15:52.391:info:oejsh.contexthandler:main: started o.e.j.w.webappcontext@711f39f9{/,file:///d:/http2/jetty/jetty-9.3.11/test-base/webapps/test.war/,available}{/test.war}
2016-09-15 21:15:52.532:info:oejs.abstractconnector:main: started serverconnector@1b68ddbd{http/1.1,[http/1.1, h2c, h2c-17, h2c-16, h2c-15, h2c-14]}{0.0.0.0:8080}
2016-09-15 21:15:52.735:info:oejus.sslcontextfactory:main: x509=x509@e320068(jetty,h=[jetty.eclipse.org],w=[]) for sslcontextfactory@1f57539(file:///d:/http2/jetty/jetty-9.3.11/test-base/etc/keystore,file:///d:/http2/jetty/jetty-9.3.11/test-base/etc/keystore)
2016-09-15 21:15:52.735:info:oejus.sslcontextfactory:main: x509=x509@76f2b07d(mykey,h=[],w=[]) for sslcontextfactory@1f57539(file:///d:/http2/jetty/jetty-9.3.11/test-base/etc/keystore,file:///d:/http2/jetty/jetty-9.3.11/test-base/etc/keystore)
2016-09-15 21:15:53.234:info:oejs.abstractconnector:main: started serverconnector@4b168fa9{ssl,[ssl, alpn, h2, h2-17, h2-16, h2-15, h2-14, http/1.1]}{0.0.0.0:8443}
2016-09-15 21:15:53.249:info:oejs.server:main: started @3940ms
根据上述日志可知,jetty启用了web应用test.war,还启动了两个serverconnector,一个支持h2c,另一个支持h2。值得注意的是,这两个serverconnector还分别支持h2c-17, h2c-16, h2c-15, h2c-14和h2-17, h2-16, h2-15, h2-14。这是因为,http/2在正式发布之前,先后发布了18个草案,其编号为00-17。所以,这里的h2c-xx和h2-xx指的就是第xx号草案。3. 客户端 其实最方便的客户端就是浏览器了。只要使用的firefox或chrome版本不是太老,肯定都已经支持了http/2,而且这一功能是默认打开的。也就是说,当使用firefox去访问前面所部署的web应用时,就是在使用http/2,但你不会感觉到这种变化。使用firefox提供的developer tools中的network工具查看服务器端的响应,会发现http版本为http/2.0。但此处希望这个客户端能够提供更为丰富的与服务器端进行交互的功能,那么浏览器就并不合适了。
jetty也实现了支持http/2的客户端,但这个客户端是一个api,需要编写程序去访问http/2服务器端。而且,目前该api的设计抽象层次较低,需要应用程序员对http/2协议,比如各种帧,有较深入的了解。这对于初涉http/2的开发者来说,显然很不合适。本文选择使用c语言编写的一个工具,其实也是http/2的客户端实现之一,curl。 curl在支持http/2时,实际上是使用了nghttp2的c库,所以需要先安装nghttp2。另外,为了让curl支持h2,就必须要有tls-alpn的支持。那么,一般地还需要安装openssl 1.0.2 。 网络上关于在linux下安装支持http/2的curl的资源有很多,过程并不难,但有点儿繁,要安装的依赖比较多,本文就不赘述了。如果是使用windows,笔者比较推荐通过cygwin来安装和使用curl。在windows中安装cygwin非常简单,在cygwin中执行各种命令时,感觉上就如同在使用linux,尽管它并不是一个虚拟机。通过cygwin安装curl,它会自动地安装所需的各种依赖程序和库。 在笔者的机器上,通过查看curl的版本会出现如下信息:curl 7.50.2 (x86_64-unknown-cygwin) libcurl/7.50.2 openssl/1.0.2h zlib/1.2.8 libidn/1.29 libpsl/0.14.0 ( libidn/1.29) libssh2/1.7.0 nghttp2/1.14.0
protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtsp scp sftp smb smbs smtp smtps telnet tftp
features: debug idn ipv6 largefile gss-api kerberos spnego ntlm ntlm_wb ssl libz tls-srp http2 unixsockets metalink psl
由上可知,笔者使用的curl版本是7.50.2,nghttp2版本是1.14.0,而openssl版本是1.0.2h。4. 第一次尝试 在第一次尝试中,只需要简单地访问第2节中部署的web应用中的静态文本文件index,以感受下h2c,完整命令如下:$ curl -v --http2 http://localhost:8080/index
在输出中包含有如下的内容:...
> get /index http/1.1
> host: localhost:8080
> user-agent: curl/7.50.2
> accept: */*
> connection: upgrade, http2-settings
> upgrade: h2c
> http2-settings: aamaaabkaaqaap__
>
...
< http/1.1 101 switching protocols
* received 101
* using http2, server supports multi-use
* connection state changed (http/2 confirmed)
...
< http/2 200
< server: jetty(9.3.11.v20160721)
< last-modified: wed, 14 sep 2016 12:52:32 gmt
< content-length: 11
< accept-ranges: bytes
<
...
http/2 test
">"是客户端发送的请求,"<"是服务器端发送的响应,而"*"是curl对当前过程的说明。结合本系列中所简述的http 2协议,可以有以下的基本理解。[1]客户端发起了一个http/1.1的请求,其中携带有upgrade头部,要求服务器端升级到http/2(h2c)。> get /index http/1.1
> host: localhost:8080
> user-agent: curl/7.50.2
> accept: */*
> connection: upgrade, http2-settings
> upgrade: h2c
> http2-settings: aamaaabkaaqaap__
>
[2]服务器端同意升级,返回响应"101 switching protocols",然后客户端收到了101响应,http/2连接进行确认。< http/1.1 101 switching protocols
* received 101
* using http2, server supports multi-use
* connection state changed (http/2 confirmed)
[3]服务器端响应最终结果。状态行中出现的http版本为http/2,状态代码为200,且后面没有跟着"ok"。最后输出了index文件的内容"http/2 test"。< http/2 200
< server: jetty(9.3.11.v20160721)
< last-modified: wed, 14 sep 2016 12:52:32 gmt
< content-length: 11
< accept-ranges: bytes
<
...
http/2 test
5. 一个局限 这次,在发起的请求中包含体部,命令如下:$ curl -v --http2 -d "body" http://localhost:8080/index
在输出中包含有如下的内容:...
> post /index http/1.1
> host: localhost:8080
> user-agent: curl/7.50.2
> accept: */*
> connection: upgrade, http2-settings
> upgrade: h2c
> http2-settings: aamaaabkaaqaap__
> content-length: 4
> content-type: application/x-www-form-urlencoded
>
...
< http/1.1 200 ok
< last-modified: wed, 14 sep 2016 12:52:32 gmt
< accept-ranges: bytes
< content-length: 11
...
http/2 test
和第4节中的输出进行比较,会发现缺少了"101 switching protocols"那一段,而且最终响应状态行中出现的http版本是http/1.1。这就说明服务器端不同意升级,后面继续使用http/1.1。刚刚部署的jetty未做任何改变怎么会突然不支持http/2了呢?或者这是curl的问题?其实,这是因为jetty服务器端在实现h2c时不支持请求中包含体部。另外,apache httpd也有同样的问题。如果是使用h2,则没有这个限制。这背后的原因超出了本文的范畴,不作表述。6. 一个bug 在这次尝试中,测试一下两端对100-continue的支持。如果请求中使用了头部"expect: 100-continue",那么正常地该请求要有体部。但由于在第5节中介绍的问题,此时不能再使用h2c,而只能使用h2。另外,这次不访问静态文件,而是访问servlet(此处为/test)。完整命令如下:$ curl -vk --http2 -h "expect: 100-continue" -d "body" https://localhost:8443/test
在输出的最后出现了如下信息:curl: (92) http/2 stream 1 was not closed cleanly: cancel (err 8)
这其实是jetty的一个,正在开发中的9.3.12已经修复了它。7. 小结 http/2依然算是新潮的技术,对各家的实现,无论是服务器端,客户端,还是分析工具,都要持有一份怀疑态度。这些实现和工具都是程序,都有可能存在bug。而且协议对许多细节没有作出规定,各家都会发挥自己的想像力。比如,apache httpd和jetty在实现服务器端推送时,其方式就不尽相同。
在开发自己的http/2实现或应用的时候,需要同时使用已有的不同服务器端和客户端去部署多套测试环境进行对比分析。