一、 前言
最近在看tomcat connector组件的相关源码,对Nio2的异步回调过程颇有兴趣,平时读源码不读,自己读的时候很多流程都没搞明白,去查网上相关解析讲的给我感觉也不是特别清晰,于是就自己慢慢看源码,以下是我自己的见解,因为开发经验也不多,刚成为社畜不久,有些地方讲错如果有大佬看到也希望能够指正指导。
以下代码基于tomcat8.5版本
二、基本流程
在tomcat的nio2流程下,会有多个Acceptor通过线程池进行管理运行,一个连接请求进来,会先被Acceptor监听
protected class Acceptor extends AbstractEndpoint.Acceptor {
@Override
public void run() {
....
// Configure the socket
if (running && !paused) {
// setSocketOptions() will hand the socket off to
// an appropriate processor if successful
if (!setSocketOptions(socket)) { // 监听到socket请求后进入到这里面
closeSocket(socket);
}
} else {
closeSocket(socket);
}
...
进入setSocketOptions()方法
protected boolean setSocketOptions(AsynchronousSocketChannel socket) {
try {
socketProperties.setProperties(socket);
Nio2Channel channel = nioChannels.pop();
...
Nio2SocketWrapper socketWrapper = new Nio2SocketWrapper(channel, this);
channel.reset(socket, socketWrapper);
...
// 用另外一个线程处理这个socketWrapper(实现了runnable)
return processSocket(socketWrapper, SocketEvent.OPEN_READ, true);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error("",t);
}
// Tell to close the socket
return false;
}
再进入processSocket()方法,sc被提交到了线程池里面处理
继续跟进源码
在workQueue.offer(command)里面可以看到提交到了任务队列里面,等待线程池的线程执行这个任务
看看执行processSocket()时,做了那些事情,这个线程调度最终会执行到Nio2EndPoint里面的doRun()方法:
在doRun()方法里面执行到这行
通过getHandler拿到了AbstactProtocol
再通过后续流程,拿到了Http11Processor来对当前这个socketWrapper进行处理,Http11Processor会调用Nio2SocketWrapper中的read()方法进行处理
注意:Nio2SocketWrapper有个回调方法,这个回调方法会被注册,后续当数据准备好后会调用这个completed()方法来进行数据读取,部分代码如下:
第一次是非回调读,主要是进行注册操作,会经历进入sockerwrapper里面的read()方法再到fillReadBuffer(),并且会在fillReadBuffer()里面进行注册回调操作
先看以下read方法(),这个地方是关键,第一次读和回调读的区别就在下面这行代码,第一次读因为应用层的buffer没有数据,不会返回,会继续执行
会继续执行到fillReadBuffer()方法里面,在这里面进行回调函数的注册,并把数据的读取交到操作系统内核,由内核将数据拷贝到应用层的buffer,再这个执行回调
这是相关的调用栈
跟进源码,会调用到WindowsAsychronusSocketChannel的相关方法,由内核去拷贝数据
数据准备完成后,我这里猜测是底层会调用我们的回调方法,进行后续的读取操作。
数据已经准备到了buffer里面,这时另外启动一个线程执行回调方法,会执行到里面最后一行,processSocket()
然后你会发现,回调的流程和首次进行注册的流程的调用栈基本一致
差别在,read()方法里面,在回调读的时候,会因为nRead>0返回,并进行后续读到数据的处理
最后再把整套逻辑捋一遍:在tomcat的nio2下,会有多个acceptor,通过tommcat的线程池管理,当一个acceptor监听到连接后,将socket包装成一个socketWrapper,再建一个SocketProcessor,丢到线程池里面,另外启动一个线程执行SocketProcessor的run方法,这时候这个acceptor的监听任务就结束,会返回继续监听其他请求。 后面执行run的时候拿到了Http11Processor来对当前这个socketWrapper进行处理,Http11Processor会调用Nio2SocketWrapper中的read()方法进行处理,在这里会进行第一次读数据,因为buffer里面并没有数据,会进行回调函数的注册,并把拷贝数据的任务交到内核去完成。内核完成后执行回调函数,回调函数再去进行第二次读,将数据从buffer里面读出来,并执行后面的操作,至此实现了非阻塞异步读的流程。
核心思想:应用程序是无法直接访问到内核空间的,内核空间涉及到的数据都需要内核将数据拷贝到用户空间。为了解决这个问题,NIO2实际上让应用程序调用读数据操作的时候,告诉内核数据应该拷贝到哪个buffer,以及将回调函数进行注册,告诉内核调用哪个回调函数。之后,内核会在网卡数据到达,产生硬件中断,内核在中断程序里面把数据从网卡拷贝到内核空间,接着做TCP/IP协议层面的数据解包重组,把数据拷贝到应用程序指定的Buffer,最后执行回调函数。
参考资料:《深入拆解Tomcat & Jetty》
标签:nio2,socket,tomcat,源码,线程,内核,里面,回调,数据 From: https://www.cnblogs.com/scottyzh/p/17804093.html