Using SPDY and HTTP transparently using Netty
Most people have already heard about SPDY, the protocol, from google, proposed as a replacement for the aging HTTP protocol. Webservers are browsers are slowly implementing this protocol and support is growing. In a recent article I already wrote about how SPDY works and how you can enable SPDY support in Jetty. Since a couple of months Netty (originally from JBoss) also has support for SPDY. Since Netty is often used for high performant protocol servers, SPDY is a logical fit. In this article I’ll show you how you can create a basic Netty based server that does protocol negotiation between SPDY and HTTP. It used the example HTTPRequestHandler from the Netty snoop example to consume and produce some HTTP content.
To get everything working we’ll need to do the following things:
- Enable NPN in Java to determine protocol to use.
- Determine, based on the negotiated protocol, whether to use HTTP or SPDY.
- Make sure the correct SPDY headers are sent back with HTTP.
SPDY uses an TLS extension to determine the protocol to use in communication. This is called NPN. I wrote a more complete explanation and shown the messages involved in the article on how to use SPDY on Jetty, so for more info look at that article. Basically what this extension does is that during the TLS exchange a server and client also exchange the transport level protocols they support. In the case of SPDY a server could support both the SPDY protocol and the HTTP protocol. A client implementation can then determine which protocol to use. Since this isn’t something which is available in the standard Java implementation, we need to extend the Java TLS functionality with NPN.
Enable NPN support in Java
So far I found two options that can be used to add NPN support in Java. One is from https://github.com/benmmurphy/ssl_npn who also has a basic SPDY/Netty example in his repo where he uses his own implementation. The other option, and the one I’ll be using, is the NPN support provided by Jetty. Jetty provides an easy to use API that you can use to add NPN support to your Java SSL contexts. Once again, in the in the article on Jetty you can find more info on this. To set up NPN for Netty, we need to do the following:
- Add NPN lib to bootpath
- Connect the SSL context to the NPN Api
Add NPN lib to boothpath
First things first. Download the NPN boot jar from http://repo2.maven.org/maven2/org/mortbay/jetty/npn/npn-boot/8.1.2.v20120308/ and make sure that when you run the server you start it like this:
java -Xbootclasspath/p:<path_to_npn_boot_jar>
With this piece of code, Java SSL has support for NPN. We still, however, need access to the results from this negotiation. We need to know whether we’re using HTTP or SPDY, since that determines how we process the received data. For this Jetty provides an API. For this and for the required Netty libraries, we add the following dependencies, since I’m using maven, to the pom.
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty</artifactId>
<version>3.4.1.Final</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.npn</groupId>
<artifactId>npn-api</artifactId>
<version>8.1.2.v20120308</version>
</dependency>
Connect the SSL context to the NPN API
Now that we’ve got NPN enabled and the correct API added to the project, we can configure the Netty SSL handler. Configuring handlers in Netty is done in a PipelineFactory. For our server I created the following PipelineFactory:
package smartjava.netty.spdy;
import static org.jboss.netty.channel.Channels.pipeline;
import java.io.FileInputStream;
import java.security.KeyStore;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import org.eclipse.jetty.npn.NextProtoNego;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.handler.ssl.SslHandler;
public class SPDYPipelineFactory implements ChannelPipelineFactory {
private SSLContext context;
public SPDYPipelineFactory() {
try {
KeyStore keystore = KeyStore.getInstance("JKS");
keystore.load(new FileInputStream("src/main/resources/server.jks"),
"secret".toCharArray());
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(keystore, "secret".toCharArray());
context = SSLContext.getInstance("TLS");
context.init(kmf.getKeyManagers(), null, null);
} catch (Exception e) {
e.printStackTrace();
}
}
public ChannelPipeline getPipeline() throws Exception {
// Create a default pipeline implementation.
ChannelPipeline pipeline = pipeline();
// Uncomment the following line if you want HTTPS
SSLEngine engine = context.createSSLEngine();
engine.setUseClientMode(false);
NextProtoNego.put(engine, new SimpleServerProvider());
NextProtoNego.debug = true;
pipeline.addLast("ssl", new SslHandler(engine));
pipeline.addLast("pipeLineSelector", new HttpOrSpdyHandler());
return pipeline;
}
}
In the constructor from this class we setup a basic SSL context. The keystore and key we use I created using the java keytool, this is normal SSL configuration. When we receive a request, the getPipeline operation is called to determine how to handle the request. Here we use the NextProtoNego class, provided by Jetty-NPN-API, to connect our SSL connection to the NPN implementation. In this operation we pass a provider that is used as callback and configuration for our server. We also set NextProtoNego.debug to true. This prints out some debugging information that makes, well, debugging easier. The code for the SimpleServerProvider is very simple:
public class SimpleServerProvider implements ServerProvider {
private String selectedProtocol = null;
public void unsupported() {
//if unsupported, default to http/1.1
selectedProtocol = "http/1.1";
}
public List<String> protocols() {
return Arrays.asList("spdy/2","http/1.1");
}
public void protocolSelected(String protocol) {
selectedProtocol = protocol;
}
public String getSelectedProtocol() {
return selectedProtocol;
}
}
This code is pretty much self-explanatory.
- The unsupported operation is called when the client doesn't support NPN. In that case we default to HTTP.
- The protocols() operation returns the protocols the server supports
- The protocolSelected operation is called when a protocol has been negotiated by the server and the client
The getSelectedProtocol is a method we will use to get the selected protocol from a different handler in the Netty pipeline.
Determine, based on the negotiated protocol, whether to use HTTP or SPDY
Now we need to configure Netty in such a way that it runs a specific pipeline for HTTPS request and a pipeline for SPDY requests. For this let’s look back at a small part of the pipelinefactory.
pipeline.addLast("ssl", new SslHandler(engine));
pipeline.addLast("pipeLineSelector", new HttpOrSpdyHandler());
The first part of this pipeline is the SslHandler that is configured with NPN support. The next handler that will be called is the HttpOrSpdyHandler. This handler determines, based on the protocol, which pipeline to use. The code for this handler is listed next:
public class HttpOrSpdyHandler implements ChannelUpstreamHandler {
public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e)
throws Exception {
// determine protocol type
SslHandler handler = ctx.getPipeline().get(SslHandler.class);
SimpleServerProvider provider = (SimpleServerProvider) NextProtoNego.get(handler.getEngine());
if ("spdy/2".equals(provider.getSelectedProtocol())) {
ChannelPipeline pipeline = ctx.getPipeline();
pipeline.addLast("decoder", new SpdyFrameDecoder());
pipeline.addLast("spdy_encoder", new SpdyFrameEncoder());
pipeline.addLast("spdy_session_handler", new SpdySessionHandler(true));
pipeline.addLast("spdy_http_encoder", new SpdyHttpEncoder());
// Max size of SPDY messages set to 1MB
pipeline.addLast("spdy_http_decoder", new SpdyHttpDecoder(1024*1024));
pipeline.addLast("handler", new HttpRequestHandler());
// remove this handler, and process the requests as spdy
pipeline.remove(this);
ctx.sendUpstream(e);
} else if ("http/1.1".equals(provider.getSelectedProtocol())) {
ChannelPipeline pipeline = ctx.getPipeline();
pipeline.addLast("decoder", new HttpRequestDecoder());
pipeline.addLast("http_encoder", new HttpResponseEncoder());
pipeline.addLast("handler", new HttpRequestHandler());
// remove this handler, and process the requests as http
pipeline.remove(this);
ctx.sendUpstream(e);
} else {
// we're still in protocol negotiation, no need for any handlers
// at this point.
}
}
}
Using the NPN API and our current SSL context, we retrieve the SimpleServerProvider we added earlier. We check whether the selectedProtocol has been set, and if so, we setup a chain for processing. We handle three options in this class:
- There is no protocol: It's possible that no protocol has been negotiated yet. In that case we don't do anything special, and just process it normally.
- There is a http protocol: We set up a handler chain to handle HTTP requests.
- There is a spdy protocol: We set up a handler chain to handle SPDY requests.
With this chain all the messages we receive eventually by the HttpRequestHandler are HTTP Requests. We can process this HTTP request normally, and return a HTTP response. The various pipeline configurations will handle all this correctly.
Make sure the correct SPDY headers are sent back with HTTP
The final step we need to do, is this test. We’ll test this with the latest version of Chrome to test whether SPDY is working, and we’ll use wget to test the normal http requests. I mentioned that the HttpRequestHandler, the last handler in the chain, does our HTTP processing. I’ve used the http://netty.io/docs/stable/xref/org/jboss/netty/example/http/snoop/HttpSnoopServerHandler.html as the HTTPRequestHandler since that one nicely returns information about the HTTP request, without me having to do anything. If you run this without alteration, you do run into an issue. To correlate the HTTP response to the correct SPDY session, we need to copy a header from the incoming request to the response: the “X-SPDY-Stream-ID” header. I’ve added the following to the HttpSnoopServerHandler to make sure these headers are copied (should really have done this in a seperate handler).
private final static String SPDY_STREAM_ID = "X-SPDY-Stream-ID";
private final static String SPDY_STREAM_PRIO = "X-SPDY-Stream-Priority";
// in the writeResponse method add
if (request.containsHeader(SPDY_STREAM_ID)) {
response.addHeader(SPDY_STREAM_ID,request.getHeader(SPDY_STREAM_ID));
// optional header for prio
response.addHeader(SPDY_STREAM_PRIO,0);
}
Now all that is left is a server with a main to start everything, and we can test our SPDY implementation.
public class SPDYServer {
public static void main(String[] args) {
// bootstrap is used to configure and setup the server
ServerBootstrap bootstrap = new ServerBootstrap(
new NioServerSocketChannelFactory(
Executors.newCachedThreadPool(),
Executors.newCachedThreadPool()));
bootstrap.setPipelineFactory(new SPDYPipelineFactory());
bootstrap.bind(new InetSocketAddress(8443));
}
}
Start up the server, fire up Chrome and let’s see whether everything is working. Open the https://localhost:8443/thisIsATest url and you should get a result that looks something like this:
In the output of the server, you can see some NPN debug logging:
[S] NPN received for 68ce4f39[SSLEngine[hostname=null port=-1] SSL_NULL_WITH_NULL_NULL]
[S] NPN protocols [spdy/2, http/1.1] sent to client for 68ce4f39[SSLEngine[hostname=null port=-1] SSL_NULL_WITH_NULL_NULL]
[S] NPN received for 4b24e48f[SSLEngine[hostname=null port=-1] SSL_NULL_WITH_NULL_NULL]
[S] NPN protocols [spdy/2, http/1.1] sent to client for 4b24e48f[SSLEngine[hostname=null port=-1] SSL_NULL_WITH_NULL_NULL]
[S] NPN selected 'spdy/2' for 4b24e48f[SSLEngine[hostname=null port=-1] SSL_NULL_WITH_NULL_NULL]
An extra check is looking at the open SPDY sessions in chrome browser by using the following url: chrome://net-internals/#spdy
Now lets check whether plain old HTTP is still working. From a command line do the following:
jos@Joss-MacBook-Pro.local:~$ wget --no-check-certificate https://localhost:8443/thisIsATest
--2012-04-27 16:29:09-- https://localhost:8443/thisIsATest
Resolving localhost... ::1, 127.0.0.1, fe80::1
Connecting to localhost|::1|:8443... connected.
WARNING: cannot verify localhost's certificate, issued by `/C=NL/ST=NB/L=Waalwijk/O=smartjava/OU=smartjava/CN=localhost':
Self-signed certificate encountered.
HTTP request sent, awaiting response... 200 OK
Length: 285 [text/plain]
Saving to: `thisIsATest'
100%[==================================================================================>] 285 --.-K/s in 0s
2012-04-27 16:29:09 (136 MB/s) - `thisIsATest' saved [285/285]
jos@Joss-MacBook-Pro.local:~$ cat thisIsATest
WELCOME TO THE WILD WILD WEB SERVER
===================================
VERSION: HTTP/1.1
HOSTNAME: localhost:8443
REQUEST_URI: /thisIsATest
HEADER: User-Agent = Wget/1.13.4 (darwin11.2.0)
HEADER: Accept = */*
HEADER: Host = localhost:8443
HEADER: Connection = Keep-Alive
jos@Joss-MacBook-Pro.local:~$
And it works! Wget uses standard HTTPS, and we get a result, and chrome uses SPDY and presents the result from the same handler. In the net couple of days, I’ll also post on article on how you can enable SPDY for the Play Framework 2.0, since their webserver is also based on Netty.