Provided ChannelHandlers and codes

本章将包括:

  • 使用 SSL/TLS 保护Netty应用程序
  • 构建Netty HTTP / HTTPS应用程序
  • 处理空闲连接和超时
  • 解码分隔和基于长度的协议
  • 写大数据

Netty为许多常用协议提供编解码器和处理程序,您可以直接使用它们,从而减少您在相当繁琐的事情上花费的时间和精力。 在本章中,我们将探索这些工具及其优点,包括对 SSL / TLS 和 WebSocket 的支持,以及通过数据压缩简单地从 HTTP 中挤出更好的性能。

11.1 Securing Netty applications with SSL/TLS

数据隐私是当今非常值得关注的问题,作为开发人员,我们需要做好准备来解决它。至少我们应该熟悉SSL和TLS等加密协议,这些协议在其他协议之上分层以实现数据安全性。我们在访问安全网站时都遇到过这些协议,但它们也用于非基于HTTP的应用程序,例如安全SMTP(SMTPS)邮件服务甚至关系数据库系统。

为了支持 SSL / TLS,Java提供了包 javax.net.ssl ,其类 SSLContextSSLEngine 使得实现解密和加密非常简单。 Netty通过名为 SslHandlerChannelHandler 实现来利用此API,该实现在内部使用 SSLEngine 来完成实际工作。

Netty’s OpenSSL/SSLEngine implementation
Netty还提供了一个使用OpenSSL工具包(www.openssl.org)的 SSLEngine 实现。 此类 OpenSslEngine 提供比JDK提供的 SSLEngine 实现更好的性能。
如果OpenSSL库可用,则可以将Netty应用程序(客户端和服务器)配置为默认使用OpenSslEngine。 如果没有,Netty将回归到JDK实现。 有关配置OpenSSL支持的详细说明,请参阅Netty官方文档
请注意,无论您使用JDK的SSLEngine还是Netty的OpenSslEngine,SSL API和数据流都是相同的。

图11.1显示使用SslHandler的数据流。

Figure 11.1 Data flow through SslHandler for decryption and encryption

清单11.1显示了如何使用ChannelInitializer将SslHandler添加到ChannelPipeline。 回想一下,ChannelInitializer用于在注册Channel后设置ChannelPipeline。

public class SslChannelInitializer extends ChannelInitializer<Channel> {
    private final SslContext context;
    private final boolean startTls;
    //Passes in the SslContext to use
    public SslChannelInitializer(SslContext context,
        boolean startTls) {
        this.context = context;
        // if ture, the first message written is not encrypted(clients shoud set to true)
        this.startTls = startTls;
    }

    @Override
    protected void initChannel(Channel ch) throws Exception {
    // For each SslHandler instance, obtains a new SSLEngine from the SslContext from the SslContext using the ByteBufAllocator of the Channel
        SSLEngine engine = context.newEngine(ch.alloc());
        // Adds the SslHandler to the pipeline as the first handler
        ch.pipeline().addFirst("ssl",
            new SslHandler(engine, startTls));
        }
   }        
  

在大多数情况下,SslHandler将成为ChannelPipeline中的第一个ChannelHandler。这确保只有在所有其他ChannelHandler将其逻辑应用于数据之后才会进行加密。

SslHandler有一些有用的方法,如表11.1所示。 例如,在握手阶段,两个对等体相互验证并就加密方法达成一致。 您可以配置SslHandler以修改其行为或在SSL / TLS握手完成后提供通知,之后将对所有数据进行加密。 SSL / TLS握手将自动执行。

Table 11.1 SslHandler methods
Table 11.1 SshHandler methods

11.2 Building Netty HTTP/HTTPS applications

HTTP / HTTPS是最常见的协议套件之一,随着智能手机的成功,它每天都被更广泛地使用,因为对于任何公司来说,拥有移动可访问的网站几乎是必须的。 这些协议也以其他方式使用。 许多组织导出的与其业务伙伴进行通信的WebService API通常基于HTTP(S)。

接下来我们将看看Netty提供的ChannelHandlers,这样您就可以使用HTTP和HTTPS而无需编写自定义编解码器。

11.2.1 HTTP decoder, encoder, and codec

