CH12-SPDY

本章介绍

  • SPDY 总览
  • ChannelHandler, Decoder, 和 Encoder
  • 引导一个基于 Netty 的应用
  • 测试 SPDY/HTTPS

SPDY(读作“speedy”)是一个谷歌开发的开放的网络协议,主要运用于 web 内容传输。SPDY 操纵 HTTP 流量,目标是减少 web 页面加载延迟,提高网络安全。SPDY 达到通过压缩、多路复用和优先级来减少延迟,虽然这取决于网络和网站部署条件的组合。“SPDY”这个名字是谷歌的一个商标,不是一个首字母缩写。(摘自http://en.wikipedia.org/wiki/SPDY

Netty 的包支持 SPDY。正如我们已经看到在其他情况下,这种支持将使您能够使用 SPDY 无需担心所有的内部细节。在这一章里,我们将提供你需要的所有信息关于在您的应用程序中启用 SPDY ,并同时支持 SPDY 和 HTTP。

SPDY 背景

Google 开发 SPDY 是为了解决扩展性的问题。主要的任务是加载内容的速度更快,做了如下工作:

  • 每个头都是压缩的,消息体的压缩是可选的,因为它可能对代理服务器有问题
  • 所有的加密都使用 TLS 每个连接多个转移是可能的 数据集可以单独设置优先级,使关键内容先被转移

下表是与 HTTP 的对比

Table 12.1 Comparison of SPDY and HTTP

浏览器HTTP 1.1SPDY
加密Not by defaultYes
Header 压缩NoYes
全双工NoYes
Server pushNoYes
优先级NoYes

一些使用场合和指标显示,可以 SPDY 让页面加载速度比H TTP 原先快50%。

现在 SPDY 的协议草案规范是 1, 2 和 3, Netty 支持 2和3,主要考虑到这个是被广大浏览器所支持的版本。 现在很多浏览器都支持 SPDY,见下表:

Table 12.2 Browsers that support SPDY

浏览器版本
Chrome19+
Chromium19+
Mozilla Firefox11+ (从 13 起默认开启)
Opera12.10+

示例程序

编写一个简单的服务器应用程序,向您展示如何将 SPDY 集成到你的下一个应用程序。它只会提供一些静态内容回客户机。这些内容将取决于所使用协议是 HTTPS 或 SPDY 。如果 服务器提供 SPDY 是可以被客户端浏览器所支持,则自动切换到 SPDY 。图12.1显示了应用程序的流程

NAME

对于这个应用程序只编写一个服务器组件处理 HTTPS 和 SPDY。为了演示其功能使用两个不同的 web 浏览器,一个支持 SPDY,另外一个不支持。

实现

SPDY 使用 TLS 的扩展称为 Next Protocol Negotiation (NPN)。在Java 中,我们有两种不同的方式选择的基于 NPN 的协议:

  • 使用 ssl_npn,NPN 的开源 SSL 提供者。
  • 使用通过 Jetty 的 NPN 扩展库。

在这个例子中使用 Jetty 库。如果你想使用 ssl_npn,请参阅https://github.com/benmmurphy/ssl_npn项目文档

Jetty NPN 库

Jetty NPN 库是一个外部的库,而不是 Netty 的本身的一部分。它用于处理 Next Protocol Negotiation, 这是用于检测客户端是否支持 SPDY。

集成 Next Protocol Negotiation

Jetty 库提供了一个接口称为 ServerProvider,确定所使用的协议和选择哪个钩子。这个的实现可能取决于不同版本的 HTTP 和 SPDY 版本的支持。下面的清单显示了将用于我们的示例应用程序的实现。

Listing 12.1 Implementation of ServerProvider

public class DefaultServerProvider implements NextProtoNego.ServerProvider {
    private static final List<String> PROTOCOLS =
            Collections.unmodifiableList(Arrays.asList("spdy/2", "spdy/3", "http/1.1"));  //1

    private String protocol;

    @Override
    public void unsupported() {
        protocol = "http/1.1";   //2
    }

    @Override
    public List<String> protocols() {
        return PROTOCOLS;   //3
    }

    @Override
    public void protocolSelected(String protocol) {
        this.protocol = protocol;  //4
    }

    public String getSelectedProtocol() {
        return protocol;  //5
    }
}
  1. 定义所有的 ServerProvider 实现的协议
  2. 设置如果 SPDY 协议失败了就转到 http/1.1
  3. 返回支持的协议的列表
  4. 设置选择的协议
  5. 返回选择的协议

在 ServerProvider 的实现,我们支持下面的3种协议:

  • SPDY 2
  • SPDY 3
  • HTTP 1.1

如果客户端不支持 SPDY ,则默认使用 HTTP 1.1

实现各种 ChannelHandler

第一个 ChannelInboundHandler 是用于不支持 SPDY 的情况下处理客户端 HTTP 请求,如果不支持 SPDY 就回滚使用默认的 HTTP 协议。

清单12.2显示了HTTP流量的处理程序。

Listing 12.2 Implementation that handles HTTP

@ChannelHandler.Sharable
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
    @Override
    public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { //1
        if (HttpHeaders.is100ContinueExpected(request)) {
            send100Continue(ctx); //2
        }

        FullHttpResponse response = new DefaultFullHttpResponse(request.getProtocolVersion(), HttpResponseStatus.OK); //3
        response.content().writeBytes(getContent().getBytes(CharsetUtil.UTF_8));  //4
        response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "text/plain; charset=UTF-8");  //5

        boolean keepAlive = HttpHeaders.isKeepAlive(request);

        if (keepAlive) {  //6
            response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, response.content().readableBytes());
            response.headers().set(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE);
        }
        ChannelFuture future = ctx.writeAndFlush(response);  //7

        if (!keepAlive) {
            future.addListener (ChannelFutureListener.CLOSE); //8
        }
    }

    protected String getContent() {  //9
        return "This content is transmitted via HTTP\r\n";
    }

    private static void send100Continue(ChannelHandlerContext ctx) {  //10
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
        ctx.writeAndFlush(response);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {  //11
        cause.printStackTrace();
        ctx.close();
    }
}
  1. 重写 channelRead0() ,可以被所有的接收到的 FullHttpRequest 调用
  2. 检查如果接下来的响应是预期的,就写入
  3. 新建 FullHttpResponse,用于对请求的响应
  4. 生成响应的内容,将它写入 payload
  5. 设置头文件,这样客户端就能知道如何与 响应的 payload 交互
  6. 检查请求设置是否启用了 keepalive;如果是这样,将标题设置为符合HTTP RFC
  7. 写响应给客户端,并获取到 Future 的引用,用于写完成时,获取到通知
  8. 如果响应不是 keepalive,在写完成时关闭连接
  9. 返回内容作为响应的 payload
  10. Helper 方法生成了100 持续的响应,并写回给客户端
  11. 若执行阶段抛出异常,则关闭管道

