本章主要内容:
- EventLoop
- 注册和注销EventLoop
- 通过Netty使用旧的Socket和Channel
Netty提供了一种简单的方式,将在Netty外面创建的Socket和Channel附加给Netty,并将它们的任务转移给Netty。这样就可以无缝继承旧的网络应用,然后一步一步迁移到Netty框架。并且Netty允许你注销一个Channel,停止处理它的IO操作。有时候需要暂停连接并释放资源就可以使用这个特性。
例如你的项目是一个非常受欢迎的社交应用,用户还在不停的增长。目前系统每秒处理了几千条消息或连接,如果还继续增长,那每秒钟处理的消息或连接就会达到上万条。
不过目前系统资源的使用是比较低效的,内存和CPU并没有有效利用,管理层相信通过优化应用不仅能提升系统服务还能够节省下一大笔钱。
这种情况下,系统不能停止工作。要保持功能并且要处理不停增加的数据。这个时候是非常适合使用注册/注销EventLoop的方式。
为了使旧有系统的Socket或Channel可以注册或注销,Netty提供了一种有效且熟练的方式与旧有系统集成,从而成为旧有系统的一部分。下面就会来介绍它们是如何集成的。
一、注册、注销Channel和Socket
前面说过,每一个Channel都需要注册到一个EventLoop上来处理I/O事件。这是在启动器初始化过程中配置好后自动执行的。如下图列出的关系。
不过还有一半没有说到,当关闭Channel的时候它就会从EventLoop上注销掉并释放资源。
前面说过,很多时候我们需要重构遗留项目,也就是要处理java.nio.channels.SocketChannel或者Java.nio.channels.Channel的实现。
好消息就是用Netty也可以使用它们,通过包装Java.nio.channels.Channel就可以将它注册到EventLoop上。也就是说你可以在遗留的项目中使用Netty的功能。而且集成方式很简单,如下面的代码。
java.nio.channels.SocketChannel mySocket = ...;
mySocket.open();
//使用Netty的SocketChannel包装JDK的SocketChannel
SocketChannel ch = new NioSocketChannel(mySocket);
EventLoopGroup group = ...;
//注册到EventLoop
ChannelFuture registerFuture = group.register(ch);
//注销Channel
ChannelFuture deregisterFuture = ch.deregister();
当然Netty的功能不仅能用在
java.nio.channels.Channel的实现上,还可以用在JDK的Socket上。用法也基本上一样,有一个区别就是包装JDK的Socket要使用OioSocketChannel。这个道理很简单,因为JDK的Socket是阻塞型的,所以要使用Netty的阻塞型OioSocketChannel进行包装。
Socket mySocket = new Socket("www.manning.com", 80);
//使用OioSocketChannel包装
SocketChannel ch = new OioSocketChannel(mySocket);
EventLoopGroup group = ...;
//注册到EventLoop
ChannelFuture registerFuture = group.register(ch);
//从EventLoop上注销
ChannelFuture deregisterFuture = ch.deregister();
JDK的Channel或Socket使用Netty时有两个很重要的事情要记得。
- 一旦将Channel或Socket包装后注册到Netty的EventLoop上之后,除了Netty操作它们之外就不能再有其他地方操作它们了。否则就会发生一些奇怪的问题。
- EventLoop.register(…)和Channel.deregister(…)是非阻塞异步的,也就是说它的操作并不是立即完成的。因为方法返回的是ChannelFuture。所以如果你需要在注册或注销完成之后进行一些操作,就要通过添加ChannelFutureListener确认完成或同步等待返回的ChannelFuture已经完成。具体是用哪一个取决于是否要阻塞当前线程,一般来说建议使用ChannelFutureListener,因为阻塞当前线程并不是一个好的方案。
这一小节我们学习了怎么将JDK的Channel或Socket注册到Netty的EventLoop上或注销掉。不过有时候我们也需要使用Netty的Channel的注销或注册功能。
因为Netty提供的Channel注册到EventLoop上和注销的功能,所以开发者也拥有强大灵活的方式处理Channel的事件。但是这些功能实际的用处在哪里,应该使用在什么场景上?
有一些场景需要使用到这些功能,为了减少篇幅我们这边介绍两个最常见的场景。
二、暂停IO处理
最常见的场景就是你需要暂停掉指定Channel的IO处理和事件。例如,为了保证应用不会内存溢出或崩溃,丢失一些消息不是什么大事的情况。这个时候可以先暂停Channel的IO处理,然后回收一些应用垃圾,待系统稳定后再将Channel重新注册,然后继续处理它的消息。
最好的方式就是将Channel从EventLoop上注销掉,这可以有效的停止掉Channel的IO事件处理。
一旦你觉得可以重新处理IO了,只需要将Channel重新注册到EventLoop上,就可以继续处理了。也就是说这个Channel可以继续从Socket上读取数据,也可以写数据到对端。除此之外你也可以继续提交任务给EventLoop执行,如下图。
下面这段代码会在Channel第一次收到消息后注销掉。
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group).channel(NioSocketChannel.class)
.handler(new SimpleChannelInboundHandler<ByteBuf>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf byteBuf) throws Exception {
ctx.pipeline().remove(this);
ctx.deregister();
}
});
ChannelFuture future = bootstrap.connect(new InetSocketAddress("www.manning.com", 80)).sync();
//这里做一些耗时的操作
...
Channel channel = future.channel();
//重新注册到EventLoop上
group.register(channel).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
System.out.println("Channel registered");
} else {
System.err.println("Register Channel on EventLoop failed");
future.cause().printStackTrace();
}
}
});
在
SimpleChannelInboundHandler中我们注销了Channel,然后在后面通过EventLoopGroup.register(…)方法重新注册了Channel,不过是异步的方式。
三、将Channel移到另一个EventLoop
另一个使用注册和注销功能的情况是实时将Channel移到另一个EventLoop上的场景。例如某一个EventLoop比较繁忙,就会把它上面的一些Channel移到其他EventLoop上;又例如要释放某一个EventLoop的资源,就会把这个EventLoop上还在激活状态的Channel移到其他EventLoop。改变Channel的EventLoop一般都是非业务关键数据。
来看看在代码中如何实现迁移功能。
//创建两个EventLoopGroup实例
EventLoopGroup group = new NioEventLoopGroup();
final EventLoopGroup group2 = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group).channel(NioSocketChannel.class)
.handler(new SimpleChannelInboundHandler<ByteBuf>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf byteBuf) throws Exception {
ctx.pipeline().remove(this);
//注销掉
ChannelFuture cf = ctx.deregister();
cf.addListener((ChannelFutureListener) future -> {
//注销成功后注册到另一个EventLoopGroup
group2.register(future.channel());
});
}
});
ChannelFuture future = bootstrap.connect(new InetSocketAddress("www.manning.com", 80));
future.addListener((ChannelFutureListener) channelFuture -> {
if (channelFuture.isSuccess()) {
System.out.println("Connection established");
} else {
System.err.println("Connection attempt failed");
channelFuture.cause().printStackTrace();
}
});
可以看到修改Channel的EventLoop也是很简单的。最需要注意的地方就是
deregister(…)和
register(…)方法是异步的,要通过检查ChannelFuture或添加ChannelFutureListener来确定你的注册或注销操作已经完成。如果不检查可能就会出现重复注册的情况,重复注册就会触发IllegalStateException异常。
四、总结
这一章我们其实就学习了Channel的注销和注册操作。利用这些操作可以暂停Channel的IO处理或者修改Channel的EventLoop。而且可以将JDK的Channel和Socket的项目集成进Netty的功能。对于保持系统的稳定性很有用的,通过先注销掉Channel,然后清理系统资源,再重新注册,不过要保证数据丢失不会对业务造成很大影响。对于旧的项目,通过Netty提供的包装功能,可以以最小的风险,逐步在旧项目中集成Netty的功能。