HTTP基于请求/响应模式:客户端向服务器发送HTTP请求,服务器发回HTTP响应。 Netty提供各种编码器和解码器,以简化此协议的使用。 图11.2和11.3显示了分别生成和使用HTTP请求和响应的方法。

如图11.2和11.3所示,HTTP请求/响应可能包含多个数据部分,并且它始终以LastHttpContent部分终止。 FullHttpRequest和FullHttpResponse消息是分别代表完整请求和响应的特殊子类型。 所有类型的HTTP消息(FullHttpRequest,LastHttpContent和清单11.2中显示的那些)都实现了HttpObject接口。

Figure 11.2 HTTP request component parts

Figure 11.3 HTTP response component parts

表11.2概述了处理和生成这些消息的HTTP解码器和编码器。

方法名描述
HttpRequestEncoder将 HttpRequest, HttpContent, 和 LastHttpContent messages 编码为 bytes
HttpResponseEncoder将 HttpResponse, HttpContent, 和 LastHttpContent messages 编码为 bytes
HttpRequestDecoder将 bytes 解码为 HttpRequest, HttpContent, 和 LastHttpContent messages
HttpResponseDecoder将 bytes 解码为 HttpResponse, HttpContent, 和 LastHttpContent messages

Table 11.2 HTTP decoders and encoders

下一个清单中的HttpPipelineInitializer类显示了向应用程序添加HTTP支持是多么简单 - 只需将正确的ChannelHandler添加到ChannelPipeline即可。

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) {
         // if client, adds HttpResponseDecoder to handle responses from the server
         pipeline.addLast("decoder", new HttpResponseDecoder());
         // if client, adds HttpRequestEncoder to send requests to the server
     } else {
        // if server, adds HttpRequestDecoder to receive requests from the client
        pipeline.addLast("decoder", new HttpRequestDecoder());
        // if server, adds HttpResonseEncoder to send responses to the client
       pipeline.addLast("encoder", new HttpResonseEncoder());
    }
 }
}           

11.2.2 HTTP message aggregation

初始化程序在 ChannelPipeline 中安装了处理程序后,您可以对不同的 HttpObject 消息进行操作。 但由于HTTP请求和响应可以由许多部分组成,因此您需要将它们聚合在一起以形成完整的消息。 为了消除这个繁琐的任务,Netty提供了一个聚合器,它将消息部分合并为FullHttpRequestFullHttpResponse消息。 这样您始终可以看到完整的消息内容。

这个操作有一点点成本,因为消息段需要缓冲,直到完整的消息可以转发到下一个ChannelInboundHandler。 权衡是你不必担心消息碎片。

引入此自动聚合是向管道添加另一个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) {
            // If client, adds HttpClientCodec
            pipeline.addLast("codec", new HttpClientCodec());
        } else {
            // If server, adds HttpServerCodec
            pipeline.addLast("codec", new HttpServerCodec());
        }
        // Adds HttpObjectAggregator with a max message size of 512 KB to the pipeline
        pipeline.addLast("aggregator",
                new HttpObjectAggregator(512 * 1024));
    }
}                

11.2.3 HTTP compression

使用HTTP时,建议采用压缩来尽可能减少传输数据的大小。 虽然压缩在CPU周期中确实有一些成本,但通常是一个好主意,特别是对于文本数据。
Netty为压缩和解压缩提供了ChannelHandler实现,支持gzip和deflate编码。

HTTP request header
客户端可以通过提供以下标头来指示支持的加密模式:
GET /encrypted-area HTTP/1.1
Host: www.example.com
Accept-Encoding: gzip, deflat
但请注意,服务器没有义务压缩它发送的数据。

以下列表中显示了一个示例。

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) {
            // if client, adds HttpClientCodec
            pipeline.addLast("codec", new HttpClientCodec()); 
            // if client, adds HttpContentDecompressor to handle compressed content from the server
            pipeline.addLast("decompressor", new HttpContDecompressor()); 
       } else {
           // if server, adds HttpServerCodec
           pipeline.addLast("codec", new HttpServerCodec());
           // if server, adds HttpContentCompressor to compress the data(if the client supports it)
           pipeline.addLast("compressor", new HttpContentCompressor());       

Listing 11.4 Automatically compressing HTTP messages

Compression and dependencies
如果你使用的是 JDK 6 或者更早的版本,你需要添加 JZlib(www.jcraft.com/jzlib/) 到你的环境变量中,以支持压缩。
对于Maven, 你需要添加一下依赖。

11.2.4 Using HTTPS

以下清单显示启用HTTPS只是向混合添加SslHandler的问题。

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());
        // Adds SslHandlder to the pipeline to use HTTPS
        pipeline.addFirst("ssl", new SslHandler(engine));

        if(isClient) {
            // If client, adds HttpClientCodec
            pipeline.addLast("codec", new HttpClientCodec());
        } else {
            // If server, adds HttpServerCodec
            pipeline.addLast("codec", new HttpServerCodec());
        }
    }
}                    

