Netty的ChannelHandler是应用程序中处理最多的。Netty应用至少有一个ChannelHandler参与。
那么ChannelHandler是什么?
我们可以理解为ChannelHandler是一段执行业务逻辑处理数据的代码,它们来来往往的通过ChannelPipeline。
Netty中有两个方向的数据流,入站(ChannelInboundHandler)和出站(ChannelOutboundHandler)之间有一个明显的区别:若数据是从用户应用程序到远程主机则是“出站(outbound)”,相反若数据时从远程主机到用户应用程序则是“入站(inbound)”。
为了使数据从一端到达另一端,一个或多个ChannelHandler将以某种方式操作数据。这些ChannelHandler会在程序的“引导”阶段被添加ChannelPipeline中,并且被添加的顺序将决定处理数据的顺序。ChannelPipeline的作用我们可以理解为用来管理ChannelHandler的一个容器,每个ChannelHandler处理各自的数据(例如入站数据只能由ChannelInboundHandler处理),处理完成后将转换的数据放到ChannelPipeline中交给下一个ChannelHandler继续处理,直到最后一个ChannelHandler处理完成。
上图显示ChannelInboundHandler和ChannelOutboundHandler都要经过相同的ChannelPipeline。
在ChannelPipeline中,如果消息被读取或有任何其他的入站事件,消息将从ChannelPipeline的头部开始传递给第一个ChannelInboundHandler,这个ChannelInboundHandler可以处理该消息或将消息传递到下一个ChannelInboundHandler中,一旦在ChannelPipeline中没有剩余的ChannelInboundHandler后,ChannelPipeline就知道消息已被所有的Handler处理完成了。
当一个ChannelHandler添加到ChannelPipeline中时获得一个ChannelHandlerContext。通常是安全的获得这个对象的引用,但是当一个数据报协议如UDP时这是不正确的,这个对象可以在之后用来获取底层通道,因为要用它来read/write消息,因此通道会保留。也就是说Netty中发送消息有两种方法:直接写入通道或写入ChannelHandlerContext对象。这两种方法的主要区别如下:
传输API的核心是Channel接口,它用于所有出站的操作。Channel接口的类层次结构如下
如上图所示,每个Channel都会分配一个ChannelPipeline和ChannelConfig。ChannelConfig负责设置并存储配置,并允许在运行期间更新它们。传输一般有特定的配置设置,只作用于传输,没有其他的实现。ChannelPipeline容纳了使用的ChannelHandler实例,这些ChannelHandler将处理通道传递的“入站”和“出站”数据。ChannelHandler的实现允许你改变数据状态和传输数据
现在我们可以使用ChannelHandler做下面一些事情:
1.Channel channel = ...
2.//Create ByteBuf that holds data to write
3.ByteBuf buf = Unpooled.copiedBuffer("your data", CharsetUtil.UTF_8);
4.//Write data
5.ChannelFuture cf = channel.write(buf);
6.//Add ChannelFutureListener to get notified after write completes
7.cf.addListener(new ChannelFutureListener() {
8. @Override
9. public void operationComplete(ChannelFuture future) {
10. //Write operation completes without error
11. if (future.isSuccess()) {
12. System.out.println(.Write successful.);
13. } else {
14. //Write operation completed but because of error
15. System.err.println(.Write error.);
16. future.cause().printStacktrace();
17. }
18. }
19.});
Channel是线程安全(thread-safe)的,它可以被多个不同的线程安全的操作,在多线程环境下,所有的方法都是安全的。正因为Channel是安全的,我们存储对Channel的引用,并在学习的时候使用它写入数据到远程已连接的客户端,使用多线程也是如此。
Netty包含的传输实现
Netty自带了一些传输协议的实现,虽然没有支持所有的传输协议,但是其自带的已足够我们来使用。Netty应用程序的传输协议依赖于底层协议,本节我们将学习Netty中的传输协议。
Netty中的传输方式有如下几种:
NIO,io.netty.channel.socket.nio,基于java.nio.channels的工具包,使用选择器作为基础的方法。
OIO,io.netty.channel.socket.oio,基于java.net的工具包,使用阻塞流。
Local,io.netty.channel.local,用来在虚拟机之间本地通信。
Embedded,io.netty.channel.embedded,嵌入传输,它允许在没有真正网络的运输中使用ChannelHandler,可以非常有用的来测试ChannelHandler的实现。
那他们在什么情况下使用呢?
OIO,在低连接数、需要低延迟时、阻塞时使用
NIO,在高连接数时使用
Local,在同一个JVM内通信时使用
Embedded,测试ChannelHandler时使用
ChannelHandler中的方法
Netty定义了良好的类型层次结构来表示不同的处理程序类型,所有的类型的父类是ChannelHandler。ChannelHandler提供了在其生命周期内添加或从ChannelPipeline中删除的方法。
handlerAdded,ChannelHandler添加到实际上下文中准备处理事件
handlerRemoved,将ChannelHandler从实际上下文中删除,不再处理事件
exceptionCaught,处理抛出的异常
上面三个方法都需要传递ChannelHandlerContext参数,每个ChannelHandler被添加到ChannelPipeline时会自动创建ChannelHandlerContext。ChannelHandlerContext允许在本地通道安全的存储和检索值。Netty还提供了一个实现了ChannelHandler的抽象类:ChannelHandlerAdapter。ChannelHandlerAdapter实现了父类的所有方法,基本上就是传递事件到ChannelPipeline中的下一个ChannelHandler直到结束。
ChannelInboundHandler
ChannelInboundHandler提供了一些方法再接收数据或Channel状态改变时被调用。下面是ChannelInboundHandler的一些方法:
channelRegistered,ChannelHandlerContext的Channel被注册到EventLoop;
channelUnregistered,ChannelHandlerContext的Channel从EventLoop中注销
channelActive,ChannelHandlerContext的Channel已激活
channelInactive,ChannelHanderContxt的Channel结束生命周期
channelRead,从当前Channel的对端读取消息
channelReadComplete,消息读取完成后执行
userEventTriggered,一个用户事件被处罚
channelWritabilityChanged,改变通道的可写状态,可以使用Channel.isWritable()检查
exceptionCaught,重写父类ChannelHandler的方法,处理异常
Netty提供了一个实现了ChannelInboundHandler接口并继承ChannelHandlerAdapter的类:ChannelInboundHandlerAdapter。ChannelInboundHandlerAdapter实现了ChannelInboundHandler的所有方法,作用就是处理消息并将消息转发到ChannelPipeline中的下一个ChannelHandler。ChannelInboundHandlerAdapter的channelRead方法处理完消息后不会自动释放消息,若想自动释放收到的消息,可以使用SimpleChannelInboundHandler。
1./**
2. * 实现ChannelInboundHandlerAdapter的Handler,不会自动释放接收的消息对象
3. * @author c.k
4. *
5. */
6.public class DiscardHandler extends ChannelInboundHandlerAdapter {
7. @Override
8. public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
9. //手动释放消息
10. ReferenceCountUtil.release(msg);
11. }
12.}
1./**
2. * 继承SimpleChannelInboundHandler,会自动释放消息对象
3. * @author c.k
4. *
5. */
6.public class SimpleDiscardHandler extends SimpleChannelInboundHandler<Object> {
7. @Override
8. protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
9. //不需要手动释放
10. }
11.}
如果需要其他状态改变的通知,可以重写Handler的其他方法。通常自定义消息类型来解码字节,可以实现ChannelInboundHandler或ChannelInboundHandlerAdapter。有一个更好的解决方法,使用编解码器的框架可以很容的实现。使用ChannelInboundHandler、ChannelInboundHandlerAdapter、SimpleChannelInboundhandler这三个中的一个来处理接收消息,使用哪一个取决于需求;大多数时候使用SimpleChannelInboundHandler处理消息,使用ChannelInboundHandlerAdapter处理其他的“入站”事件或状态改变。
ChannelInitializer用来初始化ChannelHandler,将自定义的各种ChannelHandler添加到ChannelPipeline中。
HTTP是不错的协议,但是如果需要实时发布信息怎么做?有个做法就是客户端一直轮询请求服务器,这种方式虽然可以达到目的,但是其缺点很多,也不是优秀的解决方案,为了解决这个问题,便出现了WebSocket。
WebSocket允许数据双向传输,而不需要请求-响应模式。早期的WebSocket只能发送文本数据,然后现在不仅可以发送文本数据,也可以发送二进制数据,这使得可以使用WebSocket构建你想要的程序。下图是WebSocket的通信示例图:
在应用程序中添加WebSocket支持很容易,Netty附带了WebSocket的支持,通过ChannelHandler来实现。使用WebSocket有不同的消息类型需要处理。下面列表列出了Netty中WebSocket类型:
BinaryWebSocketFrame,包含二进制数据
TextWebSocketFrame,包含文本数据
ContinuationWebSocketFrame,包含二进制数据或文本数据,BinaryWebSocketFrame和TextWebSocketFrame的结合体
CloseWebSocketFrame,WebSocketFrame代表一个关闭请求,包含关闭状态码和短语
PingWebSocketFrame,WebSocketFrame要求PongWebSocketFrame发送数据
PongWebSocketFrame,WebSocketFrame要求PingWebSocketFrame响应
为了简化,我们只看看如何使用WebSocket服务器。客户端使用可以看Netty自带的WebSocket例子。
Netty提供了许多方法来使用WebSocket,但最简单常用的方法是使用WebSocketServerProtocolHandler。看下面代码:
1./**
2. * WebSocket Server,若想使用SSL加密,将SslHandler加载ChannelPipeline的最前面即可
3. * @author c.k
4. *
5. */
6.public class WebSocketServerInitializer extends ChannelInitializer<Channel> {
7.
8. @Override
9. protected void initChannel(Channel ch) throws Exception {
10. ch.pipeline().addLast(new HttpServerCodec(),
11. new HttpObjectAggregator(65536),
12. new WebSocketServerProtocolHandler("/websocket"),
13. new TextFrameHandler(),
14. new BinaryFrameHandler(),
15. new ContinuationFrameHandler());
16. }
17.
18. public static final class TextFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
19. @Override
20. protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
21. // handler text frame
22. }
23. }
24.
25. public static final class BinaryFrameHandler extends SimpleChannelInboundHandler<BinaryWebSocketFrame>{
26. @Override
27. protected void channelRead0(ChannelHandlerContext ctx, BinaryWebSocketFrame msg) throws Exception {
28. //handler binary frame
29. }
30. }
31.
32. public static final class ContinuationFrameHandler extends SimpleChannelInboundHandler<ContinuationWebSocketFrame>{
33. @Override
34. protected void channelRead0(ChannelHandlerContext ctx, ContinuationWebSocketFrame msg) throws Exception {
35. //handler continuation frame
36. }
37. }
38.}
处理空闲连接和超时
处理空闲连接和超时是网络应用程序的核心部分。当发送一条消息后,可以检测连接是否还处于活跃状态,若很长时间没用了就可以断开连接。Netty提供了很好的解决方案,有三种不同的ChannelHandler处理闲置和超时连接:
IdleStateHandler,当一个通道没有进行读写或运行了一段时间后出发IdleStateEvent
ReadTimeoutHandler,在指定时间内没有接收到任何数据将抛出ReadTimeoutException
WriteTimeoutHandler,在指定时间内有写入数据将抛出WriteTimeoutException
最常用的是IdleStateHandler,下面代码显示了如何使用IdleStateHandler,如果60秒内没有接收数据或发送数据,操作将失败,连接将关闭:
1.public class IdleStateHandlerInitializer extends ChannelInitializer<Channel> {
2.
3. @Override
4. protected void initChannel(Channel ch) throws Exception {
5. ChannelPipeline pipeline = ch.pipeline();
6. pipeline.addLast(new IdleStateHandler(0, 0, 60, TimeUnit.SECONDS));
7. pipeline.addLast(new HeartbeatHandler());
8. }
9.
10. public static final class HeartbeatHandler extends ChannelInboundHandlerAdapter {
11. private static final ByteBuf HEARTBEAT_SEQUENCE = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer(
12. "HEARTBEAT", CharsetUtil.UTF_8));
13.
14. @Override
15. public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
16. if (evt instanceof IdleStateEvent) {
17. ctx.writeAndFlush(HEARTBEAT_SEQUENCE.duplicate()).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
18. } else {
19. super.userEventTriggered(ctx, evt);
20. }
21. }
22. }
23.}
开发网络程序过程中,很多时候需要传输结构化对象数据POJO,Java中提供了ObjectInputStream和ObjectOutputStream及其他的一些对象序列化接口。Netty中提供基于JDK序列化接口的序列化接口。
普通的JDK序列化
如果你使用ObjectInputStream和ObjectOutputStream,并且需要保持兼容性,不想有外部依赖,那么JDK的序列化是首选。Netty提供了下面的一些接口,这些接口放在io.netty.handler.codec.serialization包下面:
CompatibleObjectEncoder
CompactObjectInputStream
CompactObjectOutputStream
ObjectEncoder
ObjectDecoder
ObjectEncoderOutputStream
ObjectDecoderInputStream