大多数人已经从谷歌那里听说过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 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;
}
}
在此类的构造函数中,我们设置了基本的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.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;
}
}
此代码几乎是不言自明的。
- 当客户端不支持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 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.
}
}
}
使用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 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);
}
现在剩下的就是一台具有启动所有内容的主服务器的服务器,并且我们可以测试SPDY实现。
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));
}
}
启动服务器,启动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: /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:~$
而且有效!
Wget使用标准的HTTPS,我们得到一个结果,而chrome使用SPDY,并从同一处理程序中呈现结果。
在最后的几天里,我还将发布有关如何为Play Framework 2.0启用SPDY的文章,因为它们的Web服务器也基于Netty。
翻译自: https://www.javacodegeeks.com/2012/05/netty-using-spdy-and-http-transparently.html