探索HTTP/2: 初試HTTP/2
目前支持HTTP/2的服務(wù)器端與客戶端實(shí)現(xiàn)已有不少,探索HTTP/2系列的第二篇就分別以Jetty和curl作為服務(wù)器端和客戶端,描述了HTTP/2測(cè)試環(huán)境的搭建過程。本文還將使用這個(gè)測(cè)試環(huán)境去展示Jetty在實(shí)現(xiàn)HTTP/2時(shí)的一個(gè)局限和一個(gè)Bug。(2016.09.22最后更新)1. HTTP/2的實(shí)現(xiàn) 目前已經(jīng)有眾多的服務(wù)器端和客戶端實(shí)現(xiàn)了對(duì)HTTP/2的支持。在服務(wù)器端,著名的Apache httpd從2.4.17版,Nginx從1.9.5版,開始支持HTTP/2。在客戶端,主流的瀏覽器,如Chrome,F(xiàn)ireFox和IE,的最新版均支持HTTP/2,但它們都只支持運(yùn)行在TLS上的HTTP/2(即h2)。使用Java語言實(shí)現(xiàn)的,則有Jetty和Netty,它們都實(shí)現(xiàn)了服務(wù)器端和客戶端。此處有一份HTTP/2實(shí)現(xiàn)的列表:https://github.com/http2/http2-spec/wiki/Implementations 另外,還有一些工具支持對(duì)HTTP/2的分析與調(diào)試,如curl和WireShark。這里也有一份此類工具的列表:https://github.com/http2/http2-spec/wiki/Tools2. 服務(wù)器端 作為Java程序員,選用一款使用Java語言編寫的開源HTTP/2服務(wù)器端實(shí)現(xiàn)似乎是很自然的結(jié)果。實(shí)際上,在日后的研究中,我們也需要查看服務(wù)器端的源代碼。這對(duì)于深入地理解HTTP/2,并發(fā)現(xiàn)實(shí)現(xiàn)中可能的問題,具有現(xiàn)實(shí)意義。 本文選擇Jetty的最新版本9.3.11作為服務(wù)器端。Jetty是一個(gè)成熟的Servlet容器,這為開發(fā)Web應(yīng)用程序提供了極大便利。而本文第1節(jié)中提到的Netty是一個(gè)傳輸層框架,它專注于網(wǎng)絡(luò)程序??梢允褂肗etty去開發(fā)一個(gè)Servlet容器,但這顯然不如直接使用Jetty方便。 安裝和配置Jetty是一件很容易的事情,具體過程如下所示。 假設(shè)此時(shí)已經(jīng)下載并解壓好了Jetty 9.3.11的壓縮文件,目錄名為jetty-9.3.11。在其中創(chuàng)建一個(gè)test-base子目錄,作為將要?jiǎng)?chuàng)建的Jetty Base的目錄。$ cd jetty-9.3.11
$ mkdir test-base
$ cd test-base
在創(chuàng)建Base時(shí),加入支持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
注意,在上述過程中,會(huì)根據(jù)當(dāng)前環(huán)境變量中使用的Java版本(此處為1.8.0_60)去下載一個(gè)對(duì)應(yīng)的TLS-ALPN實(shí)現(xiàn)jar文件(此處為alpn-boot-8.1.5.v20150921.jar),該jar會(huì)用于對(duì)h2的支持。當(dāng)啟動(dòng)Jetty時(shí),該jar會(huì)被Java的Bootstrap class loader加載到類路徑中。 創(chuàng)建一個(gè)最簡(jiǎn)單的Web應(yīng)用,使它在根目錄下包含一個(gè)文本文件index,內(nèi)容為"HTTP/2 Test"。再包含一個(gè)簡(jiǎn)單的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主要是定義了一個(gè)Servlet,具體內(nèi)容如下:<?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>index</welcome-file>
</welcome-file-list>
<servlet>
<servlet-name>test</servlet-name>
<servlet-class>test.TestServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>test</servlet-name>
<url-pattern>/test/*</url-pattern>
</servlet-mapping>
</web-app>
該應(yīng)用的部署路徑為jetty-9.3.11/test-base/webapps/test.war。在該WAR文件所在的目錄下,創(chuàng)建一個(gè)test.xml,其內(nèi)容如下所示:<?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.war</Set>
</Configure>
啟動(dòng)Jetty服務(wù)器,使用默認(rèn)的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
根據(jù)上述日志可知,Jetty啟用了Web應(yīng)用test.war,還啟動(dòng)了兩個(gè)ServerConnector,一個(gè)支持h2c,另一個(gè)支持h2。值得注意的是,這兩個(gè)ServerConnector還分別支持h2c-17, h2c-16, h2c-15, h2c-14和h2-17, h2-16, h2-15, h2-14。這是因?yàn)椋琀TTP/2在正式發(fā)布之前,先后發(fā)布了18個(gè)草案,其編號(hào)為00-17。所以,這里的h2c-XX和h2-XX指的就是第XX號(hào)草案。3. 客戶端 其實(shí)最方便的客戶端就是瀏覽器了。只要使用的FireFox或Chrome版本不是太老,肯定都已經(jīng)支持了HTTP/2,而且這一功能是默認(rèn)打開的。也就是說,當(dāng)使用FireFox去訪問前面所部署的Web應(yīng)用時(shí),就是在使用HTTP/2,但你不會(huì)感覺到這種變化。使用FireFox提供的Developer Tools中的Network工具查看服務(wù)器端的響應(yīng),會(huì)發(fā)現(xiàn)HTTP版本為HTTP/2.0。但此處希望這個(gè)客戶端能夠提供更為豐富的與服務(wù)器端進(jìn)行交互的功能,那么瀏覽器就并不合適了。
Jetty也實(shí)現(xiàn)了支持HTTP/2的客戶端,但這個(gè)客戶端是一個(gè)API,需要編寫程序去訪問HTTP/2服務(wù)器端。而且,目前該API的設(shè)計(jì)抽象層次較低,需要應(yīng)用程序員對(duì)HTTP/2協(xié)議,比如各種幀,有較深入的了解。這對(duì)于初涉HTTP/2的開發(fā)者來說,顯然很不合適。本文選擇使用C語言編寫的一個(gè)工具,其實(shí)也是HTTP/2的客戶端實(shí)現(xiàn)之一,curl。 curl在支持HTTP/2時(shí),實(shí)際上是使用了nghttp2的C庫,所以需要先安裝nghttp2。另外,為了讓curl支持h2,就必須要有TLS-ALPN的支持。那么,一般地還需要安裝OpenSSL 1.0.2+。 網(wǎng)絡(luò)上關(guān)于在Linux下安裝支持HTTP/2的curl的資源有很多,過程并不難,但有點(diǎn)兒繁,要安裝的依賴比較多,本文就不贅述了。如果是使用Windows,筆者比較推薦通過Cygwin來安裝和使用curl。在Windows中安裝Cygwin非常簡(jiǎn)單,在Cygwin中執(zhí)行各種命令時(shí),感覺上就如同在使用Linux,盡管它并不是一個(gè)虛擬機(jī)。通過Cygwin安裝curl,它會(huì)自動(dòng)地安裝所需的各種依賴程序和庫。 在筆者的機(jī)器上,通過查看curl的版本會(huì)出現(xiàn)如下信息: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. 第一次嘗試 在第一次嘗試中,只需要簡(jiǎn)單地訪問第2節(jié)中部署的Web應(yīng)用中的靜態(tài)文本文件index,以感受下h2c,完整命令如下:$ curl -v --http2 http://localhost:8080/index
在輸出中包含有如下的內(nèi)容:...
> 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
">"是客戶端發(fā)送的請(qǐng)求,"<"是服務(wù)器端發(fā)送的響應(yīng),而"*"是curl對(duì)當(dāng)前過程的說明。結(jié)合本系列第一篇文章中所簡(jiǎn)述的HTTP 2協(xié)議,可以有以下的基本理解。[1]客戶端發(fā)起了一個(gè)HTTP/1.1的請(qǐng)求,其中攜帶有Upgrade頭部,要求服務(wù)器端升級(jí)到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]服務(wù)器端同意升級(jí),返回響應(yīng)"101 Switching Protocols",然后客戶端收到了101響應(yīng),HTTP/2連接進(jìn)行確認(rèn)。< HTTP/1.1 101 Switching Protocols
* Received 101
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
[3]服務(wù)器端響應(yīng)最終結(jié)果。狀態(tài)行中出現(xiàn)的HTTP版本為HTTP/2,狀態(tài)代碼為200,且后面沒有跟著"OK"。最后輸出了index文件的內(nèi)容"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. 一個(gè)局限 這次,在發(fā)起的請(qǐng)求中包含體部,命令如下:$ curl -v --http2 -d "body" http://localhost:8080/index
在輸出中包含有如下的內(nèi)容:...
> 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節(jié)中的輸出進(jìn)行比較,會(huì)發(fā)現(xiàn)缺少了"101 Switching Protocols"那一段,而且最終響應(yīng)狀態(tài)行中出現(xiàn)的HTTP版本是HTTP/1.1。這就說明服務(wù)器端不同意升級(jí),后面繼續(xù)使用HTTP/1.1。剛剛部署的Jetty未做任何改變?cè)趺磿?huì)突然不支持HTTP/2了呢?或者這是curl的問題?其實(shí),這是因?yàn)镴etty服務(wù)器端在實(shí)現(xiàn)h2c時(shí)不支持請(qǐng)求中包含體部。另外,Apache httpd也有同樣的問題。如果是使用h2,則沒有這個(gè)限制。這背后的原因超出了本文的范疇,不作表述。6. 一個(gè)Bug 在這次嘗試中,測(cè)試一下兩端對(duì)100-continue的支持。如果請(qǐng)求中使用了頭部"Expect: 100-continue",那么正常地該請(qǐng)求要有體部。但由于在第5節(jié)中介紹的問題,此時(shí)不能再使用h2c,而只能使用h2。另外,這次不訪問靜態(tài)文件,而是訪問Servlet(此處為/test)。完整命令如下:$ curl -vk --http2 -H "Expect: 100-continue" -d "body" https://localhost:8443/test
在輸出的最后出現(xiàn)了如下信息:curl: (92) HTTP/2 stream 1 was not closed cleanly: CANCEL (err 8)
這其實(shí)是Jetty的一個(gè)Bug,正在開發(fā)中的9.3.12已經(jīng)修復(fù)了它。7. 小結(jié) HTTP/2依然算是新潮的技術(shù),對(duì)各家的實(shí)現(xiàn),無論是服務(wù)器端,客戶端,還是分析工具,都要持有一份懷疑態(tài)度。這些實(shí)現(xiàn)和工具都是程序,都有可能存在bug。而且協(xié)議對(duì)許多細(xì)節(jié)沒有作出規(guī)定,各家都會(huì)發(fā)揮自己的想像力。比如,Apache httpd和Jetty在實(shí)現(xiàn)服務(wù)器端推送時(shí),其方式就不盡相同。
在開發(fā)自己的HTTP/2實(shí)現(xiàn)或應(yīng)用的時(shí)候,需要同時(shí)使用已有的不同服務(wù)器端和客戶端去部署多套測(cè)試環(huán)境進(jìn)行對(duì)比分析。