Listing 11.5 Using HTTPS

前面的代码是Netty架构方法如何将重用转化为杠杆的一个很好的例子。 只需在ChannelPipeline中添加ChannelHandler,您就可以提供新功能,甚至可以像加密一样重要。

11.2.5 WebSocket

Netty针对基于HTTP的应用程序的广泛工具包包括对其一些最高级功能的支持。 在本节中,我们将探讨WebSocket,这是2011年由互联网工程任务组(IETF)标准化的协议。

WebSocket解决了一个长期存在的问题:如果底层协议HTTP是一系列请求 - 响应交互,那么如何实时发布信息。 AJAX提供了一些改进,但数据流仍然来自客户端的请求。 还有其他或多或少的聪明方法,但最终它们仍然具有可扩展性有限的变通方法。

WebSocket规范及其实现代表了一种更有效的解决方案。 简单地说,WebSocket为两个方向的流量提供了“单个TCP连接…与WebSocket API相结合…它为从网页到远程服务器的双向通信提供了HTTP轮询的替代方案。”

也就是说,WebSockets在客户端和服务器之间提供真正的双向数据交换。 我们不会详细介绍内部结构,但我们应该提到尽管最早的实现仅限于文本数据,但现在不再是这样了。 WebSocket现在可以用于任何数据,就像普通的套接字一样。

图11.4给出了WebSocket协议的一般概念。 在这种情况下,通信以普通HTTP开始,并升级到双向WebSocket。

Figure 11.4 WebSocket protocol

要向应用程序添加WebSocket支持,请在管道中包含相应的客户端或服务器端WebSocket ChannelHandler。 该类将处理由WebSocket定义的特殊消息类型,称为帧(frame)。 如表11.3所示,WebSocketFrame可以被分类为数据或控制帧。

类型名称描述
BinaryWebSocketFrameData frame: binary data
TextWebSocketFrameData frame: text data
ContinuationWebSocketFrameData frame: text or binary data that belongs to a previous BinaryWebSocketFrame or TextWebSocketFrame
CloseWebSocketFrameControl frame: a CLOSE request, close status code, and a phrase
PingWebSocketFrameControl frame: requests a PongWebSocketFrame
PongWebSocketFrameControl frame: responds to a PingWebSocketFrame

Table 11.3 WebSocketFrame types
由于Netty主要是服务器端技术,因此我们将重点介绍如何创建WebSocket服务器。 清单11.6给出了一个使用WebSocketServerProtocolHandler的简单示例。 此类处理协议升级握手以及三个控制帧 - Close,Ping和Pong。 文本和二进制数据框将传递给下一个处理程序(由您实现)进行处理。

public class WebSocketServerInitializer extends ChannelInitializer<Channel> {
    @Override
    protected void initChannel(Channel ch) throws Exception {
        ch.pipeline().addLast(
            new HttpServerCodec(),
            // provides aggregated HttpRequests for the handshake
            new HttpObjectAggregator(65536),
            // Handles the upgrade handshake if the endpoint requested is "/websocket"
           new WebSocketServerProtocolHandler("/websocket"),
           // TextFrameHandler handles TextWebSocketFrames
           new TextFrameHandler(),
           // BinaryFrameHandler Handles BinaryWebSocketFrames
           new BinaryFrameHandler(),
           // ContinuationFrameHandler handles ContinuationWebSocketFrames
           new ContinuationFrameHandler());
      }