这就是 Netty 处理标准的 HTTP 。你可能需要分别处理特定 URI ,应对不同的状态代码,这取决于资源存在与否,但基本的概念将是相同的。

我们的下一个任务将会提供一个组件来支持 SPDY 作为首选协议。 Netty 提供了简单的处理 SPDY 方法。这些将使您能够重用FullHttpRequest 和 FullHttpResponse 消息,通过 SPDY 透明地接收和发送他们。

HttpRequestHandler 虽然是我们可以重用代码,我们将改变我们的内容写回客户端只是强调协议变化;通常您会返回相同的内容。下面的清单展示了实现,它扩展了先前的 HttpRequestHandler。

Listing 12.3 Implementation that handles SPDY

@ChannelHandler.Sharable
public class SpdyRequestHandler extends HttpRequestHandler {   //1
    @Override
    protected String getContent() {
        return "This content is transmitted via SPDY\r\n";  //2
    }
}
  1. 继承 HttpRequestHandler 这样就能共享相同的逻辑
  2. 生产内容写到 payload。这个重写了 HttpRequestHandler 的 getContent() 的实现

SpdyRequestHandler 继承自 HttpRequestHandler,但区别是:写入的内容的 payload 状态的响应是在 SPDY 写的。

我们可以实现两个处理程序逻辑,将选择一个相匹配的协议。然而添加以前写过的处理程序到 ChannelPipeline 是不够的;正确的编解码器还需要补充。它的责任是检测传输字节数,然后使用 FullHttpResponse 和 FullHttpRequest 的抽象进行工作。

