基本组件
- NioEventLoop: 监听客户端连接和处理客户端读写
- Channel: 对一个Socket连接的封装,可进行数据的读写
- Pipline: 对数据的逻辑处理链
- ChannelHandler: Pipline里的一个处理逻辑
- ByteBuf: 字节缓冲的容器
EventLoopGroup、NioEventLoop、Channel之间的关系
bossGroup一般只创建一个EventLoop,负责接受请求,workGroup默认让它创建 2*CPU 个EventLoop,负责通信。
一个EventLoop可以绑定多个Channel,一个Channel只能绑定到一个NioEventLoop。
Netty服务端启动步骤:
1.创建服务端Channel
创建JDK定义的Channel,将它包装成netty的Channel,并创建一些基本组件绑定在Channel上
newChannel方法里通过反射调用clazz.newInstance()返回一个Channel对象,这个clazz的类型是在创建ServerBootStrap的时候传入的NioServerSocketChannel。
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childOption(ChannelOption.TCP_NODELAY, true)
.childAttr(AttributeKey.newInstance("childAttr"), "childAttrValue")
.handler(new ServerHandler())
.childHandler(null);
NioServerSocketChannel的创建流程:
2.初始化服务端Channel
初始化一些基本属性,添加一些逻辑处理
3.注册Selector
将JDK的Channel注册到事件轮询器Selector上,并把netty的服务端Channel作为一个attachment绑定在JDK的Channel上
4.端口绑定
调用JDK API,实现对本地端口的监听
AbstractChannel#bind
public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
...
// wasActive表示端口绑定之前的状态
boolean wasActive = isActive();
try {
// 调用javaChannel().bind将JDK的channel绑定到指定端口
doBind(localAddress);
} catch (Throwable t) {
safeSetFailure(promise, t);
closeIfClosed();
return;
}
// 端口绑定之后,isActive返回true
if (!wasActive && isActive()) {
invokeLater(new Runnable() {
@Override
public void run() {
pipeline.fireChannelActive();
}
});
}
safeSetSuccess(promise);
}
fireChannelActive方法传播事件调用到doBeginRead方法,添加selectionKey感兴趣的事件。
就是在服务端端口绑定成功之后,开始接受连接请求
protected void doBeginRead() throws Exception {
// selectionKey是注册服务端channel到selector上的时候返回的
final SelectionKey selectionKey = this.selectionKey;
if (!selectionKey.isValid()) {
return;
}
readPending = true;
// 获取当前感兴趣的事件,初始值是0
final int interestOps = selectionKey.interestOps();
if ((interestOps & readInterestOp) == 0) {
// 添加感兴趣的事件,readInterestOp代表的是accept事件,接受请求
selectionKey.interestOps(interestOps | readInterestOp);
}
}
NioEventLoop
NioEventLoop创建
NioEventLoop在NioEventLoopGroup构造函数中被创建,默认创建 2*CPU 核数个NioEventLoop
每个NioEventLoop持有一个线程。
newChild()方法创建NioEventLoop,主要做了三件事:
- 保存线程执行器ThreadPerTaskExecutor
- 为NioEventLoop创建一个Selector
- 创建一个MpscQueue任务队列
ThreadPerTaskExecutor: 执行每个任务都去创建一个线程去执行
chooserFactory.newChooser()
chooser的作用: 采用循环的方式为新连接绑定一个NioEventLoop
NioEventLoop启动
NioEventLoop启动触发条件
- 服务端启动绑定端口
- 新连接接入通过chooser绑定一个NioEventLoop
NioEventLoop执行
protected void run() {
for (;;) {
try {
switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.SELECT:
// 轮询I/O事件
select(wakenUp.getAndSet(false));
if (wakenUp.get()) {
selector.wakeup();
}
default:
// fallthrough
}
cancelledKeys = 0;
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
try {
processSelectedKeys();
} finally {
// Ensure we always run tasks.
runAllTasks();
}
} else {
final long ioStartTime = System.nanoTime();
try {
processSelectedKeys();
} finally {
// Ensure we always run tasks.
final long ioTime = System.nanoTime() - ioStartTime;
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
}
} catch (Throwable t) {
handleLoopException(t);
}
// Always handle shutdown even if the loop processing threw an exception.
try {
if (isShuttingDown()) {
closeAll();
if (confirmShutdown()) {
return;
}
}
} catch (Throwable t) {
handleLoopException(t);
}
}
}
整个执行过程主要做三件事:
- select(): 检查是否有I/O事件
- processSelectKeys(): 处理I/O事件
- runAllTasks: 处理异步任务队列
- select():
select()方法也是在一个 for(;;) 循环里执行
- 判断select操作是否超过截止时间
- 执行阻塞式的select
- 为避免jkd空轮训的bug,检查空轮训次数;如果超过512就创建一个新的selector,并将原有selector的selectionKey赋给新的selector
- processSelectKeys()
对NioEventLoop读取到的selectionKey对应的事件进行处理
- runAllTasks()
任务有两种,一种是普通的任务,另一种是定时任务。在处理之前,任务被添加到了不同的队列里,所以要先把两种任务合并到一个任务队列,然后依次执行。
此外,还要检查当前时间是否超过了允许执行任务的时间,如果超时就直接中断,进行下一次NioEventLoop的执行循环。
新连接接入
检测新连接
总体流程
在服务端的NioEventLoop执行的第二个过程processSelectKeys中检测出accept事件后, 通过JDK的accept方法创建JDK的一个channel
创建NioSocketChannel
逐层调用父类构造函数,将服务端NioServerSocketChannel和accept()创建的channel作为参数;设置阻塞模式为false,保存读事件,创建unsafe、pipline组件,禁止Nagel算法
服务端channel在检测到新连接并创建完NioSocketChannel后,会通过ServerBootStrapAcceptor对NioSocketChannel做一些处理:
- 添加childHandler
- 设置options和attrs
- 通过chooser在childGroup中选择NioEventLoop并注册selector
创建完成之后,调用pipeline.fireChannelActive()向selector注册读事件,开始准备接受I/O数据。
在执行任务的时候,经常会用到inEventLoop()方法,这个方法是用来判断当前线程是否是EventLoop中的线程。
如果是直接执行,不是就把任务放到MpscQueue(Multi producer single consumer)任务队列中,这样任务交给指定的NioEventLoop执行,不必加锁。
pipeline
初始化
pipeline在创建channel的时候被创建
protected AbstractChannel(Channel parent) {
this.parent = parent;
id = newId();
unsafe = newUnsafe();
pipeline = newChannelPipeline();
}
pipeline中的节点是ChannelHandlerContext类型的,双向链表结构;在初始化时就会创建head、tail两个节点
protected DefaultChannelPipeline(Channel channel) {
this.channel = ObjectUtil.checkNotNull(channel, "channel");
succeededFuture = new SucceededChannelFuture(channel, null);
voidPromise = new VoidChannelPromise(channel, true);
tail = new TailContext(this);
head = new HeadContext(this);
head.next = tail;
tail.prev = head;
}
tail和head分别是inbound和outbound类型的。
channelHandler的添加在用户代码配置childHandler时完成
childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new AuthHandler());
}
})
添加时首先判断是否重复添加,然后创建节点(HandlerContext)添加至链表,最后回调添加完成的事件。
channelHandler的删除操作与添加类似,首先找到节点将它从链表上删除,最后回调删除handler事件。
事件传播
事件传播有两种方式:
- pipeline传播: 这种方式会从pipeline的head节点开始传播
- channelContext传播: 从当前节点开始传播
inbound事件
inBound事件包括register、active、active事件
每次传播都会找到下一个inbound类型的节点,最后传播给tail节点(tail也是inbound类型)
outBound事件
outBound事件传播方向与inBound相反,是从tail向head传播
异常事件传播与以上两种不同,它不关心是inBoundhandler还是outboundhandler,并且只会从发生异常的节点开始向后传播,到达tail节点。
所以,最好在tail节点前创建一个专门用于处理异常的节点,重写exceptionCaught方法,这样所有异常都能被捕获。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。