Jump to: navigation, search

Jetty/Feature/SPDY



Introduction

Jetty supports both a client and a server implementation for the SPDY protocol, beginning with versions 7.6.2 and 8.1.2. To provide the best support possible for SPDY, the Jetty project also provides an implementation for NPN. Both the SPDY and the NPN implementation require OpenJDK 1.7 or greater.

A server deployed over TLS normally advertises the SPDY protocol, via the Next Protocol Negotiation TLS Extension (NPN).

Feature

Introducing SPDY Modules

Jetty's SPDY implementation consists of four modules:

  • spdy-core – contains the SPDY API and a partial implementation. This module is independent of Jetty (the servlet container), and you can reuse it with other Java SPDY implementations. One of the goals of this module is to standardize the SPDY Java API.
  • spdy-jetty module – binds the spdy-core module to Jetty's NIO framework to provide asynchronous socket I/O. This module uses Jetty internals, but Java SPDY client applications can also use it to communicate with a SPDY server.
  • spdy-jetty-http module – provides a server-side layering of HTTP over SPDY. This module allows any SPDY compliant browser, such as Chromium/Chrome, to talk SPDY to a Jetty server that deploys a standard web application made of servlets, filters and JSPs. This module performs the conversion SPDY to HTTP and vice versa so that for the web application it is as if a normal HTTP request has arrived, and a normal HTTP response is returned.
  • spdy-jetty-http-webapp module – provides a demo application for the a spdy-jetty-http module.

Transparently Enabling HTTP over SPDY in Jetty

The spdy-jetty-http module provides an out-of-the-box server connector that performs the SPDY to HTTP conversion and vice versa (HTTP over SPDY). You can use this connector instead of Jetty's SslSelectChannelConnector (that only speaks HTTP), and it falls back to HTTPS if SPDY is not negotiated.

An example jetty-spdy.xml file that you can use instead of jetty-ssl.xml follows:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
 
<Configure id="Server" class="org.eclipse.jetty.server.Server">
 
    <New id="sslContextFactory" class="org.eclipse.jetty.util.ssl.SslContextFactory">
        <Set name="keyStorePath">your_keystore.jks</Set>
        <Set name="keyStorePassword">storepwd</Set>
        <Set name="includeProtocols">TLSv1</Set>
    </New>
 
    <Call name="addConnector">
        <Arg>
            <New class="org.eclipse.jetty.spdy.http.HTTPSPDYServerConnector">
                <Arg>
                    <Ref id="sslContextFactory" />
                </Arg>
                <Set name="Port">8443</Set>
            </New>
        </Arg>
    </Call>
 
</Configure>

This is sufficient to enable your Jetty server to speak SPDY to browsers that support it.

Remember, however, that SPDY over SSL (as set up like the configuration above) requires that you set up NPN correctly; in particular, you need to start the JVM with the NPN boot jar in the boot classpath (follow the instructions at the NPN documentation page).

Additionally, NPN is only supported for the TLS protocol, version 1 or greater; this means that it cannot be used with SSLv2, which implies that you have to configure the SslContextFactory to use TLSv1 or above, since the JDK usually sends a SSLv2 ClientHello message to secure servers. This is done by specifying the "includeProtocols" property to contain at least the value "TLSv1" (see above).

SPDY Proxy

spdy-jetty-http also provides a fully functional SPDY proxy server out of the box. Jetty's SPDY proxy is capable to receive SPDY (currently v2/v3) and HTTP requests and proxy those requests to other SPDY servers. It will if necessary translate the incoming protocol to a protocol the target host understands. The translation to the target server protocol is done by an implementation of the ProxyEngine class. Right now we only provide a SPDYProxyEngine implementation which can talk spdy/2 and spdy/3. We're planning to support other protocols soon out of the box. As always contributions are welcome.

Configuration is pretty straight forward as the following example will show. The example server will have a plain SPDY connector listening on port 9090 and a spdy proxy connector listening on port 8080. Incoming requests to port 8080 will be proxied and translated to the connector listening on port 9090. In real world scenarios you will more likely proxy to a different host.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
 