Netty 的附带一个基类,完全能做这个。所有您需要做的是实现逻辑选择协议和选择适当的处理程序。

清单12.4显示了实现,它使用 Netty 的提供的抽象基类。

public class DefaultSpdyOrHttpChooser extends SpdyOrHttpChooser {

    public DefaultSpdyOrHttpChooser(int maxSpdyContentLength, int maxHttpContentLength) {
        super(maxSpdyContentLength, maxHttpContentLength);
    }

    @Override
    protected SelectedProtocol getProtocol(SSLEngine engine) {
        DefaultServerProvider provider = (DefaultServerProvider) NextProtoNego.get(engine);  //1
        String protocol = provider.getSelectedProtocol();
        if (protocol == null) {
            return SelectedProtocol.UNKNOWN; //2
        }
        switch (protocol) {
            case "spdy/2":
                return SelectedProtocol.SPDY_2; //3
            case "spdy/3.1":
                return SelectedProtocol.SPDY_3_1; //4
            case "http/1.1":
                return SelectedProtocol.HTTP_1_1; //5
            default:
                return SelectedProtocol.UNKNOWN; //6
        }
    }

    @Override
    protected ChannelInboundHandler createHttpRequestHandlerForHttp() {
        return new HttpRequestHandler(); //7
    }

    @Override
    protected ChannelInboundHandler createHttpRequestHandlerForSpdy() {
        return new SpdyRequestHandler();  //8
    }
}
  1. 使用 NextProtoNego 用于获取 DefaultServerProvider 的引用, 用于 SSLEngine
  2. 协议不能被检测到。一旦字节已经准备好读,检测过程将重新开始。
  3. SPDY 2 被检测到
  4. SPDY 3 被检测到
  5. HTTP 1.1 被检测到
  6. 未知协议被检测到
  7. 将会被调用给 FullHttpRequest 消息添加处理器。该方法只会在不支持 SPDY 时调用,那么将会使用 HTTPS
  8. 将会被调用给 FullHttpRequest 消息添加处理器。该方法在支持 SPDY 时调用

该实现要注意检测正确的协议并设置 ChannelPipeline 。它可以处理SPDY 版本 2、3 和 HTTP 1.1,但可以很容易地修改 SPDY 支持额外的版本。

设置 ChannelPipeline

通过实现 ChannelInitializer 将所有的处理器连接到一起。正如你所了解的那样,这将设置 ChannelPipeline 并添加所有需要的ChannelHandler 的。

SPDY 需要两个 ChannelHandler:

  • SslHandler,用于检测 SPDY 是否通过 TLS 扩展
  • DefaultSpdyOrHttpChooser,用于当协议被检测到时,添加正确的 ChannelHandler 到 ChannelPipeline

除了添加 ChannelHandler 到 ChannelPipeline, ChannelInitializer 还有另一个责任;即,分配之前创建的 DefaultServerProvider 通过 SslHandler 到 SslEngine 。这将通过Jetty NPN 类库的 NextProtoNego helper 类实现

Listing 12.5 Implementation that handles SPDY

public class SpdyChannelInitializer extends ChannelInitializer<SocketChannel> {  //1
    private final SslContext context;

