用Netty开发中间件:网络编程基础
2021-06-23 07:03
用Netty开发中间件:网络编程基础
《Netty权威指南》在网上的评价不是非常高,尤其是第一版,第二版能稍好些?入手后高速翻看了大半本,不免还是想对《Netty权威指南(第二版)》吐槽一下:
- 前半本的代码排版太糟糕了,简直就是直接打印Word的版式似的。
源代码解析部分的条理性和代码排版好多了,感觉比其它部分的质量高多了。
- 假设你是刚開始学习的人可能会感觉非常具体,差点儿每部分都会来一套client和服务端的Demo。假设你不是入门者的话可能会感觉水分比較多。
- 最后一部分高级特性。内容有些混乱,不少内容都在不同的章节里反复了好几遍。
不管如何,假设你是网络通信或后台中间件的入门者。尤其是Java程序猿,那么这本书还是值得入手的。尤其是书中对I/O模型、协议解析、可靠性等方面的点拨还是会让你有非常多收获的。好了吐槽就到这了,下面就是《Netty权威指南(第二版)》的重点摘录,抽掉了水分,全部干货都在这里了。
1.Linux和Java的I/O演进之路
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。
2.四种I/O模型
2.1 堵塞BIO
堵塞BIO是我们最常见的一种形式。就不具体说了。
2.2 伪异步I/O
伪异步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异常。
2.3 非堵塞NIO
非堵塞NIO的特点是:
- 1)全部数据都是用缓冲区(ByteBuffer)处理的;
- 2)使用全双工的Channel而不是输入/输出流,能更好地映射底层操作系统的API。
- 3)多路复用器是基础。
NIO提供了非堵塞的读写操作。相比于BIO的确是异步的。因此从这个角度我们能够说NIO是异步非堵塞的。
然而假设严格依照UNIX网络编程模型定义的话,NIO并不能算是异步的,由于当事件完毕时不是由系统触发回调函数,而是须要我们不断轮询。
2.4 异步AIO
AIO才是真正的异步I/O:NIO仅仅是实现了读写操作的非堵塞。但它还是要靠轮询而非事件通知(虽然前面说过JDK 1.5里升级为epoll。但上层API还是轮询没有变化)。说它是异步的事实上就是想说它是非堵塞的。JDK 1.7 NIO 2中提供的AIO才是真正的异步I/O。
3.Netty介绍
3.1 为什么选择Netty
使用原生NIO开发的特点就是功能开发相对easy,但兴许的可靠性方面的工作量非常大。须要我们自己处理如断连重连、半包读写、网络拥堵等问题。而且,NIO中还可能有bug,如“臭名昭著”的Selector空轮询导致CPU使用率100%(大学做大作业就碰到过这个问题。当时还纳闷呢,原来是个bug啊)。
所以,要想自己高速开发出健壮可靠的高性能网络公共组件,还真不是件easy事!
Netty为我们提供了开箱即用的高性能、高可靠、安全可扩展的网络组建,同一时候还修复了NIO的一些bug,社区非常活跃。版本号升级快。相比而言,Netty真是个不错的选择!
3.2 核心API简单介绍
Netty有下面几个核心API:
- ByteBuf:JDK的ByteBuffer仅仅有一个位置指针,每次读写都要flip(),clear()等。ByteBuf有readerIndex和writerIndex两个指针。(0,readerIndex)是已读数据。[readerIndex,writerIndex)是未读的数据。[writerIndex,capacity)是可写空间。
- Channel:封装了JDK Channel的操作,统一了接口。
- EventLoop:负责轮询事件并分发给相应Channel的线程。
4.协议解析设计
4.1 TCP拆包和粘包
TCP是流协议,TCP底层并不了解上层业务数据的含义,它会依据TCP缓冲区的实际情况进行包的划分,一个完整的包可能被TCP拆分成多个包发送。也可能与其它小包封装成一个大的数据包发送,这就是所谓的拆包和粘包。
发生拆包的原因可能有:
- 1)应用程序write写入的数据大小大于Socket发送缓冲区大小;
- 2)进行MSS大小的TCP分段;
- 3)以太网帧的payload大于MTU进行IP分片。
经常使用的解决策略:
- 1)消息定长(FixedLengthFrameDecoder)。
- 2)包尾加切割符,如回车(DelimiterBasedFrameDecoder);
- 3)将消息分为消息头和消息体。在消息头中包括消息或消息体的长度(LengthFieldPrepender和LengthFieldBasedFrameDecoder)。
4.2 反序列化
我们能够在自己定义Decoder和Encoder中实现序列化和反序列化,如常见的Jackson,MsgPack。ProtoBuf等等。
5.高性能设计
5.1 Reactor模型
Reactor模型主要由多路复用器(Acceptor)、事件分发器(Dispatcher)、事件处理器(Handler)三部分组成。
深入研究的话,Reactor模型能够细分成三种:
-
单线程:全部I/O操作都由一个线程完毕,即多路复用、事件分发和处理都是在一个Reactor线程上完毕的。由于全部I/O操作都不会堵塞。所以理论上是可能的。
在一些小型应用场景下也的确能够使用单线程模型。但对于高并发应用是不合适的。即便这个NIO线程将CPU跑满也无法满足海量消息的编解码和读写。此外这种模型在可靠性上也存在问题。由于一旦这个NIO线程进入死循环就会导致整个系统的不可用。
- 多线程:一个专门的NIO线程(Acceptor线程)负责监听和接收client的TCP连接请求。而读写由一个NIO线程池负责。每一个NIO能够相应多个链路。但为了防止并发问题。每一个链路仅仅相应一个NIO线程。绝大多数场景下,多线程模型都能够满足性能需求了。但在处理百万client连接,或须要对client进行比較耗时的安全认证时,单一Acceptor还是可能存在性能不足的问题。
- 主从Reactor:Acceptor不再是一个单独的线程,而是独立的线程池,负责client的登录、握手和安全认证,一旦链路建立成功就将链路注冊到后端负责I/O读写的SubReactor线程池上。
Netty对这三种都支持。通过调整线程池的线程个数、是否共享线程池等參数在三种方式间方便的切换。一般的Netty最佳实践例如以下:
- 创建两个NioEventLoopGroup来隔离Acceptor和I/O线程。
- 假设业务逻辑非常easy。就不要在Handler中启动用户线程,直接在I/O线程中完毕业务。
- 假设业务逻辑复杂,有可能导致线程堵塞的磁盘、数据库、网络等操作,则可将解码后的消息封装成Task派发到业务线程池运行。
- 不要在用户线程中解码,而要在I/O线程上的解码Handler中完毕。
5.2 无锁化
由于在Handler内的数据读写、协议解析经常要保存一些状态,所以为了避免资源竞争。Netty对Handler採用串行化设计。即一个I/O线程会对我们配置到Netty中的Handler链的运行“负责究竟”。
正是有了这种设计,我们就能够放心的在Handler中保存各种状态。甚至使用ThreadLocal,全然无锁化的设计。
Netty的Handler在这一点上是不是与Struts2中的Action有点像呢?
5.3 零拷贝
在Netty内部,ByteBuffer默认使用堆外内存(Direct Buffer)作为缓冲区。这就避免了传统堆内存作缓冲区时的拷贝问题。使用传统堆内存时进行Socket读写时,JVM会先将堆内存缓冲区中的数据复制到直接内存中,然后再写入Socket。
此外。Netty也提供给开发人员一些工具实现零拷贝,这些工具都是我们能够利用的,比如:
- ByteBufHolder:由于不同的协议消息体能够包括不同的协议字段和功能,使用者继承ByteBufHolder接口后能够按需封装自己的实现。比如Netty内部已提供的MemcacheContent就是继承自ByteBufHolder。
- CompositeByteBuf:对外将多个ByteBuf“装饰”成一个ByteBuf,但实际上未产生不论什么数据拷贝。
- DefaultFileRegion:提供了transferTo方法,将文件内容直接发送到目标Channel,实现了文件传输的零拷贝。
5.4 内存池
随着JVM虚拟机和JIT即时编译技术的发展。对象的分配和回收成了一件非常轻量级的工作。可是对于缓冲区。特别是对于堆外直接内存,分配和回收却仍然是一件耗时的操作。所以,Netty提供了内存池来实现缓冲区的重用机制。
这里再简单介绍一下Netty内部的内存管理机制。
首先,Netty会预先申请一大块内存。在内存管理器中一般叫做Arena。
Netty的Arena由很多Chunk组成。而每一个Chunk又由一个或多个Page组成。Chunk通过二叉树的形式组织Page,每一个叶子节点表示一个Page,而中间节点表示内存区域,节点自己记录它在整个Arena中的偏移地址。当区域被分配出去后,中间节点上的标记位会被标记,这样就表示这个中间节点下面的全部节点都已被分配了。
6.可靠性设计
6.1 心跳检測
在凌晨等业务低谷期。假设发生网络闪断、连接Hang住等问题时,由于没有业务消息,应用进程非常难发现。到了白天业务高峰期时,会发生大量的网络通信失败。导致应用进程一段时间内无法处理业务消息。因此能够採用心跳检測机制。一旦发现网络故障则马上关闭链路。并主动重连。
具体来看。心跳检測机制一般的设计思路是:
1)当连续周期T没有读写消息,client主动发送Ping心跳消息给服务端。
2)假设在下一周期T到来时没有收到服务端的Pong心跳或业务消息,则心跳失败计数器加1。
3)每当client接收到服务端的Pong心跳或业务消息,则心跳失败计数器清零。当计数器达到N次,则关闭链路。间隔INTERVAL后发起重连操作(保证服务端有充足的时间释放资源。所以不能失败后马上重连)。
4)同理,服务端也要用上面的方法检測client(保证不管通信哪一方出现网络故障,都能被及时检測出来)。
6.2 内存保护
Netty依据ByteBuf的maxCapacity保护内存不会超过上限。
此外默认的TailHandler会负责自己主动释放ByteBuf的缓冲区。
6.3 优雅停机
Netty利用JVM注冊的Shutdown Hook拦截到退出信号量。然后运行退出操作:释放各个模块的占用资源、将缓冲区中剩余的消息处理完毕或者清空、将待刷新的数据持久化磁盘或数据库等。
6.安全性设计
(略)
7.扩展性设计
7.1 灵活的TCP參数配置
在Netty中能够非常方便地改动TCP的參数。比如缓冲区大小的參数SO_RCVBUF/SO_SNDBUF、关闭将大量小包优化成大包的Nagle算法的參数SO_TCPNODELAY參数来避免对时延敏感应用的影响、以及Linux软中断等。
- Netty协议栈不区分服务端和client,开发完毕后可同一时候支持。
- 可靠性设计:心跳机制。重连机制
- 安全性设计:内网採取IP白名单进行安全过滤。外网採取更加严格的SSL/TSL安全传输。
- 扩展性设计:业务功能能够在消息头中附加流水号等。利用Netty提供的attachment字段扩展。
7.2 可定制的API
Netty中关键的类库都提供了接口或抽象类以及大量的工厂类供开发人员扩展,像Handler则是直接提供了ChannelPipeline实现了责任链模式。方便我们做随意的组合和扩展。
上一篇:JSON解析
下一篇:hibernate log4j