如果 Handler处理器有一些比较长时间的耗时业务处理等操作时,我们可以交给任务队列异步处理。
在前面的 Netty 服务器与客户端的 Demo中 , 我们自定义的 Handler 处理器都是同步操作。
该处理器继承 ChannelInboundHandlerAdapter 类重写的 channelRead方法,或者继承它的子类 SimpleChannelInboundHandler类重写 channelRead0方法。
然后重写的执行业务逻辑时要注意以下两点 :
在 ChannelInboundHandlerAdapter 的 channelRead 方法执行时 , 客户端与服务器端的反应器 Reactor 线程 NioEventLoop 是处于阻塞状态的 , 此时服务器端与客户端同时都处于阻塞状态 , 这样肯定不行 , 因为 NioEventLoop 需要为多个客户端服务 , 不能因为与单一客户端交互而产生阻塞 ;
多任务执行 :
如果用户连续向任务队列中放入了多个任务 , NioEventLoop 会按照顺序先后执行这些任务 , 注意任务队列中的任务是先后执行 , 不是同时执行 ;
先后顺序执行任务 ( 不是并发 ) :
任务队列任务执行机制是顺序执行的 ; 先执行第一个 , 执行完毕后 , 从任务队列中获取第二个任务 , 执行完毕之后 , 依次从任务队列中取出任务执行 , 前一个任务执行完毕后 , 才从任务队列中取出下一个任务执行 ;
execute 方法
, 即可将上述 Runnable 异步任务放入任务队列 TaskQueue
;public class MyServerHandler extends ChannelInboundHandlerAdapter {
private static final Logger log = LoggerFactory.getLogger(MyServerHandler.class);
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("MyServerHandler 连接已建立...");
super.channelActive(ctx);
}
/**
* 读取数据 : 在服务器端读取客户端发送的数据
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 获取客户端发送过来的消息
ByteBuf in = (ByteBuf) msg;
log.info("Server Accept Client Context ("+ ctx.channel().remoteAddress() +")消息 ->" + in.toString(CharsetUtil.UTF_8));
// 1 . 从 ChannelHandlerContext中获取通道
Channel channel = ctx.channel();
// 2 . 获取通道对应的事件循环
EventLoop eventLoop = channel.eventLoop();
// 3 . 在 Runnable 中用户自定义耗时操作, 异步执行该操作, 该操作不能阻塞在此处执行
eventLoop.execute(new Runnable() {
@Override
public void run() {
//TODO 执行耗时操作
try {
TimeUnit.SECONDS.sleep(10);
log.info("异步任务 1执行,Thread = {}", Thread.currentThread().getName());
//请求讲当前的msg 通过ChannelPipeline 写入数据到目标Channel 中。
// 值得注意的是:write 操作只是将消息存入到消息发送环形数组中,并没有真正被发送,只有调用flush 操作才会被写入到Channel 中,发送给对方。
ctx.writeAndFlush(Unpooled.copiedBuffer("异步任务 1", CharsetUtil.UTF_8));
log.info("异步任务 1执行完毕");
} catch (Exception ex) {
System.out.println("发生异常" + ex.getMessage());
}
}
});
ctx.channel().eventLoop().execute(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(5);
log.info("异步任务 2执行,Thread = {}" , Thread.currentThread().getName());
ctx.writeAndFlush(Unpooled.copiedBuffer("异步任务 2", CharsetUtil.UTF_8));
log.info("异步任务 2执行完毕");
} catch (Exception ex) {
log.info("发生异常,message={}" + ex.getMessage());
}
}
});
log.info("channelReadComplete方法执行完毕,Thread = {}", Thread.currentThread().getName());
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// 发送消息给客户端
ByteBuf byteBuf = Unpooled.copiedBuffer("Server Received Client msg.", CharsetUtil.UTF_8);
ctx.writeAndFlush(byteBuf);
log.info("channelReadComplete方法执行完毕,Thread = {}", Thread.currentThread().getName());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// 发生异常,关闭通道
// cause.printStackTrace();
ctx.close();
}
}
先启动服务端,再启动客户端,结果如下:
从打印结果,可以看出:
scheduleTaskQueue延时任务队列和 taskQueue任务队列非常相似。
schedule 方法
, 即可将上述 Runnable 异步任务放入任务队列 ScheduleTaskQueue
;先启动服务端,再启动客户端,结果如下:
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 获取客户端发送过来的消息
ByteBuf in = (ByteBuf) msg;
log.info("Server Accept Client Context (" + ctx.channel().remoteAddress() + ")消息 ->" + in.toString(CharsetUtil.UTF_8));
ctx.channel().eventLoop().schedule(() -> {
try {
// TODO 执行耗时操作
log.info("延迟异步任务执行,Thread = {}", Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(5);
ctx.writeAndFlush(Unpooled.copiedBuffer("异步任务 2", CharsetUtil.UTF_8));
log.info("延迟异步任务执行完毕");
} catch (Exception ex) {
log.info("发生异常,message={}" + ex.getMessage());
}
}, 5, TimeUnit.SECONDS);
log.info("channelRead方法执行完毕,Thread = {}", Thread.currentThread().getName());
}
MyServer打印结果:
从打印结果,可以看出:任务延迟了 5s之后才被执行。
自定义任务和自定义定时任务区别流程基本类似 , 有以下两个不同之处:
(1)调度⽅法 :
(2)任务队列 :
定时异步任务提交到 ScheduleTaskQueue 任务队列中 ;
普通异步任务提交到 TaskQueue 任务队列中 ;
上面使用异步任务调度所使用的的线程仍然是和IO操作是同一个线程,因此如果做的是比较耗时的工作或不可预料的操作,⽐如数据库,⽹络请求,会严重影响 Netty 对 Socket 的处理速度。
解决方法:我们可以将耗时任务添加到异步线程池中。
使用方法比较简单,就是创建了一个EventExecutorGroup,并向其中提交任务。
可以看到,这样就是用了与IO操作不同的线程来处理业务逻辑。并且每个客户端的请求使用的都是不同的线程。
public class MyServerHandler3 extends ChannelInboundHandlerAdapter {
private static final Logger log = LoggerFactory.getLogger(MyServerHandler3.class);
private static final EventExecutorGroup eventExecutorGroup = new DefaultEventExecutorGroup(3);
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("MyServerHandler 连接已建立...");
super.channelActive(ctx);
}
/**
* 读取数据 : 在服务器端读取客户端发送的数据
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 获取客户端发送过来的消息
ByteBuf in = (ByteBuf) msg;
log.info("Server Accept Client Context (" + ctx.channel().remoteAddress() + ")消息 ->" + in.toString(CharsetUtil.UTF_8));
for (int i = 1; i <= 5; i++) {
int finalI = i;
eventExecutorGroup.submit(() -> {
//TODO 执行耗时操作
try {
TimeUnit.SECONDS.sleep(10);
log.info("异步任务 {}执行,Thread = {}", finalI, Thread.currentThread().getName());
ctx.writeAndFlush(Unpooled.copiedBuffer("异步任务 " + finalI, CharsetUtil.UTF_8));
log.info("异步任务 {}执行完毕", finalI);
} catch (Exception ex) {
System.out.println("发生异常" + ex.getMessage());
}
});
}
log.info("channelRead方法执行完毕,Thread = {}", Thread.currentThread().getName());
}
...
}
– 求知若饥,虚心若愚。