    public SpdyChannelInitializer(SslContext context) //2 {
        this.context = context;
    }

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        SSLEngine engine = context.newEngine(ch.alloc());  //3
        engine.setUseClientMode(false);  //4

        NextProtoNego.put(engine, new DefaultServerProvider());  //5
        NextProtoNego.debug = true;

        pipeline.addLast("sslHandler", new SslHandler(engine));  //6
        pipeline.addLast("chooser", new DefaultSpdyOrHttpChooser(1024 * 1024, 1024 * 1024));
    }
}
  1. 继承 ChannelInitializer 是一个简单的开始
  2. 传递 SSLContext 用于创建 SSLEngine
  3. 新建 SSLEngine,用于新的管道和连接
  4. 配置 SSLEngine 用于非客户端使用
  5. 通过 NextProtoNego helper 类绑定 DefaultServerProvider 到 SSLEngine
  6. 添加 SslHandler 到 ChannelPipeline 这将会在协议检测到时保存在 ChannelPipeline
  7. 添加 DefaultSpyOrHttpChooser 到 ChannelPipeline 。这个实现将会监测协议。添加正确的 ChannelHandler 到 ChannelPipeline,并且移除自身

实际的 ChannelPipeline 设置将会在 DefaultSpdyOrHttpChooser 实现之后完成,因为在这一点上它可能只需要知道客户端是否支持 SPDY

为了说明这一点,让我们总结一下,看看不同 ChannelPipeline 状态期间与客户连接的生命周期。图12.2显示了在 Channel 初始化后的 ChannelPipeline。

NAME

Figure 12.2 ChannelPipeline after connection

现在,这取决于客户端是否支持 SPDY,管道将修改DefaultSpdyOrHttpChooser 来处理协议。之后并不需要添加所需的 ChannelHandler 到 ChannelPipeline,所以删除本身。这个逻辑是由抽象 SpdyOrHttpChooser 类封装,DefaultSpdyOrHttpChooser 父类。

图12.3显示了支持 SPDY 的 ChannelPipeline 用于连接客户端的配置。

NAME

Figure 12.3 ChannelPipeline if SPDY is supported

每个 ChannelHandler 负责的一小部分工作,这个就是对基于 Netty 构造的应用程序最完美的诠释。每个 ChannelHandler 的职责如表12.3所示。

Table 12.3 Responsibilities of the ChannelHandlers when SPDY is used

名称职责
SslHandler加解密两端交换的数据
SpdyFrameDecoder从接收到的 SPDY 帧中解码字节
SpdyFrameEncoder编码 SPDY 帧到字节
SpdySessionHandler处理 SPDY session
SpdyHttpEncoder编码 HTTP 消息到 SPDY 帧
SpdyHttpDecoder解码 SDPY 帧到 HTTP 消息
SpdyHttpResponseStreamIdHandler处理基于 SPDY ID 请求和响应之间的映射关系
SpdyRequestHandler处理 FullHttpRequest, 用于从 SPDY 帧中解码,因此允许 SPDY 透明传输使用

当协议是 HTTP(s) 时,ChannelPipeline 看起来相当不同,如图13.4所示。

NAME

Figure 12.3 ChannelPipeline if SPDY is not supported

和之前一样,每个 ChannelHandler 都有职责,定义在表12.4

Table 12.4 Responsibilities of the ChannelHandlers when HTTP is used

名称职责
SslHandler加解密两端交换的数据
HttpRequestDecoder从接收到的 HTTP 请求中解码字节
HttpResponseEncoder编码 HTTP 响应到字节

HttpObjectAggregator 处理 SPDY session HttpRequestHandler | 解码时处理 FullHttpRequest

所有东西组合在一起

所有的 ChannelHandler 实现已经准备好,现在组合成一个 SpdyServer

Listing 12.6 SpdyServer implementation

public class SpdyServer {

    private final NioEventLoopGroup group = new NioEventLoopGroup();  //1
    private final SslContext context;
    private Channel channel;

    public SpdyServer(SslContext context) { //2
        this.context = context;
    }

