netty 初窥

先说结论,总体感觉很好,比 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 框架,主要体现在以下一些方面:

  1. 标准规范,相比起来,cpp 上 nio 框架,品种繁多,挑的人眼花缭乱,libevent libev libuv,连名字都长得差不多,但是里面各种概念各种思想各种实现又有差别,在选择成本上就大了很多,再者这些框架,例如 libevent 来说,虽说是个 cpp 框架,但是 c 的味道还是很重,就显得不够干净利落,java 就好多了,社区中基本就以 netty 为事实上的标准,不要烦心怎么选,闭着眼睛用就行了。
  2. 概念清晰,相比起来,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”)); 的写法也真是脱了裤子放屁多此一举,不说也罢了。

  3. 调试方便,这个得益于 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 抛了没接,分分钟给你指出来,不用过正式编译环节,很多问题在开发的时候顺手就解决掉了
  4. 易于部署,打包 JAR,往生产环境上一丢就完事了,java -jar **.jar 命令就跑起来,这年代的 linux 发行版,基本都自带 JRE,方便程度不亚于原生 cpp
  5. 后期维护,这个主要是涉及到团队协作,由于 java 天生就是一个适合工业化协作生产的语言,所以后期接手维护起来其实真是很有优势,这一点不仅体现在细微处,包括变量命名,缩进,括号,之类的编程规范上,而且还体现在稍微宏观的例如一个包进来了,怎么 decode,怎么业务逻辑处理,怎么回包,怎么日志,怎么落地,这些东西由于都有标准化工具,所以基本上换个不同的人来写,也都是八九不离十,能到达到不同的人写同一份代码的境界的,实现了同一个世界同一个梦想,one world one dream,同一批码农同样的代码的梦想

Leave a Reply

Your email address will not be published. Required fields are marked *