范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文

高并发编程系列(三)Netty服务端启动流程源码分析

  1 服务端ServerBootstrap
  在分析客户端的代码时,我们已经对Bootstrap启动Netty有了一个大致的认识,接下来在分析服务端时,就会相对简单一些了。下面来看服务端的启动代码。
  服务端基本写法与客户端的代码相比,没有很大的差别,基本上也是进行如下几个部分的初始化。
  (1)EventLoopGroup:无论是服务端还是客户端,都必须指定EventLoopGroup。在上面的代码中,指定了NioEventLoopGroup,表示一个NIO的EventLoopGroup,不过服务端需要指定两个EventLoopGroup,一个是bossGroup,用于处理客户端的连接请求;另一个是workerGroup,用于处理与各个客户端连接的I/O操作。
  (2)指定Channel的类型。这里是服务端,所以使用了NioServerSocketChannel。
  (3)配置自定义的业务处理器Handler。1.1 NioServerSocketChannel的创建
  我们在分析客户端Channel的初始化过程时已经提到,Channel是对Java底层Socket连接的抽象,并且知道客户端Channel的具体类型是NioSocketChannel,由此可知,服务端Channel的类型就是NioServerSocketChannel。
  我们按照分析客户端的流程,对服务端的代码也同样分析一遍,这样会方便我们对比服务端和客户端有哪些不同。通过7.1节的分析,我们知道,在客户端中,Channel类型的指定是在初始化时通过Bootstrap的channel()方法设置的,服务端也是同样的方式。
  再看服务端代码,我们调用ServerBootstarap的channel(NioServerSocketChannel.class)方法,传入的参数是NioServerSocketChannel.class对象。如此,按照与客户端代码同样的流程,我们可以确定NioServerSocketChannel的实例化也是通过ReflectiveChannelFactory工厂类来完成的,而ReflectiveChannelFactory中的clazz属性被赋值为NioServerSocketChannel.class,因此当调用ReflectiveChannelFactory的newChannel()方法时,就能获取一个NioServerSocketChannel的实例。newChannel()方法的代码如下。
  下面总结一下。
  (1)ServerBootstrap中的ChannelFactory的实现类是ReflectiveChannelFactory类。
  (2)创建的Channel具体类型是NioServerSocketChannel。
  Channel的实例化过程,其实就是调用ChannelFactory的newChannel()方法,而实例化的Channel具体类型就是初始化ServerBootstrap时传给channel()方法的实参。因此,上面代码案例中的服务端ServerBootstrap创建的Channel实例就是NioServerSocketChannel的实例。1.2 服务端Channel的初始化
  我们来分析NioServerSocketChannel的实例化过程,先看一下NioServerSocketChannel的类层次结构,如下图所示。
  首先,我们来看一下NioServerSocketChannel的默认构造器。与NioSocketChannel类似,构造器都是调用newSocket()方法来打开一个Java NIO Socket。不过需要注意的是,客户端的newSocket()方法调用的是openSocketChannel,而服务端的newSocket()方法调用的是openServerSocketChannel。顾名思义,一个是客户端的Java SocketChannel,另一个是服务端的Java ServerSocketChannel,代码如下。
  然后,调用重载构造方法,代码如下。
  在这个构造方法中,调用父类构造方法时传入的参数是SelectionKey.OP_ACCEPT。作为对比,我们回顾一下,在客户端的Channel初始化时,传入的参数是SelectionKey.OP_READ。在服务启动后需要监听客户端的连接请求,因此在这里我们设置SelectionKey.OP_ACCEPT,也就是通知Selector我们对客户端的连接请求感兴趣。
  接下来,和客户端对比分析一下,逐级调用父类的构造器,首先调用NioServerSocketChannel的构造器,其次调用AbstractNioMessageChannel的构造器,然后调用AbstractNioChannel的构造器,最后调用AbstractChannel的构造器。同样地,在AbstractChannel中实例化一个Unsafe和Pipeline,代码如下。
  不过,这里需要注意的是,客户端的Unsafe是AbstractNioByteChannel.NioByteUnsafe的实例,而服务端的Unsafe是AbstractNioMessageChannel.AbstractNioUnsafe的实例。AbstractNioMessageChannel重写了newUnsafe()方法,代码如下。
  最后,总结一下在NioServerSocketChannel实例化过程中执行的逻辑。
  (1)调用NioServerSocketChannel.newSocket(DEFAULT_SELECTOR_PROVIDER)方法创建一个新的Java NIO原生的ServerSocketChannel对象。
  (2)实例化AbstractChannel对象并给属性赋值,具体赋值的属性如下。
  ● parent:设置为默认值null。
  ● unsafe:通过调用newUnsafe()方法,实例化一个Unsafe对象,其类型是AbstractNioMessageChannel.AbstractNioUnsafe内部类。
  ● pipeline:赋值的是DefaultChannelPipeline的实例。
  (3)实例化AbstractNioChannel对象并给属性赋值,具体赋值的属性如下。
  ● ch:被赋值为Java NIO原生的ServerSocketChannel对象,通过调用NioServerSocketChannel的newSocket()方法获取。
  ● readInterestOp:被赋值为默认值SelectionKey.OP_ACCEPT。
  ● Ch:被设置为非阻塞,也就是调用ch.configureBlocking(false)方法。
  (4)给NioServerSocketChannel对象的config属性赋值为new NioServerSocketChannelConfig(this,javaChannel().socket())。1.3 服务端ChannelPipeline的初始化
  服务端ChannelPipeline的初始化和客户端一致,也可以参考7.1.5节的内容,不再单独分析。1.4 将服务端Channel注册到Selector
  服务端Channel的注册过程和客户端一致,也可以参考7.1.7节的内容,不再单独分析。1.5 bossGroup与workerGroup
  在客户端初始化的时候,我们初始化了一个EventLoopGroup对象,而在服务端的初始化时,我们设置了两个EventLoopGroup:一个是bossGroup,另一个是workerGroup。那么这两个EventLoopGroup都是干什么用的呢?接下来我们详细探究一下。其实,bossGroup只用于服务端的accept,也就是用于处理客户端新连接接入的请求。我们可以把Netty比作一个餐馆,bossGroup就像一个大堂经理,当客户来到餐馆吃饭时,大堂经理就会引导顾客就座,为顾客端茶送水等。而workerGroup就是实际干活的厨师,它们负责客户端连接通道的I/O操作:当大堂经理接待顾客后,顾客可以稍做休息,而此时后厨里的厨师们(workerGroup)就开始忙碌地准备饭菜了。bossGroup与workerGroup的关系如下图所示,前面的章节我们也分析过,这里再巩固一下。
  首先,服务端的bossGroup不断地监听是否有客户端的连接,当发现有一个新的客户端连接到来时,bossGroup就会为此连接初始化各项资源;然后,从workerGroup中选出一个EventLoop绑定到此客户端连接中;接下来,服务端与客户端的交互过程将全部在此分配的EventLoop中完成。下面我们结合代码进行分析。
  在ServerBootstrap初始化时调用了b.group(bossGroup,workerGroup),并设置了两个EventLoopGroup,代码如下。
  显然,这个方法初始化了两个属性。一个是group=parentGroup,它是在super.group(parentGroup)中完成初始化的;另一个是childGroup=childGroup。接着从应用程序的启动代码看,调用了b.bind()方法来监听一个本地端口。bind()方法会触发如下调用链。
  代码看到这里,我们发现对于AbstractBootstrap的initAndRegister()方法,我们已经很熟悉,在分析客户端程序时和它打过很多交道,现在再来回顾一下这个方法。
  这里group()方法返回的是上面我们提到的bossGroup,而这里的Channel其实就是NioServerSocketChannel的实例,因此可以猜测group().register(channel)将bossGroup和NioServerSocketChannel关联起来了。那么workerGroup具体是在哪里与NioServerSocketChannel关联的呢?继续看init(channel)方法。
  实际上,init()方法在ServerBootstrap中被重写了,从上面的代码片段中我们看到,它为Pipeline添加了一个ChannelInitializer,而这个ChannelInitializer中添加了一个非常关键的ServerBootstrapAcceptor的Handler。关于Handler的添加与初始化的过程,我们留到之后的章节再详细分析。现在,先来关注ServerBootstrapAcceptor类。在ServerBootstrapAcceptor中重写了channelRead()方法,其主要代码如下。
  ServerBootstrapAcceptor中的childGroup是构造此对象时传入的currentChildGroup,也就是workerGroup对象。而这里的Channel是NioSocketChannel的实例,因此childGroup的register()方法就是将workerGroup中的某个EventLoop和NioSocketChannel进行关联。那么,ServerBootstrapAcceptor的channelRead()方法是在哪里被调用的呢?其实当一个Client连接到Server时,Java底层NIO的ServerSocketChannel就会有一个SelectionKey.OP_ACCEPT的事件就绪,接着就会调用NioServerSocketChannel的doReadMessages()方法,代码如下。
  在doReadMessages()方法中,通过调用javaChannel().accept()方法获取客户端新连接的SocketChannel对象,紧接着实例化一个NioSocketChannel,并且传入NioServerSocketChannel对象(即this)。由此可知,我们创建的NioSocketChannel的父类Channel就是NioServerSocketChannel实例。接下来利用Netty的ChannelPipeline机制,将读取事件逐级发送到各个Handler中,于是就会触发ServerBootstrapAcceptor的channelRead()方法。1.6 服务端Selector事件轮询
  回到服务端ServerBootStrap的启动代码,它是从bind()方法开始的。ServerBootStrapt的bind()方法实际上就是其父类AbstractBootstrap的bind()方法,来看代码。
  在doBind0()方法中,调用EventLoop的execute()方法,代码如下。
  execute()方法主要就是创建线程,将线程添加到EventLoop的无锁化串行任务队列。重点关注startThread()方法,代码如下。
  我们发现startThread()方法最终调用的是SingleThreadEventExecutor.this.run()方法,这个this就是NioEventLoop对象,代码如下。
  终于看到似曾相识的代码。上面代码主要就是用一个死循环不断地轮询SelectionKey的。select()方法主要用来解决JDK空轮询Bug,而processSelectedKeys()就是针对不同的轮询事件进行处理。如果客户端有数据写入,最终也会调用AbstractNioMessageChannel的doReadMessages()方法。下面我们来总结一下Selector的轮询流程。
  (1)Selector事件轮询是从EventLoop的execute()方法开始的。
  (2)在EventLoop的execute()方法中,会为每一个任务都创建一个独立的线程,并保存到无锁化串行任务队列。
  (3)线程任务队列的每个任务实际调用的是NioEventLoop的run()方法。
  (4)在run()方法中调用processSelectedKeys()处理轮询事件。1.7 Netty解决JDK空轮询Bug
  大家应该早就听说过臭名昭著的Java NIO epoll的Bug,它会导致Selector空轮询,最终导致CPU使用率达到100%。官方声称JDK 1.6的update18修复了该问题,但是直到JDK 1.7该问题仍旧存在,只不过该Bug发生概率降低了一些而已,并没有被根本解决。出现此Bug是因为当Selector轮询结果为空时,没有进行wakeup或对新消息及时进行处理,导致发生了空轮询,CPU使用率达到了100%。我们来看一下这个问题在issue中的原始描述。
  This is an issue with poll (and epoll) on Linux.If a file descriptor for a connected socket is polled with a request event mask of 0,and if the connection is abruptly terminated (RST) then the poll wakes up with the POLLHUP (and maybe POLLERR) bit set in the returned event set.The implication of this behaviour is that Selector will wakeup and as the interest set for the SocketChannel is 0 it means there aren"t any selected events and the select method returns 0.
  具体解释为:在部分Linux Kernel 2.6中,poll和epoll对于突然中断的Socket连接会对返回的EventSet事件集合置为POLLHUP,也可能是POLLERR,EventSet事件集合发生了变化,这就可能导致Selector会被唤醒。
  这是与操作系统机制有关系的,JDK虽然仅仅是一个兼容各个操作系统平台的软件,但遗憾的是在JDK 5和JDK 6最初的版本中,这个问题并没有得到解决,而将这个"帽子"抛给了操作系统方,这就是这个Bug一直到2013年才最终修复的原因。
  在Netty中最终的解决办法是:创建一个新的Selector,将可用事件重新注册到新的Selector中来终止空轮询。我们来回顾一下事件轮询的关键代码。
  前面我们提到select()方法解决了JDK空轮询的Bug,那么它到底是如何解决的呢?下面我们来一探究竟,先来看一下select()方法的源码。
  从上面的代码中可以看出,Selector每一次轮询都计数selectCnt++,开始轮询会将系统时间戳赋值给timeoutMillis,轮询完成后再将系统时间戳赋值给time,这两个时间会有一个时间差,而这个时间差就是每次轮询所消耗的时间。从上面的逻辑可以看出,如果每次轮询消耗的时间为0s,且重复次数超过512次,则调用rebuildSelector()方法,即重构Selector,具体实现代码如下。
  实际上,在rebuildSelector()方法中,主要做了以下三件事情。
  (1)创建一个新的Selector。
  (2)将原来Selector中注册的事件全部取消。
  (3)将可用事件重新注册到新的Selector,并激活。就这样,Netty完美解决了JDK的空轮询Bug。看到这里,是不是感觉没那么神秘了?1.8 Netty对Selector中KeySet的优化
  分析完Netty对JDK空轮洵Bug的解决方案,接下来看一个很有意思的细节。Netty对Selector中存储SelectionKey的HashSet也做了优化。在前面的分析中,Netty对Selector有重构,创建一个新的Selector就会调用openSelector()方法,来看代码。
  我们再来看一下openSelector()方法的代码。
  上面代码的主要功能就是利用反射机制,获取JDK底层的Selector的Class对象,用反射方法从Class对象中获得两个属性:selectedKeys和publicSelectedKeys,这两个属性就是用来存储已注册事件的。然后,将这两个对象重新赋值为Netty创建的SelectedSelectionKeySet,是不是有种"偷梁换柱"的感觉?
  我们先看selectedKeys和publicSelectedKeys到底是什么类型,打开SelectorImpl的源码,看其构造方法的代码。
  我们发现selectedKeys和publicSelectedKeys就是HashSet。我们再来看Netty创建的SelectedSelectionKeySet对象的源代码。
  源码篇幅不长,但很精辟。SelectedSelectionKeySet同样继承了AbstractSet,因此赋值给selectedKeys和publicSelectedKeys不存在类型强制转换的问题。细心的小伙伴应该已经发现在SelectedSelectionKeySet中禁用了remove()方法、contains()方法和iterator()方法,只保留了add()方法,而且底层存储结构用的是数组SelectionKey[] keys。那么,Netty为什么要这样设计呢?主要目的还是简化我们在轮询事件时的操作,不需要每次轮询都移除Key。1.9 Handler的添加过程
  服务端Handler的添加过程和客户端的有点区别,跟EventLoopGroup一样,服务端的Handler也有两个:一个是通过handler()方法设置的Handler,另一个是通过childHandler()方法设置的childHandler。通过前面的bossGroup和workerGroup的分析,其实我们可以在这里大胆地猜测:Handler与accept过程有关,即Handler负责处理客户端新连接接入的请求;而childHandler就是负责和客户端连接的I/O交互。那么实际上是不是这样的呢?我们继续用代码来验证。在前面章节我们已经了解到ServerBootstrap重写了init()方法,在这个方法中也添加了Handler,代码如下。
  在上面代码的initChannel()方法中,首先通过handler()方法获取一个Handler,如果获取的Handler不为空,则添加到Pipeline中,然后添加一个ServerBootstrapAcceptor的实例。这里的handler()方法返回的是哪个对象呢?其实它返回的是Handler属性,而这个属性就是我们在服务端的启动代码中设置的。
  这个时候,Pipeline中的Handler情况如下图所示。
  根据对原来客户端代码的分析,将Channel绑定到EventLoop(这里是指NioServerSocketChannel绑定到bossGroup)后,会在Pipeline中触发fireChannelRegistered事件,接着会触发对ChannelInitializer的initChannel()方法的调用。因此在绑定完成后,此时的Pipeline的内容如下图所示。
  在我们分析bossGroup和workerGroup时,已经知道ServerBootstrapAcceptor的channelRead()方法会为新建的Channel设置Handler并注册到一个EventLoop中。
  而这里的childHandler就是我们在服务端启动代码中设置的Handler。
  后续的步骤我们基本上已经清楚了,在客户端连接Channel注册后,就会触发ChannelInitializer的initChannel()方法的调用。最后我们总结一下服务端Handler与childHandler的区别与联系。
  (1)在服务端NioServerSocketChannel对象的Pipeline中添加了Handler对象和ServerBootstrapAcceptor对象。
  (2)当有新的客户端连接请求时,会调用ServerBootstrapAcceptor的channelRead()方法创建此连接对应的NioSocketChannel对象,并将childHandler添加到NioSocketChannel对应的Pipeline中,而且将此Channel绑定到workerGroup中的某个EventLoop中。
  (3)Handler对象只在accept()阻塞阶段起作用,它主要处理客户端发送过来的连接请求。
  (4)childHandler在客户端连接建立以后起作用,它负责客户端连接的I/O交互。
  最后来看一张图,加深理解。下图描述了服务端从启动初始化到有新连接接入的变化过程。