      public static final class TextFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
      @Override
      public void channelRead0(ChannelHandlerContext ctx,
          TextWebSocketFrame msg) throws Exception {
              // Handle text frame
          }
      }

      public static final class BinaryFrameHandler extends
          SimpleChannelInboundHandler<BinaryWebSocketFrame> {
          @Override
          public void channelRead0(ChanelHandlerContenxt ctx,
              BinaryWebSocketFrame msg) throws Exception {
              // Handle binary frame
          }
     }

     public static final class ContinuationFrameHandler extends 
         SimpleChannelInboundHandler<ContinuationWebSocketFrame> {
         @Override
         public void channelRead0(ChannelHandlerContext ctx,
             ContinuationWebSocketFrame msg) throws Exception {
             // Handle continuation frame
         }
    }
}    
                          

Secure WebSocket
要为WebSocket添加安全性,只需将SslHandler作为管道中的第一个ChannelHandler插入即可。

有关更广泛的示例,请参阅第12章,其中深入探讨了实时WebSocket应用程序的设计。

11.3 Idle connections and timeouts

到目前为止,我们的讨论主要集中在Netty通过专用编解码器和处理程序支持HTTP变体HTTPS和WebSocket。 只要您有效地管理网络资源,这些技术可以使您的Web应用程序更加有效,可用和安全。 让我们来谈谈主要关注点,连接管理。

检测空闲连接和超时对于及时释放资源至关重要。 这是一个常见的任务,Netty为此提供了几个ChannelHandler实现。 表11.4概述了这些。

名称描述
IdleStateHandler如果连接空闲时间太长,则触发IdleStateEvent。 然后,您可以通过覆盖ChannelInboundHandler 中的 userEventTriggered() 来处理 IdleStateEvent
ReadTimeoutHandler当没有收到指定时间间隔的入站数据时,抛出ReadTimeoutException并关闭Channel。 可以通过覆盖ChannelHandler中的exceptionCaught() 来检测ReadTimeoutException。
WriteTimeoutHandler当没有收到指定时间间隔的入站数据时,抛出WriteTimeoutException并关闭Channel。 可以通过覆盖来检测WriteTimeoutException
您的ChannelHandler中的exceptionCaught()。

让我们仔细看看IdleStateHandler,它是实践中最常用的。 代码清单11.7显示了如何使用向远程对等体发送心跳消息的常用方法,在没有收到或发送60秒的数据的情况下获取通知; 如果没有响应,则关闭连接。

public class IdleStateHandlerInitializer extends ChannelInitializer<Channel> {
    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        // IdleStateHandler sends an IdleStateEvent when triggered
        pipeline.addLast(new IdleStateHandler(0, 0, 60, TimeUnit.SECONDS));
        // Adds a HeartbeatHandler to the pipeline
        pipeline.addLast(new HeartbeatHandler());
    }

    // Implements userEventTriggered() to send the heartbeat
    public static final class HeartbeatHander 
        extends ChannelStateHandlerAdapter {
        // The  heartbeat to send to the remote peer
        private static final ByteBuf HEARTBEAT_SEQUENCE =
            Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("HEARTBEAT", CharsetUtil.ISO_8859_1));

       @Override
       public void userEventTriggered(ChannelHandlerContext ctx, 
           Object evt) throws Exception {
           // Sends the heartbeat and closes the connection if the send fails
           if(evt instanceof IdleStateEvent) {
               ctx.writeAndFlush(HEARTBEAT_SEQUENCE.duplicate())
                   .addListener(ChannelFutureListener.CLOSE_ON_FAILURE); 
            } else {
               // Not an IdleStateEvent, so pass it to the next handler
               super.userEventTriggered(ctx, evt);          
            }
         }
     }
}
          

此示例说明如何使用IdleStateHandler来测试远程对等方是否仍处于活动状态,并通过关闭连接来释放资源(如果不是)。 如果连接未接收或发送数据60秒,IdleStateHandler 将使用 IdleStateEvent 调用 userEventTriggered()。 HeartbeatHandler实现userEventTriggered()。 如果此方法检测到 IdleStateEvent,它会发送心跳消息并添加一个ChannelFutureListener,如果发送操作失败,它将关闭连接

11.4 Decoding delimited and length-based protocols

在使用Netty时,您将遇到需要解码器的分隔和基于长度的协议。 接下来的部分将介绍Netty为处理这些案例提供的实现。

11.4.1 Delimited protocols

