前言
当今高并发网络通信领域,Netty凭借其卓越的性能和灵活的架构设计,已成为构建高性能网络服务的首选框架。本文将深入剖析Netty最核心的连接接入机制,揭示其如何通过精巧的线程模型设计和高效的Channel管理,实现高效的连接接入能力。
Netty的连接接入过程本质上是一个高度优化的流水线作业系统,其核心由三个关键组件协同完成:
- BossGoup线程组: 作为连接接入的第一道门户,专门负责监听和接收新的TCP连接请求并且以非阻塞方式轮询OP_ACCEPT事件。
- WorkerGroup线程组: 作为已接入连接处理的骨干力量,负责处理已建立连接的所有IO操作。每个Wokrer线程都维护着注册在自己身上的Channel集合,实现无锁化的串行处理。
- ServerBootstrapAcceptor: 作为连接接入流水线的"最后一公里处理器",这个组件实现了从连接接收到工作线程分发的完整闭环。
本文将解析Netty高效处理网络连接的两大关键设计:首先,将剖析Boss与Worker线程组的协同工作模式,揭示Netty如何通过主从Reactor架构实现连接接收与I/O业务处理的完美解耦;其次,详细讲解Boss线程组基于NIO Selector的OP_ACCEPT事件处理流程,展现其高性能的连接接入能力。这两大机制相辅相成,共同构成了Netty卓越的网络通信性能基础。
一、Boss/Worker线程组协作机制
Netty的Boss/Worker线程组协作机制是其高性能网络通信的核心设计之一,采用了主从Reactor模型,Netty的代码体现如下:
//Boss线程组作为主Reactor,用于监听指定端口的连接事件(OP_ACCEPT),在只监听一个端口的情况下设置为1
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
//Wokrer线程组作为从Reactor,用于处理已经接入的连接Channel,负责Channel的读写事件,根据业务情况可以设置为多个,默认为CPU的个数
EventLoopGroup workerGroup = new NioEventLoopGroup();
final EchoServerHandler serverHandler = new EchoServerHandler();
try {
ServerBootstrap b = new ServerBootstrap();
//将Boss线程组和Wokrer线程组注册进去,来构建主从Reactor的基本架构,用于后续的Boss/Worker线程组协作
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
、、、
ChannelFuture f = b.bind(8888).sync();
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
以上代码中,b.group(bossGroup, workerGroup)这行代码构成了Netty主从Reactor模式,后续Boss/Worker协作的线程都是来源于此处。
接下来我们结合源码来分析一下主Reactor的建立过程
主Reactor的启动由此开始,绑定指定的端口,随后便开始进行端口的监听,准备接入新的客户端连接
ChannelFuture f = b.bind(PORT).sync();
继续跟进代码,doBind这个方法核心的功能一个是初始化NioServerSocketChannel并且将NioServerSocketChannel注册到EventLoop中,这个EventLoop来自于bossGroup,另一个就是绑定到指定的端口。
private ChannelFuture doBind(final SocketAddress localAddress) {
//初始化NioServerSocketChannel并且将NioServerSocketChannel注册到EventLoop中
final ChannelFuture regFuture = initAndRegister();
final Channel channel = regFuture.channel();
if (regFuture.cause() != null) {
return regFuture;
}
if (regFuture.isDone()) {
ChannelPromise promise = channel.newPromise();
//进行端口的绑定
doBind0(regFuture, channel, localAddress, promise);
return promise;
} else {
final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
regFuture.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
Throwable cause = future.cause();
if (cause != null) {
promise.setFailure(cause);
} else {
promise.registered();
doBind0(regFuture, channel, localAddress, promise);
}
}
});
return promise;
}
}
initAndRegister这个核心方法也做了两件事情,一个是创建一个新的NioServerSocketChannel对象并且将其进行初始化,另一个就是将其注册到boss线程组中的一个eventLoop上去。这里有个关键点,就是当register这个注册方法执行完毕后,这个eventLoop才开始启动,其实也是属于懒加载机制。
final ChannelFuture initAndRegister() {
Channel channel = null;
//创建一个Netty的NioServerSocketChannel对象
channel = channelFactory.newChannel();
// 初始化这个ServerSocketChannel
init(channel);
//将NioServerSocketChannel注册到boss线程组中的一个eventLoop上去
ChannelFuture regFuture = config().group().register(channel);
init(Channel channel)这个方法一方面是为NioServerSocketChannel配置一些参数,另一个重要的点就是给NioServerSocketChannel的ChannelPipeline增加一个关键的ChannelHandler(ServerBootstrapAcceptor)
void init(Channel channel) {
....省略一些代码
//获取 NioServerSocketChannel的ChannelPipeline
ChannelPipeline p = channel.pipeline();
p.addLast(new ChannelInitializer<Channel>() {
@Override
public void initChannel(final Channel ch) {
final ChannelPipeline pipeline = ch.pipeline();
ChannelHandler handler = config.handler();
if (handler != null) {
pipeline.addLast(handler);
}
//向pipeline添加ServerBootstrapAcceptor
//这个处理器专门处理新连接接入
ch.eventLoop().execute(new Runnable() {
@Override
public void run() {
pipeline.addLast(new ServerBootstrapAcceptor(
ch, // 当前ServerChannel
currentChildGroup, // worker线程组
currentChildHandler, // 业务处理器
currentChildOptions, / 子Channel选项
currentChildAttrs));// 子Channel属性
}
});
}
});
}
ServerBootstrapAcceptor负责处理新连接的接收和初始化工作,这个组件串联起了Netty的主从Reactor模式,可以说是连接接入流水线的"最后一公里处理器",我们通过源码分析一下
public void channelRead(ChannelHandlerContext ctx, Object msg) {
//新接入的客户端SocketChannel
final Channel child = (Channel) msg;
//为客户端SocketChannel配置channelHandler
child.pipeline().addLast(childHandler);
//设置SocketChannel的选项和属性
setChannelOptions(child, childOptions, logger);
setAttributes(child, childAttrs);
try {
//将新连接注册到worker线程组
//后续新连接的IO操作交由worker线程负责
//至此,主Reactor的连接接入工作结束
childGroup.register(child).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
forceClose(child, future.cause());
}
}
});
} catch (Throwable t) {
forceClose(child, t);
}
}
以上内容主要介绍了主Reactor的建立过程以及Boss/Worker线程是如何进行协作的,还有就是其中的一些关键方法和组件内容。简要概括的流程图如下
二、NIO Selector的OP_ACCEPT事件处理流程
Netty对NIO的OP_ACCEPT事件处理进行了高度优化和封装,其实本质上处理OP_ACCEPT事件的流程与处理网络IO读写的事件的流程是一样的,都封装在NioEventLoop中的run方法里面,NioEventLoop基于selector来监听获取就绪的事件,只不过根据不同的事件类型进行不同的处理流程,源码参考如下:
/**
* 处理已选择的SelectionKey,执行相应的I/O操作
*
*/
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
// 获取底层操作的unsafe实例,当我们处理OP_ACCEPT时,ch为NioServerSocketChannel
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
...省略部分源码
try {
// 获取就绪的操作集
int readyOps = k.readyOps();
...省略部分源码
//处理读/接受连接(OP_READ/OP_ACCEPT)事件
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
// 处理读事件或接受新连接
unsafe.read();
}
} catch (CancelledKeyException ignored) {
// 如果SelectionKey已被取消,安全地关闭通道
unsafe.close(unsafe.voidPromise());
}
}
根据以上的源码分析可知,unsafe.read()方法用于处理读事件或者接受新连接事件,当参数ch为NioServerSocketChannel时,unsafe实例为NioMessageUnsafe,参数ch为NioSocketChannel时,unsafe实例为NioByteUnsafe,所以不同实例的read方法走的流程也是不一致的,我们重点分析NioMessageUnsafe的read方法,这个是接受新连接的核心方法。源码如下:
public void read() {
// 确保在EventLoop线程中执行
assert eventLoop().inEventLoop();
//获取通道配置
final ChannelConfig config = config();
//获取NioServerSocketChannel的ChannelPipeline
final ChannelPipeline pipeline = pipeline();
//获取内存分配控制器并且reset重置分配器状态
final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
allocHandle.reset(config);
boolean closed = false;
Throwable exception = null;
try {
try {
do {
//核心的获取新连接的方法,是模版方法,执行的是NioServerSocketChannel的doReadMessages
int localRead = doReadMessages(readBuf);
// 无数据可读
if (localRead == 0) {
break;
}
//NioServerSocketChannel的doReadMessages不会返回小于0的,忽略
if (localRead < 0) {
closed = true;
break;
}
// 更新读取消息计数
allocHandle.incMessagesRead(localRead);
//动态判断是否继续读取
} while (continueReading(allocHandle));
} catch (Throwable t) {
exception = t;
}
//readBuf里面读取到的是新接入的NioSocketChannel,for循环依次处理fireChannelRead,并将NioSocketChannel作为参数传入
//这里的pipeline是NioServerSocketChannel的pipeline,最终会让ServerBootstrapAcceptor去执行
int size = readBuf.size();
for (int i = 0; i < size; i ++) {
readPending = false;
pipeline.fireChannelRead(readBuf.get(i));
}
readBuf.clear();
allocHandle.readComplete();
//本次处理完成,触发fireChannelReadComplete事件
//触发的也是NioServerSocketChannel的pipeline
pipeline.fireChannelReadComplete();
...省略部分代码
} finally {
...省略部分代码
}
}
通过源码分析可知,新接入的连接NioSocketChannel会逐一的被NioServerSocketChannel的pipeline进行处理,最终会执行到ServerBootstrapAcceptor的channelRead方法上面,继而将NioSocketChannel绑定到Worker线程组中的线程上去,进行后续的IO读写操作。
我们再来看一下doReadMessages方法里面的内容,看一下是如何接入新连接的,源码分析如下:
protected int doReadMessages(List<Object> buf) throws Exception {
//根据jDK原生的ServerSocketChannel来获取新接入的连接SocketChannel,SocketChannel也是JDK原生的
SocketChannel ch = SocketUtils.accept(javaChannel());
try {
if (ch != null) {
//将JDK原生的SocketChannel封装进Netty的NioSocketChannel中,便于后续的操作以及管理
//这个this参数指的是当前的NioServerSocketChannel实例,用于表示当前的NioSocketChannel是由谁而来
buf.add(new NioSocketChannel(this, ch));
return 1;
}
} catch (Throwable t) {
logger.warn("Failed to create a new channel from an accepted socket.", t);
try {
ch.close();
} catch (Throwable t2) {
logger.warn("Failed to close a socket.", t2);
}
}
return 0;
}
以上这段源码向我们揭示了Netty是如何接入新连接的,底层也是依赖JDK原生的连接接入,只不过在Netty框架层面对这些原生的类进行了Netty层面的封装。
下面用一张图来简单概括一下Netty的NIO Selector的OP_ACCEPT事件处理流程
总结
以上内容主要结合源码分析了Boss/Worker线程组协作与 OP_ACCEPT事件处理流程,希望以上内容对你学习Netty有所助益,谢谢大家阅读。