SPDY is a new protocol proposed by Google as a new protocol for the web. SPDY is compatible with HTTP but tries to reduce web page loading by using compression, mulitplexing and prioritization.To be more precise, the goals for speedy are: (http://dev.chromium.org/spdy/spdy-whitepaper)
The SPDY project defines and implements an application-layer protocol for the web which greatly reduces latency.
The high-level goals for SPDY are:
Some specific technical goals are:
In this article we won't look too much into the technical implementation of this protocol, but we'll show you how you can start using and experimenting with SPDY yourself. For this we'll use Jetty that has a SPDY implementation available in it's latest release (http://wiki.eclipse.org/Jetty/Feature/SPDY).
So let's get started. For this example we'll let Maven handle the dependencies. And we'll use the following POM.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>smartjava.jetty.spdy</groupId> <artifactId>SPDY-Example</artifactId> <version>0.0.1-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.eclipse.jetty.aggregate</groupId> <artifactId>jetty-all-server</artifactId> <version>8.1.2.v20120308</version> <type>jar</type> <scope>compile</scope> <exclusions> <exclusion> <artifactId>mail</artifactId> <groupId>javax.mail</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.eclipse.jetty.spdy</groupId> <artifactId>spdy-jetty</artifactId> <version>8.1.2.v20120308</version> </dependency> <dependency> <groupId>org.eclipse.jetty.spdy</groupId> <artifactId>spdy-core</artifactId> <version>8.1.2.v20120308</version> </dependency> <dependency> <groupId>org.eclipse.jetty.spdy</groupId> <artifactId>spdy-jetty-http</artifactId> <version>8.1.2.v20120308</version> </dependency> <dependency> <groupId>org.eclipse.jetty.npn</groupId> <artifactId>npn-api</artifactId> <version>8.1.2.v20120308</version> <scope>provided</scope> </dependency> </dependencies> </project>
With this POM the correct libraries are loaded, so we can start using the specific SPDY classes in Jetty. Before we can really use SPDY though, we also need to configure Java to use an extension to the TLS protocol: TLS Next Protocol Negotiation or NPN for short. The details of this extension can be found on a googles technote (http://technotes.googlecode.com/git/nextprotoneg.html), but in short it comes down to this problem. What if we want to use a different protocol than HTTP when we're making a connection to a server via TLS. We don't know whether the server supports this protocol, and, since SPDY is focussed on speed, we don't want the added latency of making a round trip. Even though there are a couple of different solutions , most suffer from unpredictability, extra roundtrips or breaks existing proxies (see (http://www.ietf.org/proceedings/80/slides/tls-1.pdf for more info).
The proposed solution by Google is use the TLS's extension mechanism to determine the protocol to be used. This is called "Next Protocol Negotiation" or NPN for short. With this extension the following steps are taken during the TLS handshake:
This results in the following TLS handshake:
Client Server ClientHello (NP extension) --------> ServerHello (NP extension & list of protocols) Certificate* ServerKeyExchange* CertificateRequest* <-------- ServerHelloDone Certificate* ClientKeyExchange CertificateVerify* [ChangeCipherSpec] NextProtocol Finished --------> [ChangeCipherSpec] <-------- Finished Application Data <-------> Application Data
For more information on TLS/SSL handshakes look at my previous article on how to analyze Java SSL errors: http://www.smartjava.org/content/how-analyze-java-ssl-errors.
So we need NPN to quickly determine the protocol we want to use. Since this isn't standard TLS we need to configure Java to use NPN. Standard Java doesn't (yet) support NPN so we can't run SPDY on the standard JVM. To solve this Jetty has created a NPN implementation that can be used together with OpenJDK 7 (see http://wiki.eclipse.org/Jetty/Feature/NPN for more details). You can download this implementation from here: http://repo2.maven.org/maven2/org/mortbay/jetty/npn/npn-boot/ and you have to add it to your boot classpath as such:
java -Xbootclasspath/p:<path_to_npn_boot_jar> ...
Now you can start using SPDY from Jetty. Jetty has support for this feature in two different ways. You can use it to transparently convert from SPDY to HTTP and back again or you can use it to directly talk SPDY. Let's create a simple server configuration that hosts some static content using a SPDY enabled connection. For this we'll use the following Jetty configuration:
import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ResourceHandler; import org.eclipse.jetty.spdy.http.HTTPSPDYServerConnector; import org.eclipse.jetty.util.ssl.SslContextFactory; public class SPDYServerLauncher { public static void main(String[] args) throws Exception { // the server to start Server server = new Server(); // the ssl context to use SslContextFactory sslFactory = new SslContextFactory(); sslFactory.setKeyStorePath("src/main/resources/spdy.keystore"); sslFactory.setKeyStorePassword("secret"); sslFactory.setProtocol("TLSv1"); // simple connector to add to serve content using spdy Connector connector = new HTTPSPDYServerConnector(sslFactory); connector.setPort(8443); // add connector to the server server.addConnector(connector); // add a handler to serve content ContextHandler handler = new ContextHandler(); handler.setContextPath("/content"); handler.setResourceBase("src/main/resources/webcontent"); handler.setHandler(new ResourceHandler()); server.setHandler(handler); server.start(); server.join(); } }
Since Jetty also has a very flexible XML configuration language, you can do the same thing using the following XML configuration.
<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/spdy.keystore</Set> <Set name="keyStorePassword">secret</Set> <Set name="protocol">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> // Use standard XML configuration for the other handlers and other // stuff you want to add </Configure>
As you can see in thia listing we specify a SSL context. This is needed since SPDY works over TLS. When we run this configuration Jetty will start listening on port 8443 for SPDY connections. Not all browser yet support SPDY, I've tested this example using the latest chrome browser. If you browse to https://localhost:8443/dummy.html (a file I created to test with) you'll see the content of this file, just like you requested it using HTTPS. So what is happening here? Let's first look at the SPDY session view that Chrome provides to determine whether we're really using SPDY. If you navigate to the following url: chrome://net-internals/#events&q=type:SPDY_SESSION%20is:active. You'll see something like the following figure.
In this view you can see all the current SPDY sessions. If everything was configured correctly you can also see a SPDY session connected to localhost. An additional check to see if everything is working as intended it to enable debugging of the NPN extension. You can do this by adding the following line to the Java code you use to start up the server:
NextProtoNego.debug = true;
Now that we've got the HTTP over SPDY working, let's look at the other option Jetty provides that allows us to directly send and recieve SPDY messages. For this example we'll just create a client that sends a message to the server every 5 seconds. The server sends responses to a connected client with the number of received messages every second. First we create the server code.
import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.spdy.SPDYServerConnector; import org.eclipse.jetty.spdy.api.DataInfo; import org.eclipse.jetty.spdy.api.ReplyInfo; import org.eclipse.jetty.spdy.api.Stream; import org.eclipse.jetty.spdy.api.StreamFrameListener; import org.eclipse.jetty.spdy.api.StringDataInfo; import org.eclipse.jetty.spdy.api.SynInfo; import org.eclipse.jetty.spdy.api.server.ServerSessionFrameListener; public class SPDYListener { public static void main(String[] args) throws Exception { // Frame listener that handles the communication over speedy ServerSessionFrameListener frameListener = new ServerSessionFrameListener.Adapter() { /** * As soon as we receive a syninfo we return the handler for the stream on * this session */ @Override public StreamFrameListener onSyn(final Stream stream, SynInfo synInfo) { // Send a reply to this message stream.reply(new ReplyInfo(false)); // and start a timer that sends a request to this stream every 5 seconds ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); Runnable periodicTask = new Runnable() { private int i = 0; public void run() { // send a request and don't close the stream stream.data(new StringDataInfo("Data from the server " + i++, false)); } }; executor.scheduleAtFixedRate(periodicTask, 0, 1, TimeUnit.SECONDS); // Next create an adapter to further handle the client input from specific stream. return new StreamFrameListener.Adapter() { /** * We're only interested in the data, not the headers in this * example */ public void onData(Stream stream, DataInfo dataInfo) { String clientData = dataInfo.asString("UTF-8", true); System.out.println("Received the following client data: " + clientData); } }; } }; // Wire up and start the connector org.eclipse.jetty.server.Server server = new Server(); SPDYServerConnector connector = new SPDYServerConnector(frameListener); connector.setPort(8181); server.addConnector(connector); server.start(); server.join(); } }
And the client code looks like this:
import java.net.InetSocketAddress; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.eclipse.jetty.spdy.SPDYClient; import org.eclipse.jetty.spdy.api.DataInfo; import org.eclipse.jetty.spdy.api.SPDY; import org.eclipse.jetty.spdy.api.Session; import org.eclipse.jetty.spdy.api.Stream; import org.eclipse.jetty.spdy.api.StreamFrameListener; import org.eclipse.jetty.spdy.api.StringDataInfo; import org.eclipse.jetty.spdy.api.SynInfo; /** * Calls the server every couple of seconds. * * @author jos */ public class SPDYCaller { public static void main(String[] args) throws Exception { // this listener receives data from the server. It then prints out the data StreamFrameListener streamListener = new StreamFrameListener.Adapter() { public void onData(Stream stream, DataInfo dataInfo) { // Data received from server String content = dataInfo.asString("UTF-8", true); System.out.println("SPDY content: " + content); } }; // Create client SPDYClient.Factory clientFactory = new SPDYClient.Factory(); clientFactory.start(); SPDYClient client = clientFactory.newSPDYClient(SPDY.V2); // Create a session to the server running on localhost port 8181 Session session = client.connect(new InetSocketAddress("localhost", 8181), null).get(5, TimeUnit.SECONDS); // Start a new session, and configure the stream listener final Stream stream = session.syn(new SynInfo(false), streamListener).get(5, TimeUnit.SECONDS); //start a timer that sends a request to this stream every second ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); Runnable periodicTask = new Runnable() { private int i = 0; public void run() { // send a request, don't close the stream stream.data(new StringDataInfo("Data from the client " + i++, false)); } }; executor.scheduleAtFixedRate(periodicTask, 0, 1, TimeUnit.SECONDS); } }
This shows the following output on the client, and on the server:
client: .. SPDY content: Data from the server 3 SPDY content: Data from the server 4 SPDY content: Data from the server 5 SPDY content: Data from the server 6 .. server: ... Received the following client data: Data from the client 2 Received the following client data: Data from the client 3 Received the following client data: Data from the client 4 Received the following client data: Data from the client 5 ...
The code itself should be easy to understand from the inline comments. The only thing to remember, when you want to sent more then one data message over a stream is to make sure the second parameter of the constructor to StringDataInfo is set to false. If set to true, the stream will be closed after the data has been sent.
stream.data(new StringDataInfo("Data from the client " + i++, false));
This just shows a simple use case of how you can use the SPDY protocol directly. More information and examples can be found at the Jetty wiki and the SPDY API documentation.