引言
一个优秀的框架能从中学到很多东西,撇开代码不说,它所涉及到的技术也是非常通用的,藉此总结下NIO、JMX、HttpClient、Continunation在Jetty中的应用,当然对于Http的解析HttpParser也算是一个。
本文主要是总结网络服务器的架构与NIO在Jetty中的应用,关于NIO的操作系统底层原理并没有深入研究,只总结了我对于NIO的理解。
网络架构对于应用服务器而言还是比较重要的,在服务器中有一些“池”的概念,你立马会想到线程池,Jetty中还有缓存池、HttpClient连接池。而这里就出没了缓存池。
NIO简介
多数应用程序已不受cpu的束缚,而更多的受I/O的束缚,设想买火车票的时候,虽然前面只有10个人,但是每个人花上5分钟,你也快疯了。
NIO对于传统I/O的改进无非是缓冲区、通道和选择器。
1、缓冲区
一个Buffer对象是固定数量数据的容器,作用是个存储器或分段运输区,这里的数据可以被存储和检索。而缓冲区设计的目的就是能高效得传输数据,操作系统与java基于流的i/o模型有些格格不入,操作系统往往传送的都是大块的数据,而jvm的i/o类操作的是小数据,往往是单个字节或几行文本,结果就是操作系统送来的大数据将被分割处理。这个时候你可能会想到,如果使用基于数组的read或者write会不会效果更加好呢,但Buffer类可能更加的高效,因为其利用了本地代码及其他优化方法实现了数据的移动。
既然你已经邂逅了Buffer,那你毫无疑问需要拜访下Channel了,因为如果Buffer是载体的话,那么Channel便是传送带。
为了理解本文的内容,下面有几个知识点需要了解下:
1)JDK中NIO的Buffer:
InDirectByteBuffer是基于堆实现的,也就是利用JVM中字节数组byte[]实现了缓存;而DirectByteBuffer是基于Unix系统的MMap机制实现。
对于DirectBuffer,Java虚拟机会尽最大的努力通过它来执行本地IO操作。这意味着虚拟机将在每一次底层操作系统的IO操作调用前后,尝试避免将buffer中的内容拷贝到一个中间buffer。但是正因为如此,其内容会驻留在被垃圾收集器所管理的堆之外,所以其对于一个应用程序的内存占用的影响是显而易见的,创建和销毁的开销会比普通的InDirect来的更高,因此direct buffer仅在需要底层操作系统执行大量,长时间的IO操作时推荐使用。
2)Jetty中的Buffer
对于上图有几点需要留意的:
(1)Jetty的Buffer类型也可分为基于字节数组的IndirectNIOBuffer和基于Native MMap的DirectNIOBuffer
(2)Jetty主要用到了IndirectNIOBuffer、DirectNIOBuffer和非NIO的ByteArrayBuffer
(3)NIOBuffer与JDK的区别是什么?仅是封装了而已,一切为了框架嘛。
3)Jetty中的缓存池
HttpBuffers中持有缓存池中缓存的配置(类型,容量,大小等参数),还持有request和response缓存池,即上图的PooledBuffers。那么HttpBuffersImpl的身份如何,就要追溯到SelectChannelConnector了,它是以addBean的方式关联了SelectChannelConnector的生命周期中。
2、通道
通道是NIO的第二个创新,他提供了字节Buffer与I/O服务的直接连接,通常是Socket或者文件。通道是一种途径,借助该途径,可以用最小的开销来访问操作系统本身的服务,而Buffer则就是通道内部用来发送和接受数据的endpoint。
通道可以以阻塞或者非阻塞的模式运行,非阻塞的通道永远不会让调用的线程休眠,不过只有面向流的通道才支持非阻塞特性,正如socket。
ServerSocketChannel:该类是一个基于通道的socket监听器,区别于传统的serverSocket在于,它带有channel语义,具有非阻塞的特性。不过你设想下,如果accept都非阻塞了似乎不太优雅,因此jetty的NIO中对于该类的配置是阻塞的,只有accept到socket再注册在selectorSet中。
SocketChannel:使用最多的Channel,封装了点对点,有序的网络连接,每一个SocketChannel都是和一个对等的Socket串联的。
既然了解了通道怎样简单高效得访问本地I/O服务,那是该看看select是如何来管理这些通道的。
3、选择器
选择器是NIO第三个创新,它提供了可以选择已经就绪任务的能力,通过epool回调的方式获取已经就绪的任务,实现多路I/O复用。
Jetty中的NIO体系结构
上文中出现的各种Connection,Endpoint都属于NIO体系中的成员,初学者(即我)会犯迷糊,本段将带你扯清楚HttpParser、HttpBuffers、SelectChannelConnector、SelectChannelEndPoint、AsyncHttpConnection、SelectorManager之间乱七八糟的关系和Request和Response的缘由。
1、简介
1)HttpParser:从缓存池中取缓存,将channel的请求数据读取到缓存中,并解析成Request。
2)HttpBuffers:上面用到的那个缓存池
3)SelectChannelEndPoint:持有SocketChannel与基本的读写操作,正如其名:连接的端点。虽然不是请求的入口,但确是有效解析与处理操作的入口。
4)SelectorManager:持有Selector与SelectorSet,封装了NIO中的Selector,增强了其功能,也起到了负载均衡的效果。
5)AsyncHttpConnection:名字带有"Http",持有Httpparser,负责管理请求的解析和request、response的生产。
6)SelectChannelConnector:协调或生产上面那些玩意,控制并组装上面那些货的生命周期,名为:“SelectChannelConnectorControl”或许更为贴切。
2、主要组件的结构图
上图概括起来可分为三类:工具类(HttpBuffer,HttpParser),IO类(Select*),Server类(Server,SelectChannelConnector),Server类接受请求交给IO类来读取,IO类借助Server类和工具类解析并生产HttpConnection(里面持有Request、Response等产物),随后开始handler生产出来的Request。
3、时序图
阶段一:侦听请求
server启动的时候先是配置serverSocket,而后多线程doSelect(负载均衡),最后多线程accpet,serverSocketChannel设置为阻塞模式,有请求时将SocketChannel注册到SelectorManager,最终由上面的select线程来处理请求。
阶段二:建立连接
建立连接的意思是有read事件时生产EndPoint,HttpConnection,为解析请求和handler请求准备好环境。
阶段三:处理请求
主要是从SocketChannel中读取数据到缓存中,解析并生成Request,Response,最后handle该Request。
4、总结
概括起来,Jetty中的NIO架构大致模型如下图所示:
用户请求被侦听连接线程处理,平均注册读时间到两个SelectorSet中,分别为两个set起线程doSelect(),有Read事件时读取请求并解析Request,处理完成之后交给数据处理线程处理,因此对于servlet而言可没有复用的概念了,每个servlet消费一个线程,但是在消费的过程中如果有阻塞怎么搞呢,这就涉及到continunation和HttpClient了,下面的文章会做总结。
HttpClient的NIO
也许你已经习惯了HttpParser的作用就是解析Request,而HttpGenerator就是生产Response了。但是在HttpClient的应用中确截然相反。关于HttpClient的总结下文有详细介绍,这里简要介绍HttpClient中的NIO。
1、HttpConnection
AbstractHttpConnection(Buffers requestBuffers, Buffers responseBuffers, EndPoint endp) { super(endp); _generator = new HttpGenerator(requestBuffers,endp); _parser = new HttpParser(responseBuffers,endp,new Handler()); }
区别于上面的HttpConnection,parser主要是解析response;而generator则先生产request。
2、线程
1)HttpClient初始化时候线程池的配置
protected HttpClient createHttpClient(ServletConfig config) throws Exception { HttpClient client = createHttpClientInstance(); client.setConnectorType(HttpClient.CONNECTOR_SELECT_CHANNEL); String t = config.getInitParameter("maxThreads"); if (t != null) { client.setThreadPool(new QueuedThreadPool(Integer.parseInt(t))); } else { client.setThreadPool(new QueuedThreadPool()); }
可以看到,HttpClient自身new的一份线程池。
2)HttpClient中NIO用到的线程池
class Manager extends SelectorManager { Logger LOG = SelectConnector.LOG; @Override public boolean dispatch(Runnable task) { return _httpClient._threadPool.dispatch(task); }
当然除了doSelect用到的线程,也有监控超时连接和超时请求的线程,不属于该NIO内容,细节由HttpClient章节总结。
Request&Response的前因后果
黑线表示默认走的流程,红线表示可选流程,完全由应用的具体业务逻辑而定。单纯的看代码挺烦的,这里就不多总结了,了解了这个图就差不多够了,如果您想具体了解下的话,可以参看Request-HttpParser-HttpConnection-Buffer,Response-HttpGenerator-HttpConnection-Buffer,这里对于协议的解析采用了注册事件回调函数的方式,不过仔细留意的话对于Request的解析Content解析完成后的事件回调函数没有对Content做任何处理,任由Content安静的呆在HttpParser中。
对于HttpClient的处理流程刚好相反,这里就不多画了。