John Jiang

          a cup of Java, cheers!
          https://github.com/johnshajiang/blog

             :: 首頁 ::  :: 聯系 :: 聚合  :: 管理 ::
            131 隨筆 :: 1 文章 :: 530 評論 :: 0 Trackbacks
          探索HTTP/2: 初試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實現的列表:https://github.com/http2/http2-spec/wiki/Implementations
              另外,還有一些工具支持對HTTP/2的分析與調試,如curl和WireShark。這里也有一份此類工具的列表:https://github.com/http2/http2-spec/wiki/Tools

          2. 服務器端
              作為Java程序員,選用一款使用Java語言編寫的開源HTTP/2服務器端實現似乎是很自然的結果。實際上,在日后的研究中,我們也需要查看服務器端的源代碼。這對于深入地理解HTTP/2,并發現實現中可能的問題,具有現實意義。
              本文選擇Jetty的最新版本9.3.11作為服務器端。Jetty是一個成熟的Servlet容器,這為開發Web應用程序提供了極大便利。而本文第1節中提到的Netty是一個傳輸層框架,它專注于網絡程序??梢允褂肗etty去開發一個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>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>
          該應用的部署路徑為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.war</Set>
          </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的一個Bug,正在開發中的9.3.12已經修復了它。

          7. 小結
              HTTP/2依然算是新潮的技術,對各家的實現,無論是服務器端,客戶端,還是分析工具,都要持有一份懷疑態度。這些實現和工具都是程序,都有可能存在bug。而且協議對許多細節沒有作出規定,各家都會發揮自己的想像力。比如,Apache httpd和Jetty在實現服務器端推送時,其方式就不盡相同。
              在開發自己的HTTP/2實現或應用的時候,需要同時使用已有的不同服務器端和客戶端去部署多套測試環境進行對比分析。
          posted on 2016-09-20 16:42 John Jiang 閱讀(4430) 評論(1)  編輯  收藏 所屬分類: Java 、原創HTTP/2 、探索HTTP/2

          評論

          # re: 探索HTTP/2: 初試HTTP/2(原) 2016-09-22 10:54 John Jiang
          Jetty剛剛發布了9.3.12,其中就包含了對本文中提到的100-continue問題的修復。  回復  更多評論
            

          主站蜘蛛池模板: 和静县| 万全县| 尼勒克县| 乐山市| 英德市| 长汀县| 微博| 翁源县| 乌审旗| 资源县| 鄂尔多斯市| 新河县| 永春县| 大足县| 安国市| 马鞍山市| 衡东县| 浠水县| 永胜县| 英吉沙县| 屏边| 莫力| 子洲县| 青海省| 宽城| 米易县| 双牌县| 东乌珠穆沁旗| 临邑县| 宝坻区| 青神县| 鸡东县| 工布江达县| 凤冈县| 扎兰屯市| 南澳县| 噶尔县| 潞城市| 永安市| 夹江县| 盐源县|