<Configure id="Server" class="org.eclipse.jetty.server.Server">
 
    <New id="sslContextFactory" class="org.eclipse.jetty.util.ssl.SslContextFactory">
        <Set name="keyStorePath">src/main/resources/keystore.jks</Set>
        <Set name="keyStorePassword">storepwd</Set>
        <Set name="trustStore">src/main/resources/truststore.jks</Set>
        <Set name="trustStorePassword">storepwd</Set>
        <Set name="protocol">TLSv1</Set>
    </New>
 
    <!--
    <Set class="org.eclipse.jetty.npn.NextProtoNego" name="debug" type="boolean">true</Set>
    -->
 
    <!--
    This is the upstream server connector. It speaks non-SSL SPDY/2(HTTP) on port 9090.
    -->
    <Call name="addConnector">
        <Arg>
            <New class="org.eclipse.jetty.spdy.http.HTTPSPDYServerConnector">
                <Set name="Port">9090</Set>
                <Set name="defaultAsyncConnectionFactory">
                    <Call name="getAsyncConnectionFactory">
                        <Arg>spdy/2</Arg>
                    </Call>
                </Set>
            </New>
        </Arg>
    </Call>
 
    <!--
    This ProxyEngine translates the incoming SPDY/x(HTTP) request to SPDY/2(HTTP)
    -->
    <New id="spdyProxyEngine" class="org.eclipse.jetty.spdy.proxy.SPDYProxyEngine">
        <Arg>spdy/2</Arg>
        <Arg>
            <New class="org.eclipse.jetty.spdy.SPDYClient$Factory">
                <Call name="start"/>
            </New>
        </Arg>
    </New>
 
    <!--
    The ProxyEngineSelector receives SPDY/x(HTTP) requests from proxy connectors below
    and is configured to process requests for host "localhost".
    Such requests are converted from SPDY/x(HTTP) to SPDY/2(HTTP) by the configured ProxyEngine
    and forwarded to 127.0.0.1:9090, where they are served by the upstream server above.
    -->
    <New id="proxyEngineSelector" class="org.eclipse.jetty.spdy.proxy.ProxyEngineSelector">
        <Call name="putProxyEngine">
            <Arg><Ref id="spdyProxyEngine" /></Arg>
        </Call>
        <Set name="proxyServerInfos">
            <Map>
                <Entry>
                    <Item>localhost</Item>
                    <Item>
                        <New class="org.eclipse.jetty.spdy.proxy.ProxyEngineSelector$ProxyServerInfo">
                            <Arg type="String">spdy/2</Arg>
                            <Arg>127.0.0.1</Arg>
                            <Arg type="int">9090</Arg>
                        </New>
                    </Item>
                </Entry>
            </Map>
        </Set>
    </New>
 
    <!--
    These are the reverse proxy connectors accepting requests from clients.
    They accept non-SSL (on port 8080) and SSL (on port 8443) HTTP,
    SPDY/2(HTTP) and SPDY/3(HTTP).
    Non-SPDY HTTP requests are converted to SPDY internally and passed to the
    ProxyEngine above.
    -->
    <Call name="addConnector">
        <Arg>
            <New class="org.eclipse.jetty.spdy.proxy.HTTPSPDYProxyConnector">
                <Arg><Ref id="proxyEngineSelector" /></Arg>
                <Set name="Port">8080</Set>
            </New>
        </Arg>
    </Call>
    <Call name="addConnector">
        <Arg>
            <New class="org.eclipse.jetty.spdy.proxy.HTTPSPDYProxyConnector">
                <Arg><Ref id="proxyEngineSelector" /></Arg>
                <Arg><Ref id="sslContextFactory" /></Arg>
                <Set name="Port">8443</Set>
            </New>
        </Arg>
    </Call>
 
</Configure>

Let's take this apart:

<!--
This ProxyEngine translates the incoming SPDY/x(HTTP) request to SPDY/2(HTTP)
-->
<New id="spdyProxyEngine" class="org.eclipse.jetty.spdy.proxy.SPDYProxyEngine">
    <Arg>spdy/2</Arg>
    <Arg>
        <New class="org.eclipse.jetty.spdy.SPDYClient$Factory">
            <Call name="start"/>
        </New>
    </Arg>
</New>

This is the configuration of the ProxyEngine. It's a SPDYProxyEngine and will be able to translate to spdy/2 as configured above. If your target host is capable of speaking spdy/3, you just change the first constructor argument to be spdy/3. If you've different target hosts speaking different protocols you simply configure multiple proxy engines and feed them to the ProxyEngineSelector as you'll see below.

<!--
The ProxyEngineSelector receives SPDY/x(HTTP) requests from proxy connectors below
and is configured to process requests for host "localhost".
Such requests are converted from SPDY/x(HTTP) to SPDY/2(HTTP) by the configured ProxyEngine
and forwarded to 127.0.0.1:9090, where they are served by the upstream server above.
-->
<New id="proxyEngineSelector" class="org.eclipse.jetty.spdy.proxy.ProxyEngineSelector">
    <Call name="putProxyEngine">
        <Arg><Ref id="spdyProxyEngine" /></Arg>
    </Call>
    <Set name="proxyServerInfos">
        <Map>
            <Entry>
                <Item>localhost</Item>
                <Item>
                    <New class="org.eclipse.jetty.spdy.proxy.ProxyEngineSelector$ProxyServerInfo">
                        <Arg type="String">spdy/2</Arg>
                        <Arg>127.0.0.1</Arg>
                        <Arg type="int">9090</Arg>
                    </New>
                </Item>
            </Entry>
        </Map>
    </Set>
