带你读《Apache Tomcat的云原生演进》——Tomcat的技术内幕和在喜马拉雅的实践(1)https://developer.aliyun.com/article/1377542
一般情况下,app发一个请求过来,你的包可能是完整的,但如果发一半过来,这个请求行可能是不完整的,这个时候它是不会阻塞的,它就会注册一个读事件读剩余的。读完请求行以后,它会继续解析头。解析头和解析行是一样的,也是不会阻塞。只需要配置好头的大小,它就会按照头的大小读。
因为Tomcat里有很多filter和servlet,然后我们自己做了一些路由,去匹配filter和servlet。一般我们这里处理完以后,我们就会去读它的body,但它和请求行、请求头不一样,它会阻塞。所以如果body不完整,比如content-length是1000,body只读到500,就会阻塞在Catalina work线程。这个时候它会通过增加一个wait,等到下次Poller线程来唤醒Catalina work线程。
前面介绍了NIO模型主要分为三个部分Accept线程,Poller线程,Catalina work线程。
目前Tomcat已经支持了NIO2,它和NIO有一个很大的不同,中间没有Accept线程,Poller线程,它把NIO模式简化了。所以很多人说NIO2是异步的,但其实也不是,它只是基于自己的JDK实现epoll、API进行异步模拟的。
NIO2在事件可读的时候会告诉操作系统,然后我这个时候就返回去做其他事情了。操作系统会把数据读到我的包里,然后告诉我可以用数据了,我就直接读数据就行了。而NIO同步要发几个读,我需要自己触发操作系统去copy数据过来。然后我还需要在这里等着,等copy完了,再去把数据读出来,是会阻塞在这里的。
NIO2并不是真正的NIO,它的API使用机制模拟会告诉你读和写都完成了,所以整个模型是一个EPoller线程。它相当于直接操作EPoller 的wait,只要线程有事件来,它就会生成连接事件,读/写事件,他这里有一个队列,只要有事件就会一直读,不会说新建一个连接事件,我就处理读事件,它会把所有完整的事件先读出来。
读完以后就可以去处理了,如果是连接,我就执行accept这个操作,把连接读出来。接受连接不像NIO是单路线,有一个区别是,一般连接是有限制的,默认是1万。如果你是-1或者是没有限制的,accept就一直不会阻塞。如果是有限制,可能达到目标就会阻塞。
所以这个时候就会有两个不同的处理方式,如果是有限制的,不会用当前的Poller线程去做accept的操作,它会新提交一个任务到线程池里,让后面接着做accept的连接,因为不能阻塞Poller线程。
如果当时我是个读事件的话,我只要读出来就能提交给Catalina work线程,这个时候读是由Poller线程完成的。
假如我新建一个连接,做了三次握手,但是我不给你发数据。那么Poller线程就读不到数据,因此也就不会做处理。它只是注册一个事件到Poller线程,等数据来了再来读。还是和之前的不同,不会像NIO一样,刚开始肯定要去注册一个读事件。NIO2会直接读,一般很多的网络框架优化都是在这个地方做的。我是直接尝试读,不会去注册读事件。因为大部分情况下建立连接以后,后面就会发现不需要去做读事件的操作。因为你做一个读事件,这个读事件就会系统调用,就会涉及到上下文切换的开销。
我一直在这里习惯用select,有事件就EPoll这个地方,没有就阻塞就EPoll这个地方。EPoller线程就是做这个事情。刚刚我们接触的连接它也是通过Catalina work线程,它们起来的时候可以添加一个accept task,就是先让你接受请求。
它和刚才说的读body一样,当body不够的时候,才会去注册一个事件到Epoller线程上去,它的读也是阻塞的。
NIO和Catalina不一样的是,它只负责读和写,比NIO少了注册事件的操作。
带你读《Apache Tomcat的云原生演进》——Tomcat的技术内幕和在喜马拉雅的实践(3)https://developer.aliyun.com/article/1377540