先说结论,总体感觉很好,比 cpp 上的 nio 框架好。
缘起是一个同学来问 java web 的问题,帮他调了一下,两年没怎么碰过 java web 的东西了,各种生疏,而且在调试的过程中,也再次感觉到 java 这个东西实在不适合做 web,一个是重,ssh 框架一上来就给人压得喘不过气的感觉,新增一个功能,要 spring struts 里面 xml 各种配,访问一下数据库,需要 hibernate 里面一层层的 dao,看着就累,就算不用 ssh,自己写 servlet,也是各种繁琐,而且还不方便调试,做个什么改动,还要重启一下 web 容器,tomcat 也不是什么好东西,本身也复杂,总而言之,在做 web 这个事情上,跟动态脚本语言一比,几乎没有任何优势,唯一的优势可能就是效率?我宁可不要这个效率,反正量上来了最终瓶颈都是卡在数据库这块,逻辑层再快也没用。
不过转念一想到 netty,这个 java 社区里面著名的 nio 框架,想着以前都是只闻其声未见其人,趁着 eclipse 装着,也就是尝试了一把,整体下来的感觉非常好,相比 cpp 上自己的一些 nio 框架和一些自己写的 nio 框架,主要体现在以下一些方面:
- 标准规范,相比起来,cpp 上 nio 框架,品种繁多,挑的人眼花缭乱,libevent libev libuv,连名字都长得差不多,但是里面各种概念各种思想各种实现又有差别,在选择成本上就大了很多,再者这些框架,例如 libevent 来说,虽说是个 cpp 框架,但是 c 的味道还是很重,就显得不够干净利落,java 就好多了,社区中基本就以 netty 为事实上的标准,不要烦心怎么选,闭着眼睛用就行了。
-
概念清晰,相比起来,libevent 之流的框架,概念绕来绕去,cpp 的 nio 框架里面,除了 muduo 算是概念比较清晰的以外,其他的,我觉得不太好上手理解,而对于业务来说,能快速上手,能投入开发,才是王道,我们用一个框架不是为了来学习他的内部概念,而是为了来上线业务的,(虽然说用到后期,理解其内部概念也很重要,不过这是后话了),相比之下,netty 的概念就很清晰,channel,handler 的封装非常明了,这点从样例的 echo server 的代码,甚至从日志中,都能看的出来,先来看日志
2015-5-1 15:53:35 io.netty.util.internal.PlatformDependent <clinit> 信息: Your platform does not provide complete low-level API for accessing direct buffers reliably. Unless explicitly requested, heap buffer will always be preferred to avoid potential system unstability. 2015-5-1 15:53:35 io.netty.handler.logging.LoggingHandler channelRegistered 信息: [id: 0x7d92c9d5] REGISTERED 2015-5-1 15:53:35 io.netty.handler.logging.LoggingHandler bind 信息: [id: 0x7d92c9d5] BIND(0.0.0.0/0.0.0.0:8007) 2015-5-1 15:53:35 io.netty.handler.logging.LoggingHandler channelActive 信息: [id: 0x7d92c9d5, /0.0.0.0:8007] ACTIVE 2015-5-1 15:53:37 io.netty.handler.logging.LoggingHandler logMessage 信息: [id: 0x7d92c9d5, /0.0.0.0:8007] RECEIVED: [id: 0xb07295eb, /127.0.0.1:51027 => /127.0.0.1:8007] 2015-5-1 15:53:37 io.netty.handler.logging.LoggingHandler channelRegistered 信息: [id: 0xb07295eb, /127.0.0.1:51027 => /127.0.0.1:8007] REGISTERED 2015-5-1 15:53:37 io.netty.handler.logging.LoggingHandler channelActive 信息: [id: 0xb07295eb, /127.0.0.1:51027 => /127.0.0.1:8007] ACTIVE 2015-5-1 15:53:38 io.netty.handler.logging.LoggingHandler logMessage 信息: [id: 0xb07295eb, /127.0.0.1:51027 => /127.0.0.1:8007] RECEIVED(4B) +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 31 32 33 0a |123. | +--------+-------------------------------------------------+----------------+ 2015-5-1 15:53:38 io.netty.handler.logging.LoggingHandler logMessage 信息: [id: 0xb07295eb, /127.0.0.1:51027 => /127.0.0.1:8007] WRITE(4B) +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f | +--------+-------------------------------------------------+----------------+ |00000000| 31 32 33 0a |123. | +--------+-------------------------------------------------+----------------+ 2015-5-1 15:53:38 io.netty.handler.logging.LoggingHandler flush 信息: [id: 0xb07295eb, /127.0.0.1:51027 => /127.0.0.1:8007] FLUSH 2015-5-1 15:53:39 io.netty.handler.logging.LoggingHandler exceptionCaught 信息: [id: 0xb07295eb, /127.0.0.1:51027 => /127.0.0.1:8007] EXCEPTION: java.io.IOException: 远程主机强迫关闭了一个现有的连接。 java.io.IOException: 远程主机强迫关闭了一个现有的连接。 at sun.nio.ch.SocketDispatcher.read0(Native Method) at sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:25) at sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:233) at sun.nio.ch.IOUtil.read(IOUtil.java:206) at sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:236) at io.netty.buffer.UnpooledHeapByteBuf.setBytes(UnpooledHeapByteBuf.java:256) at io.netty.buffer.AbstractByteBuf.writeBytes(AbstractByteBuf.java:881) at io.netty.channel.socket.nio.NioSocketChannel.doReadBytes(NioSocketChannel.java:241) at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:119) at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:511) at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:468) at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:382) at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:354) at io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:111) at io.netty.util.concurrent.DefaultThreadFactory$DefaultRunnableDecorator.run(DefaultThreadFactory.java:137) at java.lang.Thread.run(Thread.java:619) 2015-5-1 15:53:39 io.netty.handler.logging.LoggingHandler close 信息: [id: 0xb07295eb, /127.0.0.1:51027 => /127.0.0.1:8007] CLOSE() 2015-5-1 15:53:39 io.netty.handler.logging.LoggingHandler channelInactive 信息: [id: 0xb07295eb, /127.0.0.1:51027 :> /127.0.0.1:8007] INACTIVE 2015-5-1 15:53:39 io.netty.handler.logging.LoggingHandler channelUnregistered 信息: [id: 0xb07295eb, /127.0.0.1:51027 :> /127.0.0.1:8007] UNREGISTERED
这个是拿 nc 当客户端,做的一次连接,可以看到,TCP 连接的整个过程清晰明了,状态机一步步的转换清清楚楚,不看代码只看日志都能把流程理清
下面再来看对应的代码,官方的样例代码在 http://netty.io/4.0/xref/io/ne…,但是其实这个代码写的并不好,对于一个担负着 helloworld 功能的 echo 服务来说,还给他搞什么 SSL 支持,实属画蛇添足,精简过后的代码,可以看这里:
package me.zrj; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; public final class EchoServer { static final int PORT = Integer.parseInt(System.getProperty("port", "8007")); public static void main(String[] args) throws Exception { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup,workerGroup) .channel(NioServerSocketChannel.class) //.option(ChannelOption.SO_BACKLOG, 100) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { ChannelPipeline p = ch.pipeline(); p.addLast(new LoggingHandler(LogLevel.INFO)); p.addLast(new EchoServerHanlder()); } }); ChannelFuture f = b.bind(PORT).sync(); f.channel().closeFuture().sync(); } finally { bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } }
package me.zrj; import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; @Sharable public class EchoServerHanlder extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ctx.write(msg); } @Override public void channelReadComplete(ChannelHandlerContext ctx) { ctx.flush(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { //cause.printStackTrace(); ctx.close(); } }
话说前面的那个 static final int PORT = Integer.parseInt(System.getProperty(“port”, “8007”)); 的写法也真是脱了裤子放屁多此一举,不说也罢了。
- 调试方便,这个得益于 java vm 的跨平台,确实是有好处,相比开发 cpp,由于 linux 终端下没有好用的 IDE,所以开发的时候都是在 windows 下拿 visual studio 来写,完事了还要搭一个自动的同步环境,及时的把 windows 上的改动自动推到 linux 编译机上,再 make,然后才能跑起来,过冒烟测试,没问题了,才能往 svn 上交,来回一折腾,确实不方便,相比之下,java 可以直接在 windows 上写完了,跑起来,看着日志输出,爱怎么调怎么调,小农经济自给自足,说道日志输出,这里又要说两点,一个是虽然说,在多线程并发的环境下,日志几乎是唯一的 debug 手段,但是,在 cpp 上,是想单步也没的单步,拿 gdb 去单步的话,各种命令背的晕,而且想看个 STL 结构也是望洋兴叹基本等同于痴心妄想,visual studio 倒是可以很方便的单步,还带 STL 内容各种展开,但是奈何远水救不了近火,还是歇菜,然后另外一点就是再次要吐槽,cpp 社区里面,连个标准的日志库都没,这点相比 java 简直就是被甩几条街,这个还不止体现在日志这一个环节上,cpp 上连一个 string 的 spilt 都要自己动手才能丰衣足食,对于业务开发来说,要的是开发效率,不造轮子,string spilt 之流,应该是属于基本中的基本了。顺着这个再扯到 profiling 工具,java 不愧是企业级的开发平台,各种周边配套,服务完善,profiling 之类的任务基本就是衣来伸手饭来张口,而 cpp 虽然也有一些所谓的覆盖度,所谓的 profiling,但是奈何年迈,很多都是在没有多进程多线程混用的环境下可以工作的好,要是进程线程关系一复杂,就傻眼了,这点其实也难怪工具,主要是 cpp 里面 tricky 的写法太多,什么静态语法检查的事情,做起来确实是头疼,eclipse 就不一样了,IDE 可以做到对于代码的理解几乎是实时的,哪个地方 death code,哪个 package 是 import 了没用,哪里的 exception 抛了没接,分分钟给你指出来,不用过正式编译环节,很多问题在开发的时候顺手就解决掉了
- 易于部署,打包 JAR,往生产环境上一丢就完事了,java -jar **.jar 命令就跑起来,这年代的 linux 发行版,基本都自带 JRE,方便程度不亚于原生 cpp
- 后期维护,这个主要是涉及到团队协作,由于 java 天生就是一个适合工业化协作生产的语言,所以后期接手维护起来其实真是很有优势,这一点不仅体现在细微处,包括变量命名,缩进,括号,之类的编程规范上,而且还体现在稍微宏观的例如一个包进来了,怎么 decode,怎么业务逻辑处理,怎么回包,怎么日志,怎么落地,这些东西由于都有标准化工具,所以基本上换个不同的人来写,也都是八九不离十,能到达到不同的人写同一份代码的境界的,实现了同一个世界同一个梦想,one world one dream,同一批码农同样的代码的梦想