IO-Netty
前言
在大多数场景下,并不适合直接使用NIO来进行开发,除非非常精通NIO或者有特殊的需求。因此在大多数场景下我们可以用一个基于NIO封装的Netty框架来代替原生的NIO进行开发。
NIO存在的问题
- NIO类库和API繁杂,使用麻烦
- 对开发者编程水平要求高,需要熟练掌握额外的技能,如多线程
- 开发工作量和工作难度很大,如:心跳检测、断线重连、粘包拆包、网络阻塞等
- JDK NIO的BUG,如epoll可能会在某些情况下导致Selector空转,导致CPU 100%。
Netty
Netty是一个基于NIO的异步的、事件驱动的高性能网络编程框架.它的健壮性、功能、性能、可扩展性都是首屈一指的。常见的应用有:阿里的RPC框架Dubbo.
Netty有着如下优点:
- API使用简单,开发门槛低
- 设计优雅
- 高性能、高吞吐量、可扩展性强
- 比较成熟稳定,社区活跃,更新比较快
Netty整体架构
传输服务:
传输服务层提供了网络传输能力的定义和实现方法。它支持 Socket、HTTP 隧道、虚拟机管道等传输方式。Netty 对 TCP、UDP 等数据传输做了抽象和封装,用户可以更聚焦在业务逻辑实现上,而不必关系底层数据传输的细节。
协议支持:
Netty支持多种常见的数据传输协议,包括:HTTP、WebSocket、SSL、zlib/gzip、二进制、文本等,还支持自定义编解码实现的协议。Netty丰富的协议支持降低了开发成本,基于 Netty 我们可以快速开发 HTTP、WebSocket 等服务。
Core核心:
Netty的核心,提供了底层网络通信的通用抽象和实现,包括:可扩展的事件驱动模型、通用的通信API、支持零拷贝的Buffer缓冲对象。
Netty逻辑架构
Netty采用典型的三层网络架构进行设计和开发。
网络通信层
网络通信层的职责主要是监听网络的读写和连接操作,负责将网络层的数据读取到内存缓冲区中,然后触发各种网络事件,如连接创建、连接激活、读事件、写事件等,这些网络事件会分发给事件调度层进行处理。网络通信层的核心组件有Bootstrap 、Channel、ByteBuf。
核心组件 Bootstrap
启动引导,主要负责整个Netty程序的启动、初始化、服务器连接等过程,它相当于一条主线,把Netty的其他核心组件给串联起来。
Netty中的引导器分为两种:Bootstrap:客户端启动引导,另一种是ServerBootstrap:服务端启动引导。它们都继承自抽象类 AbstractBootstrap。
核心组件 Channel
网络通信的载体,提供了基本的用于I/O操作的API,如:register、bind、connect、read、write、flush等。
Netty的Channel是在JDK的NIO Channel基础上进行封装的,提供了更高层次的抽象,同时屏蔽了底层Socket的复杂性,赋予了Channel更加强大的功能。
核心组件 ByteBuf
当我们在进行数据传输的时候,往往需要使用到缓冲区。常用的缓冲区就是JDK NIO类库提供的java.nio.Buffer。
在NIO中,除了boolean类型外,其他JAVA基础数据类型都有自己的缓冲区实现,对于NIO来说,使用比较多的是ByteBuffer。但是ByteBuffer本身有一定的局限性:
- 长度固定,一旦分配完成, 不能扩容和缩容
- 只有一个标识位置的指针position,读写切换需要手动调用flip()方法
- API封装的功能有限
因此,Netty提供了比BufferBuffer更为丰富强大的缓冲区实现-ByteBuf。
事件调度层
事件调度层的职责是通过 Reactor 线程模型对各类事件进行聚合处理,通过 Selector 主循环线程集成多种事件( I/O 事件、信号事件、定时事件等),实际的业务处理逻辑是交由服务编排层中相关的 Handler 完成。
事件调度层的核心组件有:
- EventLoopGroup
- EventLoop
EventLoopGroup
本质上是一个线程池,主要负责接收I/O请求,并分配线程处理请求。一个EventLoopGroup包含一个或者多个EventLoop,EventLoop用于处理Channel生命周期内的所有I/O事件,如accept、read、write等。
EventLoop
同一时间会与一个线程绑定,每个EventLoop负责处理多个Channel。每新建一个Channel,EventLoopGroup会选择一个EventLoop与其绑定,该Channel生命周期内都可以对EventLoop进行多次绑定和解绑。
服务编排层
负责组装各类服务,它是Netty的核心业务处理链,用于实现网络事件的动态编排和有序传播。
服务编排层的核心组件有:
-
ChannelPipeline:handler处理器链列表,类似于容器,内部通过双向链表将不同的ChannelHandler串联在一起,当I/O读写事件发生时,ChannelPipeline会依次调用ChannelHandler列表对Channel的数据进行拦截处理。每个Channel创建时就会绑定一个新的ChannelPipeline,他们是一一对应的关系。
-
ChandlerHandler:事件业务处理器顶层接口,它不处理入站和出站事件,而是由其子类来实现,核心子类包括:
-
- ChannelInboundHandler:负责处理入站事件,以及用户自定义事件
- ChannelOutboundHandler:负责处理出站事件
- ChannelDuplexHandler:同时实现了ChannelInboundHandler和ChannelOutboundHandler两个接口,既可以处理入站事件,也可以处理出站事件
-
ChannelHandlerContext:ChandlerHandler的上下文对象,在调用ChandlerHandler方法时,都会传入该上下文对象,ChannelHandlerContext 包含了 ChannelHandler 生命周期的所有事件,如 connect、bind、read、flush、write、close 等。
由于Netty的分层架构设计合理、优雅,因此,基于Netty开发的各种服务器程序才会越来越多。
线程模型
Netty采用基于NIO的多路复用技术,用一个Selector单线程就可以接入成千上万的客户端连接,降低了线程资源消耗,并且可以避免多线程的切换带来的性能开销。
Reactor线程模型是对于传统的I/O线程模型的一种优化。
传统的I/O线程模型采用阻塞I/O来获取输入流数据,并且每个连接都需要独立的线程完成数据的输入、业务处理、数据返回等一个完整的操作链路。这种模型在高并发场景下,有两个比较明显的缺点:
- 每个连接都需要创建一个对应线程,线程大量创建占用大量的服务器资源
- 线程没有数据可读情况下的阻塞会对性能造成很大的影响
Reactor线程模型为了解决这两个问题,提供了以下解决方案:
- 基于I/O多路复用:多个客户端连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接。当某个连接有新的数据可以处理时,通过事件驱动通知应用程序,线程从阻塞状态返回,开始进行业务处理
- 基于线程池技术减少线程创建:基于线程池,不必再为每一个连接创建线程,将连接完成后的业务处理分配给线程池进行调度。
根据Reactor线程和handler处理线程的模型不同,有三种Reactor线程模型:
- 单Reactor单线程模型
- 单Reactor多线程模型
- 主从Reactor线程模型
单Reactor单线程模型
单Reactor单线程模型是指用一个线程通过多路复用来完成所有的I/O操作(accept、read、write等)。
服务端处理流程:
- Reactor对象通过select监听客户端请求,收到请求后通过Dispatch分发
- 如果是建立连接请求,则由Acceptor通过accept处理连接,然后创建一个handler对象处理完成连接后的各种事件
- 如果不是建立连接请求,则分发调用对应的Handler来完成数据读取、业务处理,并将结果返回给客户端
单Reactor单线程模型的优点在于模型简单,没有多线程、进程间通信、竞争的问题,全部都在一个线程中完成。
缺点是:
- 性能问题:只有一个线程去处理任务,在高并发情况下很容易阻塞
- 可靠性问题:一旦线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障
单Reactor多线程模型
该模型和单Reactor单线程模型的最主要区别在于,Reactor主线程只负责监听、接收客户端请求以及派发任务,而比较耗时的I/O操作由另一个worker线程池来进行分配线程去处理。
服务端处理流程:
- Reactor对象通过select监听客户端请求,收到请求后通过Dispatch分发
- 如果是建立连接请求,则由Acceptor通过accept处理连接,然后创建一个handler对象处理完成连接后的各种事件
- 如果不是建立连接请求,则分发调用对应的Handler来处理
- Handler 只负责响应事件,不做具体的业务处理,通过 read 读取数据后,会分发给后面的 Worker 线程池的某个线程处理业务。
- Worker线程池会分配独立线程完成真正的业务,并将结果返回给 Handler,Handler 收到响应后,通过 send 将结果返回给 客户端。
在绝大多数场景下,该模型性能表现优异,可以充分发挥多核CPU的处理能力。
但是在并发上百万的场景下,一个NIO线程负责监听和处理所有客户端连接可能存在性能问题。例如,某些场景下,会对客户端的请求进行安全认证等,这类请求非常耗时。在此场景下,单独一个Reactor线程可能存在性能不足的问题,为了解决这个问题,诞生了第三种线程模型:主从Reactor多线程模型。
主从Reactor线程模型
该线程模型与单Reactor多线程模型主要在于Reactor线程分为了主从Reactor线程两部分,即下图中的Main Reactor和Sub Reactor。
服务端处理请求流程:
- Reactor主线程对象通过seletct监听连接事件,收到事件后,通过Acceptor处理连接事件
- 当Acceptor处理连接事件后,主Reactor线程将连接分配给子Reactor线程
- Reactor子线程将连接加入连接队列进行监听,并创建handler进行各种事件处理
- 当有新事件发生时,子Reactor线程会调用对应的handler进行处理
- handler读取数据分发给worker线程池分配一个独立的线程进行业务处理,并返回结果给handler
- handler收到响应的结果后,通过send将结果返回给客户端
该模型虽然编程复杂度高,但是其优势比较明显,体现在:
- 主从线程职责分明,主线程只需要接收新请求,子线程完成后续的业务处理
- 主从线程数据交互简单,主线程只需要把新连接传给子线程
因此该模型在许多项目中广泛使用,包括Nginx、Memcached、Netty等。
Netty线程模型
Netty的线程模型不是一成不变的,它实际取决于用户的启动参数配置。通过设置不同的启动参数,Netty可以同时支持Reactor单线程模型、多线程模型和主从Reactor多线程模型。Netty官方推荐的是基于主从Reactor线程模型实现的NioEventLoopGroup线程模型。
NioEventLoopGroup相当于一个事件循环组,这个组里面有多个事件循环,称为NioEventLoop,类似于线程池和中间的线程的关系。NioEventLoop表示一个不断循环的执行处理任务的线程,每个NioEventLoop都有一个selector,用于监听绑定在其上面的socket网络通信。一个NioEventLoopGroup可以有多个NioEventLoop,可以通过构造函数指定,默认是CPU核心数x2。
Netty线程模型抽象出两组NioEventLoopGroup:
- BossGroup:专门负责接收客户端的请求
- WorkerGroup:专门负责网络的读写、业务处理
Boss NioEventLoop职责:
- select:轮询注册在其上的ServerSocketChannel的accept事件(OP_ACCEPT)
- processSelectedKeys:处理accept事件,与client事件建立连接,生成NioSocketChannel,并将其注册到某个worker NIoEventLoop上的selector上
- runAllTasks:再循环处理任务队列的任务
Worker NioEventLoop职责:
- select:轮询注册在其上的NioSocketChannel的read/write事件(OP_READ和OP_WRITE)
- processSelectedKeys:在对应的NIoSocketChannel 上进行I/O处理
- runAllTasks:再循环处理任务队列的任务
每个Worker NioEventLoop处理业务时,会通过pipeline(管道),pipeline中包含了channel,管道中维护了很多的处理器,数据会在管道中的各个处理器间进行流转、处理。
Selector BUG出现的原因
若Selector的轮询结果为空,也没有wakeup或新消息处理,则发生空轮询,CPU使用率100%,
Netty的解决办法
- 对Selector的select操作周期进行统计,每完成一次空的select操作进行一次计数,
- 若在某个周期内连续发生N次空轮询,则触发了epoll死循环bug。
- 重建Selector,判断是否是其他线程发起的重建请求,若不是则将原SocketChannel从旧的Selector上去除注册,重新注册到新的Selector上,并将原来的Selector关闭。
序列化协议
影响序列化性能的关键因素总结如下:
- 序列化后的码流大小(网络带宽的占用);
- 序列化&反序列化的性能(CPU资源占用);
- 是否支持跨语言(异构系统的对接和开发语言切换)。
**NettyRPC中对RPC消息进行编码、解码采用的是Netty自带的ObjectEncoder、ObjectDecoder(对象编码、解码器),该编码、解码器基于的是Java的原生序列化机制。**Java的原生序列化性能效率不高,而且产生的序列化二进制码流太大,故本次在优化中,引入RPC消息序列化协议的概念,将对象转变成字节流。
所谓消息序列化协议,就是针对RPC消息的序列化、反序列化过程进行特殊的定制,引入第三方编解码框架。本次引入的第三方编解码框架有Kryo、Hessian。
心跳检测机制
心跳:即在 TCP 长连接中,客户端和服务器之间定期发送的一种特殊的数据包,通知对方自己还在线,以确保 TCP 连接的有效性。
心跳机制主要是客户端和服务端长时间连接时,客户端需要定时发送心跳包来保证自己是存活的,否则一个连接长时间没有作用就会浪费服务端的资源
心跳检测机制的适用场景
长连接的应用场景非常的广泛,比如监控系统,IM系统,即时报价系统,推送服务等等。像这些场景都是比较注重实时性,如果每次发送数据都要进行一次DNS解析,建立连接的过程肯定是极其影响体验。
而长连接的维护必然需要一套机制来控制。比如 HTTP/1.0 通过在 header 头中添加 Connection:Keep-Alive参数,如果当前请求需要保活则添加该参数作为标识,否则服务端就不会保持该连接的状态,发送完数据之后就关闭连接。HTTP/1.1以后 Keep-Alive 是默认打开的。
Netty 是基于 TCP 协议开发的,在四层协议 TCP 协议的实现中也提供了 keepalive 报文用来探测对端是否可用。TCP 层将在定时时间到后发送相应的 KeepAlive 探针以确定连接可用性。
netty的心跳检测机制
在Netty 中提供了 IdleStateHandler 类专门用于处理心跳。
//IdleStateHandler 的构造函数
CopypublicIdleStateHandler(long readerIdleTime, long writerIdleTime,
long allIdleTime,TimeUnit unit){
}
//IdleStateHandler : 是netty 提供的处理空闲状态的处理器
//long readerIdleTime : 表示多长时间没有读, 就会发送一个心跳检测包检测是否连接
//long writerIdleTime : 表示多长时间没有写, 就会发送一个心跳检测包检测是否连接
//long allIdleTime : 表示多长时间没有读写, 就会发送一个心跳检测包检测是否连接
//要实现 Netty服务端心跳检测机制,需要在服务器端的 ChannelInitializer中加入如下的代码:
//pipeline.addLast(new IdleStateHandler(3, 0, 0, TimeUnit.SECONDS));
案例:
服务端:
public class HeartBeatServer {
public static void main(String[] args) throws Exception {
EventLoopGroup boss = new NioEventLoopGroup();
EventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boss, worker)
.channel(NioServerSocketChannel.class).
childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
// IdleStateHandler的readerIdleTime参数指定超过3秒还没收到客户端的连接,
// 会触发IdleStateEvent事件并且交给下一个handler处理,下一个handler必须实现userEventTriggered方法处理对应事件
pipeline.addLast(new IdleStateHandler(3, 0, 0, TimeUnit.SECONDS));
pipeline.addLast(new HeartBeatServerHandler());
}
});
System.out.println("服务端已经准备就绪...");
ChannelFuture future = bootstrap.bind(19000).sync();
System.out.println("服务器启动完成,等待客户端的连接和数据...");
future.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
worker.shutdownGracefully();
boss.shutdownGracefully();
}
}
}
服务端Handler处理:
public class HeartBeatServerHandler extends SimpleChannelInboundHandler<String> {
private AtomicInteger readIdleTimes = new AtomicInteger(0);
@Override
protected void channelRead0(ChannelHandlerContext ctx, String s) throws Exception {
System.out.println(" ====== > [server] message received : " + s);
if ("Heartbeat Packet".equals(s)) {
ctx.channel().writeAndFlush("ok");
} else {
System.out.println(" 其他信息处理 ... ");
}
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
IdleStateEvent event = (IdleStateEvent) evt;
String eventType = null;
switch (event.state()) {
case READER_IDLE:
eventType = "读空闲";
readIdleTimes.incrementAndGet(); // 读空闲的计数加1
break;
case WRITER_IDLE:
eventType = "写空闲";
// 不处理
break;
case ALL_IDLE:
eventType = "读写空闲";
// 不处理
break;
}
System.out.println(ctx.channel().remoteAddress() + " 超时事件:" + eventType);
if (readIdleTimes.get() > 3) {
System.out.println("[server]读空闲超过3次,关闭连接,释放更多资源");
ctx.channel().writeAndFlush("idle close");
ctx.channel().close();
}
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.err.println("=== " + ctx.channel().remoteAddress() + " is active ===");
}
}
2、客户端
public class HeartBeatClient {
public static void main(String[] args) throws Exception {
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("decoder", new StringDecoder());
pipeline.addLast("encoder", new StringEncoder());
pipeline.addLast(new HeartBeatClientHandler());
}
});
System.out.println("客户端准备就绪,随时可以连接服务端");
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 19000).sync();
System.out.println("客户端已连接到服务器...");
//获取通道,模拟发送心跳包
Channel channel = channelFuture.channel();
String text = "Heartbeat Packet";
Random random = new Random();
while (channel.isActive()) {
int num = random.nextInt(8); //随机睡眠
Thread.sleep(num * 1000);
channel.writeAndFlush(text);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
eventLoopGroup.shutdownGracefully();
}
}
}
客户端Handler处理:
public class HeartBeatClientHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println(" client received :" + msg);
if (msg != null && msg.equals("idle close")) {
System.out.println("服务端关闭了该连接,客户端也关闭");
ctx.channel().closeFuture();
}
}
}
源码分析
查看 IdleStateHandler中的 channelRead方法:在 Read 网络数据时,如果我们可以确保每个 InboundHandler 都把数据往后传递了,就调用了相关的 fireChannelRead 方法。该方法只是进行了 handler火炬传递,不做任何业务逻辑处理,让 channelPipe中的下一个 handler处理 channelRead方法。
查看 channelActive方法,initialize()方法是 IdleStateHandler的核心,这里会触发一个Task任务(ReaderIdleTimeoutTask)。
查看这个 ReaderIdleTimeoutTask类的 run方法。nextDelay:是用当前时间减去最后一次channelRead方法调用的时间。
- 如果nextDelay <= 0L,说明超时了,那么会触发下一个handler的 userEventTriggered方法。
- 如果没有超时,则不触发 userEventTriggered方法。
TCP粘包拆包
介绍
TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议。所谓流,就是没有界限的一串数据。发送端为了将多个发给接收端的数据包,更有效地发送到接收端,会使用Nagle算法。Nagle算法会将多次时间间隔较小且数据量小的数据合并成一个大的数据块进行发送。虽然这样的确提高了效率,但是因为面向流通信,数据是无消息保护边界的,就会导致接收端难以分辨出完整的数据包了。
TCP底层并不了解上层业务数据的具体含义,他会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的的数据包进行发送,这就是所谓的TCP的粘包和拆包机制。所谓的粘包和拆包问题,就是因为TCP消息无保护边界导致的。
拆包粘包产生问题的原因:
粘包产生的问题:
每次结果都有可能不太一样
解决方案
底层的TCP无法理解上层的业务数据,需要在上层的应用协议栈调来来解决。
- 消息定义,例如每个报文的长度大小固定200字节,如果不够,空格补空位。
- 在包尾增加回车换行符,如FTP协议 通常会加\n\r、\t或者其他的符号。
- 将消息分成消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用int32来表示消息的总长度
- 更复杂的应用层协议
Netty提供半包解码器来解决TCP粘包/拆包问题
LineBasedFrameDecoder
LineBasedFrameDecoder 是依次遍历ByteBuf中的可读字节,判断看是否有\n 或 \r\n,如果有,就以此位置为结束位置,以换行符为结束标志的解码器。它支持携带结束符或者不携带结束符两种解码方式,同时支持配置单行的最大长度。如果连续读取到最大长度后仍然没有发现换行符,就会抛出异常,同时忽略掉之前读到的异常码流。
StringDecoder的功能非常简单,就是将接受到的对象转换成字符串,然后继续调用后面的Handler。LineBasedFrameDecoder+StringDecoder组合就是按行切换的文本解码器,它被设计用来支持TCP的粘包和拆包
DelimiterBasedFrameDecoder
DelimiterBasedFrameDecoder是以分隔符作为码流结束标识的消息的解码。示例:Echo服务,以$_作为分隔符。
DelimiterBasedFrameDecoder 有多个构造方法,这里我们传递两个参数:第一个1024表示单条最大长度,当达到该长度之后仍然没有找到分隔符,就抛出TooLongFrameException异常,防止由于异常流缺失分隔符导致的内存溢出,这就是Netty解码器的可靠性保证。第二个参数就是分隔符缓冲对象。
由于DelimiterBasedFrameDecoder自动对请求消息进行了编码,后续的ChannelHandler接受到的msg对象就是个完整的消息包;第二个ChannelHandler是StringDecoder,它将ByteBuffer解码成字符串对象;第三个EchoServerHandler接受到的msg消息就是解码后的字符串对象。
FixedLengthFrameDecoder
FixedLengthFrameDecoder是固定长度解码器,它能够按照指定的长度对消息进行自动解码,按照指定的长度对消息进行自动解码,开发者不需要考虑TCP的粘包/拆包问题。
在服务端的ChannelPipeline中新增FixedLengthFrameDecoder,长度设置为20,然后再依次增加字符串解码器和EchoHandler。
利用FixedLengthFrameDecoder解码器,无论一次接收到多少数据报,他都会按照构造函数中设置的固定长度间解码,如果是半包消息,FixedLengthFrameDecoder会缓存半包消息并等待下一个包到达后进行拼包,直到读取到一个完整的包。
总结
LineBasedFrameDecoder 是以换行符为结束标志的解码器。读取到最大长度后仍然没有发现换行符,就抛出异常。
DelimiterBasedFrameDecoder 用于对使用分隔符结尾的消息间自动解码。读取到最大长度后仍然没有发现分割符,就抛出异常。
FixedLengthFrameDecoder用于对固定长度的消息进行自动解码。
其他
1. 零拷贝
Netty的零拷贝体现在三个方面:
- Netty的接收和发送ByteBuf采用DIRECT BUFFERS,使用堆外内存进行socket读写,不需要进行自己缓冲区的二次拷贝。
- Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个Buffer那样方便的对组合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。
- Netty的文件传输采用了transfer()方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write发送导致的内存拷贝问题。
2. 内存池
由于ByteBuf分配的是堆外内存空间,其分配和回收过程不像JAVA实例内存分配和回收那么简单,为了尽量重用缓冲区,Netty提供了基于内存池的缓冲区重用机制。基于内存池重用的ByteBuf性能比普通的ByteBuf高几十个数量级。
3. 无锁化串行设计
为了尽可能提升性能,**Netty采用了串行无锁化设计,在I/O线程内部进行串行操作,避免多线程竞争导致的性能下降。**表面上看,串行化设计似乎CPU利用率不高,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。
4. 高效并发编程
包括:
- volatile关键字的大量、正确使用
- CAS和原子类的广泛使用
- 线程安全容器的使用
- 通过读写锁提升并发性能
总结
一些高级的知识点还未介绍,如高效序列化、心跳检测、TCP粘包拆包问题解决、多协议支持等。
JAVA中的三种I/O模式:
- BIO:传统同步阻塞IO
- NIO:同步非阻塞IO
- AIO:异步非阻塞IO
虽然NIO功能很强大,性能很好,但是由于其开发难度大,并且存在BUG,因此业界普通使用封装更完善、功能更强大、扩展性更好的Netty来实现高并发的网络编程。
Netty的整体架构分为:
- 传输服务
- 协议支持
- Core核心
Netty的逻辑架构分为:
- 网络通信层
- 事件调度层
- 服务编排层
Netty的核心组件有:
- Bootstrap:启动引导
- Channel:网络通信载体
- ByteBuf:数据缓冲区,读写都是直接操作缓冲区
- EventLoopGroup和EventLoop:线程模型,实现高并发的核心
- ChannelPipeline:handler处理器链容器
- ChandlerHandler:事件业务处理器顶层接口
- ChannelHandlerContext:ChandlerHandler的上下文对象
Netty实现高性能的原因:
- 非阻塞I/O模型
- 良好的线程模型设计
- 高效序列化协议支持
- 其他:零拷贝、内存池、无锁化减少线程切换开销、高效并发编程等。