Netty 框架学习 —— 基于 Netty 的 HTTP/HTTPS 应用程序


经过 SSL/TLS 保护应用程序

SSL 和 TLS 安全协议层叠在其余协议之上,用以实现数据安全。为了支持 SSL/TLS,Java 提供了 javax.net.ssl 包,它的 SSLContext 和 SSLEngine 类使得实现解密和加密变得至关简单。Netty 经过一个名为 SsLHandler 的 ChannelHandler 实现了这个 API,其中 SSLHandler 在内部使用 SSLEngine 来完成实际工做java

Netty 还提供了基于 OpenSSL 工具包的 SSLEngine 实现,比 JDK 提供的 SSLEngine 具备更好的性能。若是 OpenSSL 可用,能够将 Netty 应用程序配置为默认使用 OpenSSLEngine。若是不可用,Netty 将会退回到 JDK 实现web

下述代码展现了如何使用 ChannelInitializer 来将 SslHandler 添加到 ChannelPipeline 中安全

public class SslChannelInitializer extends ChannelInitializer<Channel> {

    private final SslContext context;
    private final boolean startTls;

    public SslChannelInitializer(SslContext context, boolean startTls) {
        this.context = context;
        this.startTls = startTls;
    }

    @Override
    protected void initChannel(Channel ch) throws Exception {
        SSLEngine engine = context.newEngine(ch.alloc());
        ch.pipeline().addFirst("ssl", new SslHandler(engine, startTls));
    }
}

大多数状况下,Sslhandler 将是 ChannelPipeline 中的第一个 ChannelHandler,这确保了只有在全部其余的 ChannelHandler 将它们的逻辑应用到数据以后,才会进行加密服务器

SSLHandler 具备一些有用的方法,如表所示,例如,在握手阶段,两个节点将相互验证而且商定一种加密方式,你能够经过配置 SslHandler 来修改它的行为,或者在 SSL/TLS 握手一旦完成以后提供通知,握手阶段以后,全部的数据都将会被加密websocket

方法名称 描述
setHandshakeTimeout(long, TimeUnit)
setHandshakeTimeoutMillis(long)
getHandshakeTimeoutMillis()
设置和获取超时时间,超时以后,握手 ChannelFuture 将会被通知失败
setCloseNotifyTimeout(long, TimeUnit)
setCloseNotifyTimeoutMillis(long)
getCloseNotifyTimeoutMillis()
设置和获取超时时间,超时以后,将会触发一个关闭通知并关闭链接,这也会致使通知该 ChannelFuture 失败
handshakeFuture() 返回一个在握手完成后将会获得通知的 ChannelFuture,若是握手先前已经执行过,则返回一个包含了先前握手结果的 ChannelFuture
close()
close(ChannelPipeline)
close(ChannelHandlerContext, ChannelPromise)
发送 close_notify 以请求关闭并销毁底层的 SslEngine

HTTP 编解码器

HTTP 是基于请求/响应模式的,客户端向服务器发送一个 HTTP 请求,而后服务器将会返回一个 HTTP 响应,Netty 提供了多种多种编码器和解码器以简化对这个协议的使用socket

下图分别展现了生产和消费 HTTP 请求和 HTTP 响应的方法ide

如图所示,一个 HTTP 请求/响应可能由多个数据部分组成,而且总以一个 LastHttpContent 部分做为结束工具

下表概要地介绍了处理和生成这些消息的 HTTP 解码器和编码器性能

名称 描述
HttpRequestEncoder 将 HTTPRequest、HttpContent 和 LastHttpContent 消息编码为字节
HttpResponseEncoder 将 HTTPResponse、HttpContent 和 LastHttpContent 消息编码为字节
HttpRequestDecoder 将字节编码为 HTTPRequest、HttpContent 和 LastHttpContent 消息
HttpResponseDecoder 将字节编码为 HTTPResponse、HttpContent 和 LastHttpContent 消息

