Netty编程笔记
参考资料:
- Netty权威指南 第二版
I/O模型:阻塞模型、非阻塞模型、IO复用模型(Linux提供了select、poll)、信号驱动IO模型、异步IO模型。
-
阻塞与非阻塞模型,这两种模型均涉及到用户态与内核台的切换
-
IO复用模型,Linux提供了select、poll和epoll三种方式,其中select、poll是顺序扫描fd是否就绪,切支持的fd数量有限;epoll使用事件驱动方式代替顺序扫描,因此新能更高,当有fd就绪时,立即回调rollback
-
信号驱动IO模型:先开启套接口信号驱动IO功能,通过系统调用sigaction执行一个信号处理函数(该操作时非阻塞的),当数据准备就绪时,为该进程生成一个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据,并通知主循环函数处理数据。
-
异步IO模型:告知内核启动某个操作,并让内核在整个操作完成后通知我们。与信号驱动的区别是:信号驱动由内核通知我们何时开始下一个io操作,异步IO是由内核通知我们IO操作何时已经完成。
IO多路复用技术
Linux OS中,支持多路复用技术的有:select、pselect、poll、epoll,首选epoll,其优点如下:
-
支持一个进程打开的socket描述fd 不受限制(仅受限于操作系统的最大文件句柄数)。
-
select最大的缺陷是单个进程所打开的fd是由FD_SETSIZE设置的,默认是1024个,设置改大后会带来网络效率的下降。
-
epoll没有这个限制,可以通过cat /proc/sys/fs/file-max 查看最大具柄的上线,通常这个值跟系统的内存关闭较大。
-
-
IO效率不回随着FD数目的增加而线性下降。
-
select/poll每次调用都会进行线性扫描全部的集合,导致效率呈线性下降。
-
epoll只会针对活跃的socket进行操作,在内核实现中epoll是根据每个fd上面的callback函数实现的,只有活跃的socket才会去主动调用callback函数,其他idle状态的socket则不会调用。
-
-
使用mmap加速内核与用户空间的消息传递
- 无论哪种模型,都会涉及到将内核的fd传递到用户空间,epoll是通过内核和用户空间mmap同一块内存实现的。
-
epoll的API更简单
Java在1.4之前只支持BIO,1.4新增了NIO,但有很多不足,尤其是文件系统的处理,不支持异步文件读写操作,切文件操作都是同步阻塞的方式,1.7升级为NIO2.0,提供了AIO功能,支持基于文件的异步IO操作,以及针对网络套接字的异步操作。
BIO主要的问题是:一客户端一个线程,因此诞生了伪异步IO模型,但这种方式只是增加了一个线程池,用于维护一个消息队列和线程,虽然可以避免资源耗尽导致的宕机,但由于其底层依然是采用同步阻塞模型,所以无法从根本上解决问题。
伪异步IO弊端:
-
读取输入流是阻塞的
-
线程池采用阻塞队列实现,当队列挤满后,后续如队列的操作将被阻塞
-
前面只有一个accptor线程接收客户端的接入,当线程池的同步阻塞队列阻塞后,新的客户端请求消息将被拒绝,客户端连接超时。
NIO包含:缓冲区Buffer、通道Channel、多路复用器Selector
Java序列化的缺点
业界主流的编解码框架
-
Google的Protobuf
-
特点:
-
结构化数据存储格式
-
搞笑的编解码性能
-
语言无关,平台无关,扩展性好
-
官方支持java、c++、python三种语言
-
-
代码示例
-
private void start() throws InterruptedException {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.remoteAddress(new InetSocketAddress(host, port))
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new ProtobufVarint32FrameDecoder())
.addLast(new ProtobufVarint32LengthFieldPrepender())
.addLast(new ProtobufEncoder());
}
});
ChannelFuture f = b.connect().sync();
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}
}
-
Facebook的Thrift
-
JBoss的Marshalling框架
-
可插拔的类解析器
-
可插拔的对象替换技术,不需要通过继承的方式
-
可插拔的预定义类缓存表,可以减少序列化的字节数组长度,提升常用类型的对象序列化性能。
-
无需实现Serializable接口,即可实现java序列化
-
通过缓存技术提升对象的序列化性能
-
-
MessagePack
-
特点:
-
编解码高效,性能高
-
序列化之后的码流小
-
支持跨语言
-
-
使用方式
- 引入maven依赖
-
<dependency>
<groupId>org.msgpack</groupId>
<artifactId>msgpack</artifactId>
<version>0.6.12</version>
</dependency>
/**
* MessagePack 编码器
*/
public class MsgPackEncoder extends MessageToByteEncoder<Object> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, Object o, ByteBuf byteBuf) throws Exception {
MessagePack pack = new MessagePack();
byte[] write = pack.write(o);
channelHandlerContext.write(write);
}
}
/**
* MessagePack 解码器
*/
public class MsgPackDecoder extends MessageToMessageDecoder<ByteBuf> {
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
final int length = byteBuf.readableBytes();
final byte[] array = new byte[length];
byteBuf.getBytes(byteBuf.readerIndex(), array, 0, length);
MessagePack pack = new MessagePack();
list.add(pack.read(array));
}
}
private void start() throws InterruptedException {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.remoteAddress(new InetSocketAddress(host, port))
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast("msgpack decoder", new MsgPackDecoder())
.addLast("msgpack encoder", new MsgPackEncoder())
// 以下解决粘包/半包问题
.addLast("frame encoder", new LengthFieldPrepender(2))
.addLast("frame decoder", new LengthFieldBasedFrameDecoder(65535, 0, 2, 0, 2));
}
});
ChannelFuture f = b.connect().sync();
f.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}
}
Netty的架构设计是如何实现高性能的
-
采用异步非阻塞的IO类库,基于Reactor模式实现,解决了传统同步阻塞的IO模式下一个服务端无法平滑的处理线性增长的客户端的问题。
-
TCP接收和发送缓冲区使用直接内存代替堆内存,避免了内存复制,提升了IO读取和写入的性能。
-
支持通过内存池的方式循环利用ByteBuf,避免了频繁创建和销毁ByteBuf带来的性能损耗。
-
可配置的IO线程数、TCP参数等,为不同的用户场景提供定制化的调优参数,满足不同的性能场景。
-
采用环形数组缓冲区实现无锁化并发编程,代替传统的线程安全容器或者锁。
-
合理地使用线程安全容器、原子类等,提升系统的并发处理能力。
-
关键资源的处理使用单线程串行化的方式,避免多线程并发访问带来的锁竞争和额外的CPU资源消耗问题。
-
通过引用计数器及时的申请释放不再被引用的对象,细粒度的内存管理降低了GC的频率,减少了频繁的GC带来的时延增大和CPU损耗。