定界消息协议使用定义的字符来标记消息或消息段的开头或结尾,通常称为帧。 RFC文档正式定义的许多协议都是如此,例如SMTP,POP3,IMAP和Telnet。 当然,私人组织通常拥有自己的专有格式。 无论使用哪种协议,表11.5中列出的解码器都可以帮助您定义自定义解码器,这些解码器可以提取由任何令牌序列分隔的帧。

名称描述
DelimiterBasedFrameDecoder一种通用解码器,使用任何用户提供的分隔符提取帧。
LineBasedFrameDecoder一种解码器,用于提取由行结尾分隔的帧\ n或\ r \ n。 这个解码器比DelimiterBasedFrameDecoder更快。

Table 11.5 Decoders for handling delimited and length-based protocols
图11.5显示了在行结束序列\ r \ n(回车符+换行符)分隔时如何处理帧。

Frames delimited by line endings

下面的清单显示了如何使用LineBasedFrameDecoder来处理图11.5所示的情况。

public class LineBasedHandlerInitializer extends ChannelInitializer<Channel> {
    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        // The LineBasedFrameDecoder forwards extracted frames to the next handler
        pipeline.addLast(new LineBasedFrameDecoder(64 * 1024));
        // Adds the FrameHandler to receive the frames
        pipeline.addLast(new FrameHandler());
    }

    public static final class FrameHandler
        extends SimpleChannelInboundHandler<ByteBuf> {
        // Passes in the contents of a single frame
        @Override
        public void channelRead0(ChannelHandlerContext ctx,
            ByteBuf msg) throws Exception {
            // Do something with the data extracted from the frame
        }
    }
}                

如果您正在使用由行结尾之外的其他内容分隔的帧,则可以以类似的方式使用DelimiterBasedFrameDecoder,为构造函数指定特定的分隔符序列。

这些解码器是用于实现自己的分隔协议的工具。 例如,我们将使用以下协议规范:

  • 传入的数据流是一系列帧,每个帧由换行符(\ n)分隔。
  • 每个框架由一系列 item 组成,每个 item 由单个空格字符分隔。
  • frame 的内容(content)表示一个命令,定义为名称后跟可变数量的参数。

我们为此协议定制的解码器将定义以下类:

  • Cmd —— 将帧的内容(命令)存储在一个ByteBuf中,用于名称,另一个用于参数。
  • CmdDecoder —— 从重写的 decode() 方法中检索一行,并从其内容构造一个Cmd实例。
  • CmdHandler —— 从 CmdDecoder 接收解码的 Cmd 对象并对其执行一些处理。
  • CmdHandlerInitializer —— 为简单起见,我们将前面的类定义为将在管道中安装处理程序的专用ChannelInitializer的嵌套类。

正如您在下一个清单中所看到的,此解码器的关键是扩展LineBasedFrameDecoder。

public class CmdHandlerInitializer extends ChannelInitializer<Channel> {
    final byte SPACE =(byte)' ';
    @Override
    protected void initChannel(Channel ch) throws Exception {
        // Adds a CmdDecoder to extract a Cmd object and forwards it to the next handler
        pipeline.addLiast(new CmdDecoder(64 * 1024));
        // Adds a CmdHandler to receive and process the Cmd objects
    }

    // The Cmd POJO
    public static final class Cmd {
        private final ByteBuf name;
        private final ByteBuf args;

        public Cmd(ByteBuf name, ByteBuf args) {
            this.name = name;
            this.args = args;
        }

        public ByteBuf name() {
            return name;
        }

        public ButeBuf args() {
            return args;
        }
    }

    public static final class CmdDecoder extends LineBasedFrameDecoder {
        public CmdDecoder(int maxLength) {
            super(maxLength);
        }

        @Override
        protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer)
            throws Exception {
            // Extracts a frame delimited by an end-of-line sequence from the ByteBuf
            ByteBuf frame = (ByteBuf) super.decode(ctx, buffer);
            // Nuul is returned if there is no frame in the input.
            if( frame == null) {
                return null;
            }
            //Finds the index of the first space character. The command name precedes it, the arguements follow.
            int index = frame.indexOf(frame.readerIndex(),
                frame.writeIndex(), SPACE);
            // New Cmd object instantiated with slices that hold the command name and arguments    
            return new Cmd(frame.slice(frame.readerIndex(), index),
                frame.slice(index + 1, frame.writerIndex()));  
            }
        }

        public static final class CmdHandler extends SimpleChannelInboundHandler<Cmd> {
            @Override
            public void channelRead0(ChannelHandlerContext ctx, Cmd msg)
                throws Exception {
                // Do something with the command
                // Processes the Cmd object passed through the pipeline
            }
       }
  }                                                    

