做那么久web开发工程师,却一直没时间亲自研究tomcat中间件到底是个啥,网络传输怎么玩法,对于程序员的好奇心非常重要,针对于此,个人亲自研究一下tomcat NIO启动模式(tomcat9具有四种模式NIO、NIO2、BIO、Apr)
前提
这里提及前提,那是因为如果没有前提知识,要读懂tomcat是有一定的困难,个人认为要解读tomcat源码之前,最好具备以下前提知识(可自行选择阅读,若有时间均阅读更佳)
- 三次握手和TCP原理 https://blog.csdn.net/lijin_12456/article/details/84887878
- Linux下网络编程 https://blog.csdn.net/weixin_44895651/article/details/108163533
- http协议 https://www.cnblogs.com/an-wen/p/11180076.html
- 网络拓扑和安全 https://blog.csdn.net/soft_z1302/article/details/114678095
- Java NIO Tutorial http://tutorials.jenkov.com/java-nio/index.html
- TCP和NIO流程 https://blog.csdn.net/u011381576/article/details/79876754
- java常见各种锁 https://www.jianshu.com/p/e6e794b64f80
- tomcat简单配置和spring mvc以及过滤器拦截器的基本理念
- 线程池多线程 https://blog.csdn.net/soft_z1302/article/details/110440449
Non-blocking Server讲解
为了增加阅读兴趣,个人来看一下Jakob Jenkov写的文章描述和样例源码,Jakob Jenkov编写的源码服务器,由两个线程协同处理socket请求,一个接收socket,存放到队列,一个处理socket队列,并输出流(图片引用Jakob Jenkov描述)
创建两个线程
1.线程监听serversocketChannel,存放socketChannel到queue中
2.线程不停轮询queue,若有则注册SelectionKey.OP_READ中,通过selectedKeys读取通信(信号读半消息,存放到list中,最终合并),
然后注册到读代理队列,从读代理队列获取注册到写中,写入返回数据
3.buffer缓存循环利用,每个线程占用一段byte【begin,end】,不相干扰。
接下来执行一下代码(源码可自行下载,上面链接),查看请求报文和返回报文
在com.jenkov.nioserver.Message#writePartialMessageToMessage方法中增加输出读取的缓存数据
public void writePartialMessageToMessage(Message message, int endIndex) {
int startIndexOfPartialMessage = message.offset + endIndex;
int lengthOfPartialMessage = (message.offset + message.length) - endIndex;
// 读取报文数据
System.arraycopy(message.sharedArray, startIndexOfPartialMessage, this.sharedArray, this.offset, lengthOfPartialMessage);
for (int i = message.offset; i < endIndex; i++) {
System.out.print((char)message.sharedArray[i]);
}
System.out.println();
}
启动程序com.jenkov.nioserver.example.Main#main
使用postman或者curl请求http://localhost:9999/ ,报文体随便填写,这里只是样例报文
{
"password": "string",
"phone": "string"
}
查看查看请求结果
可以看到服务器能接受前端请求,同时查看返回数据易能解析报文。
综合上面可知,其实我们所有网络传输都只是使用了操作系统的tcp三次握手建立连接,即tcp_connect,传输符合http协议报文的二进制数据,最终按照http协议处理数据和返回数据,即网络通讯流程,其实Tomcat亦如此,不过tomcat设计比这个复杂一些,有以上理念,更加容易读懂源码
Tomcat总览
我们来看一个简单tomcat配置文件server.xml如上图,由图看见,tomcat不是一个工具,是一个容器也是一个中间件。个人总结tomcat组成为
- Server全局服务,也就是服务器
- Listener 监听器,一序列全局监听器
- GlobalNamingResources全局配置文件
- Service对外提供服务,也就是我们web服务关注地方
- Connector连接,对于tomcat来说,每次请求过来一个socket处理连接,配置端口协议等
- Engine 主机,可配置域名访问等
- Host应用host,若没有,使用engine
- Context 应用,全局应用,一个context一个应用(源码解读得知)
- Wrapper即包装的servlet,记录所有实例化的servlet(源码解读得知)
- Pipeline(value),其中Engine、Host、Context 均具有通讯管道。(源码解读得知)
- tomcat应用部署方式四种:war、文件夹、jar包、节点Context。不管是哪一种均需要servlet,遵循servlet协议。
Spring Boot内置tomcat源码NIO解读
启动流程
个人比较懒,这里不详细编写图,大体流程如下(整个流程中Context传递bean工厂):
初始化beanFactory-》onRefresh()-》createWebServer()-》Tomcat-》StandardServer-》StandardService-》Connector-》Http11NioProtocol-》TomcatWebServer tomcat.start()-》startInternal()->NioEndpoint#startInternal()->
createExecutor()->PollerEvent->Poller->startAcceptorThread()
请求返回流程
NIO有buffer、channel、selector组成。channel网络传输用ServerSocketChannel,调用accept()方法,获取SocketChannel,通过通过注册到selector监控数据到达,完成以及传输过程,因此tomcat设计也因此设计
- Acceptor监听网络传输
- 添加处理同步队列事件PollerEvent
- 监听同步队列事件若有只则注册到Channel到Selector中,操作方式为SelectionKey.OP_READ
- selector.selectedKeys()处理信号指令,拿到通讯socketChannel
- 拿到SocketProcessor,通过ThreadPoolExecutor执行SocketProcessor线程
- Http11Processor处理协议,处理Connector、Engine 、Host、Context 、Wrapper、Pipeline
- 处理完后执行完请求和返回,返回Http11OutputBuffer,执行输出流NioSocketWrapper#doWrite,终止请求request.finishRequest()和返回response.finishResponse()
总结
总而言之,学会tomcat,你将会明白计算机如何传输数据,网络如何通讯,以及web服务器如何处理请求数据,解析数据,包装请求设计等。
1.了解tomcat只不过是针对网络传输协议http进行解析和包装而已。
2.tomcat的NIO处理请求数据时,最终交给线程池去处理,这里可以优化tomcat线程池配置,至于线程池优化,可查阅java多线程-学习总结(完整版)
server:
tomcat:
min-spare-threads: 16
max-threads: 150
max-connections: 200000
accept-count: 128
accesslog:
enabled: true
pattern: "%h %l %u %t %r %s %b %D"
3.部署方式有四种,也就是最终都是tomcat的nioendpoint处理,即可以设置部署路径
management:
endpoints:
web:
base-path: /
4.tomcat其实就是ServerSocketChannel使用Poller和worker进行双线程处理。
5.可以自定义请求方法,自定义servlet,指定请求方法。
6.你将会了解,spring boot的web应用,不仅仅是一个spring的封装而已,也做了大量tomcat和启动相关协议封装。此外,程序都是一个规范,规范的好坏要经过数年岁月的验证。
笔记
tomcat底层原理
1.tomcat超强的容器也就是一个中间件
2.四大组件servlet和context
3.wrapper容器详解
4.tomcat的bio和nio
5.tomcat和socket的关系
应用部署方式:war、文件夹、jar包、节点Context
org.apache.catalina.mbeans.MBeanFactory#createStandardServiceEngine
servlet容器
org.apache.catalina.startup.Tomcat#getServer
启动流程
onRefresh()-》createWebServer()-》Tomcat-》StandardServer-》StandardService-》Connector-》
Http11NioProtocol-》TomcatWebServer tomcat.start()-》startInternal()->NioEndpoint#startInternal()->
createExecutor()->PollerEvent->Poller->startAcceptorThread()
请求接受
Acceptor<Thread> -> NioEndpoint.serverSocketAccept()
NioEndpoint#setSocketOptions 设置SocketChannel
NioSocketWrapper#register(SelectionKey.OP_READ) 作为标志,超时使用
Poller#wakeupCounter increment 增加记录
selector.wakeup() 唤醒selector
Poller#run#events()(loop检查队列,若有信号,则sc.register注册读管道到selector)
Poller#events#eventCache.push(PollerEvent)
NIO(selector.selectedKeys())
Executor.execute(SocketProcessor)处理请求数据
AbstractProtocol.ConnectionHandler#process
coyote.AbstractProcessorLight#process
coyote.http11.Http11Processor#service
==================
setSocketWrapper(socketWrapper); 设置缓存大小8 * 1024开始 headerBufferSize,若超限,则扩容
SocketProperties.appReadBufSize和SocketProperties.appWriteBufSize
protected final void setSocketWrapper(SocketWrapperBase<?> socketWrapper) {
super.setSocketWrapper(socketWrapper);
inputBuffer.init(socketWrapper);
outputBuffer.init(socketWrapper);
}
Http11InputBuffer#parseRequestLine 处理请求行,读取数据,只操作头,包装request
NioSocketWrapper#fillReadBuffer(boolean, java.nio.ByteBuffer) 读取全部数据
prepareRequestProtocol() 设置请求响应信息
prepareRequest() 解析主机域名等相关信息
getAdapter().service(request, response) 调用servelt
connector-》StandardService[Tomcat]-》StandardEngine[Tomcat]-》StandardPipeline[StandardEngine[Tomcat]]
-》StandardEngineValve[StandardEngine[Tomcat]].invoke()-》StandardEngine[Tomcat].StandardHost[localhost]
->StandardPipeline[StandardEngine[Tomcat].StandardHost[localhost]]
->ErrorReportValve[StandardEngine[Tomcat].StandardHost[localhost]]->
StandardHostValve.invoke()->Context->
org.apache.catalina.core.StandardWrapperValve#invoke() -------------------------- tomcat调用servlet
StandardWrapper[dispatcherServlet]
DispatcherServlet(servlet)
ApplicationFilterChain.doFilter().doFilterInternal() -------------------------- 处理过滤器开始
OncePerRequestFilter
CharacterEncodingFilter
WebMvcMetricsFilter
FormContentFilter
RequestContextFilter
WsFilter
internalDoFilter(servlet.service(request, response))
servlet
controller
输出结果
org.apache.catalina.connector.OutputBuffer#close()关闭输出流
org.apache.coyote.http11.Http11OutputBuffer.SocketOutputBuffer#end() socketWrapper.flush(true);
org.apache.tomcat.util.net.SocketWrapperBase#doWrite(boolean)
org.apache.tomcat.util.net.NioEndpoint.NioSocketWrapper#doWrite
--------------------------处理过滤器结束
------------------
================
Pipeline
List<Value> value;
Engine
List<Host> hosts;
Host
List<Context> contexts;
Context
list<Wrapper> wrappers;
Wrapper
List<Servlet> servlets
讲解NIO
http://tutorials.jenkov.com/java-nio/index.html
TCP和NIO流程
https://blog.csdn.net/u011381576/article/details/79876754
创建两个线程
1。线程监听serversocketChannel,存放socketChannel到queue中
2。线程不停轮询queue,若有则注册SelectionKey.OP_READ中,通过selectedKeys读取通信(信号半读取,存放到list中,最终合并),
然后注册到读代理队列,从读代理队列获取注册到写中,写入返回数据
每个线程占用一段byte【begin,end】
https://github.com/jjenkov/java-nio-server
需要三次握手,tcp传输原理、https协议组成、操作系统流程、IO、NIO、java一些理念和数据结构
参考文献
【1】tomcat接受、分配连接(socket)解析
【2】三次握手和TCP原理
【3】Linux下网络编程
【4】Java NIO Tutorial
【5】TCP和NIO流程
【6】java常见各种锁