下述代码中的 HttpPipelineInitializer 类展现了将 HTTP 支持添加到你的应用程序是多么简单 —— 只须要将正确的 ChannelHandler 添加到 ChannelPipeline 中this

public class HttpPipelineInitializer extends ChannelInitializer<Channel> {

    private final boolean client;

    public HttpPipelineInitializer(boolean client) {
        this.client = client;
    }

    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        if (client) {
            // 若是是客户端,则添加 HttpResponseDecoder 处理来自服务器的响应
            pipeline.addLast("decoder", new HttpResponseDecoder());
            // 若是是客户端,则添加 HttpRequestEncoder 向服务器发送请求
            pipeline.addLast("encoder", new HttpRequestEncoder());
        } else {
            // 若是是服务端,则添加 HttpRequestDecoder 处理来自客户端的请求
            pipeline.addLast("decoder", new HttpRequestDecoder());
            // 若是是客户端,则添加 HttpResponseEncoder 向客户端发送响应
            pipeline.addLast("encoder", new HttpResponseEncoder());
        }
    }
}

聚合 HTTP 消息

在 ChannelInitializer 将 ChannelHandler 安装到 ChannelPipeline 中以后,你就能够处理不一样类型的 HTTPObject 消息了。但因为 HTTP 请求和响应可能由许多部分组成,所以你须要聚合它们以造成完整的消息。Netty 提供了一个聚合器,它能够将多个消息部分合并为 FullHttpRequest 或者 FullHttpResponse 消息

因为消息分段须要被缓冲,直到能够转发下一个完整的消息给下一个 ChannelInboundHandler,因此这个操做有轻微的开销,其所带来的好处就是你能够没必要关心消息碎片了

引入这种自动聚合机制只不过是向 ChannelPipeline 中添加另一个 ChannelHandler 罢了,下述代码展现了如何作到这一点:

public class HttpAggregatorInitializer extends ChannelInitializer<Channel> {

    private final boolean isClient;

    public HttpAggregatorInitializer(boolean isClient) {
        this.isClient = isClient;
    }

    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        if (isClient) {
            // 若是是客户端,则添加 HttpClientCodec
            pipeline.addLast("codec", new HttpClientCodec());
        } else {
            // 若是是服务器,则添加 HttpServerCodec
            pipeline.addLast("codec", new HttpServerCodec());
        }
        // 将最大的消息大小为 512KB 的 HTTPObjectAggregator 添加到 ChannelPipeline
        pipeline.addLast("aggregator", new HttpObjectAggregator(512 * 1024));
    }
}

HTTP 压缩

当使用 HTTP 时,建议开启压缩功能以尽量多地减少传输数据的大小。虽然压缩会带来一些消耗,但一般来讲它都是一个好主意,尤为是对于文本数据而言

Netty 为压缩和解压都提供了 ChannelHandler 实现,它们同时支持 gzip 和 deflate 编码

客户端能够经过提供如下头部信息来指示服务器它所支持的压缩格式

GET /encrypted-area HTTP/1.1

Host: www.example.com

Accept-Encoding: gzip, deflate

然而,须要注意的是,服务器没有义务压缩它所发送的数据

public class HttpCompressionInitializer extends ChannelInitializer<Channel> {

    private final boolean isClient;

    public HttpCompressionInitializer(boolean isClient) {
        this.isClient = isClient;
    }

    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        if (isClient) {
            // 若是是客户端,则添加 HTTPClientCodec
            pipeline.addLast("codec", new HttpClientCodec());
            // 若是是客户端,则添加 HttpContentDecompressor 以处理来自服务器的压缩内容
            pipeline.addLast("decompressor", new HttpContentDecompressor());
        } else {
            // 若是是服务端,则添加 HttpServerCodec
            pipeline.addLast("codec", new HttpServerCodec());
            // 若是是服务器,则添加 HttpContentDecompressor 来压缩数据
            pipeline.addLast("decompressor", new HttpContentDecompressor());
        }
    }
}