    public ChannelFuture start(InetSocketAddress address) {
        ServerBootstrap bootstrap  = new ServerBootstrap(); //3
        bootstrap.group(group)
                .channel(NioServerSocketChannel.class)
                .childHandler(new SpdyChannelInitializer(context)); //4
        ChannelFuture future = bootstrap.bind(address); //5
        future.syncUninterruptibly();
        channel = future.channel();
        return future;
    }

    public void destroy() { //6
        if (channel != null) {
            channel.close();
        }
        group.shutdownGracefully();
    }

    public static void main(String[] args) throws Exception {
        if (args.length != 1) {
            System.err.println("Please give port as argument");
            System.exit(1);
        }
        int port = Integer.parseInt(args[0]);

        SelfSignedCertificate cert = new SelfSignedCertificate();
        SslContext context = SslContext.newServerContext(cert.certificate(), cert.privateKey());  //7
        final SpdyServer endpoint = new SpdyServer(context);
        ChannelFuture future = endpoint.start(new InetSocketAddress(port));

        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                endpoint.destroy();
            }
        });
        future.channel().closeFuture().syncUninterruptibly();
    }
}
  1. 构建新的 NioEventLoopGroup 用于处理 I/O
  2. 传递 SSLContext 用于加密
  3. 新建 ServerBootstrap 用于配置服务器
  4. 配置 ServerBootstrap
  5. 绑定服务器用于接收指定地址的连接
  6. 销毁服务器,用于关闭管道和 NioEventLoopGroup
  7. 从 BogusSslContextFactory 获取 SSLContext 。这是一个虚拟实现进行测试。真正的实现将为 SslContext 配置适当的密钥存储库。

启动 SpdyServer 并测试

请注意,当您使用 Jetty NPN 库需要提供它的位置通过 bootclasspath 的 JVM 参数。这一步是必需的,这样才能访问 SslEngine接口。(-Xbootclasspath 选项允许您覆盖标准 JDK 附带的实现类)。

下面的清单显示了特殊的参数(-Xbootclasspath)使用。

Listing 12.7 SpdyServer implementation

java -Xbootclasspath/p:<path_to_npn_boot_jar> ....

最简单的方式是使用 Maven 项目管理:

Listing 12.8 Compile and start SpdyServer with Maven

$ mvn clean package exec:exec -Pchapter12-SpdyServer
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building netty-in-action 0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
...
...
...
[INFO]
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ netty-in-action ---
[INFO] Building jar: /Users/norman/Documents/workspace-intellij/netty-in-actionprivate/
target/netty-in-action-0.1-SNAPSHOT.jar
[INFO]
[INFO] --- exec-maven-plugin:1.2.1:exec (default-cli) @ netty-in-action ---

可以用2个浏览器进行测试,一个支持 SPDY 一个不支持,这里我们用的是 Google Chrome (支持 SPDY) 和 Safari。

浏览器访问 https://127.0.0.1:9999,会显示 SpdyRequestHandler 的处理结果,如下图

NAME

Figure 12.4 SPDY supported by Google Chrome

Google Chrome 的一个很好的功能是可以统计数据,可以很好的看到连接情况。 在浏览器中访问 chrome://net-internals/#spdy 可以看到详细的统计数据

NAME

Figure 12.5 SPDY statistics

若不支持 SPDY ,比如我们用 Safari 浏览器访问 https://127.0.0.1:9999 ,则响应将会用 HttpRequestHandler 处理

NAME

Figure 12.7 SPDY not supported by Safari

总结

在这一章里,你学习了如何在基于Netty应用程序同时简单的使用 SPDY 和 HTTP(s) 。这提供了一个基础,您可以受益于性能于 SPDY 提供的增强,同时允许现有客户访问您的应用程序。

您学习了如何使用 Netty 提供的 SPDY 助手类,如何使用 Google Chrome获取更多的运行时信息协议。

一路上我们看到了再次修改 ChannelPipeline 如何帮助您构建强大的多路复用器在单个连接的生命周期切换协议。

下一章你学习如何利用高性能、无连接的 UDP。