体制内工作,被拖欠工资五险一金,导致自己的商业贷款无法审批通过,这个需要怎么办呢?你这个工作铁定不是体制内的工作吧,体制内的工作是不可能拖欠五险一金的,以前确实存在拖欠工资的情况,但现在已经大为改观,基本上不存在拖欠工资的问题。你的商业贷款无法审批通过,说明你的如果你在石家庄买房你会选择哪个区域?在石家庄也住过几年了,总得感觉买房也不是非得买市中心或者二环内的房子。如果现在买房,我会选择上班较近的地方买房,我不看城市发展方向,对小老百姓没啥用。现在买房我会选择石家庄高新区,请问65岁以上老人可办理免费乘车公交卡,是全国统一政策吗?那小县城的老年人怎么办?你提问的问题应该不是全国性的统一政策,如果是全国性的统一政策那么就不需要,我们费用这么大的劲力办理老年乘车免费卡了,只要凭个人的有效身份证就完全可以享受乘车免费了。问题不是这样的是人的命运是注定的吗?如果是,是不是我们做什么都没有意义了?人的命是注定的,生死是自然规律,任何人都逃脱不了。既然有生死,人是无法更改。有了生,才有能力去扑死,生死两难之间,活着的人要负出代价,需要做出努力。这种负重,就是人生死的价值和使命现在月薪1万在社会中是什么水平?中等水平月薪一万如果在一二线城市属于一般,在三线一下地区是高水平。月薪1万在北上广这些一线城市不算什么,属于一般水平,若在二线或二线以下城市就属于高工资了。一般普通打工者,月薪都在年轻时打工,老了如何养老?努力做好今時的工作,有一步走一步,不喪失志氣,不要多慮,盼前顧後,朝著自己奮博的目標邁步向前,到老時一定有好歸宿。老了休息呀!为止年轻打工不交社保老了又赖上政府了!吃白食要养老金!俗语说命里有时终须有,命里无时莫苦求,现实往往印证了这话是对的,你怎么看?年轻时相信人定胜天,人到中年才明白,万般皆是命,半点不由人。是不是消极我不懂,原来也不是信命的人。感觉那都是失败者的借口,也一直以为只要有足够的勇气和胆量,只要努力和坚持。就不可能你家有两个孩子,一男孩一女孩。将来家产都留给男孩还是平均分?我老公有个姐姐,公公现在两套房子,已经把早先的那套小的给我们了现在还有他自己住的这一套。有一次,饭桌上说起将来俩老人老了,家产怎么分?公公说,孩子都一样,家产不能都留给儿子。我很赞大学毕业,错过了考公务员和事业单位,社区工作,有没有可能转编?题主您好!目前国家并没有相关的规定,社区工作者将来会转成事业编制,但某些地方可能会出台政策,将某些社区工作者转为事业编制,比如疫情期间做出贡献的某些社区工作者,这种机会是可遇不可求职工的档案都放在人社局,单位怎么知道职工应该何时退休?单位职工档案是不放在人社局的,是放在本单位人力资源部门的(有的单位叫人事科或人事处)。单位人事部门的工作人员,会根据职工的档案掌握职工的退休时间。人事部门的工作人员,会定期整理和查最近在看平凡的世界,少平读了那么多书,为什么要在煤矿呆着?一是孙少平没有读过多少书,只是高中毕业。况且,那时候的高中只是名义上的高中,学制两年,所学课程不过皮毛而已,特别是物理改成什么工基,根本没有涉及力学电学热学等知识,根本没有什么理论
525万,回归湖人!紫金军又一溢价合同,詹姆斯无法拯救你的生涯早些时候,爵士和骑士完成了一笔震惊联盟的8换1大交易,爵士顺利送走了米切尔,他们从骑士得到了塞克斯顿马尔卡宁阿巴基以及5个首轮选秀权(3个首轮签2个首轮签互换),这笔交易达成之后,2022年9月19日新闻简报,工作愉快正文共908字预计阅读时间3分钟码字不易,如果有用,请记得支持点赞关注转发!美好的一天,从读报开始,9月19日,壬寅年八月廿四,星期一,周末愉快,幸福生活1印度首富成世界第2富豪刷穆里尼奥像极了生二胎后安慰老大塔米做的工作,比进球还重要虽然是属于小魔仙迪巴拉的夜晚,他还给白头发老头行了吻手礼,同时也是红贝贝在六万多名罗马球迷跟前的亮相,老穆还专门打圆场,让贝洛蒂上场,是奖励他为了加盟罗马,而付出的努力,以及等待罗霍华德愿260万底薪签76人哈登不欢迎篮网计划给他合同辅杜兰特费城询问报记者斯基庞培最新报道,德怀特霍华德有意以老将底薪合同签约76人队,再次出任乔尔恩比德的替补,可詹姆斯哈登似乎并不欢迎这名三届最佳防守队员来到球队。哈登降薪续约后,76人队夏决垃圾话,阿水没人能真正意义上压制我,Viper可否一战?相信兄弟们都已经知道了,在上周滔搏就已经30EDG成功挺进夏决,而在昨天滔搏和京东按照流程开了一场新闻发布会为决赛造势。熟悉LPL的兄弟们这个阶段一般都是垃圾话时间,滔搏和京东也确邦奇威尔斯10年职业生涯签了3份合同,吃饭睡觉打邓肯邦奇威尔斯,职业生涯始于开拓者邦奇威尔斯1976年出生于美国印第安纳州,1998年第11顺位被活塞队选中,但随后被交易至开拓者队,10年职业生涯效力过开拓者灰熊国王火箭和黄蜂队,由欧锦赛的第一控卫施罗德为何如今还没找到工作到底有何玄机当下最炙手可热的篮球比赛非男篮欧锦赛莫属,在法国德国波兰和西班牙作为四强将竞争最后的总冠军。在塞尔维亚希腊和斯洛文尼亚先后被淘汰出局,NBA在欧锦赛中的一线球星,东契奇约基奇和字母不管是工作还是生活中,遇到以下4种人,要趁早远离你好呀,我是你们的李姑娘欢迎关注我,我们一起感悟人生!一个人生活在这个世界上,无论是工作还是生活中,都免不了和人打交道。人这一生会和形形色色的人打交道,有和自己志趣相投的人,也有和两性之间,一旦有了爱的关系后,女人一辈子都忘不掉了爱情是什么,幸福是什么,我们定义不一样,我们看法也是不一样的,我们理解的更多,但是我们可以去感触感动的更少,这就是生活的定义,生活的价值。重要的是我们要深度地懂这些,才会有着更多无离开阿的江出场时间不减反增,成为CBA联赛唯一不拼爹的球员当新疆队阿的江指导黯然下课之后,作为新疆队唯一在主教练光环下生长的阿尔斯兰,也成为球迷们心目中离队呼声最高的球员之一。毕竟阿尔斯兰来到新疆队的确有点阿的江指导的面子成分,在八一队解日本神户市神户市(日语神戸市Kbeshi)是位于日本兵库县东南部的都市,为兵库县首府,也是政令指定都市之一,下辖有9个区。全市面积557。02平方公里,人口则有1,522,273人(至202