jetty内部比较重要的实现,jetty里面的内容比较的多,代码量也是比较大的,我们不可能做一个面面俱到的一个讲述,
我们不会去介绍他整个系统架构上的一些设计,作为一个servlet容器,它是怎么处理JSP,不是我们的重点,我们的重点是
他作为一个高性能的,吞吐量比较高的,这么一个HTTP服务器,他后面采用了什么样的技术,多线程的手段,很好的来提高
他系统的吞吐量,着眼点和立足点呢,是从这个角度去看,我们不会去讲他,系统当中,还有哪些模块,UML图,还有架构图,
毕竟多线程,并发这门课,他更多的还是会有实现细节,所以我们也是停留在代码的层面,做一些比较细节的东西,分析的
主要内容有三块,因为他是一个http的server,他也可以做一个嵌入式的实现,我们需要一个http server的时候呢,把jetty
给include进来,直接把这个server给new出来,http请求的这个处理,内部也是使用这个server去做的,http server包下的
这个类,主要分三块,如果我们想把一个server跑起来的话,并且能够正常的处理一些工作,首先我们把server给new出来,
把这个server的实例创建出来,创建实例我们要把它给start起来,server启动,等待http的请求,当有http请求之后呢它会做处理,
这三块就会把怎么跑起来的,处理请求的,比较完整的给理一遍,这两块内容是我们的重点,到底有些什么东西,如何排放的,
这个设计到请求处理,1请求处理之后,他后面一定是相当复杂的,涉及到service容器的JSP的处理,我们这块内容是讲的
比较简略的,我们把这个请求分给某个线程,那就中断了,线程怎么处理,这是一系列的Handler的调用,我们就不做进一步的
介绍了,jetty使用上的一些问题,这个看下代码也不是很困难的事情,我们可以看一下server实例的新建,大概最简单的
一个函数
在端口上做监听,这个里面到底做什么事情呢,他里面做的事情主要是有这四个,一个是初始化线程池,
什么叫初始化线程池呢,像jetty这样的一个服务端的,这么一个容器,服务端的一个产品,他后台执行的一定是
线程池的,不可能你有一个什么任务,他自己就new一个线程出来,这不是很靠谱的事情,但是这里一定要说明的是呢,
也没有使用JDK的线程池,他也可以配置JDK的线程池,他实现了一个新的线程池,QueueThreadPool,所以在初始化线程池的
时候呢,把自己的线程池给初始化了一下,他就实现了SizedThreadPool接口,也是jetty当中的一个接口,线程池我们知道,
他最重要的是execute方法,也就是执行,我们可以看到在这个执行当中,是怎么做的呢,如果你提交一个Runnable的接口,
Runnable的一个实例上去,他的做法对于线程池来讲,他只是把它放到队列当中去,除此之外没有更多的处理了,他只是把我们
要执行的任务,入队,这个jobs是什么呢,它是一个BlockingQueue,这个jobs是一个BlockingQueue,他保存需要由这个线程池执行的,
由此我们可以看到说,这个BlockingQueue的性能不会特别好,BlockingQueue不是一个高性能实现,从这里侧面也说明呢,execute方法
不会非常频繁的被调用,如果非常的频繁的执行execute方法的话,而且BlockingQueue还是一个性能提升的一个点,我印象当中这个
BlockingQueue,jetty当中还是实现了自己的BlockingQueue实现,这个性能也不怎么样,就是和JDK的BlockingQueue是大同小异的,
也是会做一些阻塞的操作,这个是线程池的初始化
ServerConnector的东西,服务端的一个连接,当然是用来处理http的一些连接,NIO ByteChannel的一些东西,
他继承自AbstractConnector,初始化他的时候会做什么事情呢,这里列举的是主要的工作,并不是所有的工作,
他的代码是非常的繁琐的,如果直接看代码会直接晕掉,所以还是做了一些整理,去掉一些不是很重要的东西,初始化
这么一个ScheduleExecutor,这个东西是干什么用的呢,我们知道JDK当中有一个ScheduledThreadPoolExecutor这个类,
他就是一个调度器,像服务器当中,不可避免的我们有些任务每隔一段时间执行一次的,每一分钟我们要检查一下东西,
诸如此类的一些任务,这是由Schedule来调度的,ByteBufferPool,ByteBuffer的一个池子,他为什么要初始化byteBuffer
的一个池子呢,这个池子是什么东西呢,跟线程池一样,线程池里面放的什么呢,放的的线程,ByteBufferPool里面放的什么呢,
BuyteBuffer,ByteBuffer是什么呢,NIO的时候也说过,Buffer是个数组,简单理解成一个数组,ByteBuffer有两种,indirect是
在堆当中的,direct是在直接内存当中的,ByteBufferPool他其实就是一个对象池,池子当中的所有的ByteBuffer,实际上是可以
被复用的,对象池的一大好处,就是我可以减少GC,减少对象的产生,如果我每次需要bytebuffer,我接收某一个数据的时候,因为
我毕竟是一个http的server,坦白的讲我是一个TCP的服务器,我要去网络上读取某些数据,读数据之前我可能需要,new一个byteBuffer
出来,如果我写数据回去,写到通道里面去,把这个bytebuffer填满,把数据写到通道里面去,这个时候我们在系统中大量的去
new bytebuffer,那么new完之后,这样做也无可厚非,但是为了进一步的提高系统的性能,允许我们会想到说,
我们不需要每次都释放掉,先准备好一堆,有人需要用的时候就去拿一个,拿一个你就用,用完之后你也不用回收,
返给我byteBuffer这个池子就可以了,这样有个什么好处呢,我对象是复用的,我不会每次去new对象,
同时也会减少压力,否则会出现什么情况呢,如果我连接数非常强大的情况之下,
如果我要处理好多好多Channel的情况之下,那么我new这个数据,可能会不停的new他,你new其实是一个很快的操作,在JAVA当中
new这个关键字,它是被绝对的优化的,所以他的性能是很高的,大家不要认为new会影响太大的性能,但是回收会比较麻烦,
你可能会导致大量的碎片会回收,新生代的回收会更加的频繁,这个是我不愿意看到的,但是如果我们随随便便的去搞一个
对象池,哪有什么坏处呢,其实在很多情况之下,如果我们自己随便写一个对象池,还不如直接去new,回收来的好,因为对象池的
一个特点呢,必须要满足线程安全,因为你的对象不可能是一个人用的,你有好多线程在中间用,有一好多线程在用,你拿到对象,
跟我退还对象的时候,你是需要保证线程安全的,如果你简单的加synchronized,那基本上可以断定,除非你这个对象是非常特别的
对象,非常大的超级对象,在这种情况之下呢,如果你用synchronized粗暴的方式去做,你这个性能会比new好一点,否则的话呢,
你用synchronized做出来的线程池,对象池呢,性能是很差很差的,你还不如new一个对象来的划算,所以我们这里要学的一个核心呢,
线程池是线程安全的,并且是无锁的,他不是用synchronized来实现的,ByteBufferPool在Jetty里的实现呢,有几个,这里选一个
常用的ArrayByteBufferPool来讲
是ByteBufferPool的一个实现了,他和普通的对象池有什么不一样的呢,普通对象池当中,所有的对象都是对立的,
换句话说,我拿任何一个对象都是一样的,所有的对象都是等价的,就跟线程池里的线程,无论挑到那个给你执行,
都是一样的,数据库连接池当中,所有的线程,不论你挑到哪个给你执行,数据库连接对你来说都是一样的,但是ByteBufferPool,
所以在这一点来讲,比普通的对象池稍微复杂一些,因为你可能会使用,可能你的请求当中,需要1K的byteBuffer,你也可能需要
2K的byteBuffer,你也可能需要2M的byteBuffer,这都是有可能的,那我不可能把所有大小的byteBuffer都保存N份让你来使用,
这是不切实际的,换句话说呢,这个ArrayBytePool当中,他所有的对象,他并不是等价的,这就是他实现上会有非常麻烦的地方,
他这个地方是怎么做的呢,这个ArrayByteBufferPool他有三个参数
增量最大大小,最小大小是什么呢,我这个池子当中,我最小的那一个,byteBuffer,起始大小,增量指的是,
我最大的byteBuffer有多大呢,比如这里有64K,比如这里是0,默认是0,64K,我以1K为增量,在这个池子当中呢,
一直到64K为止,像这么大的buffer,会保存到那边,那么保存的哪些buffer,他怎么存放呢,当你把它去new出来的
时候,你没有去存放他,你并没有把实际的byteBuffer去给他new出来,因为是没有必要的,也许你只用一种大小32K,
其他大小从来都不用的,这是有可能的,如果你把6K,64k都new出来,那是没有必要的,所以这里是延迟加载的策略,
你不同的大小的buffer,你放到pool当中呢,它是会放在一个叫做bucket的一个篮子里
我们可以把这个bucket理解成一个篮子,一个bucket可能就放一个大小的buffer,你用了64种大小的buffer呢,
那你自然需要64种byte,因为这种地方会涉及到说,是有可能是直接内存的,也有可能是堆的,有直接内存和堆的
两个的bucket,一个bucket里面有什么东西呢,bucket里面有两样,size就是byteBuffer有多大,还有实际的存放
byteBuffer的一个容器,一个queue,这个地方要注意的,高并发的ConcurrentLinkedQueue,大家应该比较清楚了,
他的并发性能很好,它是一个无锁的实现,所以当你有大量的请求的时候呢,他的性能是非常的不错的,在初始化
ByteBufferPool的时候呢,它是会把所有的bucket,都创建一遍,但是bucket里面queue里面的内容,它是延迟加载的,
额外Pool有两个非常重要的方法,就是acquire
我要池子里面申请一个buffer,就是我这个buffer用完了,我要归还你这个池子,我申请相当于我去new一个buffer出来,
这里有两个参数,一个是你要多大的一个buffer,第二个你是要从直接内存,还是从堆,这个acquire主要分三步,第一个我要
找到一个合适的bufferBucket,因为我知道没有一个bucket的大小都不一样,那我1到64K,就是当我64个Bucket,分别是1K,2K,
一直到64K,加入你只要了5.5K,那我要取多少呢,来给你用,那就应该给你6K的,就是我要找到一个合适的bucket来用,这个大小的
bucket是我要的,然后我要从这个bucket当中取到,我要的buffer,默认情况之下呢,当然是没有东西的,这是延迟加载的,在没有
东西的情况下呢,他就会新建,如果存在就返回
Queue当中有数据,不存在新建,新建出来,把这个buffer返回,应该说还是比较容易理解的,此外就是release,
release最重要的就是返回线程池,也是分三步,第一步就是我要取得合适的bucket,那我这个buffer有多大,
那我就要还到bucket一样的大小里面去,我得把bucket给取出来,取完之后我要清空buffer,因为buffer用完了,
limit,capacity,当前的position,全部都要清空,都要初始化,可以让别人拿到之后继续使用,最后归还到Queue
当中去,这样以后别人用的时候呢,就会拿到,此外他还可以支持另外的处理
你现在只有1到64K的buffer,现在突然出现一个线程说,128K你给不出来,如果说给不出来,
我会给你成功的申请,刚才看到的acquire当中,不存在是会给你申请的,我没有办法给你归还,
只是换不进去而已,还不进去怎么办呢,这个池子当中是不会有pool出现了,因此你还不进去的buffer,
在将来是必然会被系统回收的,只要你过了作用域之后,是会被回收掉,这是没有关系的,这个就是ByteBufferPool的
一个实现,这个实现我个人认为呢,是非常重要的,它主要体现的思想呢,一个是无锁,对象池在多线程的情况之下,
他必须是无锁的,否则他还不如去new一个来的快,第二个我们看到他比较好的处理了,各个不同的大小的buffer,它是
如何做一个处理,从这个角度上来讲呢,比普通的对象池呢,会复杂一些,这里是ByteBufferPool,接着他会初始化
ConnectionFactory
ConnectionFactory它是一个工厂,一个典型的ConnectionFactory呢,HttpConnectionFactory,他就用于创建连接,
当有人客户端连接上来,我自然要创建一个对象,来维护这个连接,这个就是Factory的一个作用,来创建连接的一个
对象,接着他取得CPU的一个数量,这个是很重要的,下面jetty会根据你CPU的数量,来决定我在这个系统当中,应该使用
多少个accept线程,他都要由这个cores算出来的,这也是对于一个高并发的程序来讲,你必须做的一个事情,你必须去
自适应CPU的数量,否则你在两个CPU上,你完全没有办法很好的去协调,然后你根据CPU的数量呢,更新或者是计算,acceptor
线程的数量,我又多少个线程用于accept,我们知道我们在做网络编程的时候,一个服务端你是要做accept等待的,那你有几个
线程来做accept等待呢,这里大家可以看一下,jetty提供一个经验的一个公式,有几个线程来做这个等待,是比较合适的,首先
他取了一个最小值,第一个是4,换句话说,他认为,你用做accept的线程数量,应该是很少的,不应该是很多的,如果你CPU数量真的
很多,也不会超过四个,最多也不超过4个去做accept,这个也是我们在编程中可以借鉴的一个地方,有时候我们也要写一个网络
程序,我们也要accept一个东西,这个时候有几个线程去accept呢,不要超过4个,如果你不到4个,你有10个CPU,假设你只有10个
CPU,那你只能用一个线程,如果你有20个CPU,那你就去使用2个线程,这个数量不会太多,有了这个acceptor数量之后呢,他就要创建
一个acceptor线程数组,并没有初始化线程,只是把维护数组的线程给创建出来,其实它会初始化ServerConnectorManager的东西,
它是继承SelectorManager,什么叫SelectorManager呢,对Selector做一个管理,之前有讲过NIO,也说过Selector是干嘛用的,
就是当你通道准备好的时候呢,你有select到我,具体的NIO的副本呢,就不在这里展开了,我有多少个线程来做select这个事情,
你有这个通道准备好,他这里给了一个经验的值,之多也不超过4个,比如你有4个CPU的话呢,你可能有2个线程去做select,
但是很明显selector的数量比acceptor要多,selector不处理具体的业务逻辑,所以也不需要太多的线程,真正的把数据
拿下来之后呢,你还是会使用实际的线程去做操作,这里是初始化serverConnector,下一步就会设置port
我们需要在8080端口监听,关联server和Connector,这个是server初始化时候做的事情,我们看一下server启动
以后会做什么事情,server启动以后主要是做三个,前两个其实是没有意义的,设置启动状态,我用于系统的管理和
维护,正是启动过程,然后我启动完毕了,包括server在内,很多jetty当中的对象,他都是有生老病死的过程,生命周期
在里面,他们都会继承lifeCircle,所以我们没有在文档当中给展示出来,只是给大家做了一个介绍,而系统当中也会
有一些manager,会有一些管理器,会统一的对lifeCircle的对象呢,管理做维护,start,end,这个就有点类似在Spring
当中,对Bean声明周期的管理,有点接近,server.start最核心的呢,就是doStart方法,它是真正启动要做的事情,主要是
这么几步
第一个注册shutdownMonitor,允许你远程把jetty服务给关掉,作为一款商用的,它是一个比较成熟的server,
要提供一些比较强大的功能,Monitor线程就是用来做这个事情的,继而我们拿到线程池,这里只是把它拿出来,
有点类似Spring的方法,但是他用的不是Spring,自己自成了一套管理体系,他也是归整个系统托管,设置selector
的数量,因为我们前面设置了selector的数量,那只是一个ServerConnector当中,计算这个selector数量,事实上server
当中呢,jetty允许有多个connector,累计所有的需求
就是你所需要的线程数量,如果线程大于200呢,你这个程序就直接终止掉了,因为这个时候觉得
已经没有执行下去的价值了,因为200个线程去做selector,跟做acceptor,性能也是很差很差的那种了,
然后会维护这个bean
它会启动ThreadPool,启动调用doStart方法,ThreadPool他做什么事情呢,它是会启动若干个线程,把你所需要的线程
都给建立起来,它会做三件事,第一个创建线程,创建线程使用Runnable的接口,设置线程的属性,比如优先级,线程是否是
Daemon的线程,设置线程的名字,我这个设置线程的名字我认为是非常重要的,可以方便调试,最后启动线程,把线程起来,
创建线程它使用的是Runnable的接口
这个Runnable接口中做的事情是什么呢,去jobs当中去拿我们的任务,这就是QueueThreadPool里面的,
就是去jobs当中取Runnable的任务,jobs中的任务是哪里来的呢,就是execute方法当中塞进去的,所以
execute的时候他并没有执行,他只是塞到jobs队列当中去,只有当你线程起来之后,这里每一个真实执行的
线程,他才会不停的从jobs里面拿任务,他这里肯定是一个死循环,作用是不停的去jobs里面获取,另外一个
bean呢就表示WebAppContext,如果有需要的时候呢,Servlet容器所规范的内容,最后会启动Connector
启动Connector又可以分为这么几步,第一步会去的ConnectorFactory,ConnectionFactory在前面维护
起来了,然后会创建Connector的线程并启动,因为之前已经计算了Connector线程的数量,所以可以根据数量创建
Connector线程,这里要注意的是,就是ManagerSelector,就是一个线程,但是他封装了对于NIO来讲,他封装了Selector
的操作,一个可管理的selector,然后把它给启动,接着会创建Acceptor线程
Acceptor线程的作用就是用来等待客户端连接,那我们来看看Acceptor线程,做了什么事情,你要创建
几个acceptor线程,然后把Acceptor线程给创建出来,去做这个执行,Acceptor线程做什么呢,首先他要设置
线程的名字,这是一个非常好的习惯,都带有acceptor的关键字,设置优先级,并且将自己加入到acceptors
数组当中去
然后监听端口,在server上面做accept,在这个地方做等待,如果你只有一个线程,那你只有一个线程在这里等待,
如果你有两个accept,那就有两个accept在这里做等待,这里还有一种情况呢,就是没有Acceptor的情况,在默认的
情况之下呢,Acceptor线程数量是有的,是大于0个的,但是也有可能accptor的数量是0,如果没有专门的线程,用来做
acceptor操作,对于NEW IO来讲,accept本身也可以是非阻塞的,当有线程真正accept上来的时候呢,他在做一个select
的一个返回,来告诉你有人accept上来了,但是在默认情况之下,他是一个阻塞的操作
当acceptor线程数量为0,他就会把acceptChannel配置成非阻塞的形式
把server启动完了,然后http请求,http请求是和accept相关的,我们可以看到这个地方,成功以后就会去做一些配置,
它会把拿到的channel配置成非阻塞模式,然后去配置一些socket
设置为非阻塞模式,然后配置socket,最后做一些正是的处理,那正式处理当中呢,他要把channel交给SelectorManager
做处理
SelectorManager在处理的时候呢,它会选择一个可用的线程,其实就封装了selector操作,那么在选择这个线程的时候呢,
他这里有一个非常有意思的注释,它会根据你不同的selectIndex,每次都会++,这是一个成员变量,把这个值映射到select
当中去,这是一个线程,这个操作本身并不是原子操作,因此在多线程的时候,但是这并没有关系的,因为他只需要这个值变化
就行了,你变了多变了少呢,关系不大,因为他并不需要一个精确地需求,只要你这个值在变,只要你在变化呢,我就能够保证呢,
在绝大部分场合,我们其实并不需要一些线程安全的手段,在你可有可无的情况之下呢,应该忽略线程安全的,因为这样性能是
最好的,如果你在这里做一个同步,哪怕你这里用一个原子的类来做,性能还不如这样做来的好,下面就是ManagerSelector来做
处理,他呢是一个线程,封装了Selector的使用,在这个Selector当中呢,他提交一个任务,这个任务是和accept相关的,提交任务
之后呢,最终是会加入到Queue队列当中去,changes是ConcurrentArrayQueue的队列
它是jetty当中自己实现的,这个性能和ConcurrentLinkedQueue是非常类似的,他也是使用了无锁的实现,但是他有一个
比较好的地方是什么呢,对于ConcurrentLinkedQueue来讲它是一个链表,当你去保存某些数据的时候,你其实是需要在elements
的外面,你要套一个node上去,但是对于ConccurentArrayQueue来讲呢,他没有这个概念,直接存放的就是这个元素了,当你有大量的
元素压入队列的时候呢,其实要少一般,因为除了elements之外还有node,而这个是不需要,所以对于GC来讲性能要好一些,这也是我们
可以值得思考的一个地方,对于ConcurrentArrayQueue大家可以去看一下源码的实现,我们介绍了并发包里的容器,很多也都是
大同小异的,它是一个高并发的实现,所以即便你有大量的连接上来,大量的任务提交上来
处理请求使用的是ManagerSelector的run方法,也就是在这个地方,他提交任务,但是他不处理任务,
他只把任务压到队列当中去,在线程执行过程当中,他才会去做任务的处理,他里面的代码比较多,只截取了两行,
主要是select,只要你还在running,那你就要不停的select,select做什么呢,就是runchange,什么叫changes呢,
当有任务上来,就应该runchanges
去做这个任务的执行,它会把changes队列当中的任务呢,拿出来去做这个执行,执行的时候就是做了一个核心的操作呢,
相互注册一下,绑定一下,得到SelectionKey,然后去创建这个连接,代表这个连接的endPoint,这样在后面就能够拿到我们的key,
继续做这个处理,返回给endpoint,这个是做runchanges,然后他会做select,实际上它是关联了channel和selector,现在你要等待
read或者write,它是一个可用的状态,需要做select操作,一旦发现有任何select可用,任何一个channel的select可用的话呢,
他就会去做处理,已经准备好的selectKey拿出来,会使用新的线程做HTTP的处理