源码解析目标
- 理解Netty内部心跳服务源码实现。
源码解析使用案例
public class MyServer {
public static void main(String[] args) {
EventLoopGroup boosGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try{
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boosGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new IdleStateHandler(7000, 7000, 10, TimeUnit.SECONDS));
// pipeline.addLast(new ReadTimeoutHandler(7));
// pipeline.addLast(new WriteTimeoutHandler(7));
pipeline.addLast(new MyServerHandler());
}
});
//启动服务器
ChannelFuture channelFuture = bootstrap.bind(7000).sync();
channelFuture.channel().closeFuture().sync();
}catch (Exception e){
}finally {
boosGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
/**
* Created by jiamin5 on 2022/11/29.
*/
public class MyServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent){
IdleStateEvent event = (IdleStateEvent) evt;
String eventType = null;
switch (event.state()){
case ALL_IDLE:
eventType = "读写空闲";
break;
case READER_IDLE:
eventType = "读空闲";
break;
case WRITER_IDLE:
eventType = "写空闲";
break;
}
System.out.println(ctx.channel().remoteAddress()+ "--超时时间--" + eventType);
System.out.println("服务器处理....");
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
super.channelReadComplete(ctx);
}
}
- 如上源码中我们对Netty服务器的(读/写)活动情况进行监听,利用IdleStateHandler 来完成,其中三个参数分别读,写,读/写 空闲时间
- 方法效果总结如下表中
序号 | 名称 | 作用 |
---|---|---|
1 | IdleStateHandler | 当连接的空闲时间(读/写 ) 太长时候,会触发IdleStateHandler事件,然后,可以通过我们在ChannelInboundHandler中重写 userEventTriggered 方法来处理对应的事件 |
2 | ReadTimeoutHandler | 如果在指定的时间内没有发生读事件,会抛出异常,并且自动关闭连接,我们可以在exceptionCaught 方法中处理这个异常 |
3 | WriteTimeoutHandler | 当一个写操作不能在一定的时间内完成时候,抛出此异常,并且关闭连接,同样可以在exceptionCaught 方法中处理这个异常 |
- ReadTimeoutHandler,WriteTimeoutHandler 两个事件都会自动关闭连接,而且术语异常处理,我们以下中点介绍一下下IdleStateHandler 源码
源码分析
- 首先当 调用addLast添加IdleStateHandler到pipeline中时候,会触发handlerAdd方法
//添加操作
pipeline.addLast(new IdleStateHandler(7000, 7000, 10, TimeUnit.SECONDS));
//handlerAdder
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
if (ctx.channel().isActive() && ctx.channel().isRegistered()) {
initialize(ctx);
} else {
}
}
- 关注initialize 方法,如下代码中,对IdleStateHandler的属性进行初始化过程,涉及到以下几个
private void initialize(ChannelHandlerContext ctx) {
// Avoid the case where destroy() is called before scheduling timeouts.
// See: https://github.com/netty/netty/issues/143
switch (state) {
case 1:
case 2:
return;
}
state = 1;
initOutputChanged(ctx);
lastReadTime = lastWriteTime = ticksInNanos();
if (readerIdleTimeNanos > 0) {
readerIdleTimeout = schedule(ctx, new ReaderIdleTimeoutTask(ctx),
readerIdleTimeNanos, TimeUnit.NANOSECONDS);
}
if (writerIdleTimeNanos > 0) {
writerIdleTimeout = schedule(ctx, new WriterIdleTimeoutTask(ctx),
writerIdleTimeNanos, TimeUnit.NANOSECONDS);
}
if (allIdleTimeNanos > 0) {
allIdleTimeout = schedule(ctx, new AllIdleTimeoutTask(ctx),
allIdleTimeNanos, TimeUnit.NANOSECONDS);
}
}
- private final boolean observeOutput; : 是否考虑出站时候较慢的情况,默认是false
- private final long readerIdleTimeNanos; : 读事件空闲时间,0 则禁用事件
- private final long writerIdleTimeNanos; : 写事件空闲时间,0 则禁用事件
- private final long allIdleTimeNanos; : 读/写事件空闲时间,0 则禁用事件
- 源码中,当参数 > 0 时候,就创建一个定时任务,每个事件都创建,同时将state设置为1 ,避免重复初始化,调用initOutputChanged方法初始化监控出站数据属性。
- 创建定时任务源码如下, 利用channel中的EventLoop来添加一个Scheduler,看源码并debug,观察EvengLoop中Scheduler线程池中是否有新增任务,如下:
ScheduledFuture<?> schedule(ChannelHandlerContext ctx, Runnable task, long delay, TimeUnit unit) {
return ctx.executor().schedule(task, delay, unit);
}
- 在ScheduledTaskQueue中的确有3个Task,因为我们addLast的方法中有三个参数都被初始化了,那么我们来看下对应的三个Task实现方法。
- 三个Task都是ChannelDuplexHandler 中的内部类,并且有一个共同父类,AbstractIdleTask
private abstract static class AbstractIdleTask implements Runnable {
private final ChannelHandlerContext ctx;
AbstractIdleTask(ChannelHandlerContext ctx) {
this.ctx = ctx;
}
@Override
public void run() {
if (!ctx.channel().isOpen()) {
return;
}
run(ctx);
}
protected abstract void run(ChannelHandlerContext ctx);
}
-
AbstractIdleTask内部类设计用来单例模式,模板模式UML如下
-
模板方法的通用流程:当通道关闭,就不执行任务,反之则执行抽象方法run方法,
读事件task分析
- 先看ReaderIdleTimeoutTask 方法中的run方法的实现
private final class ReaderIdleTimeoutTask extends AbstractIdleTask {
ReaderIdleTimeoutTask(ChannelHandlerContext ctx) {
super(ctx);
}
@Override
protected void run(ChannelHandlerContext ctx) {
long nextDelay = readerIdleTimeNanos;
if (!reading) {
nextDelay -= ticksInNanos() - lastReadTime;
}
if (nextDelay <= 0) {
// Reader is idle - set a new timeout and notify the callback.
readerIdleTimeout = schedule(ctx, this, readerIdleTimeNanos, TimeUnit.NANOSECONDS);
boolean first = firstReaderIdleEvent;
firstReaderIdleEvent = false;
try {
IdleStateEvent event = newIdleStateEvent(IdleState.READER_IDLE, first);
channelIdle(ctx, event);
} catch (Throwable t) {
ctx.fireExceptionCaught(t);
}
} else {
// Read occurred before the timeout - set a new timeout with shorter delay.
readerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
}
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (readerIdleTimeNanos > 0 || allIdleTimeNanos > 0) {
reading = true;
firstReaderIdleEvent = firstAllIdleEvent = true;
}
ctx.fireChannelRead(msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
if ((readerIdleTimeNanos > 0 || allIdleTimeNanos > 0) && reading) {
lastReadTime = ticksInNanos();
reading = false;
}
ctx.fireChannelReadComplete();
}
-
readerIdleTimeNanos 是我们在addLast方法的时候自定义的事件
-
reading 属性值在channelRead,channelReacComplete方法中设置,我们之前源码分析中已经解析过
- 当读事件触发时候,首先会执行channelRead方法,此时我们将reading = true,并且设置firstReaderIdleEvent,firstAllIdleEvent第一次读,第一次读/写 事件标志设置为true
- 当读事件完成时候,会执行channelReadComplete,此时我们将reading =false,并且设置最后一次读的时间lastReadTime
-
我们在第二步骤中,读取操作结束,用当前时间ticksInNanos() 减去 最后一次读的时间lastReadTime,如果 < 0,就出发事件,反之,重新放入队列,并且间隔时间是重新计算后的时间
-
事件触发逻辑:
- 将任务再次放入队列,并且时间重置为我们最初设置的时间,也就是在一次监听读事件,此处返回的是一个promise,此处需要解释如下图类结构,schedule 方法返回的是ScheduledFuture 是一个接口,他的一个实现类是ScheduledFutureTask,这个实现类是promise的一个子类,因此此处返回的promise作用是做取消草足,
- 接着设置first = firstReaderIdleEvent;,并且将firstReaderIdleEvent设置为false,也就是定义第一次的读事件结束了
- 创建一个IdleStateEvent,是一个枚举,并且将对象传递给UserEventTriggered,
- UserEventTriggered 就是我们自己实现的方法,这样就将事件传递到我们自己的方法,我们可以安返回事件类型做对应业务处理。
写事件task分析
- 写事件的run方法WriterIdleTimeoutTask, 和读事件的差不多,如下有一点区别
private final class WriterIdleTimeoutTask extends AbstractIdleTask {
WriterIdleTimeoutTask(ChannelHandlerContext ctx) {
super(ctx);
}
@Override
protected void run(ChannelHandlerContext ctx) {
long lastWriteTime = IdleStateHandler.this.lastWriteTime;
long nextDelay = writerIdleTimeNanos - (ticksInNanos() - lastWriteTime);
if (nextDelay <= 0) {
// Writer is idle - set a new timeout and notify the callback.
writerIdleTimeout = schedule(ctx, this, writerIdleTimeNanos, TimeUnit.NANOSECONDS);
boolean first = firstWriterIdleEvent;
firstWriterIdleEvent = false;
try {
if (hasOutputChanged(ctx, first)) {
return;
}
IdleStateEvent event = newIdleStateEvent(IdleState.WRITER_IDLE, first);
channelIdle(ctx, event);
} catch (Throwable t) {
ctx.fireExceptionCaught(t);
}
} else {
// Write occurred before the timeout - set a new timeout with shorter delay.
writerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
}
}
}
- 不通地方在于,run代码中多了一个判断hasOutputChanged 用来判断出站事件处理是否很慢,例如,我一个出站需要写的数据量大,需要处理20秒,但是我们执行写事件监测的时间是 5秒,此时我们只有第一次会触发,因为 hasOutputChanged 中有判断first 的时候还是会执行异常抛出
所有事件(读/写)事件task分析
- 与写事件基本一致,不通地方在于事件判断
- nextDelay -= ticksInNanos() - Math.max(lastReadTime, lastWriteTime); 当前时间 - 最后一次写/读 的时间 如果> 0说明超时了
- 此处的时间判断是取读,写事件中的最大值,然后像写事件一样,判断是否发生了写很慢的情况
Netty心跳机制总结
- IdleStateHandler实现心跳检测功能,当服务器和客户端没有任务读写,并且超过设置事件,会触发handler的userEentTriggered方法,用户可以在这个方法中实现自己的逻辑
- IdleStateHandler的实现基于EventLoop的定时任务,每次读写都会记录一个最后读/写事件,定时任务执行的时候,根据最后读写事件与间隔时间的差值来判断是否执行
- 内部有3 个定时任务,分别对应读,写,读/写事件,通常我们监听读/写事件就足够
- IdleStateHandler 内部考虑了极端情况,例如客户端接收缓慢问题,一次接收数据的事件超过了我们设置的最长等待时间。Netty通过构造方法中 observeOutput 属性来决定是否对出站缓冲区的情况进行判断
- 如果observeOutput = true情况,并且出现出站缓慢,Netty将不认为是空闲,也不会执行定时任务,除非是第一次写或者读/写事件,第一次无论如何也会触发,因为第一次无法判断是出站缓慢还是空闲,
- 出站缓慢可能造成OOM,所以当我们应用出现OOM之类,并且写空闲极少发生,使用了observeOutput = true,那么可能需要注意是不是出站速度过慢导致的(源码中注释)
// We can take this shortcut if the ChannelPromises that got passed into write()
// appear to complete. It indicates "change" on message level and we simply assume
// that there's change happening on byte level. If the user doesn't observe channel
// writability events then they'll eventually OOME and there's clearly a different
// problem and idleness is least of their concerns.
- 以上都是对IdleStateHandler 源码的分析
- ReadTimeoutHandler 继承自IdleStateHandler,当触发读空闲,触发 ctx.fireExceptionCaught,并给一个Exception,如下源码
protected void readTimedOut(ChannelHandlerContext ctx) throws Exception {
if (!closed) {
ctx.fireExceptionCaught(ReadTimeoutException.INSTANCE);
ctx.close();
closed = true;
}
}
- WriteTimeoutHandler 的实现不是基于IdleStateHandler,继承ChannelOutboundHandlerAdapter,当调用write方法时候,会创建一个定时任务,参数是promise ,定时任务根据promise 的完成情况来判断是否超出写事件,当定时任务发现task.scheduledFuture.isDone() 返回false,表示没写完,超时抛出异常,否则打断定时任务
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (timeoutNanos > 0) {
promise = promise.unvoid();
scheduleTimeout(ctx, promise);
}
ctx.write(msg, promise);
}
private void scheduleTimeout(final ChannelHandlerContext ctx, final ChannelPromise promise) {
// Schedule a timeout.
final WriteTimeoutTask task = new WriteTimeoutTask(ctx, promise);
task.scheduledFuture = ctx.executor().schedule(task, timeoutNanos, TimeUnit.NANOSECONDS);
if (!task.scheduledFuture.isDone()) {
addWriteTimeoutTask(task);
// Cancel the scheduled timeout if the flush promise is complete.
promise.addListener(task);
}
}