11.4.2 Length-based protocols

基于长度的协议通过在帧的标题段中编码其长度来定义帧,而不是通过用特殊定界符标记其末尾来定义帧。 表11.6列出了Netty为处理此类协议提供的两个解码器。

名称描述
FixedLengthFrameDecoder提取在调用构造函数时指定的固定大小的帧。
LengthFieldBasedFrameDecoder根据帧头中字段中编码的长度值提取帧; 字段的偏移量和长度在构造函数中指定。

Table 11.6 Decoders for length-based protocols
图11.6显示了使用8字节的帧长度构造的FixedLengthFrameDecoder的操作。

Figure 11.6 Decoding a frame length of 8 bytes

您经常会遇到协议,其中邮件头中编码的帧大小不是固定值。 要处理这样的可变长度帧,您将使用LengthFieldBasedFrameDecoder,它根据头字段确定帧长度,并从数据流中提取指定的字节数。

图11.7显示了一个示例,其中标头中的长度字段位于偏移量0并且长度为2个字节。

LengthFieldBasedFrameDecoder提供了几个构造函数来覆盖各种头配置案例。 代码清单11.10显示了一个构造函数的使用,它的三个参数是maxFrameLength,lengthFieldOffset和lengthFieldLength。 在这种情况下,帧的长度在帧的前8个字节中编码。

public class LengthBasedInitializer extends ChannelInitializer<Channel> {
    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        // LengthFieldBasedFrameDecoder for messages that encode frame length in the first 8 bytes.
        pipeline.addLast( new LengthFieldBasedFrameDecoder(64 * 1024, 0, 8));
        // Adds a FrameHandler to handle each frame
        pipeline.addLiast(new FrameHandler());
    }

    public static final class FrameHandler extends SimpleChannelInboundHandler<ByteBuf> {
        @Override
        public void channelRead0(ChannelHandlerContext ctx,
            ByteBuf msg) throws Exception {
            // Processes the frame data
            // Do something with the frame.
            }
     }
}                

Decoder for the command and the handler
您现在已经看到Netty提供的编解码器通过指定协议帧的分隔符或长度(固定或变量)来支持定义字节流结构的协议。 您将发现这些编解码器的众多用途,因为许多常见协议属于一个或其他类别。

11.5 Writing big data

由于网络饱和的可能性,在异步框架中有效地编写大块数据是一个特殊问题。 由于写操作是非阻塞的,因此即使所有数据都没有写出,它们也会在完成时返回并通知ChannelFuture。 发生这种情况时,如果您不停止写入,则可能会出现内存不足的风险。 因此,在编写大量数据时,您需要准备好处理与远程对等方的慢速连接可能导致释放内存延迟的情况。 让我们考虑将文件内容写入网络的情况。

在我们对传输的讨论中(参见4.2节),我们提到了NIO的零拷贝功能,它消除了将文件内容从文件系统移动到网络堆栈的复制步骤。 所有这些都发生在Netty的核心,因此所需要的只是应用程序使用接口FileRegion的实现,在Netty API文档中定义为“通过支持零拷贝文件传输的通道发送的文件区域”。

此列表显示了如何通过从FileInputStream创建DefaultFileRegion并将其写入Channel来使用零拷贝传输文件的内容。

// Creates a FileInputStream
FileInputStream in = new FileInputStream(file);
// Creates a new DefaultFileRegion for the full length of the file.
FileRegion region = new DefaultFileRegion(
    in.getChannel(), 0, file.length());
// Sends the DefaultFileRegion and registers a ChannelFutureListener    
channel.writeAndFlush(region).addListener(    
    new ChannelFutureListener() {
    @Override
    public void operationComplete(ChannelFuture future) throws Exception {
        if(!future.isSuccess()) {
            // Handlers failure
            Throwable cause = future.cause();
            // Do something
        }
    }
});            

