《Netty权威指南》在网上的评价不是很高,尤其是第一版,第二版能稍好些?入手后快速翻看了大半本,不免还是想对《Netty权威指南(第二版)》吐槽一下:
不管怎样,如果你是网络通信或后台中间件的入门者,尤其是Java程序员,那么这本书还是值得入手的。尤其是书中对I/O模型、协议解析、可靠性等方面的点拨还是会让你有很多收获的。好了吐槽就到这了,以下就是《Netty权威指南(第二版)》的重点摘录,抽掉了水分,所有干货都在这里了。
Linux从select -> poll -> epoll机制。简要说epoll的优点就是:从主动轮询+线性扫描变为被动事件通知,mmap避免到用户态的拷贝,更加简单的API。
Java方面呢,JDK 1.3之前只有阻塞I/O,到1.4加入了NIO。在JDK 1.5 update 10和Linux 2.6以上版本,JDK使用epoll替换了select/poll。1.7加入了AIO。
阻塞BIO是我们最常见的一种形式,就不详细说了。
伪异步I/O利用阻塞I/O的Acceptor+线程池实现的是伪异步I/O,它只是对同步阻塞I/O在系统资源方面使用方面做了“一小点”的优化(重用了线程),但是 它没法从根本上解决同步I/O导致的通信线程阻塞问题。
TCP/IP知识复习:当消息接收方处理缓慢时,将不能及时从TCP缓冲区读取数据,这将会导致发送方的TCP window size不断变小直到为0。此时双方处于Keep-Alive状态,发送方将不能再向TCP缓冲区写入消息。如果使用的是同步阻塞I/O,write操作将无限期阻塞直到window size大于0或发生I/O异常。
非阻塞NIO的特点是:
NIO提供了非阻塞的读写操作,相比于BIO的确是异步的,因此从这个角度我们可以说NIO是异步非阻塞的。然而如果严格按照UNIX网络编程模型定义的话,NIO并不能算是异步的,因为当事件完成时不是由系统触发回调函数,而是需要我们不断轮询。
AIO才是真正的异步I/O:NIO只是实现了读写操作的非阻塞,但它还是要靠轮询而非事件通知(尽管前面说过JDK 1.5里升级为epoll,但上层API还是轮询没有变化)。说它是异步的其实就是想说它是非阻塞的。JDK 1.7 NIO 2中提供的AIO才是真正的异步I/O。
使用原生NIO开发的特点就是功能开发相对容易,但后续的可靠性方面的工作量非常大,需要我们自己处理如断连重连、半包读写、网络拥堵等问题。并且,NIO中还可能有bug,如“臭名昭著”的Selector空轮询导致CPU使用率100%(大学做大作业就碰到过这个问题,当时还纳闷呢,原来是个bug啊)。
所以,要想自己快速开发出健壮可靠的高性能网络公共组件,还真不是件容易事!Netty为我们提供了开箱即用的高性能、高可靠、安全可扩展的网络组建,同时还修复了NIO的一些bug,社区非常活跃,版本升级快。相比而言,Netty真是个不错的选择!
Netty有以下几个核心API:
TCP是流协议,TCP底层并不了解上层业务数据的含义,它会根据TCP缓冲区的实际情况进行包的划分,一个完整的包可能被TCP拆分成多个包发送,也可能与其他小包封装成一个大的数据包发送,这就是所谓的拆包和粘包。
发生拆包的原因可能有:
常用的解决策略:
我们可以在自定义Decoder和Encoder中实现序列化和反序列化,如常见的Jackson,MsgPack,ProtoBuf等等。
Reactor模型主要由多路复用器(Acceptor)、事件分发器(Dispatcher)、事件处理器(Handler)三部分组成。深入研究的话,Reactor模型可以细分成三种:
Netty对这三种都支持,通过调整线程池的线程个数、是否共享线程池等参数在三种方式间方便的切换。一般的Netty最佳实践如下:
由于在Handler内的数据读写、协议解析经常要保存一些状态,所以为了避免资源竞争,Netty对Handler采用串行化设计。即一个I/O线程会对我们配置到Netty中的Handler链的执行“负责到底”。正是有了这样的设计,我们就可以放心的在Handler中保存各种状态,甚至使用ThreadLocal,完全无锁化的设计。
Netty的Handler在这一点上是不是与Struts2中的Action有点像呢?
在Netty内部,ByteBuffer默认使用堆外内存(Direct Buffer)作为缓冲区,这就避免了传统堆内存作缓冲区时的拷贝问题。使用传统堆内存时进行Socket读写时,JVM会先将堆内存缓冲区中的数据拷贝到直接内存中,然后再写入Socket。
此外,Netty也提供给开发者一些工具实现零拷贝,这些工具都是我们可以利用的,例如:
随着JVM虚拟机和JIT即时编译技术的发展,对象的分配和回收成了一件非常轻量级的工作。但是对于缓冲区,特别是对于堆外直接内存,分配和回收却仍然是一件耗时的操作。所以,Netty提供了内存池来实现缓冲区的重用机制。
这里再简单介绍一下Netty内部的内存管理机制。首先,Netty会预先申请一大块内存,在内存管理器中一般叫做Arena。Netty的Arena由许多Chunk组成,而每个Chunk又由一个或多个Page组成。Chunk通过二叉树的形式组织Page,每个叶子节点表示一个Page,而中间节点表示内存区域,节点自己记录它在整个Arena中的偏移地址。当区域被分配出去后,中间节点上的标记位会被标记,这样就表示这个中间节点以下的所有节点都已被分配了。
在凌晨等业务低谷期,如果发生网络闪断、连接Hang住等问题时,由于没有业务消息,应用进程很难发现。到了白天业务高峰期时,会发生大量的网络通信失败,导致应用进程一段时间内无法处理业务消息。因此可以采用心跳检测机制,一旦发现网络故障则立即关闭链路,并主动重连。
具体来看,心跳检测机制一般的设计思路是:
1)当连续周期T没有读写消息,客户端主动发送Ping心跳消息给服务端。
2)如果在下一周期T到来时没有收到服务端的Pong心跳或业务消息,则心跳失败计数器加1。
3)每当客户端接收到服务端的Pong心跳或业务消息,则心跳失败计数器清零;当计数器达到N次,则关闭链路,间隔INTERVAL后发起重连操作(保证服务端有充足的时间释放资源,所以不能失败后立即重连)。
4)同理,服务端也要用上面的方法检测客户端(保证无论通信哪一方出现网络故障,都能被及时检测出来)。
Netty根据ByteBuf的maxCapacity保护内存不会超过上限。此外默认的TailHandler会负责自动释放ByteBuf的缓冲区。
Netty利用JVM注册的Shutdown Hook拦截到退出信号量,然后执行退出操作:释放各个模块的占用资源、将缓冲区中剩余的消息处理完成或者清空、将待刷新的数据持久化磁盘或数据库等。
(略)
在Netty中可以很方便地修改TCP的参数。例如缓冲区大小的参数SO_RCVBUF/SO_SNDBUF、关闭将大量小包优化成大包的Nagle算法的参数SO_TCPNODELAY参数来避免对时延敏感应用的影响、以及Linux软中断等。
- Netty协议栈不区分服务端和客户端,开发完成后可同时支持。
- 可靠性设计:心跳机制;重连机制
- 安全性设计:内网采取IP白名单进行安全过滤;外网采取更加严格的SSL/TSL安全传输。
- 扩展性设计:业务功能可以在消息头中附加流水号等,利用Netty提供的attachment字段扩展。
Netty中关键的类库都提供了接口或抽象类以及大量的工厂类供开发者扩展,像Handler则是直接提供了ChannelPipeline实现了责任链模式,方便我们做任意的组合和扩展。