HTTPS

启用 HTTPS 只须要将 SslHandler 添加到 ChannelPipeline 的 ChannelHandler 组合中

public class HttpsCodecInitializer extends ChannelInitializer<Channel> {

    private final SslContext context;
    private final boolean isClient;

    public HttpsCodecInitializer(SslContext context, boolean isClient) {
        this.context = context;
        this.isClient = isClient;
    }

    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        SSLEngine engine = context.newEngine(ch.alloc());
        pipeline.addLast("ssl", new SslHandler(engine));
        if (isClient) {
            pipeline.addLast("codec", new HttpClientCodec());
        } else {
            pipeline.addLast("codec", new HttpServerCodec());
        } 
    }
}

WebSocket

WebSocket 解决了一个长期存在的问题:既然底层协议(HTTP)是一个请求/响应模式的交互序列,那么如何实时地发布信息呢?AJAX必定程度上解决了这个问题,但数据流仍然是由客户端所发送的请求驱动的

WebSocket 提供了在单个 TCP 链接上提供双向的通讯,它为网页和远程服务器之间的双向通讯提供了一种替代 HTTP 轮询的方案

要想向你的应用程序添加对于 WebSocket 的支持,你须要将适当的客户端或者服务器 WebSocketChannelHandler 添加到 ChannelPipeline 中。这个类将处理由 WebSocket 定义的称为帧的特殊消息类型,如表所示,WebSocketFrame 能够被归类为数据帧或者控制帧

名称 描述
BinaryWebSocketFrame 数据帧:二进制数据
TextWebSocketFrame 数据帧:文本数据
ContinuationWebSocketFrame 数据帧:属于上一个 BinaryWebSocketFrame 或者 TextWebSocketFrame 的文本或者二进制的数据
CloseWebSocketFrame 控制帧:一个 CLOSE 请求,关闭的状态码以及关闭的缘由
PingWebSocketFrame 控制帧:请求一个 PongWebSocketFrame
PongWebSocketFrame 控制帧:对 PingWebSocketFrame 请求的响应

由于 Netty 主要是一种服务器端技术,因此咱们重点建立 WebSocket 服务器。下述代码展现了使用 WebSocketChannelHandler 的简单示例,这个类会处理协议升级握手,以及三种控制帧 —— Close、Ping 和 Pong,Text 和 Binary 数据帧将会被传递给下一个 ChannelHandler 进行处理

public class WebSocketServerInitializer extends ChannelInitializer<Channel> {

    @Override
    protected void initChannel(Channel ch) throws Exception {
        ch.pipeline().addLast(
                new HttpServerCodec(),
                new HttpObjectAggregator(65536),
                // 若是被请求的端点是 /websocket,则处理该升级握手
                new WebSocketServerProtocolHandler("/websocket"),
                // TextFrameHandler 处理 TextWebSocketFrame
                new TextFrameHandler(),
                // BinaryFrameHandler 处理 BinaryWebSocketFrame
                new BinaryFrameHandler(),
                // ContinuationFrameHandler 处理 Continuation WebSocketFrame
                new ContinuationFrameHandler());
    }

    public static final class TextFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

        @Override
        protected void messageReceived(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
            // do something
        }
    }

    public static final class BinaryFrameHandler extends SimpleChannelInboundHandler<BinaryWebSocketFrame> {

        @Override
        protected void messageReceived(ChannelHandlerContext ctx, BinaryWebSocketFrame msg) throws Exception {
            // do something
        }
    }

    public static final class ContinuationFrameHandler extends SimpleChannelInboundHandler<ContinuationWebSocketFrame> {

        @Override
        protected void messageReceived(ChannelHandlerContext ctx, ContinuationWebSocketFrame msg) throws Exception {
            // do something
        }
    }
}
相关文章
相关标签/搜索