netty的IM项目小结
Posted 千念飞羽
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了netty的IM项目小结相关的知识,希望对你有一定的参考价值。
概述
之前在看netty相关的东西的时候想做一个切实的项目来作为一个netty的练手,也就是写个IM。目前为止的话大概写了万把行,不过差不多也删了一半。总体的结构是在了,不过还有诸多需要调整的地方。但是随着认识的深入发现自己好像走歪了,所以决定停一停把诸多的问题总结一下。
个人菜鸟学生一个,学的东西也无非书上网上,无人指导,无实践经验,走走岔路在所难免,暂时所写也只是一时的观点看法不保证以后不会有更深入的理解,大神勿喷,不过如果是比我更白的小白来看的话大概也能帮你少走些弯路了,且本文多为软文,扯淡干货各占一半,不过扯淡的文章都用引用标起来了,请酌情食用。
项目地址
前面说的都是关于netty的一些理解,只是想看项目相关的东西可以直接跳转到MINIIM项目
关于netty的诸多思考
netty到底是用来干嘛的?
本人差不多1年前大概确定以后走java服务器端路线。当时学的时候什么都不懂,java做服务器的资料网上一搜满天飞的都是各种培训班ssh,然而余年轻气盛,好歹我也得是正规院校毕业的总不能根培训机构学一样的吧。(哈哈,想想当时都好玩)终于有一天,记不清从哪里看来的,“学netty啊,netty多牛逼”so走上了这条不归路
初学的时候不太明白ssh之类的框架和netty之类的框架到底有什么区别。按照官方的说法netty是一个异步的事件驱动的网络应用框架,用于快速开发可维护的高性能的协议服务器和客户端。(懵逼树上懵逼果 懵逼树下你和我)
Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.
那到底是个什么鬼?一个定义是很难描述的,这里还是从多个角度去说明。
netty适合做长连接?
首先,对于网络相关的任务其实可以粗略的分成两大体系,即web体系和其他体系(并不是说应用服务器,说应用服务器不是太正确,这里还是用其他代指),这种分法是很不严谨的,就好像你小时候你爸妈告诉你世界上的人分为中国人和外国人一样。web体系占了互联网的大半壁江山,非常的成熟稳定,有一套完善的从小项目到巨无霸的解决方案,而且特色鲜明,比如短连接、HTTP协议、无状态等等,在java的环境下催生了一大批的库和快速开发的诸多解决方案。比如Tomcat、servlet、ssh、nginx等等多如牛毛的东西。这些东西的特点是专业化程度高,上手快,集成度高。而其他的技术体系则相对来说繁杂的多,内容也宽泛的多,比如IM、FTP、邮件、游戏等等,有的很成熟有的不成熟,很多时候需要灵活的配置,也就是要造轮子,这时候netty在某些情况下就非常有用了。
而这些web相关的东西其实和netty本身关系并不大,这并不是说netty不能做,而是说有更好用的集成度更高的框架比如ssh之类,所以你可以看到很多web相关的java职位并没有提到netty之类的东西。ssh这个所谓的框架和netty所谓的框架之间的区别,举个例子,比如刀在生活中大部分的时候是用来切菜的,也就是菜刀用的多,但是你出去旅游总不能带一个菜刀吧?你要带瑞士军刀,因为灵活配置高效利用。菜刀就是ssh,高度专业化,瑞士军刀就是netty,灵活配置。
当然web体系和其他体系之间并不是泾渭分明的,web体系下的很多东西大家都用的非常熟,很多时候在其他体系下的应用也可以借鉴一部分web的东西来做,既然有更好的东西,更熟悉的东西为什么还要用netty造轮子?这个问题反过来也是成立的,对于大量的短连接的需求,其实可以套用web体系下短连接的实现来完成,这时候web体系的一套东西更容易应用,所以netty很多时候就变成了用来做长连接。而且从效率上来说还是长连接有优势的。
当然长短连接并不是严格的界限,HTTP1.1默认是长连接的,web体系下长连接也是非常完善的。netty也可以用来做短连接,也可以用来做长连接,比如。只是netty的模型用来做短连接会显得比较怪。因为短连接本身比较简单,如果你用netty基本上就说明这个逻辑会比较灵活比较复杂一些,却依然用短连接来实现显得效率很低。
当然以上只是我个人的观点,netty和长短连接并没有什么直接的关系,短连接相对长连接比较简单也容易管理,只存在有效连接,所以一般只有逻辑比较简单的情况下采用,只是没比要用netty这类框架把问题搞复杂而已。如果逻辑比较复杂还是用长连接来效率更高。
netty来做IM?
好,既然netty可以用来做一些非web的东西,比较适合做长连接,比较适合造轮子,我想到的最具体的东西就是IM。那netty适不适合做IM?
这个问题可以说对也可以说不对,要看在何种情况下来问,如果你的目的是做一个IM。通信的部分用netty来做。这样是可以的,或者说是一种比较上道的做法(不是小作坊式的,虽然最后我做出来东西还是小作坊)。但是如果说你是想学习netty,IM只是你能想到的用来练习netty的东西那这样说就是错误的。
可以把做IM比喻成你和你同学想在网上聊天,你需不需要一个路由器一样。IM就是需求——你要和你同学网上聊天,netty就是路由器——帮助你上网的东西。你可以选用路由器,但是你也可以选择一种更简单的方式,笔记本的网线直接插入到网口里,就好像你可用直接用socket编程。而路由器也不是你上网这个需求的主要业务逻辑,你的主要业务逻辑跟路由器是解耦的,不同层次上的问题,主要业务逻辑——打开QQ,跟人聊天,聊不聊的来要看你的水平,跟你的路由器是不是地摊货没有直接的关系。但是并不是说netty——路由器就没有用了,当你的业务拓展的时候,就需要路由器了,比如这时候你室友回来的,要用无线。这时候你总不能再独占这网线了,这时候路由器功能就显示出来了。可以方便你业务拓展,方便大量的复杂的任务分配给不同的服务器去处理的时候有用。
好那么我们在回到IM这个问题上,比如你的IM要求相对比较高,或者任务比较复杂那么会怎么样?比如你用户比较多,你单个服务器不好用,你需要多个服务器,这时候你需要一个处理业务逻辑的服务器的集群。为了保证性能和一致性,数据库也要独立出来,用单独的服务器去做,也许你还支持上传文件的功能,那这样你又需要一个文件服务器,说不定你还需要一个提供账号密码验证的过程的服务器,这时候你需要多种服务器协作,从网络涌入的大量信息需要做分流,那么你的需求就来了。你需要一个网关服务器。这就也就是netty最适合做的任务。
我一直都认为把netty的功能比作一个路由器比较恰当,netty可以根据你设计的协议把任务解包然后转发。这个过程就和路由器一样。
netty到底适合做什么
就如上一节所解释的问题。想理解netty的功能只要想着路由器的功能就好,netty适合做的东西就是协议的实现。比如netty中有解码器编码器,缓存,有通道,可以绑定端口之类的。所以如果初学netty又希望做一个练手的项目来说,做一个协议实现的东西是最适合练手的项目,如果现在让我重新写一个项目我就会写类似的东西。这里举几个例子。
- 网关服务器
比如你可以设计一个私有的协议,按照IM可能需要的功能来实现。但是具体的业务逻辑服务器可以不用做。而是用一个简单的消费者生产者模型替代。比如用protobuf或者json把任务标记成业务逻辑包,登录验证包,文件数据包,数据库查询包,等等诸多类型,然后按照分别转发到不同的消费者模型所在的端口等等。这就是个比较靠谱的netty的实现项目我觉得这样是在个人的能力范围内最好的熟悉netty的方法了。尤其需要注意的是netty服务器和业务逻辑基本上可以解耦的。我在做IM的时候写了大量的业务逻辑的实现。其实现在来看都是不必要的。 - 基于现有的协议去做实现
当然一般个人没什么经验去做难免会走一些弯路,自己去写一个设想中的东西难免会出现一些错误,就好像我最后做了个四不像。所以基于现有协议写实现也是一个有效的方式。比如网上很流行的shadowsocks,其实就是SOCKS5协议,其实这个协议并不是特别难。也是一个比较老的协议,我还看到过多年前的java版本的源码。shadowsocks的客户端本身好像是python写的。如果是新人可以尝试写一个这个东西的netty版本相信会很有意思。此外其实还有很多很多的协议,比如SIP、FTP等等等。找一个相对比较简单的协议写一个实现也会学到很多。 - 向分布式方面的衍生
其实由于netty这种灵活高效的工具性能,有很多的分布式应用都需要用到,比如用来做RPC、消息队列做负载均衡等待。如果你水平很好的话,其实可以尝试他来做一些这方面的东西,用来构建一些平台型的软件,中间件之类的东西。不过这需要一些分布式方面的消息,以我目前的水平来讨论这些东西还是显得太无畏了。简单的当然可以扯扯,但是没有工程经验还是误导向了。
netty为何好用
关于netty的性能大家都谈的比较多。官网上也列出了很多听起来很牛逼的特性。下面我就从我个人的观点来谈谈为何netty好用。还是那句话,现在的观点只是一时的看法也许以后会有更深入的理解,请酌情食用。
高性能服务器的要求
首先谈到高性能服务器,好像大家都觉得要异步,要NIO、要缓存不要流,这样性能才高?确实的情况是不是这样?其实个人觉得这个道真不一定,还是要分情况来看的。
关乎服务器的性能,其实主要大概在这两大类因素,当然还有一些其他的因素你比如你业务逻辑的效率、协议的设计等等。但是这些事框架不可控的,框架可控的只有线程模型和IO模型。
一类是线程模型,线程模型会影响你的线程数量,线程数量越多上下文切换也就越多,占用资源也就越多,自然是不好的,此外线程模型还会影响资源的访问权限,或者说很多时候需要争用锁或者其他的同步组件。这样也会大量的影响性能。
第二类就是IO模型,主要也是两个方面,一个就是很多都喜欢谈的5种线程模型,这个其实是《UNIX网络编程》里说的。还有一个就是操作系统里的概念,即用户区内核区的内存复制问题。
那下面我就从这两个方面去说明下为何netty的好用。
线程模型
关于netty的线程模型其实之前李林峰写过一篇文章说的比较清楚了
Netty系列之Netty线程模型
这里顺便插一句,我正式算是接触编程差不多也就1年的时间,刚开始学的时候总觉得算法才是最重要的,以至于把大量的实际都花在了基础算法的练习上了,(我是说基础算法,不是机器学习哪类专用的算法,就是LeetCode刷题那种,我目前差不多刷到第二遍300多道)当然一部分也是因为我需要找工作要用,但是现在写了一些才觉得其实更重要的是对编程的理解,一个实践经验丰富的工程师写出来的代码非常漂亮,那种写出那种千锤百炼式的精巧程序的程序员才是真正的架构师,当然我并不是说算法不重要,很多人觉得自己算法牛逼,其实只是拾人牙慧而已,真正牛逼的是算法的发明者,懂不代表会用,会用不代表会用的好,收放自如才是真正的大师,说到底都是解决问题的工具而已。
后来看的说多些了,看的程序多些了,写的多些了,才发现其实算法之外有很多很多的东西需要去理解,比如并发就是一个非常重要的东西,尤其是对服务器端编程来说非常非常重要,很多问题,尤其是找不到原因的问题都是由于对并发不够了解才发生的。并发并不是简单的知道锁、知道CAS就行,不同情况下有不同的编程模式,还有很多底层的问题,虚拟机的问题,这些都是需要去理解才能灵活运用的。用了netty不代表你就高性能了,如果没有很好的理解并发不管用什么都写不出来高性能的程序。我比较坑,是遇到了问题之后才发觉并发重要,以至于后来相当长的时间脱离了netty去看java并发库和容器库的源码还有各种并发的书(另外推荐下《java并发编程实践》,非常好的并发的书),主要就是了理解并发方面的问题。如果一开始就看到这个netty的线程模型的文章的话会省去很多时间。
netty的模型基本上就是仿造了Reacter模型,这个模型可以看成是饱经考验的服务器编程模型。我这里也不想多做介绍具体的情况网上有很多文章已经说的很明白了。我在看完这个模型后想的问题是为什么这个模型会比原始的模型快?以下是我个人的理解
首先事件驱动模型本质是什么?按照我的理解实际上就是本应由一个线程完成的任务,用事件驱动模型重写后会将这个任务分成多个阶段然后分别交给不同线程组协作去完成。就好像在抗洪的时候,在运沙袋的时候不会让一个人扛着沙袋网上跑,而是大家连城一条线,然后一个一个的交付沙袋一样。这么说当然简单但是电脑不是人,不会觉得累,而且线程之间的交付还是需要代价的为何一个任务分成多段完成就会更快?
限制线程的数量这是一部分考虑,但是并不是最主要的,原因很简单,即使用一个线程完跑完全程,也可以用线程池加任务队列的方式来限制线程数量。这在java中并不是什么难的问题。我认为最重要的原因在于两点,一点是不同任务所走的路径是不同的,其次一点是在线程的不同阶段所允许的并行程度是不同的。
我们在讨论线程数量的设置的时候,大部分都提供了这样一个设置的标准,对于IO密集型任务线程数量应该是cpu数量的数倍,对于cpu密集型任务应该是cpu数量加1,或者混合的来说设定一个阻塞的系数,线程数=CPU可用核心数/(1 - 阻塞系数),其中阻塞系数在在0到1范围内。计算密集型程序的阻塞系数为0,IO密集型程序的阻塞系数接近1。这样的概念应该稍微了解计算机的人都应该了解,那么现在的问题来了,对于一个web网站我怎么去确定线程数量?
这个问题其实很难回答,因为没有办法确定线程的阻塞系数。不同的阶段可并行的程度是不同的,比如数据删除的操作或者IO读写的也许很慢,只是做加法运算之类计算性的任务就很快。举个例子,比如说数据库读效率的极限差不多允许一个10个线程的线程池满状态运行,这时候实际上有100个任务提交给服务器,这100个任务中前20个需要数据操作的任务。那么问题来了剩下80个线程需要等到这个20任务都完成了之后才能运行。但是如果你用事件驱动模型就没有这个问题。你可以设置两个线程组,一个线程组完成计算任务,另一个完成数据库操作任务,这样剩下的80个线程就不会阻塞。也就是说不通任务不走相同的路径这样细分之后可以降低阻塞后所影响的线程数量,这样就高效的多了。这种其实相当于并发编程中竟可能降低同步区长度,道理是一样的。
此外还有一点,就是资源的获取,很多时候并行程度不同就代表着需要并行度低的部分需要争用,也就是说需要锁或者资源的占有权限之类的问题。如果不使用事件驱动模型,用一个线程去跑全程,就会出现每次遇到并行度不同的问题都需要获取锁,大量的锁对于程序来说是非常危险的,不仅效率很低还会带来死锁之类的活跃性问题,这对服务器编程来说是非常致命的(所以现在流行非阻塞算法)。如果是事件驱动模型就可以降低获取锁的步骤的次数,比如还是数据库的操作,这个操作需要获得数据库的链接的,那如果不用事件驱动模型,你可能需要一个类似阻塞队列或者其他同步措施来获得锁或者获得锁的权限,这是非常危险且低效的。如果你使用一个线程组去完成所有的数据库操作任务。你可以把数据库的链接写成一个ThreadLocal变量,赋给每一个数据操作线程池的线程,这样就没有争用链接的情况。对于不同的并行程度的任务,不通资源许可的任务构造一个线程组,这些线程组天然具有特殊的权限去执行任务这样就降低了同步的代价。
以上两点主要是从性能的角度来考虑的,但是在我看来其实性能并不是事件驱动模型的全部,当然事件驱动模型本身是高效的,但是我认为更重要的是这种模型的解耦做的非常好,你需要什么类型的任务只要添加一个相应的线程组去做就好了,这样各个线程组的功能非常的明确,发生问题了也非常好解决,除了问题也非常好查找。可拓展性、可维护性都要好的多。
理解了线程模型之后再去考虑netty就简单很多了。
比如说netty的IO线程可以看做是一个单独特权的线程组是操作IO的,所有读取和写入都需要经过IO线程。当然你可以保留channel来自己进行读写,但是这样是不合理的,背离了IO线程组的原意。
由于事件驱动模型这样的结构下,IO线程甚至可以看成是串行的,基本上不需要考虑并发问题,在写IO线程的时候需要注意需要尽可能的快的完成操作,而且要避免IO阻塞,因为如果IO线程池的任务都阻塞了那就整个服务器都会停止下来,其实只要你理解的所谓事件驱动的概念,IO线程中不会出现阻塞的情况,因为如果有阻塞的任务就是说明这种任务需要打包交给其他线程组去完成。而不是傻等着,这也就是netty所谓的异步的概念。
此外在netty这种线程模型下,也不会再IO线程中创建新的线程,一方面因为在io线程里创建的线程他的生命周期不好控制,另一方面在事件驱动模型这样的概念中不会出现额外的突然出现的线程,线程都是在特定任务的线程池中进行管理。
关于耗时的业务逻辑的问题以及IO的回写
首先应该明白的是在网络环境下绝大多数情况IO的速度肯定是慢于逻辑业务的处理的,所以对于绝大多数任务都不应该丢弃给后端的业务逻辑,当然这个不是绝对的,比如如果需要读写文件或者数据库,这也属于IO,很可能比网络IO更慢这种情况下就算是耗时的业务逻辑了。
其次,耗时的任务应该分两大类来讨论,如果任务确实会带来阻塞那是一定要异步的处理的,因为netty的IO线程实际上是单线程的一旦阻塞,其他的线程都会难以继续进行,影响很大,但是对于非阻塞只是比较慢的业务逻辑是否应该丢给业务逻辑处理?这个其实我觉得应该看情况。因为这个实际上相当于是输入的任务量高于我的负载能力的,如果我丢给后端线程去做实际上是在欺骗客户端我的实际处理能力,有可能引发更多的输入,这是不科学的。所以我觉得应该还是优先在IO线程处理,并考虑拒绝或者抑制的机制。知乎上也看到类似的说法,不过好像赞同的人比较少。
最后关于写回的问题。这个我在做项目的时候没有考虑到实际上这了对于ChannelPipeline.executer()可以得到当前任务的处理器,所以只要将处理好的任务提交给成处理就可以让IO线程调用去完成写入任务了,这种方法是非常好的,比我自己保存Channel散列表的方法安全的多。上面推荐的李林峰的那篇文章也说过,不过可惜我之前没看懂。
IO模型
IO模型算是我发现的在并发编程之后相对于服务器编程来说非常重要的方面,netty本身的IO模型并没有什么好研究的,只是把java的基本接口包装一下,让他更容易使用而已,所以更重要的其实是对操作系统中IO模型和缓存的理解。此外还有一个问题为虽然知道这部分很重要但是NIO部分确实用的少,我这差不多刚刚算把并发的问题搞得差不多,javaNIO和net部分的源码还没怎么详细看过,所以这部分也只是道听途说了,以后发现在补充了吧。
这里其实也有一篇文章说的很好但是我一时间找不到了以后找到了在补充。那篇文章中提到了一个观点我个人是比较赞同的,但是没有想象查询过文中所说的信息是不是都真实可靠的。
5种IO模型网上资料也已经满天飞了,我这里也不想全面的介绍一遍,但是资料之间有一些出入,我还是想说明一下。
关于什么是异步
首先5中IO模型中其实只有最后一种是异步的模型,其余的都是同步的模型,而真正可以讨论的其实只有三种,首先第一种同步阻塞模型,(恕我直言,同步非阻塞模型看起来很二)第三种IO复用模型(也就时现在最广泛使用的模型,select,poll,epoll),(事件驱动模型时候和IO复用这种比较类似,只不过通过信号来实现,似乎需要多些一些信号驱动事件,不过linux底层方面了解还不够不妄加评判),第五种异步模型。
我想说的是这三种之间的异同点,在我看来:
首先13两种其实在单线程模型下其实是一样的,没有区别,但是在多线模型下就有区别了,第三种的前半段,差不多类似于Reacter中的Acceptor,后半部分类似于Reacter中的IO线程以及一部分工作线程的任务,当然你在netty中也可以找到类似的对应。这样就可以一个acceptor或者少量的acceptor线程对应很多个IO线程构成的线程组。
而35之间的差别其实在于,数据是到达系统内核区之后是不是做一次通知/唤醒?还是等到数据到达用户内存区在在同一唤醒?也就说进程介不介入数据从内核区到用户区的复制过程是两者的主要区别。
异步是不是最好的?
有一个问题是异步是不是最好的?其实这个要分情况来看,在我看来异步只是一种编程的方式,而netty所谓的异步其实更多的是指这种编程方式,未必说的是这5中IO模型中的第五种异步模型这两者并没有什么关联性,只不过很多人看书的时候混淆了一些概念而已,就好像java的序列化会有人理解成串行化的概念一样。
反过来看如果异步是最好的模型那为何大量普及的实现采用的是IO复用模型,也就是所谓的epoll?其实这里有一个问题就是虽然唤醒带来一定的消耗,但是异步的模型却丧失了灵活性,当一个消息到达的时候必须在内核区申请一个区域,完成从网卡到内核区的复制后,再在用户区申请一个区域再复制。我们都知道申请内核区的内存需要陷入内核是代价比较大的。
java中其实提供了一种内存映射的方法可以减少一次复制,创建一个内存映射区域,如果我们将这部分区域池化,反复利用岂不是效率更高?也就说netty其实在一定程度上绕过了java的内存分配,通过直接管理内存来提高效率。更不要说C++这种灵活性更强的语言了,有更强的内存控制方式。
所以说第5中异步模型并不是最好的,最灵活最高效的模型是基于边缘触发的epoll模型即IO复用模型配合灵活的内存管理机制,所以你有了你希望的最高效的实现框架,即netty。
真正的异步
很多人在讨论Java的nio的时候说这并不是真正的aio。这种说法其实是没有错的的。
首先java分了两个版本的NIO,一开始的NIO是java5出来之后推出的,在此之前的java的io都是阻塞式IO也就是流的概念,但是流是一个比较麻烦的东西,远没有缓存的概念灵活,而且对应的操作系统上,其实当我们操作一个问题文件的时候不存在什么输入流和输出流,这都是java自己抽象出来的。java5,推出了非阻塞io的概念。才有了缓存,通道之类的东西。对于java的nio来说目前来说一般在linux下select都是通过水平触发的epoll来实现的,低版本的有可能是poll。
之后java7的时候又提出了AIO的概念或者叫NIO2.0。这个东西从概念上说是真正的异步io,但是有一个比较麻烦的情况是在linux环境下aio并没有很好的被支持。所以在linux实际上java中的aio似乎依然是水平触发的epoll实现。
好那么在看看netty的问题,netty实际上并没有采用java的实现,而是自己写了底层的实现,所以netty中的select采用的是边缘触发的epoll实现。所以说即使你用java的SE API重写类似netty的东西也许效率依然没有netty高,因为底层不一样,netty4的时候并不支持AIO,netty5支持java中的AIO,但是目前netty5似乎是被废除了,版本滚回到netty4了(我想到了那本netty权威指南,好惨,不过道理是一样的哈,还是感谢大大领我入门)。所以目前来说netty在linux环境下,他的select依然是采用边缘触发的epoll实现,至于在window环境下,哈哈,今天天气不错哈。
缓存的概念
netty其实只是对java的缓存包装了一下是他更容易使用,另外增加了一些申请、池化之类的辅助类,使得其使用更加方便而已。所以理解javaNIO中的缓存以及操作系统之类的背景知识其实才是理解netty缓存的关键。
java会不会内存泄漏
了解java的都大概了解Java的内存回收机制GC,新生代老生代之类的,这也是我比较喜欢java的一个重要原因——省事。但是需要注意这里有一个前提就是这里的内存管理的是堆内存。也就说你在java中使用new这个关键字申请的内存差不多都是通过GC完成管理了。不需要你自己去考虑怎么协调。
但是java中并不是所有内存都是这样的。比如NIO中的内存映射,我一直理解是通过这种方式是在系统内核区申请一块区域,但是现在来看似乎并不是这样,只是提供了一种内存映射,但是无论实际上究竟如何,这部分内存可是不归GC管理的,而且可以减少系统调用的次数,缺点是需要你自己去控制管理。当然NIO的内存映射也分有方向和无方向的,GC回收也不是完全无效,等等可以讨论的问题还很多。
这样做的好处就是灵活,高效,代价就是有可能出现内存泄漏。比如你在传送一个大文件的时候或者需要经常发送消息,这种情况下可以申请一片大的内存映射区域然后反复的利用。这样带来的效率提升其实还是很客观的。内存映射的效率非常高比BIO和基于通道的方式传递的速度都要更快。
数据的复制是一个值得商榷的问题。一般来说从一个数据从一台计算传到另一台计算需要5遍复制,即用户内存空间到系统内核区、系统内核区到网卡发送缓存,网卡发送缓存到另一台电脑的网卡接受缓存,网卡接受缓存到系统内核区,系统内核区到用户内存空间。怎么最大程度的减少这部分的消耗也是一种有效的手段。当然也许有人说但是网络延时的实际就必这个高了还有必要讨论这个?这个其实不一定,首先网络延时作为服务器端编程人员来说是基本上是不可控的。所以再讨论也没用,其次虽然从单次上来看网络延时很多,但是对于一个大吞吐量的服务器处理速度带来的延迟也相当的客观,所以还是需要重视的。
netty的0拷贝
上面小节说的内容,其实更多的是JAVANIO的问题,其实netty本身在这方面并没有做更多的工作,在介绍Netty的特性的时候曾经说过一个0拷贝的概念,其实这和上说讨论的内存区和操作系统内核区直接的系统调用次数并没有什么关系,我理解netty所谓的0拷贝实际上意思是,ByteBuf可以比ByteBuffer更加容易的结合在一起,其实就是API更好用。
比如如果一段信息分别使用消息头和消息体来完成,如果你用ByteBuffer你大概有两种方法一种是创建两个ByteBuffer,然后分别发送,这就要调用两次,或者你创建一个新的ByteBuffer这样就就需要一次额外的创建和复制,都比较麻烦,但是如果用ByteBuf就简单很多,可以一次性发送消息头和消息体,用起来更方便。
MiniIM项目
项目概述
这个程序现在来看其实挺失败的,因为没有充分利用netty的特性,只是用到了皮毛,和直接用socket去写也没什么区别。但是无论好坏算是我的第一个个人项目,总结还是必要的。而且作为一个单纯的IM项目来说基本还是完成了的。
首先项目用的应用层协议websocket,不过后来觉得根本没毕业。当初做的时候主要是希望能否做成web和客户端双端的那种,不过后来发现还是当初太傻。
数据序列化基本上都是JSON,这里用的fastJSON的包做解析,JSON的内容都在JSON的包中定义。
自定的hander里面采用的是状态模式,主要是验证和加密,如果都通过则会进入最终的状态也就是互相发json来进行通信。完成任务交互。
基本的内容差不多就是这样。下面还是用图来说明比较容易看懂。
线程模型:
netty的线程模型都差不多。只是根据任务的类型来确定线程池就好。
服务器端
服务器端的状态转移图:
这部分主要是工作线程组的任务,建立连接后会创建一个ServerStatementMangement的对象,然后就是状态模式,定义了几个状态ServerInitState、SelectAlgorithmandPubKey、ServerACK和ServerDealwithJSON几个状态。目前这几个状态主要完成的任务类似于ssl,代码比较好懂看看就行。在ServerDealWithJSON状态下就说明通过了验证,这时候会创建一个DealWithJSON实例,然后该连接(回话)内的所有JSON的处理都交给这个DealWithJSON实例来完成。
当初为何用状态模式,其实还是自己想的比较多,我是想把这个软件做成一个比较多功能的东西,目前就好像是可以聊天的软件,但是比如视频的时候或者像刷微博的时候接受的json肯定是不一样的。需要对发送和接受的json和状态进行限定,这样使用状态模式如果将来需要拓展只需要加不同的状态就行了。然后针对状态编程就好。如果需要废弃功能也只需要把状态转移图的入口删掉就好。简单的说就是加强可拓展性。可惜人算不如天算哈哈。。
主要注意的是所有的用户信息都是保存在ServerStateManagement中的,所以后续DealWithJSON需要从这里获取数据。另外所有状态的输入和输出都是String,这里写是JSON其实并不完全,在加密之前其实就是普通的JSON的字符串形式,但是加密后是需要解密才能得到有效的信息的。具体看看代码就好。
服务器工作流程图:
这个工作流程比较麻烦了,首先我们知道DealWithJSON是在连接完成验证的阶段创建的,每个连接只有一个。这个DealWithJSON会受到来自于客户端传来的JSON信息然后进行处理,也就是业务逻辑的主体部分,当然这里做了细分,如果该任务不需要数据库任务就会把直接进行处理然后转发,比如说聊天的互相发信息时候就是这这样的。
如果是需要访问数据的任务(比如好友列表等等)则DealWithJSON则会把任务包装成一个DBCallable类的实例存入数据库缓存队列中,会有另一个全局的数据库线程池来不停的从这个DBCallble阻塞队列中去任务执行,然后再打包成SendBackJSON类型的数据,然后放入SendBackJSON阻塞队列。然后同样的套路,会有一个全局的回写线程SendBackThreadPool从阻塞队列中取SendBackJSON,SendBackJSON中会包含有当初DealWithJSON中的Channel的信息,结合一个Channel散列表可以确定写回那个Channel。
此外还有一个问题就是怎么发离线信息,这里我实际上做了一个离线的数据库,比如说好友申请或者离线信息,如果在写入信息的时候发现需要写入的通道不存在(即用户离线),则需要将这个JSON写回到数据库中,在每次用户登录的时候回查询这个离线数据库是否有需要写回给用户的离线信息。这里其实用一个全局Channel散列表来写回是一个非常二的行为。具体失败的地方后面再总结。
客户端
服务器端看完了客户端基本上是类似的,这里就直画个图不详细说明了。
客户端的状态模式:
客户端的工作流程图:
首先也是和服务器一样状态模式,然后再进入最后阶段的时候会创建两个单线程的线程池,一个用来处理接受到的JSON一个用来处理发送JSON的过程,这里之所以和服务器不同主要是考虑到有可能多开,但是后来发现想多了,不过这样也可以工作就没改。
然后是一个ClientMange类,这个类是一个静态类。包括了GUI和发送和返回的阻塞队列,这两个阻塞队列会和创建的两个单线程线程池配合工作,完成信息的交互,GUI用的就是swing。
主要的几个问题
首先如前面第一小节中说的Netty主要用来做什么,其实从一开始在写的时候就没搞清楚就乱写了。
首先最大的问题是没有很好的利用netty:
比如说我想做一个文件传输的功能,如果是小文件的话当然可以用json包装后传输,但是如果是比较大一些的问题在这样做会影响通信。这时候我想到的是做一个带外传输的功能。但是在我现在的这模式下做带外是比较麻烦的,所以我才说这样做失败了,如果让我重新设计功能,我会用netty做一个类似网关的功能,对于文件传输的功能可以在客户端单独开一个线程,进行带外传输,在服务器端做一个网关,如果是聊天服务器的业务逻辑信息就传给工作线程的服务器,如果是传输的是文件就把信息传给处理文件的服务器,当然这里不一定是服务器也许是同一个服务器的不同端口也一样的道理。但是在现有的模式上做网关要改的东西太多了。这里就没改,而且都用json做包装也是不太合适的传文件的。
此外还有一个很失败的地方就是破坏了IO读写Channel的特性。我这里用一个Channel的散列表(其实是一堆散列表)把用户名-Channel一一对应起来,然后写回的时候就可以根据用户名来找到Channel然后调用Channel的write写入信息了。这样做当然是可以的,但是问题是对Channel的读写就不是IO线程独有的权限了。而且还有可能会发生同步的错误的问题。这其实是非常危险的。而应该吧任务都包装给IO读写线程去做,不过这部分究竟该如何去做我还不是太清楚。netty很多地方还是不是太明白。
下一步的工作
接下来的问题,这个项目该不该继续做下去的问题,
首先,如果从IM的角度来说实现这个只是实现了基本的功能,还存在IO线程写回的大隐患应该是要改一改的,但是如果继续下去只能越走越歪。和当初学习netty的初衷相悖了。
其次,如果希望进一步熟悉netty的角度来看,项目应该继续,不过不在是现在的形式而是需要改很多,如我在第一小节中说过的,如果现在让我重新写一个netty的练手项目我不会在写这么多无关的业务逻辑程序,可以只写一个网关或者只写一个socksv5的实现这样。不然不能算是理解的netty。这个项目本身其实跟netty关系并不大,netty如果从使用的角度来说主要还是会写处理器和编码解码器。但是这样基本上就算是另一个完全不同的项目了。
最后,如果从增强编程能力的角度来说,我觉得应该把重点放在java的NIO上面,这样应该会对编程有更深入的了解,学习netty毕竟只是增强编程能力的一个载体。把NIO的源码看一看从深入的角度说更好一些。
现在到底该怎么办我还没完全想好,不过个人比较倾向于把主要精力放在NIO上,因为之前看容器和并发的源码感觉学到的东西比较有深度,最近杂事也比较多。
以上是关于netty的IM项目小结的主要内容,如果未能解决你的问题,请参考以下文章
基于 netty4 的在线 IM 解决方案 QIQI-IM | 软件推介
荧客技荐基于 netty4 的在线 IM 解决方案 QIQI-IM