jetty 9.3.0.M2を使ってHTTP2の通信を試す (サーバプッシュ)

masakiが2015/07/06 10:05:25に投稿

jetty 9.3.0.M2を使ってHTTP/2の通信を試す (サーバプッシュ)

動機

jettyでHTTP/2の対応がされているようなのでサンプルプログラムを使って試してみる。

HTTP/2とは

HTTP/2は、

HTTP/1.1との互換性を維持したまま、1つのTCP接続でリクエストとレスポンスの多重化、またサーバプッシュを行うことによってパフォーマンスを改善すること

を目的としている。

「HTTP/1.1との互換性を維持したまま」という点がwebsocketと異なる。(websocketにはHTTPリクエスト/レスポンスはない。)

今回確認する内容

今回はHTTP/2のリクエストとレスポンスの多重化、サーバプッシュの様子を確認する。

jetty 9.3.0.M2を使って以下のサンプルプログラムを作成する。

  • クライアントからHTTP/2で「/」にGETリクエストを送る。
  • サーバは「/」のGETリクエストのレスポンスとして「<html>hello world</html>」を返す。
    また、サーバは「/push」のレスポンス「<html>push</html>」も返す。(サーバプッシュ)

サンプルプログラム

サーバ側

public class App
{
    private Server server;
    private ServerConnector connector;

    public static void main( String[] args ) throws Exception
    {
       new App().start();
    }

    private void start() throws Exception
    {
        ConnectionFactory connectionFactory = new HTTP2CServerConnectionFactory(new HttpConfiguration());

        server = new Server();
        connector = new ServerConnector(server, 1,1, connectionFactory);
        connector.setPort(5443);
        server.addConnector(connector);
        ServletContextHandler context = new ServletContextHandler(server, "/", true, false);

        HttpServlet servlet = new HttpServlet()
        {
            @Override
            protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
            {
                Request jettyRequest = (Request) request;
                if (jettyRequest.getRequestURI().equals("/") &amp;&amp; jettyRequest.isPushSupported()) {
                    jettyRequest.getPushBuilder()
                                .path("/push")
                                .push();
                }

                if (jettyRequest.getRequestURI().equals("/")) {
                  final byte[] content = "&lt;html&gt;hello world&lt;/html&gt;".getBytes(StandardCharsets.UTF_8);
                  response.getOutputStream().write(content);
                  System.out.println("response helloworld");
                } else if (jettyRequest.getRequestURI().equals("/push")) {
                  final byte[] content = "&lt;html&gt;push&lt;/html&gt;".getBytes(StandardCharsets.UTF_8);
                  response.getOutputStream().write(content);
                  System.out.println("response push");
                }
            }
        };

        context.addServlet(new ServletHolder(servlet), "/");
        server.start();
    }
}

クライアント側

public class App 
{
    public static void main( String[] args ) throws Exception
    {    	
        HTTP2Client client = new HTTP2Client();
        client.start();

        String host = "192.168.1.55";
        int port = 5443;
        
        FuturePromise&lt;Session&gt; sessionPromise = new FuturePromise&lt;&gt;();
        client.connect(new InetSocketAddress(host, port), new ServerSessionListener.Adapter(), sessionPromise);
        Session session = sessionPromise.get(5, TimeUnit.SECONDS);

        HttpFields requestFields = new HttpFields();
        requestFields.put("User-Agent", client.getClass().getName() + "/" + Jetty.VERSION);
        MetaData.Request metaData = new MetaData.Request("GET", new HttpURI("http://" + host + ":" + port + "/"), HttpVersion.HTTP_2, requestFields);
        System.out.println(metaData.getURIString());
        HeadersFrame headersFrame = new HeadersFrame(0, metaData, null, true);
        final Phaser phaser = new Phaser(2);
        session.newStream(headersFrame, new Promise.Adapter&lt;Stream&gt;(), new Stream.Listener.Adapter()
        {
            @Override
            public void onHeaders(Stream stream, HeadersFrame frame)
            {
            	
            	System.out.println("[" + stream.getId() + "] HEADERS " + frame.getMetaData().toString());
                if (frame.isEndStream())
                    phaser.arrive();
            }

            @Override
            public void onData(Stream stream, DataFrame frame, Callback callback)
            {
                byte[] bytes = new byte[frame.getData().remaining()];
                frame.getData().get(bytes);
                System.out.println("[" + stream.getId() + "] DATA " + new String(bytes));
                callback.succeeded();
                if (frame.isEndStream())
                    phaser.arrive();
            }

            @Override
            public Stream.Listener onPush(Stream stream, PushPromiseFrame frame)
            {
            	System.out.println("[" + stream.getId() + "] PUSH_PROMISE " + frame.getMetaData().toString());
                phaser.register();
                return this;
            }
        });

        phaser.awaitAdvanceInterruptibly(phaser.arrive(), 5, TimeUnit.SECONDS);
       
        client.stop();
    }
}

