Netty和Mina是Java世界非常知名的通讯框架。它们都出自同一个作者,Mina诞生略早,属于Apache基金会,而Netty开始在Jboss名下,后来出来自立门户netty.io。关于Mina已有@FrankHui的Mina系列文章,我正好最近也要做一些网络方面的开发,就研究一下Netty的源码,顺便分享出来了。 Netty目前有两个分支:4.x和3.x。4.0分支重写了很多东西,并对项目进行了分包,规模比较庞大,入手会困难一些,而3.x版本则已经被广泛使用。
Netty团队大概从3.3.0开始,移到netty.io
从4开始,Netty团队做了模块依赖的优化,像Grizzly一样,分离出很多功能独立的Package。比方说,你希望使用Netty的buffer组件,只需简单依赖这个包就好了。不过本着严谨的态度,我们还是来看下netty-all这个一揽子包里究竟都有哪些依赖,
<dependencies> <dependency> <groupId>io.nettygroupId> <artifactId>netty-handlerartifactId> <version>4.0.23.Finalversion> dependency> dependencies>
Netty3只保证 upstream事件在IO线程里执行,但是所有的downstream事件会被调用线程处理,
它可能是IO线程,也可能是用户自定义线程,这就带来了一个问题,用户需要小心地处理同步操作。
除此之外,还会面临线程上下文切换的风险,设想一下,你在write的时候遇到了异常,
转而触发exceptionCaught,但这是一个upstream事件,怎么办?
Netty4的线程模型则不存在此类问题,因为所有的操作都被保证在同一个EventLoop里的同一个Thread完成。
也就是说Netty4不存在并发访问 ChannelHandler,当然这个前提是你没有给该handler打上Sharable注解。
同时它也能保证 happens-before关系,所以你也没必要在 ChannelHandler声明volatile field。
用户可以指定自己的 EventExecutor来执行特定的 handler。
通常情况下,这种EventExecutor是单线程的,
当然,如果你指定了多线程的 EventExecutor或者 EventLoop,线程sticky特性会保证,
除非出现 deregistration,否则其中的一个线程将一直占用。
如果两个handler分别注册了不同的EventExecutor,这时就要注意线程安全问题了。
Netty4的线程模型还是有很多可以优化的地方,比方说目前Eventloop对channel的处理不均等问题,而这些问题都会在Netty 5里面优化掉,
Netty 4
这里就产生了两个问题:
其一,channelRegistered and channelUnregistered 不等价于 channelOpen and channelClosed,它是Netty4新引入的状态为了实现Channel的dynamic registration, deregistration, and re-registration。
第二, 既然是合并,那原先针对channelOpen的方法如何迁移?简单来做,可以直接迁移到替代方法里面。
1. ChannelPipelineFactory ----> ChannelInitializer
这里需要注意的是,ChannelPipeline的创建方式发生了变化,原先是这么玩的,
ChannelPipeline cp = Channels.pipeline();现在得这么玩
ChannelPipeline cp = ch.pipeline();
用Netty团队的话来说就是:
“Please note that you don't create a new ChannelPipeline by yourself. After observing many use cases reported so far, the Netty project team concluded that it has no benefit for a user to create his or her own pipeline implementation or to extend the default implementation. Therefore, ChannelPipeline is not created by a user anymore. ChannelPipeline is automatically created by a Channel.”
2. SimpleChannelHandler ----> ChannelDuplexHandler
之前是这么玩的
public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e)
throws Exception
{
if (e instanceof ChannelStateEvent) {
ChannelStateEvent cse = (ChannelStateEvent) e;
switch (cse.getState()) {
case OPEN:
if (Boolean.TRUE.equals(cse.getValue())) {
// connect
channelCount.incrementAndGet();
allChannels.add(e.getChannel());
}
else {
// disconnect
channelCount.decrementAndGet();
allChannels.remove(e.getChannel());
}
break;
case BOUND:
break;
}
}
if (e instanceof UpstreamMessageEvent) {
UpstreamMessageEvent ume = (UpstreamMessageEvent) e;
if (ume.getMessage() instanceof ChannelBuffer) {
ChannelBuffer cb = (ChannelBuffer) ume.getMessage();
int readableBytes = cb.readableBytes();
// compute stats here, bytes read from remote
bytesRead.getAndAdd(readableBytes);
}
}
ctx.sendUpstream(e);
}
public void handleDownstream(ChannelHandlerContext ctx, ChannelEvent e)
throws Exception
{
if (e instanceof DownstreamMessageEvent) {
DownstreamMessageEvent dme = (DownstreamMessageEvent) e;
if (dme.getMessage() instanceof ChannelBuffer) {
ChannelBuffer cb = (ChannelBuffer) dme.getMessage();
int readableBytes = cb.readableBytes();
// compute stats here, bytes written to remote
bytesWritten.getAndAdd(readableBytes);
}
}
ctx.sendDownstream(e);
}
改成ChannelDuplexHandler之后,我只需要重写read和write方法,来完成同样的功能。
1. 通过下面的代码来完成Channel的限流
ctx.channel().setReadable(false);//Before ctx.channel().config().setAutoRead(false);//After2. TCP参数优化
// Before: cfg.setOption("tcpNoDelay", true); cfg.setOption("tcpNoDelay", 0); // Runtime ClassCastException cfg.setOption("tcpNoDelays", true); // Typo in the option name - ignored silently // After: cfg.setOption(ChannelOption.TCP_NODELAY, true); cfg.setOption(ChannelOption.TCP_NODELAY, 0); // Compile error3. 单元测试经常用到的CodecEmbedder类已经变名为EmbeddedChannel
@Test
public void testMultipleLinesStrippedDelimiters() {
EmbeddedChannel ch = new EmbeddedChannel(new DelimiterBasedFrameDecoder(8192, true,
Delimiters.lineDelimiter()));
ch.writeInbound(Unpooled.copiedBuffer("TestLine\r\ng\r\n", Charset.defaultCharset()));
assertEquals("TestLine", releaseLater((ByteBuf) ch.readInbound()).toString(Charset.defaultCharset()));
assertEquals("g", releaseLater((ByteBuf) ch.readInbound()).toString(Charset.defaultCharset()));
assertNull(ch.readInbound());
ch.finish();
}
4. 简化的关闭操作,以前我是这么玩stop的
if (serverChannel != null) {
log.info("stopping transport {}:{}",getName(), port);
// first stop accepting
final CountDownLatch latch = new CountDownLatch(1);
serverChannel.close().addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
// stop and process remaining in-flight invocations
if (def.getExecutor() instanceof ExecutorService) {
ExecutorService exe = (ExecutorService) getExecutor();
ShutdownUtil.shutdownExecutor(exe, "dispatcher");
}
latch.countDown();
}
});
latch.await();
serverChannel = null;
}
// If the channelFactory was created by us, we should also clean it up. If the
// channelFactory was passed in by Bootstrap, then it may be shared so don't clean it up.
if (channelFactory != null) {
ShutdownUtil.shutdownChannelFactory(channelFactory, bossExecutor, ioWorkerExecutor,allChannels);
}
}
现在我得这么玩
public void stop() throws InterruptedException {
// Wait until the server socket is closed.
channelFuture.channel().closeFuture().syncUninterruptibly();
bossGroup.shutdownGracefully().syncUninterruptibly();
workerGroup.shutdownGracefully().syncUninterruptibly();
}
5. 编解码命名改变
FrameDecoder ----> ByteToMessageDecoder
OneToOneEncoder ----> MessageToMessageEncoder
OneToOneDecoder ----> MessageToMessageDecoder
6. 心跳逻辑优化,之前我是这么玩的
cp.addLast("idleTimeoutHandler", new IdleStateHandler(getTimer(), getClientIdleTimeout().toMillis(), NO_WRITER_IDLE_TIMEOUT, NO_ALL_IDLE_TIMEOUT, TimeUnit.MILLISECONDS)); cp.addLast("heartbeatHandler", new HeartbeatHandler());
其中HeartbeatHandler 继承了IdleStateAwareChannelHandler。在Netty4里,IdleStateAwareChannelHandler已经去除,但 IdleStateHandler类还存在,所以我会这么玩
cp.addLast("idleTimeoutHandler", new IdleStateHandler( NO_WRITER_IDLE_TIMEOUT, NO_WRITER_IDLE_TIMEOUT, NO_ALL_IDLE_TIMEOUT, TimeUnit.MILLISECONDS)); cp.addLast("heartbeatHandler", new HeartbeatHandler());
其中,HeartbeatHandler 继承了ChannelInboundHandlerAdapter。具体的实现逻辑这里就不贴出来了。再啰嗦几句,很多同学喜欢自己启线程去做心跳逻辑,我是不推荐这种方式的。利用Netty的链路空闲检测机制可以很好的完成这个功能,能更好地配合Netty线程模型和异常捕获机制。自己定制,处理不好,会带来很大的线上隐患。
这篇文章简单记录了升级过程中遇到的一些比较higher的话题,配上代码,希望能更好的重现整个升级思路和过程,也希望能给大家带来帮助。如果你在升级过程中遇到了问题,欢迎留言交流。最后,祝玩的开心~
不像Netty3.x和4.x之间的变化,5.x没有那么大的变化,不过也取得了其简化设计中的一些突破性进展.。我们力求尽可能平滑的从4.x版本过度到5.x版本,如果你在迁移过程中遇到任何问题,请告知我们。