</New>

This is the ProxyEngineSelector. The ProxyEngineSelector is responsible to keep the configurations for the known target hosts as well as he has to choose the right ProxyEngine for the protocol the target host speaks.

Lets take even smaller parts of the snipplet above to explain them in detail:

<Call name="putProxyEngine">
    <Arg><Ref id="spdyProxyEngine" /></Arg>
</Call>

This adds the SPDYProxyEngine we've configured above to the ProxyEngineSelector. The SPDYProxyEngine was configured to translate to spdy/2. By adding it to the Selector, it now knows how to translate to spdy/2.

<Set name="proxyServerInfos">
   <Map>
       <Entry>
            <Item>localhost</Item>
            <Item>
                 <New class="org.eclipse.jetty.spdy.proxy.ProxyEngineSelector$ProxyServerInfo">
                     <Arg type="String">spdy/2</Arg>
                     <Arg>127.0.0.1</Arg>
                     <Arg type="int">9090</Arg>
                 </New>
            </Item>
       </Entry>
   </Map>
</Set>

You configure target hosts and the protocol you want to communicate with them by adding so called ProxyServerInfos. Configuration is pretty straight forward. Key for the map is the hostname. Protocol, host and port are the configurations for the ProxyServerInfo.

That's pretty much all that is needed to setup a SPDY proxy in jetty.

Request flow with the proxy given above:

Using SPDY Client Applications

The spdy-jetty module provides a SPDY client (that speaks pure SPDY, and not HTTP over SPDY), which you can use in this way:

// Start a SPDYClient factory shared among all SPDYClient instances
SPDYClient.Factory clientFactory = new SPDYClient.Factory();
clientFactory.start();
 
// Create one SPDYClient instance
SPDYClient client = clientFactory.newSPDYClient(SPDY.V2);
 
// Obtain a Session instance to send data to the server that listens on port 8181
Session session = client.connect(new InetSocketAddress("localhost", 8181), null).get(5, TimeUnit.SECONDS);
 
// Sends SYN_STREAM and DATA to the server
Stream stream = session.syn(new SynInfo(false), null).get(5, TimeUnit.SECONDS);
stream.data(new StringDataInfo("Hello, World", true));

To listen to SPDY frames the server sends, you need to pass a StreamFrameListener upon stream creation:

StreamFrameListener streamListener = new StreamFrameListener.Adapter()
{
    public void onReply(Stream stream, ReplyInfo replyInfo)
    {
        // Reply received from server, send DATA to the server
        stream.data(new StringDataInfo("Hello, World", true));
    }
 
    public void onData(Stream stream, DataInfo dataInfo)
    {
        // Data received from server
        String content = dataInfo.asString("UTF-8", true);
        System.err.printf("SPDY content: %s%n", content);
    }
};
 
// Sends SYN_STREAM to the server, adding headers
Headers headers = new Headers();
headers.put("url", "/echo");
Stream stream = session.syn(new SynInfo(headers, false), null).get(5, TimeUnit.SECONDS);

Using SPDY Server Applications

The spdy-jetty module provides a server connector that speaks pure SPDY, and that you can use in conjunction with the SPDY client (see above).

You can start the SPDYServerConnector in this way:

// The application code that handles incoming SYN_STREAMS
ServerSessionFrameListener application = new ServerSessionFrameListener.Adapter()
{
    public StreamFrameListener onSyn(Stream stream, SynInfo synInfo)
    {
        // Reply upon receiving a SYN_STREAM
        stream.reply(new ReplyInfo(false));
 
        // Inspect the headers
        Headers headers = synInfo.getHeaders();
        if ("/echo".equals(headers.get("url").value()))
        {
            return new StreamFrameListener.Adapter()
            {
                public void onData(Stream stream, DataInfo dataInfo)
                {
                    // Upon receiving hello data from the client, echo it back
                    String clientData = dataInfo.asString("UTF-8", true);
                    stream.data(new StringDataInfo("Echo for " + clientData, true));
                }
            };
        }
        return null;
    }
};
 
// Wire up and start the connector
org.eclipse.jetty.server.Server server = new Server();
server.addConnector(new SPDYServerConnector(application));
server.start();

The ServerSessionFrameListener can inspect the incoming SynInfo and provide different execution paths depending on headers and/or data content.

Become familiar with the SPDY API to understand how to use it.