How to use SPDY with Jetty

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:

  • To target a 50% reduction in page load time. Our preliminary results have come close to this target (see below).
  • To minimize deployment complexity. SPDY uses TCP as the underlying transport layer, so requires no changes to existing networking infrastructure.
  • To avoid the need for any changes to content by website authors. The only changes required to support SPDY are in the client user agent and web server applications.
  • To bring together like-minded parties interested in exploring protocols as a way of solving the latency problem. We hope to develop this new protocol in partnership with the open-source community and industry specialists

Some specific technical goals are:

  • To allow many concurrent HTTP requests to run across a single TCP session.
  • To reduce the bandwidth currently used by HTTP by compressing headers and eliminating unnecessary headers.
  • To define a protocol that is easy to implement and server-efficient. We hope to reduce the complexity of HTTP by cutting down on edge cases and defining easily parsed message formats.
  • To make SSL the underlying transport protocol, for better security and compatibility with existing network infrastructure. Although SSL does introduce a latency penalty, we believe that the long-term future of the web depends on a secure network connection. In addition, the use of SSL is necessary to ensure that communication across existing proxies is not broken.
  • To enable the server to initiate communications with the client and push data to the client whenever possible.

Setup maven

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>

NTP TLS extension

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:

  1. Client shows support for this extension
  2. Server responds with this support and includes a list of supported protocols
  3. Client sends the protocol he wants to use, which doesn't have to be one offerede by the server.

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> ...

Wrap HTTP request in SPDY

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.

How to use SPDY with Jetty_第1张图片

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;

Use the SPDY protocol directly

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.

你可能感兴趣的:(How to use SPDY with Jetty)