netty学习笔记
官方文档:https://netty.io/3.8/guide/
文档里的例子举的不错,循序渐进、深入浅出。
公司里一个老项目用的是netty 3.x 版本,索性就拿3.x 版本来学习了。
1. 前言
1.1 问题
现如今,人们使用通用应用程序或库进行通信。例如,我们经常用Http client库从web server获取信息,并经由web服务发起远程过程调用(RPC)。
然而,通用协议或其实现不能很好地扩展。就像我们不会用通用HTTP server去传大文件、email信息和诸如财经消息、多媒体游戏数据这样的近实时消息。这需要高度优化的专用协议实现。例如,你可能想实现一个HTTP server,它专门为AJAX聊天应用、流媒体、大文件做过优化。你甚至可能想设计和实现全新协议以适应你的需求。
另一种不可避免的情况是,你不得不处理旧的专有协议,去和旧系统互操作。这种情况下最重要的是,在不牺牲最终程序的稳定性和性能的条件下,实现该协议的速度。
1.2 解决方案
Netty致力于提供异步事件驱动的网络应用框架,以快速开发可维护、可扩展、高性能的服务器和客户端。
换句话说,Netty是一个NIO客户端服务器框架,可以快速开发网络应用程序。它极大地简化、流水线化了诸如TCP、UDP套接字之类的网络编程。
快速易上手,不意味着损失程序的可维护性和性能。Netty经过精心设计,结合了许多协议(FTP、SMTP、HTTP以及基于二进制、文本的协议)的实现经验。Netty成功找到了轻松开发与性能、稳定性、灵活性之间的平衡。
Netty有其他一些竞品,你可能想知道Netty与众不同之处。答案是Netty的底层哲学。Netty旨在从一开始就在API和实现方面为你提供最舒适的体验。这不是有形的东西,但是你会意识到,当你阅读本指南并把玩Netty时,这种哲学将使你愉悦。
2. 入门
本章将通过简单是示例介绍Netty的核心构造,以使你快速入门。之后,你将能够立即在Netty基础上编写客户端和服务器。
如果你更喜欢自上而下地学习新事物,你可以先看第三章,再回头看第二章。
2.1 入门准备
跑通本章示例的最低要求有两个:Netty(3.x)以及 jdk 1.5(或以上)。
阅读时,你可能对本章介绍的类有更多疑问。如果想进一步了解,请参考API文档。
2.2 Discard Server
世界上最简单的协议不是“Hello World”,而是“DIACARD”。它是这样一个协议,丢弃任何收到的数据,没有任何响应。
实现“DIACARD”协议,唯一要做的事情就是忽略所有收到的数据。直接上handler实现,处理Netty产生的IO事件。
package org.jboss.netty.example.discard;
public class DiscardServerHandler extends SimpleChannelHandler { // 1 @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) { // 2 } @Override public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) { // 3 e.getCause().printStackTrace(); Channel ch = e.getChannel(); ch.close(); } }
(1) DiscardServerHandler继承SimpleChannelHandler,后者是ChannelHandler的一个实现。SimpleChannelHandler提供多种事件处理方法,供你覆写。现在,继承SimpleChannelHandler就足够了,啥都不用你实现。
(2) 这里覆写了messageReceived方法。每当从客户端接收到新数据时,该方法以MessageEvent实参调用,参数包含了接收到的数据。本例中,通过空实现来忽略接收到的数据。
(3) 当Netty由于网络IO错误抛异常,或者某个handler实现处理event抛异常的时候,exceptionCaught方法会被调用。大部分场景,应该打个日志、关闭相关channel,当然也可以不同。比如,你想关闭连接之前,发送个带错误码的response之类的。
截止目前,一切看起来都不错。我们已经实现了DISCARD server的前半部分。剩下的就是写个main方法,启动server。
package org.jboss.netty.example.discard; import java.net.InetSocketAddress; import java.util.concurrent.Executors; public class DiscardServer { public static void main(String[] args) throws Exception { ChannelFactory factory = new NioServerSocketChannelFactory( // 4 Executors.newCachedThreadPool(), Executors.newCachedThreadPool()); ServerBootstrap bootstrap = new ServerBootstrap(factory); // 5 bootstrap.setPipelineFactory(new ChannelPipelineFactory() { // 6 public ChannelPipeline getPipeline() { return Channels.pipeline(new DiscardServerHandler()); } }); bootstrap.setOption("child.tcpNoDelay", true); // 7 bootstrap.setOption("child.keepAlive", true); bootstrap.bind(new InetSocketAddress(8080)); // 8 } }
(4) ChannelFactory是创建和管理Channel及相关资源的工厂。它处理所有IO请求并执行IO以生成ChannelEvents。Netty提供多种ChannelFactory实现。本例中我们想实现一个服务端应用,因此选用了NioServerSocketChannelFactory。另外要声明的是,它并没有创建IO线程。它会从你在构造函数中指定的线程池中获取线程,以使你更好地控制线程管理方式,例如带安全管理的服务。
(5) ServerBootstrap是设置服务器的辅助类。你可以设置server直接使用一个Channel。但请注意,这是一个繁琐的过程,大多数情形下,你不需要这么做。
(6) 此处配置了ChannelPipelineFactory。每当服务器接受一个新连接,这个指定的ChannelPipelineFactory就会创建一个新的ChannelPipeline。新的pipeline会包含DiscardServerHandler。随着应用程序变得复杂,你可能会往pipeline里添加更多的handlers,并最终将此(实现了ChannelPipelineFactory接口的)匿名类提取到顶级类中。
(7) 可以设置针对特定Channel实现的参数。我们写了一个TCP/IP服务,所以允许设置socket选项,如tcpNoDelay、keepAlive等。请注意所有选项都添加了“child.”前缀。这意味着,这些选项会被用在accepted channels,而不是ServerSocketChannel。设置ServerSocketChannel的选项,要这么写:
bootstrap.setOption("reuseAddress", true);
(8) 准备出发。剩下的事情是绑定端口、启动服务。这里我们绑定了本机所有网卡的8080端口。bind方法可以多次调用,传不同的address。
恭喜你!你已经在Netty的基础上,完成了你的第一个服务。
2.3 细查Received Data
既然我们已经写了一个服务器,就要测试下它是否工作。最简单的测试方式是使用telnet命令。比如,你可以在终端输入“telnet localhost 8080”,随便打点什么。
然而,我们就敢说服务工作正常吗?它是个discard服务,所以我们没法知道。你什么响应也get不到。为了证明它可以工作,我们对服务稍加修改,打印接收到的数据。
我们已经知道数据到达时,会生成MessageEvent,继而messageReceived函数会被调用。那我们就在messageReceived方法里加点代码吧。
@Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) { ChannelBuffer buf = (ChannelBuffer) e.getMessage(); // 9 while(buf.readable()) { System.out.println((char) buf.readByte()); System.out.flush(); } }
(9) 假设socket里传输的消息类型总是ChannelBuffer是安全的。在Netty里,ChannelBuffer是储存字节序列的基本数据结构。和NIO ByteBuffer很像,但是更简单灵活。例如,Netty允许创建复合ChannelBuffer以组合多个ChannelBuffer,这样可以减少内存拷贝。尽管它与NIO ByteBuffer非常相似,但强烈建议你参考API手册。学习如何正确使用ChannelBuffer是轻松使用Netty的关键步骤。
再运行一遍telnet命令,你会看到服务打印了接收到的数据。
Discard服务的整个源代码就在 org.jboss.netty.example.discard 包里。
2.4 Echo Server
目前,我们已经不做响应的前提下,把接收到的数据消费掉了。但作为一个服务器,毕竟是要有响应的。通过实现ECHO协议(收到什么,就原封不动发回去),来学习一下如何给client发送response消息吧。
它和前面实现的DISCARD唯一的区别就是把接收数据发回去,而不是仅仅打印出来。因此,改改messageReceived方法就够了。
@Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) { Channel ch = e.getChannel(); // 10 ch.write(e.getMessage()); }
(10) ChannelEvent对象有关联Channel的引用。这里,返回的Channel代表了接收到MessageEvent的连接。我们可以获取到这个Channel,调用write方法去写点什么给远端。
再运行一遍telnet命令,你会看到服务把你发过去的内容原封不动发回来了。
Echo服务的整个源代码就在 org.jboss.netty.example.echo 包里。
2.5 Time Server
本节要实现的是TIME协议。和之前的例子不同,它发送一个32-bit整数,不接受任何请求,发送完消息就断开连接。本例中你将学到如何构造、发送一个消息,以及发送完成后关闭连接。
因为我们要在连接建立以后立刻发送消息、忽略接收到的数据,所以这次就不能用messageReceived了。而是要覆写channelConnected方法。下面是实现代码:
package org.jboss.netty.example.time; public class TimeServerHandler extends SimpleChannelHandler { @Override public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) { // 11 Channel ch = e.getChannel(); ChannelBuffer time = ChannelBuffers.buffer(4); // 12 time.writeInt((int) (System.currentTimeMillis() / 1000L + 2208988800L)); ChannelFuture f = ch.write(time); // 13 f.addListener(new ChannelFutureListener() { // 14 public void operationComplete(ChannelFuture future) { Channel ch = future.getChannel(); ch.close(); } }); } @Override public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) { e.getCause().printStackTrace(); e.getChannel().close(); } }
(11) 前面解释过,连接建立的时候,channelConnected方法会被调起来。我们就写一个代表当前时间的32-bit整数进去。
(12) 为发送一条新消息,需要分配一个新buffer来容纳这条消息。要写32-bit整数,因此需要ChannelBuffer的容量是4字节。ChannelBuffers辅助类用于分配新buffer。除了buffer方法,ChannelBuffers提供了许多ChannelBuffer相关的有用方法。详细信息,请参考API文档。
另一方面,对ChannelBuffer使用static import是个好主意。
import static org.jboss.netty.buffer.ChannelBuffers.*; ... ChannelBuffer dynamicBuf = dynamicBuffer(256); ChannelBuffer ordinaryBuf = buffer(1024);
(13) 像往常一样,来写一下constructed message。
不过,等一下,flip哪去了?在NIO里,我们发送消息之前,不是要调用ByteBuffer.flip()吗?ChannelBuffer没有这样的一个方法,因为它有两个指针:一个读、一个写。向ChannelBuffer写数据的时候,写偏移增加,但读偏移保持不变。读偏移和写偏移分别代表了消息的开始、结束位置。
相反,NIO buffer没有提供一个简洁的方法来定位消息的起止,只能调用flip方法。忘了调用flip方法,你就会有麻烦,会发出错误的数据。Netty世界里没有这种错误,因为它为不同的操作类型提供了不同的索引。你会发现它让你的生活变得简单——没有flippling out的生活。
另外值得说的一点,write方法返回一个ChannelFuture。ChannelFuture代表一个还没有发生的IO操作。它意味着任何请求的操作可能还没有执行,因为在Netty里,所有的操作都是异步的。例如,下面的代码可能会在发送消息之前就把连接关闭了:
Channel ch = ...;
ch.write(message);
ch.close();
因此,你需要在write方法返回的ChannelFuture通知你写操作完成时,再调用close方法。请注意,close方法也不是立刻关闭连接,而是也返回了一个ChannelFuture。
(14) 写请求完成后,我们如何被通知到呢?很简单,给返回的ChannelFuture添加一个ChannelFutureListener。这里我们创建了一个匿名ChannelFutureListener,在操作完成以后关闭Channel。
或者,你也可以用一个预定义的listener来简化代码:
f.addListener(ChannelFutureListener.CLOSE);
测试我们的Time服务是否正常工作,可以用Unix命令“rdate”:
$ rdate -o <port> -p <host>
port是你在main方法里指定的端口号,host是localhost。
2.6 Time Client
不像DISCARD和ECHO服务,我们需要一个client来解析TIME协议。本节,我们讨论如何确保服务正常工作,学习如何用Netty写一个client。
用Netty写server还是client,最大也是唯一的区别是需要不同的Bootstrap和ChannelFactory。请看如下代码:
package org.jboss.netty.example.time; import java.net.InetSocketAddress; import java.util.concurrent.Executors; public class TimeClient { public static void main(String[] args) throws Exception { String host = args[0]; int port = Integer.parseInt(args[1]); ChannelFactory factory = new NioClientSocketChannelFactory( // 15 Executors.newCachedThreadPool(), Executors.newCachedThreadPool()); ClientBootstrap bootstrap = new ClientBootstrap(factory); // 16 bootstrap.setPipelineFactory(new ChannelPipelineFactory() { public ChannelPipeline getPipeline() { return Channels.pipeline(new TimeClientHandler()); } }); bootstrap.setOption("tcpNoDelay", true); // 17 bootstrap.setOption("keepAlive", true); bootstrap.connect(new InetSocketAddress(host, port)); // 18 } }
(15) NioClientSocketChannelFactory,取代NioServerSocketChannelFactory,创建client-side Channel。
(16) ClientBootstrap对应ServerBootstrap。
(17) 请注意这里没有“child.”前缀。Client端SocketChannel没有parent。
(18) 调用connect方法,而不是bind方法。
如你所见,和server端的启动没啥大区别。ChannelHandler实现呢?接收一个32-bit整数,翻译成可读格式,打印时间,然后关闭连接:
package org.jboss.netty.example.time; import java.util.Date; public class TimeClientHandler extends SimpleChannelHandler { @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) { ChannelBuffer buf = (ChannelBuffer) e.getMessage(); long currentTimeMillis = buf.readInt() * 1000L; System.out.println(new Date(currentTimeMillis)); e.getChannel().close(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) { e.getCause().printStackTrace(); e.getChannel().close(); } }
看起来很简单,和server端例子没啥区别。然而,这个handler偶尔会罢工,抛出 IndexOutOfBoundsException。我们下节讨论具体原因。
2.7 处理流式传输
2.7.1 Socket Buffer警告
TCP/IP这种基于流式的传输,会将接收到的数据存储在socket buffer里。不幸的是,buffer里存的不是packet序列,而是字节序列。意味着,你发两个独立的包,OS不会把它们当成两个独立的包,而是当成一堆字节。因此,不能保证你读到的和远端写的一模一样。例如,假设TCP/IP协议栈收到3个packets:
+-----+-----+-----+
| ABC | DEF | GHI |
+-----+-----+-----+
由于流式协议的一般属性,很有可能你在程序里读到的是如下的分段形式:
+----+-------+---+---+
| AB | CDEFG | H | I |
+----+-------+---+---+
因此,接收端,不管它是server还是client,应该将接收到的数据整理到一个或多个有意义的帧中,以使应用程序逻辑易于理解。在上面的示例中,接收数据应该以下面的格式分帧:
+-----+-----+-----+
| ABC | DEF | GHI |
+-----+-----+-----+
2.7.2 解决方案一
现在我们回到TIME client的例子。遇到了同样的问题,32-bit整数是一个很小的数据量,不太可能被频繁分段。然而,问题是有这个可能性,而且可能性会随着流量的增大而增大。
一种简单的解决方案是创建一个内部累积buffer,等4个字节都到齐。下面的TimeClientHandler是修改过的版本:
package org.jboss.netty.example.time; import static org.jboss.netty.buffer.ChannelBuffers.*; import java.util.Date; public class TimeClientHandler extends SimpleChannelHandler { private final ChannelBuffer buf = dynamicBuffer(); // 19 @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) { ChannelBuffer m = (ChannelBuffer) e.getMessage(); buf.writeBytes(m); // 20 if (buf.readableBytes() >= 4) { // 21 long currentTimeMillis = buf.readInt() * 1000L; System.out.println(new Date(currentTimeMillis)); e.getChannel().close(); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) { e.getCause().printStackTrace(); e.getChannel().close(); } }
(19) 动态缓冲(ChannelBuffer),需要的时候可以增加容量。在你不知道消息长度的时候,很有用。
(20) 首先,所有收到的数据都被累积到buf。
(21) 然后,handler必须检查数据是否足够(本例中是4字节),进行实际的业务逻辑。否则,Netty会在更多数据到达的时候,再次调用messageReceived。终究,4字节是会被集齐的。
2.7.3 解决方案二
尽管解决方案一已经解决了TIME client遇到的问题,但修改后的handler代码并不是那么整洁。假设一个更复杂的协议来了,很多字段,有的字段还是变长的。你的ChannelHandler实现很快就会变得不可维护。
你可能已经注意到,可以给ChannelPipeline添加不止一个ChannelHandler,因此你可以把整个ChannelHandler拆成多个模块,以降低程序复杂度。例如,你可以把TimeClientHandler拆成两个handler:
- TimeDecoder处理碎片问题
- 最开始的那个简单版本TimeClientHandler
很幸运,Netty提供了一个可扩展的类,可以帮助你开箱即用地编写第一个类:
package org.jboss.netty.example.time; public class TimeDecoder extends FrameDecoder { // 22 @Override protected Object decode( ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) { // 23 if (buffer.readableBytes() < 4) { return null; // 24 } return buffer.readBytes(4); // 25 } }
(22) FrameDecoder是ChannelHandler的一个实现,可以轻松处理碎片化问题。
(23) 新数据一到,FrameDecoder就调用decode方法,而且内部维护了一个累积buffer。
(24) 如果返回null,代表数据还不够。当有足够数据的时候,FrameDecoder会被再次调用。
(25) 如果返回非null值,意味着decode方法已经成功decode了一条message。FrameDecoder将忽略内部累积buffer的读部分。请记住,你不需要decode多条消息。FrameDecoder将持续调用decode,直到它返回null。
既然有了另外一个handler要插入pipeline,我们需要修改一下TimeClient里的ChannelPipelineFactory实现:
bootstrap.setPipelineFactory(new ChannelPipelineFactory() { public ChannelPipeline getPipeline() { return Channels.pipeline( new TimeDecoder(), new TimeClientHandler()); } });
如果你是个爱冒险的人,你可能想尝试下ReplayingDecoder,它可以更大程度上简化decoder。那么你就得参考API手册了。
package org.jboss.netty.example.time; public class TimeDecoder extends ReplayingDecoder<VoidEnum> { @Override protected Object decode( ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer, VoidEnum state) { return buffer.readBytes(4); } }
此外,Netty提供了系列开箱即用的解码器,使你轻松实现大多数协议,避免最终以一个单体的、不可维护的处理逻辑实现而告终。
- org.jboss.netty.example.factorial 二进制协议
- org.jboss.netty.example.telnet 文本协议
2.8 POJO vs. ChannelBuffer
到目前为止,我们看到的例子都是以ChannelBuffer作为协议的主要数据结构的。本节,我们改进TIME协议的client、server实现,用POJO替换ChannelBuffer。
使用POJO的优势在于:通过分离从ChannelBuffer中提取信息的代码,你的程序将更易于维护、易于重用。在TIME client和server例子中,我们只读了32-bit整数,用ChannelBuffer还不是什么大问题。当你实现现实世界中的一个协议的时候,你会发现这中分离是必须的。
首先,我们定义一个新的类型,叫UnixTime。
package org.jboss.netty.example.time; import java.util.Date; public class UnixTime { private final int value; public UnixTime(int value) { this.value = value; } public int getValue() { return value; } @Override public String toString() { return new Date(value * 1000L).toString(); } }
现在我们可以修改TimeDecoder,返回一个UnixTime,而不是ChannelBuffer了。
@Override protected Object decode( ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) { if (buffer.readableBytes() < 4) { return null; } return new UnixTime(buffer.readInt()); // 26 }
(26) FrameDecoder和ReplayingDecoder允许你返回任意类型的实例。如果他们被限制成只返回ChannelBuffer,那我们还得加一个类型转换的handler。
配合这个新版decoder,TimeClientHandler将不再使用ChannelBuffer:
@Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) { UnixTime m = (UnixTime) e.getMessage(); System.out.println(m); e.getChannel().close(); }
简单、优雅,是吧?Server端可以用上同样的技术。更新下TimeServerHandler:
@Override public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) { UnixTime time = new UnixTime(System.currentTimeMillis() / 1000); ChannelFuture f = e.getChannel().write(time); f.addListener(ChannelFutureListener.CLOSE); }
现在缺的就是一个encoder,把UnixTime转回ChannelBuffer。这要比写decoder容易的多,因为不用处理packet碎片了。
package org.jboss.netty.example.time; import static org.jboss.netty.buffer.ChannelBuffers.*; public class TimeEncoder extends SimpleChannelHandler { public void writeRequested(ChannelHandlerContext ctx, MessageEvent e) { // 27 UnixTime time = (UnixTime) e.getMessage(); ChannelBuffer buf = buffer(4); buf.writeInt(time.getValue()); Channels.write(ctx, e.getFuture(), buf); // 28 } }
(27) Encoder覆写writeRequested方法,拦截写请求。请注意这里的MessageEvent参数和messageReceived里指定的参数类型一致,但它们的解释不同。ChannelEvent既可以是上游事件,又可以是下游事件,取决于事件的流向。例如,当调用messageReceived时,MessageEvent是上游事件;当调用writeRequested时,MessageEvent是下游事件。请参考API文档,详查上下游事件的区别。
(28) 一旦完成了POJO到ChannelBuffer的转换,你应该把这个新buffer转发给ChannelPipeline里的先前的ChannelDownstreamHandler。Channels提供了多种辅助方法来生成、发送ChannelEvent。本例中,Channels.write()方法创建一个MessageEvent,发送给ChannelPipeline里的先前的ChannelDownstreamHandler。
另一方面,静态导入Channels是个好主意:
import static org.jboss.netty.channel.Channels.*; ... ChannelPipeline pipeline = pipeline(); write(ctx, e.getFuture(), buf); fireChannelDisconnected(ctx);
最后剩下的任务是在Server端的ChannelPipeline里插入TimeEncoder,这留作一个小练习吧。
2.9 程序退出
如果运行TimeClient,你会注意到应用程序并不退出,什么也不做,hang在那里。观察call stack,你会发现几个IO线程在运行。想要关闭IO线程、整个程序优雅退出,你需要释放ChannelFactory分配的资源。
关闭一个典型网络应用的进程有下面三步:
- 关闭所有server socket,如果有的话
- 关闭所有non-server socket(如client socket、accepted socket),如果有的话
- 释放所有ChannelFactory使用的资源
在TimeClient上应用上面的三步,TimeClient.main()会通过关闭client连接、释放ChannelFactory使用的资源来优雅退出。
package org.jboss.netty.example.time; public class TimeClient { public static void main(String[] args) throws Exception { ... ChannelFactory factory = ...; ClientBootstrap bootstrap = ...; ... ChannelFuture future = bootstrap.connect(...); // 29 future.awaitUninterruptibly(); // 30 if (!future.isSuccess()) { future.getCause().printStackTrace(); // 31 } future.getChannel().getCloseFuture().awaitUninterruptibly(); // 32 factory.releaseExternalResources(); // 33 } }
(29) ClientBootstrap的connect方法返回ChannelFuture,它会通知连接尝试成功还是失败。它还有一个指向Channel的引用(和这个连接相关联)。
(30) 等待返回的ChannelFuture决定连接成功还是失败。
(31) 失败了,打印下失败原因。如果连接没成功、也没被取消,ChannelFuture的getCause方法将返回失败原因。
(32) 现在连接尝试结束了,我们需要等待Channel的closeFuture,直到连接被关闭。每个Channel都有自己的closeFuture,以便于在关闭是获得通知并执行某些操作。
即便连接尝试失败了,closeFuture也会被通知,因为连接失败,Channel自动关闭。
(33) 在这个节点,所有连接关闭。剩下的就是释放ChannelFactory使用的资源。简单地调用releaseExternalResources()。所有资源包括NIO选择子、线程池都会关闭。
关闭client挺简单,那么关闭server呢?你需要解绑端口,关闭所有accepted连接。需要一个数据结构来记录active连接列表,这不是个小任务。幸好,我们有ChannelGroup的解决方案。
ChannelGroup是Java 集合API的一个特殊扩展,代表了一组开放(open)的Channel。如果一个Channel加入了某ChannelGroup,这个Channel在关闭之后,会被自动移出ChannelGroup。你也可以对同组的Channel批量执行某操作。例如,程序退出前关闭组内所有Channel。
为记录追踪所有open的socket,你需要修改TimeServerHandler,把新开的Channel加到全局ChannelGroup(TimerServer.allChannels)里:
@Override public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e) { TimeServer.allChannels.add(e.getChannel()); // 34 }
(34) 是的,ChannelGroup是线程安全的。
现在所有active Channel都被自动管理起来了,关闭server就像关闭client那么容易了:
package org.jboss.netty.example.time; public class TimeServer { static final ChannelGroup allChannels = new DefaultChannelGroup("time-server"); // 35 public static void main(String[] args) throws Exception { ... ChannelFactory factory = ...; ServerBootstrap bootstrap = ...; ... Channel channel = bootstrap.bind(...); // 36 allChannels.add(channel); // 37 waitForShutdownCommand(); // 38 ChannelGroupFuture future = allChannels.close(); // 39 future.awaitUninterruptibly(); factory.releaseExternalResources(); } }
(35) DefaultChannelGroup需要一个名字作为构造函数参数。group-name单纯就是为了做个区分。
(36) bind方法返回一个server端Channel,绑定一个指定的本地地址。在这个Channel上调用close方法,会使其与本地地址解绑。
(37) 任何类型的Channel都可以加到ChannelGroup里,不管它是server端、client端、accepted的。因此,关闭server时,可以一揽子关闭Channel。
(38) waitForShutdownCommand()是一种等待关闭信号的假想(imaginary)的方法。你可以等一个特权client的消息或者JVM shutdown hook。
(39) 你可以在一个ChannelGroup的所有Channel上执行相同操作。这种情况下,关闭所有Channel,意味着绑定的server端Channel全都解绑、accepted connections全都异步关闭。为通知这些异步关闭动作什么时候完成,返回了一个ChannelGroupFuture,作用和ChannelFuture差不多。
2.10 总结
本章,我们快速浏览了Netty,并演示了如何在Netty上编写功能全面的网络应用程序。
接下来的章节将会有更多的细节信息。强烈建议看一下org.jboss.netty.example包里的例子。
我们的社区也等着你的问题和idea,在你的反馈之下,Netty将持续改进。
3. 架构概览
TODO
3.1 数据结构
3.2 异步IO API
3.3 基于事件模型的拦截链模式
3.4 用于快速开发的高级组件
4. FAQ
FAQ节选自StackOverflow。
4.1 何时可以写下游数据
只要你有指向Channel(或ChannelHandlerContext)的引用,你就可以在任何地方、任何线程调用Channel.write()(或者Channels.write())。
当你通过调用Channel.write()或者调用ChannelHandlerContext.sendDownstream(MessageEvent)来触发writeRequested事件时,writeRequested()会被调用。
参考:https://stackoverflow.com/questions/3222134/how-does-downstream-events-work-in-jbosss-netty
4.2 怎样融合我的“阻塞”应用代码和“非阻塞”NioServerSocketChannelFactory
TODO
4.3 假设多个事件可能同时发生,是否需要同步我的 handler 代码
你的ChannelUpstreamHandler会被在同一个线程(例如一个IO线程)里顺序调用,因此一个handler不必担心在前一个上游事件处理完以前,就被新上游事件调起。
然而,下游事件可能由多个线程同时触发。如果你的ChannelDownstreamHandler访问了共享资源或者存储了状态信息,你需要做同步。
参考:https://stackoverflow.com/questions/8655674/is-netty-receive-events-concurrency-how-about-downstream-and-upsream-events
4.4 如何在同一条Channel的handlers之间传递数据
使用ChannelLocal。
// Declare public static final ChannelLocal<int> data = new ChannelLocal<int>(); // Set data.set(e.getChannel(), 1); // Get int a = data.get(e.getChannel());
参考:https://stackoverflow.com/questions/8449663/usage-of-nettys-channellocal