サーバ側実行結果 クライアント接続前

2015-06-05 14:35:37.446:INFO::jp.co.linkode.http2.server.example.App.main(): Logging initialized @1996ms
2015-06-05 14:35:37.530:INFO:oejs.Server:jp.co.linkode.http2.server.example.App.main(): jetty-9.3.0.M2
2015-06-05 14:35:37.596:INFO:oejsh.ContextHandler:jp.co.linkode.http2.server.example.App.main(): Started o.e.j.s.ServletContextHandler@11b11eee{/,null,AVAILABLE}
2015-06-05 14:35:37.618:INFO:oejs.ServerConnector:jp.co.linkode.http2.server.example.App.main(): Started ServerConnector@33e7c8f5{h2c,[h2c]}{0.0.0.0:5443}
2015-06-05 14:35:37.619:INFO:oejs.Server:jp.co.linkode.http2.server.example.App.main(): Started @2168ms

h2c : http2 clear text の略。今回のサーバプログラムでは通信の暗号化を行っていない。

クライアント実行結果

2015-06-05 14:36:07.335:INFO::jp.co.linkode.http2.client.example.App.main(): Logging initialized @3102mshttp://192.168.1.55:5443/
[2] PUSH_PROMISE GET{uhttp://192.168.1.55:5443/push,HTTP/2.0,h=3}
[2] HEADERS HTTP/2.0{s=200,h=1}
[2] DATA <html>push</html>
[1] HEADERS HTTP/2.0{s=200,h=4}
[1] DATA <html>hello world</html>

サーバ側実行結果 クライアント接続後

response push
response helloworld

実際の通信内容

上記サンプルプログラムの通信内容をwireshark(version:1.99.6-676-gdc 14e3c)でのぞいてみる。

HTTP/2では1つの接続上にストリームと呼ばれる仮想的な双方向シーケンスを作ることができる。
以下はストリームのやりとりの説明である。
(ストリームの前後の処理についての説明は後日。)

#232
サーバ → クライアント
ストリーム0(新) SETTINGS

各ストリームはメッセージ(リクエスト/レスポンス)単位で通信を行う。
各メッセージは 1 つ以上のフレームで構成される。

フレームには

  • SETTINGS
  • HEADER
  • DATA
  • GOAWAY
    などがある。

SETTINGSフレームでストリーム作成のためのネゴシエーションを行う。

  • サーバプッシュの可否
  • HTTPヘッダ圧縮の可否
  • 最大ストリーム数
  • ストリーム作成時の初期ウィンドウサイズ
    など

HTTP/2における「サーバプッシュ」とは、クライアントからの該当するリクエスト無しでレスポンスを返すことである。
ここではサーバプッシュ「可」としている。

ストリームはそれぞれIDを持つ。
クライアントから開始したストリーム ID は奇数、
サーバから開始したストリームIDは偶数である。

#234,#235
クライアント → サーバ
ストリーム0 SETTINGS
クライアントからのネゴシエーション返答を行っている。

#237
サーバ → クライアント
ストリーム0 SETTINGS
ネゴシエーション結果を通知している。

#238
クライアント → サーバ
ストリーム1(新) HEADERS
パス「/」の「GET」リクエストを行っている。

リクエストメッセージのHEADERSフレームには

  • パス
  • Method(GET/POSTなど)
  • ユーザエージェント名
    などが含まれる。

また、ストリーム片側終了を行っている。(ストリーム1においてクライアントからはもう送信を行わないという意味。)

#240
サーバ → クライアント
ストリーム2(新) PUSH_PROMISE
パス「/push」の「GET」の内容をプッシュ送信することを予告している。

#243
サーバ → クライアント
ストリーム2 HEADERS, DATA
パス「/push」への「GET」レスポンスのヘッダと内容をプッシュしている。
HEADERSフレームにはHTTPステータス、
DATAフレームにはレスポンス内容が含まれる。
また、ストリーム片側終了を行っている。

#246
サーバ → クライアント
ストリーム1 HEADERS
パス「/」の「GET」レスポンスのヘッダを送信している。

#250,#252
WINDOW_UPDATEについては省略。(未調査)

#251
サーバ → クライアント
ストリーム1 DATA
パス「/」の「GET」レスポンスの内容を送信している。

#256
クライアント → サーバ
ストリーム0 GOAWAY
コネクション終了を通知している。

上記フレームのHTTP2フレームシーケンス図は以下の通り。

次回以降はHTTP/2の以下について説明する予定である。

  • バイナリフレームの構成
  • ALPN接続
  • 通信暗号化
  • フレームサイズ変更
  • サーバ側フレーム受信毎の処理