前面两篇基础介绍了Netty的一些核心概念,本篇将从netty的设计上进行讲解。
4. Netty的线程模型
netty的线程模型是基于reactor模式的,关于reactor模式的理解,可参考Netty百万级高并发支持_西木风落的博客-CSDN博客_netty高并发解决方案
在讲netty的线程模型之前,我们需要理解为什么设计出netty多种线程模型,其根本原因是我们应用系统网络通信需要,来看一下真实场景下的网络通信:
总结起来客户端和服务端的网络交互有:
- server启动,监听web端口;
- client端TCP三次握手,与server建立连接,监听连接事件OP_AACEPT;
- server通过channel从网卡中读取数据;
- server根据通信协议解析二进制码流;
- server执行对应的业务操作,将业务执行结果返回到client,通常涉及到协议编码、压缩等。
线程模型需要解决的问题:连接监听、网络读写、编码、解码、业务执行这些操作步骤如何运用多线程编程,提升性能。
4.1 Reactor单线程模型
所有的IO都在一个NIO线程上完成,
由于Reactor使用的异步非阻塞IO,理论上可以处理所有的IO操作。但对于高负荷,大并发的场景,却不适合,主要原因有
- 单NIO处理成千上百的链路,性能上无法支撑,即使NIO的CPU达到100%,仍不能满足海量的消息编码、解码,接收和发送。
- NIO线程负载过重后,会导致处理速度变慢,造成大量客户端连接超时后重发,会更进一步加剧消息积压
- 可靠性问题,一旦NIO进入死循环或者异常后,整个通信系统都将不可用。
public static void main(String[] args) {
EventLoopGroup eventLoopGroup = null;
try {
eventLoopGroup = new NioEventLoopGroup(1);
//创建客户端启动对象
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel socketChannel) throws Exception {
//字符串解码器
socketChannel.pipeline().addLast("docode", new StringDecoder());
//字符串编码器
socketChannel.pipeline().addLast("encode", new StringEncoder());
//信息读取处理器
socketChannel.pipeline().addLast(new ClientInputMessageHandler());
}
});
// 绑定端口
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6666).sync();
//关闭通道进行监听
channelFuture.channel().closeFuture().sync();
} catch (Exception e){
e.printStackTrace();
} finally {
if(eventLoopGroup != null){
eventLoopGroup.shutdownGracefully();
}
}
}
public static class ClientInputMessageHandler extends SimpleChannelInboundHandler<String>{
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
System.out.println("服务端发送的消息:" + s);
}
// 建立连接时信息
@Override
public void channelActive(ChannelHandlerContext ctx) {
ctx.writeAndFlush("hello 服务端...");
}
// 异常时处理机制
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
}
}
}
4.2 Reactor多线程模型
与单线程模型最大的区别是:
- 有一个专门的线程Acceptor,处理客户端TCP的连接请求
- 网络IO操作交给一个NIO线程池, 线程池中的线程处理链路的编码、解码,发送和接收
- 一个NIO线程可以处理多个链路,但是一个链路只会属于一个NIO线程,防止并发操作引入的数据污染。
4.3 主从reactor多线程模型
在个别特殊情况下,一个acceptor线程处理高并发的连接请求,可能存在性能问题。比如,需要对客户端连接进行安全认证,认证本身是非常耗性能的。所以引入了主从模式的多线程模型。
服务端用于接收客户端连接请求不再是单一的NIO线程,而是一个线程池。
Netty的线程模型并不是一直不变的,它取决于启动时的参数设置。Netty的最佳实践是
- 创建两个EventLoopGroup,用于逻辑隔离Acceptor NIO和 NIO IO。
- 尽量不要在channelHandler中启动用户线程
- 解码要放在解码的Handler中,尽量不要放到用户线程中
- 如果业务逻辑简单,没有复杂的业务计算,也没有可能会导致线程阻塞的磁盘、DB或者网络操作等,可以把业务逻辑放到NIO线程中完成,不要切换到用户线程
- 如果业务逻辑复杂,尽量把业务逻辑派发到业务逻辑的线程池中,尽快释放Netty的NIO线程。