此示例仅适用于文件内容的直接传输,不包括应用程序对数据的任何处理。 如果需要将数据从文件系统复制到用户存储器中,可以使用ChunkedWriteHandler,它支持异步写入大数据流而不会产生高速内存消耗。

关键是接口 ChunkedInput ,其中参数B是方法 readChunk() 返回的类型。 提供了此接口的四种实现,如表11.7中所列。 每个代表一个由 ChunkedWriteHandler 消耗的无限长度的数据流。

名称描述
ChunkedFile从块中获取文件块中的数据,以便在平台不支持零拷贝或需要转换数据时使用
ChunkedNioFile与ChunkedFile类似,不同之处在于它使用FileChannel
ChunkedStream从InputStream按块传输内容块
ChunkedNioStream通过ReadableByteChannel中的块传输内容块

Table 11.7 ChunkedInput Implementations
清单11.12说明了ChunkedStream的使用,ChunkedStream是实践中最常用的实现。 显示的类用File和SslContext实例化。 调用initChannel()时,它会使用显示的处理程序链初始化通道。

当通道变为活动状态时,WriteStreamHandler将按块作为ChunkedStream从文件块中写入数据。 在传输之前,数据将由SslHandler加密。

public class ChunkedWriteHandlerInitializer extends ChannelInitializer<Channel> {
    private final File file;
    private final SslContext sslCtx;

    public ChunkedWriteHandlerInitializer(File file, SslContext sslCtx) {
        this.file = file;
        this.sslCtx = sslCtx;
    }

    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        // Adds an SslHandler to the ChannelPipeline
        pipeline.addLast(new SslHandler(sslCtx.createEngine());
        // Adds a ChunckedWriteHandler to handle data passed in as ChunkedInput
        pipeline.addLast(new ChunkedWriteHandler());
        // WriteStreamHandler starts to write the file data onece the connection is established
        pipeline.addLast(new WriteStreamHandler());
    }

    public final class WriteStreamHandler
        extends ChannelInboundHandlerAdapter {
 
       // channelActive() writes the file data using ChunkedInput when the connection is established
       @Override
       public void channelActive(ChannelHandlerContext ctx)
           throws Exception {
           super.channelActive(ctx);
           ctx.writeAndFlush(new ChunkedStream(new FileInputStream(file))); 
       }
    }
}                  

chunked input 要使用您自己的ChunkedInput实现,请在管道中安装ChunkedWriteHandler。

在本节中,我们讨论了如何使用零拷贝功能有效地传输文件,以及如何使用ChunkedWriteHandler编写大数据而不会冒OutOfMemoryErrors的风险。 在下一节中,我们将研究几种序列化POJO的方法。

11.6 Serializing data

JDK提供ObjectOutputStream和ObjectInputStream,用于通过网络序列化和反序列化POJO的原始数据类型和图形。 API并不复杂,可以应用于任何实现java.io.Serializable的对象。 但它也不是非常有效。 在本节中,我们将看到Netty提供的内容。

名称描述
CompatibleObjectDecoder解码器,用于与使用JDK的非Netty对等体进行互操作
序列化。
CompatibleObjectEncoder用于与使用JDK的非Netty对等体进行互操作的编码器
序列化。
ObjectDecoder在JDK序列化之上使用自定义序列化进行解码的解码器; 当排除外部依赖关系时,它可以提高速度。 否则,其他序列化实现更可取。
ObjectEncoder在JDK序列化之上使用自定义序列化进行编码的编码器; 当排除外部依赖关系时,它可以提高速度。 否则,其他序列化实现更可取。

Table 11.8 JDK serialization codecs

11.6.2 Serialization with JBoss Marshalling

如果你可以自由地使用外部依赖,JB oss编组是理想的:它比JDK序列化快三倍,而且更紧凑。 JBoss Marshalling主页的概述以这种方式定义:

JBoss Marshalling is an alternative serialization API that fixes many of the problems found in the JDK serialization API while remaining fully compatible with java.io.Serializable and its relatives, and adds several new tunable parameters and additional features, all of which are pluggable via factory configuration (externalizers, class/instance lookup tables, class resolution, and object replacement, to name a few).

