Netty源码剖析:handler 中加入线程池和 Context 中添加线程池的源码剖析

源码剖析目的

1) 在 Netty 中做耗时的,不可预料的操作,比如数据库,网络请求,会严重影响 Netty 对 Socket 的处理速度。

2) 而解决方法就是将耗时任务添加到异步线程池中。但就添加线程池这步操作来讲,可以有 2种方式,而且这 2种方式实现的区别也蛮大的。
3) 处理耗时业务的第一种方式---handler 中加入线程池
4) 处理耗时业务的第二种方式---Context 中添加线程
5) 我们就来分析下两种方式
源码剖析
 说明
演示两种方式的实现,以及从源码来追踪两种方式执行流程
源码剖析
处理耗时业务的第一种方式--handler 种加入线程池
对前面的 Netty demo源码进行修改,在 EchoServerHandler 的 channelRead 方法进行异步

@Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {	
static final EventExecutorGroup group = new DefaultEventExecutorGroup(16);
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws UnsupportedEncodingException,
InterruptedException {
final Object msgCop = msg;
final ChannelHandlerContext cxtCop = ctx;
group.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
ByteBuf buf = (ByteBuf) msgCop;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, "UTF-8");
Thread.sleep(10*1000);
System.err.println(body + " " + Thread.currentThread().getName());
String reqString = "Hello i am server~~~";
ByteBuf resp = Unpooled.copiedBuffer(reqString.getBytes());
cxtCop.writeAndFlush(resp);
return null;
}
});
System.out.println("go on ..");	
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// Close the connection when an exception is raised.
cause.printStackTrace();
ctx.close();
}
}

说明:
1) 在 channelRead 方法,模拟了一个耗时 10 秒的操作,这里,我们将这个任务提交到了一个自定义的业务线程池中,这样,就不会阻塞 Netty 的 IO 线程。    
这样处理之后,整个程序的逻辑如图


说明:
1) 解释一下上图,当 IO 线程轮询到一个 socket 事件,然后,IO 线程开始处理,当走到耗时 handler 的时候,将耗时任务交给业务线程池。
2) 当耗时任务执行完毕再执行 pipeline write 方法的时候 ,(代码中使用的是 context 的 write 方法,上图画的是执行 pipeline 方法, 是一个意思)会将任务这个任务交给 IO 线程

write 方法的源码(在 AbstractChannelHandlerContext 类)

private void write(Object msg, boolean flush, ChannelPromise promise) {
AbstractChannelHandlerContext next = findContextOutbound();
final Object m = pipeline.touch(msg, next);
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
if (flush) {
next.invokeWriteAndFlush(m, promise);
} else {
next.invokeWrite(m, promise);
}
} else {	
AbstractWriteTask task;
if (flush) {
task = WriteAndFlushTask.newInstance(next, m, promise);
} else {
task = WriteTask.newInstance(next, m, promise);
}
safeExecute(executor, task, promise, m);
}
}

说明:
1) 当判定下个 outbound 的 executor 线程不是当前线程的时候,会将当前的工作封装成 task ,然后放入mpsc 队列中,等待 IO 任务执行完毕后执行队列中的任务。
2) 这里可以 Debug 来验证(提醒:Debug 时,服务器端 Debug ,客户端 Run 的方式 ),当我们使用了group.submit(new Callable<Object>(){} 在 handler 中加入线程池,就会进入到 safeExecute(executor, task,promise, m); 如果去掉这段代码,而使用普通方式来执行耗时的业务,那么就不会进入到 safeExecute(executor,task, promise, m); (说明:普通方式执行耗时代码,看我准备好的案例即可)    
处理耗时业务的第二种方式-Context 中添加线程池
1.1 在添加 pipeline 中的 handler 时候,添加一个线程池
    //属性

static final EventExecutorGroup group = new DefaultEventExecutorGroup(16);
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)	
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(ch.alloc()));
}
//p.addLast(new LoggingHandler(LogLevel.INFO));
//p.addLast(new EchoServerHandler());
p.addLast(group,new EchoServerHandler() );
}
});

 说明:
1) handler 中的代码就使用普通的方式来处理耗时业务。
2) 当我们在调用 addLast 方法添加线程池后,handler 将优先使用这个线程池,如果不添加,将使用 IO 线程
3) 当走到 AbstractChannelHandlerContext 的 invokeChannelRead 方法的时候,executor.inEventLoop() 是不会通过的,因为当前线程是 IO 线程 Contex(t 也就是 Handler)的 executor 是业务线程,所以会异步执行, debug下源码

static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
next.invokeChannelRead(m);	
} else {
executor.execute(new Runnable() { //执行 run
@Override
public void run() {
next.invokeChannelRead(m);
}
});
}
}

4) 验 证 时 , 我 们 如 果 去 掉 p.addLast(group,new EchoServerHandler() ); 改 成 p.addLastnew
EchoServerHandler() ); 你会发现代码不会进行异步执行
5) 后面的整个流程就变成和第一个方式一样了    
两种方式的比较
1) 第一种方式在 handler 中添加异步,可能更加的自由,比如如果需要访问数据库,那我就异步,如果不需要,就不异步,异步会拖长接口响应时间。因为需要将任务放进 mpscTask 中。如果 IO 时间很短,task 很多,可能一个循环下来,都没时间执行整个 task,导致响应时间达不到指标。
2) 第二种方式是 Netty 标准方式(即加入到队列),但是,这么做会将整个 handler 都交给业务线程池。不论耗时不耗时,都加入到队列里,不够灵活。
3) 各有优劣,从灵活性考虑,第一种较好

Supongo que te gusta

Origin blog.csdn.net/weixin_46300935/article/details/119819567
Recomendado
Clasificación