大多數人已經從谷歌那里聽說過SPDY,該協議被提議作為老化的HTTP協議的替代品。 Web服務器是瀏覽器正在緩慢地實現該協議,并且支持正在增長。 在最近的文章中,我已經寫過SPDY的工作方式以及如何在Jetty中啟用SPDY支持。 由于Netty(最初來自JBoss)幾個月以來也支持SPDY 。由于Netty通常用于高性能協議服務器,因此SPDY是合乎邏輯的。 在本文中,我將向您展示如何創建一個基于Netty的基本服務器,該服務器在SPDY和HTTP之間進行協議協商。 它使用Netty snoop示例中的示例HTTPRequestHandler來消耗并產生一些HTTP內容。
為了使一切正常,我們需要做以下事情:
- 在Java中啟用NPN以確定要使用的協議。
- 根據協商的協議,確定使用HTTP還是SPDY。
- 確保使用HTTP發送回正確的SPDY標頭。
SPDY使用TLS擴展來確定要在通信中使用的協議。 這稱為NPN。 我寫了一個更完整的說明,并顯示了文章中有關如何在Jetty上使用SPDY的消息,因此,有關該文章的更多信息,請參見。 基本上,此擴展的作用是在TLS交換期間,服務器和客戶端也會交換它們支持的傳輸級別協議。 對于SPDY,服務器可以同時支持SPDY協議和HTTP協議。 然后,客戶端實現可以確定要使用的協議。
由于這不是標準Java實現中可用的功能,因此我們需要使用NPN擴展Java TLS功能。
在Java中啟用NPN支持
到目前為止,我發現了兩個可以用來在Java中添加NPN支持的選項。 其中一個來自https://github.com/benmmurphy/ssl_npn ,他的回購中也有一個基本的SPDY / Netty示例,他使用自己的實現。 我將要使用的另一個選項是Jetty提供的NPN支持。 Jetty提供了一個易于使用的API,可用于將NPN支持添加到Java SSL上下文中。 再次,在關于碼頭的文章中,您可以找到關于此的更多信息。 要為Netty設置NPN,我們需要執行以下操作:
- 將NPN lib添加到引導路徑
- 將SSL上下文連接到NPN Api
將NPN庫添加到boothpath
首先是第一件事。 從http://repo2.maven.org/maven2/org/mortbay/jetty/npn/npn-boot/8.1.2.v2012下載NPN引導jar,并確保在運行服務器時像這樣啟動它:
java -Xbootclasspath/p:<path_to_npn_boot_jar>
通過這段代碼,Java SSL支持NPN。 但是,我們仍然需要訪問此協商的結果。 我們需要知道我們使用的是HTTP還是SPDY,因為這決定了我們如何處理接收到的數據。 為此,Jetty提供了一個API。 為此和所需的Netty庫,我們將以下依賴項添加到pom中,因為我使用的是maven。
<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>
將SSL上下文連接到NPN API
現在,我們已啟用NPN并將正確的API添加到項目中,我們可以配置Netty SSL處理程序。 在Netty中配置處理程序是在PipelineFactory中完成的。 對于我們的服務器,我創建了以下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 HTTPSSSLEngine 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;}
}
在此類的構造函數中,我們設置了基本的SSL上下文。 我們使用的密鑰庫和密鑰是我使用java keytool創建的,這是常規的SSL配置。 收到請求后,將調用getPipeline操作來確定如何處理該請求。 在這里,我們使用Jetty-NPN-API提供的NextProtoNego類將SSL連接連接到NPN實現。 在此操作中,我們傳遞一個提供程序,該提供程序用作服務器的回調和配置。 我們還將NextProtoNego.debug設置為true。 這會打印出一些調試信息,從而使調試更加容易。 SimpleServerProvider的代碼非常簡單:
public class SimpleServerProvider implements ServerProvider {private String selectedProtocol = null;public void unsupported() {//if unsupported, default to http/1.1selectedProtocol = "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;}
}
此代碼幾乎是不言自明的。
- 當客戶端不支持NPN時,將調用不受支持的操作。 在這種情況下,我們默認為HTTP。
- protocol()操作返回服務器支持的協議
- 服務器和客戶端協商協議后,將調用protocolSelected操作
getSelectedProtocol是一種用于從Netty管道中的其他處理程序獲取所選協議的方法。
根據協商的協議確定使用HTTP還是SPDY
現在,我們需要配置Netty,使其為HTTPS請求和SPDY請求運行特定的管道。 為此,讓我們回顧一下管道工廠的一小部分。
pipeline.addLast("ssl", new SslHandler(engine));
pipeline.addLast("pipeLineSelector", new HttpOrSpdyHandler());
該管道的第一部分是配置了NPN支持的SslHandler。 下一個將被調用的處理程序是HttpOrSpdyHandler。 該處理程序根據協議確定要使用的管道。 接下來列出此處理程序的代碼:
public class HttpOrSpdyHandler implements ChannelUpstreamHandler {public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e)throws Exception {// determine protocol typeSslHandler 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 1MBpipeline.addLast("spdy_http_decoder", new SpdyHttpDecoder(1024*1024)); pipeline.addLast("handler", new HttpRequestHandler());// remove this handler, and process the requests as spdypipeline.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 httppipeline.remove(this);ctx.sendUpstream(e);} else {// we're still in protocol negotiation, no need for any handlers// at this point.}}
}
使用NPN API和當前的SSL上下文,我們可以檢索之前添加的SimpleServerProvider。 我們檢查是否已設置selectedProtocol,如果已設置,則設置一條鏈進行處理。 我們在此類中處理三個選項:
- 沒有協議 :可能尚未協商任何協議。 在那種情況下,我們沒有做任何特別的事情,只需正常處理即可。
- 有一個http協議 :我們建立了一個處理程序鏈來處理HTTP請求。
- 有一個spdy協議 :我們建立了一個處理程序鏈來處理SPDY請求。
有了這個鏈,我們最終由HttpRequestHandler接收到的所有消息都是HTTP請求。 我們可以正常處理此HTTP請求,然后返回HTTP響應。 各種管道配置將正確處理所有這些問題。
確保使用HTTP發送回正確的SPDY標頭
我們需要做的最后一步是測試。 我們將使用最新版本的Chrome進行測試,以測試SPDY是否正常運行,并使用wget測試正常的http請求。 我提到了鏈中最后一個處理程序HttpRequestHandler進行我們的HTTP處理。 我已經使用http://netty.io/docs/stable/xref/org/jboss/netty/example/http/snoop/Http…作為HTTPRequestHandler,因為那很好地返回了有關HTTP請求的信息,而我卻沒有做任何事。 如果不做任何更改就運行它,則會遇到問題。 為了將HTTP響應與正確的SPDY會話相關聯,我們需要將傳入請求中的標頭復制到響應中:“ X-SPDY-Stream-ID”標頭。 我將以下內容添加到HttpSnoopServerHandler中,以確保復制了這些標頭(實際上應該在單獨的處理程序中完成此操作)。
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 addif (request.containsHeader(SPDY_STREAM_ID)) {response.addHeader(SPDY_STREAM_ID,request.getHeader(SPDY_STREAM_ID));// optional header for prioresponse.addHeader(SPDY_STREAM_PRIO,0);}
現在剩下的就是一臺具有啟動所有內容的主服務器的服務器,并且我們可以測試SPDY實現。
public class SPDYServer {public static void main(String[] args) {// bootstrap is used to configure and setup the serverServerBootstrap bootstrap = new ServerBootstrap(new NioServerSocketChannelFactory(Executors.newCachedThreadPool(),Executors.newCachedThreadPool()));bootstrap.setPipelineFactory(new SPDYPipelineFactory());bootstrap.bind(new InetSocketAddress(8443));}
}
啟動服務器,啟動Chrome,然后查看是否一切正常。 打開https:// localhost:8443 / thisIsATest網址,您應該得到如下所示的結果:

在服務器的輸出中,您可以看到一些NPN調試日志記錄:
[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]
額外的檢查是使用以下網址查看chrome瀏覽器中打開的SPDY會話:chrome:// net-internals /#spdy

現在讓我們檢查普通的舊HTTP是否仍在工作。 從命令行執行以下操作:
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
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: /thisIsATestHEADER: User-Agent = Wget/1.13.4 (darwin11.2.0)
HEADER: Accept = */*
HEADER: Host = localhost:8443
HEADER: Connection = Keep-Alivejos@Joss-MacBook-Pro.local:~$
而且有效! Wget使用標準的HTTPS,我們得到一個結果,而chrome使用SPDY,并從同一處理程序中呈現結果。 在最后的幾天里,我還將發布有關如何為Play Framework 2.0啟用SPDY的文章,因為它們的Web服務器也基于Netty。
參考:來自Smart Java博客的JCG合作伙伴 Jos Dirksen 透明地使用Netty使用SPDY和HTTP 。
翻譯自: https://www.javacodegeeks.com/2012/05/netty-using-spdy-and-http-transparently.html