Netty支持JB oss编组,使用表11.9中所示的两个解码器/编码器对。 第一组与仅使用JDK序列化的对等项兼容。 第二个提供最大性能,适用于使用JBoss编组的对等体。

名称描述
CompatibleMarshallingDecoder and CompatibleMarshallingEncoder与使用JDK序列化的对等项兼容。
MarshallingDecoder and MarshallingEncoder用于使用JBoss编组的对等方。 这些类必须一起使用。

Table 11.9 JBoss Marshalling codecs

以下清单显示了如何使用MarshallingDecoder和MarshallingEncoder。 同样,这主要是适当配置ChannelPipeline的问题。

public class MarshallingInitializer extends ChannelInitializer<Channel> {
    private final MarshallerProvider marshallerProvider;
    private final UnmarshallerProvider unmarshallerProvider;

   public MarshallingInitalizer (
       UnmarshallerProvider unmarshallerProvider,
       MarshallerProvider marshallerProvider) {
       this.marshallerProvider = marshallerProvider;
       this.unmarshallerProvider = unmarshallerProvider;
   }

   @Override
   protected void initChannel(Channel channel) throws Exception {
       ChannelPipeline pipeline = channel.pipeline();
       // Adds a MarshallingDecoder to convert ByteBufs to POJOs
       pipeline.addLast(new MarshallingDecoder(unmarshallerProvider));
       // Adds a MarshallingEncoder to convert POJOs to ByteBufs
       pipeline.addLast(new MarshallingEncoder(marshallerProvider));
       // Adds an ObjectHandler for normal POJOs that implement Serializable
       pipeline.addLast(new ObjectHandler());
   }

  public static final class ObjectHandler
      extends SimpleChannelInboundHandler<Serializable> {
      @Override
      public void channelRead0(
          ChannelHandlerContext channelHandlerContext,
          Serializable serializable) throws Exception {
          // Do something
          }
    }
}                  

11.6.3 Serialization via Protocol Buffers

Netty的序列化解决方案的最后一个是使用协议缓冲区的编解码器,这是一种由谷歌开发的数据交换格式,现在是开源的。 该代码可以在https://github.com/google/protobuf找到。

协议缓冲区以紧凑和高效的方式对结构化数据进行编码和解码。 它具有许多编程语言的绑定,使其非常适合跨语言项目。 表11.10显示了用于protobuf支持的ChannelHandler实现Netty供应。

名称描述
ProtobufDecoder使用protobuf 解码message
ProtobufEncoder使用protobuf 编码message
ProtobufVarint32FrameDecoderSplits通过消息中的Google协议“Base 128 Varints”整数长度字段的值动态地接收ByteBuf

同样,使用protobuf是将正确的ChannelHandler添加到ChannelPipeline的问题,如清单11.14所示。

public class ProtoBufInitializer extends ChannelInitializer<Channel> {
    private final MessageLite lite;
    
    public ProtoBufInitializer(MessageLite lite) {
        this.lite = lite;
    }

    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        // Adds ProtobufVarint32FrameDecoder to break down frames
        pipeline.addLast(new ProtobufVarint32FrameDecoder());
        // Adds ProtobufEncoder to handle encoding of messages
        pipeline.addLast(new ProtobufEncoder());
        // Adds ProtobufDecoder to decode messages
        pipeline.addLast(new ProtobufDecoder(lite));
        // Adds ObjectHandler to handle the decoded messages
        pipeline.addLast(new ObjectHandler());
    }

    public static final class ObjectHandler
        extends SimpleChannelInboundHandler<Object> {
        @Override
        public void channelRead0(ChannelHandlerContext ctx, Object msg)
            throws Exception {
            // Do something with the object
        }
    }
}                    

在本节中,我们探讨了Netty专用解码器和编码器支持的不同序列化选项:标准JDK序列化,JBoss编组和Google协议缓冲区。

11.7 Summary

Netty提供的编解码器和处理程序可以组合和扩展,以实现非常广泛的处理场景。 此外,它们是经过验证的强大组件,已在许多大型系统中使用。

请注意,我们仅介绍了最常见的示例; API文档提供更广泛的覆盖范围。

在下一章中,我们将研究另一种高级协议,该协议是为了提高Web应用程序的性能和响应能力而开发的:WebSocket。 Netty提供了您需要的工具,可以快速轻松地利用其强大的功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值