Windows下两种iocp实现的差距

 

 

之前几天说过,因为经典iocp实现(以下简称经典实现)多个io线程绑定在一个iocp上,这样内部管理了iocp队列的处理,内部决定是不是需要线程切换,我上次修改的一个版本(以下简称实现2),用了多个io线程,每个iocp队列仅绑定一个io线程,一组用户共享一个io线程,这和经典的多线程epoll模型的做法是很相似的,这样每个io线程是可以独立控制了,但理论上这种做法没有发挥iocp自动管理线程切换的优势,昨晚没事用这两种实现分别做了个echoserver测试了一下,这两套实现代码仅40行左右不同,其他完全一样,效果真的是差很多,测试仅用一个进程模拟了4000个客户端,每秒1个包,先看实现2的,cpu14%2io线程,1accept线程,1个主线程,其他线程都没干活闲置。

 

Cpu

Memory

Threads

handles

14

40088k

8

4236

 

再看经典实现,cpu几乎一直是0%2io线程,accept也是在io线程里面处理,其他跟实现2一样,测试客户端也一样。

Cpu

Memory

Threads

handles

0

39244k

7

4336

 

说实话,在测试之前我也没想到有这么大的差距,经典实现就是1.2w个连接连上来还是这样,就是内存占用多一点:

Cpu

Memory

Threads

handles

0

112068k

7

12280

 

习惯上总有人喜欢拿epolliocp来对比,我到现在也没看到真正公平的对比,就算是相对公平的也没见到,因为在我看来,要对比硬件应该是一样的,os都应该是最新的,最重要的是,server端程序应该都是发挥了各自优势的,如果拿我这里的实现2去代表iocp的水平和epoll对比,势必造成比epoll差很多的结果,然而这显然是不正确的。

 

epoll经典多线程模式实际实现和实现2很相似,理论上也有类似的线程切换问题,不知道效率怎样。

 

 

posted @ 2011-02-01 10:48 袁斌 阅读(11908) | 评论 (3)编辑 收藏

回调函数的常见实现方式及速度比较

 

 

回调函数实在是用得太广泛,回调函数又有多种实现方式,如:

1、  静态函数

2、  虚函数

3、  函数对象

4、  传统c函数,通过一个void *传递对象地址,内部强制转换

5、  fastdelegate

6、  Tr1::function + bind

7、  Boost::Function + bind

基本上速度是按照由快到慢的顺序排列的,就是

1 > 2 > 3 > 4 > 5 > 6 > 7

其实234速度很接近,有的时候函数对象效率更高一点,基本上越是高级的方法使用起来越方便,但速度越慢,越是传统的方法速度越快,呵呵,看来做server端程序要综合考虑效率太新的东西还是要少用啊,还是用传统的方法比较靠谱一点,当然如果调用次数不多的地方,使用更方便的方法还是好一些,毕竟我们要综合权衡,而不能死板恪守教条。

 

 

posted @ 2011-01-30 11:19 袁斌 阅读(4528) | 评论 (3)编辑 收藏

一套网络框架的杯具

之前设计了一套网络框架,持续改进了很多年,使用在很多项目上,综合效率还行,也很稳定,一直以来对这套东西信心满满,总以为啥问题都好解决,但最近就有个需求让我选择还是改了下这个框架。

之前的框架是这样的,可以开一组Nio线程,可以开一组N个同步线程(默认1个),可以开一组N个异步线程(默认1个),可以开一组Ntimer线程(默认1个),可以开一组N个异步线程(默认cpu个),每组可独立受控,每组可支持自定义消息,可支持timer,一组N个如果N大于1则无法直接给这组里面的特定线程发消息,只能给一组发消息,这个组里面会选择某个合适的线程处理这个消息,这也是iocp高效和典型的用法了,但这也正是问题的结症所在。

Linux下的多线程服务器更常见的做法跟这个不大相似,一般都是将某些socket分配到某些线程epoll,分好之后就是固定的,不再变化,跟Iocpsocket绑定到一组线程的做法不同,由于某个socket直接绑定到了某个线程,所以有些问题就变得简单了,如同一个连接的在同一个线程内消息进行了同步,要跟io线程绑定私有化数据也简单了,而且每个线程可独立受控,所以很容易实现一组io各自挂tls(线程局部存储)数据,而我现在做的这套框架就是这方面不好控了,其实也很难说这两种意义上的框架到底谁更优,如用在web型应用上这种socket被一组io线程管理的模式很方便效率也高,但我现在的需求需要某个socket使用线程相关数据,以避免数据之间的锁,我用内存换时间,由于在原来的框架上增逻辑难以实现可直接控制io线程的框架的,所以花了一个晚上重新改写了一套框架,在原来iocpframe的基础上派生了一组带2名称的类,除替换类名之外只修改了几十行代码就做好了,总的来说花的时间还是比较少的。修改后io线程一组,但独立受控,外部可对这组线程中的某一个直接发消息,基本满足了需求,现在要给每个io线程绑定私有数据并触发特定消息比之前简单多了,而且绝对无锁。

 

posted @ 2011-01-26 16:14 袁斌 阅读(2746) | 评论 (1)编辑 收藏

 

由于原先的appserver功能不断增多,最近又增了两个功能,需要不断从后端memcached中提取数据并进行计算,由于提取数据量大且频繁,导致效率很低,粗测了一下,获取数据和格式化等操作花了90%以上的时间,由此设想将memcached改写或重写一个支持memcached的服务器,将计算功能和memcached做到一起,让获取数据的路径最短,也就最大限度减少了数据传输和格式化等操作,就是类似存储过程一样啦,这部分可以考虑使用插件来实现,甚至可考虑使用脚本语言来实现。

网上搜了一下,果然发现早有人这么干了,正所谓英雄所见啊,呵呵。具体方法倒很多,自定义key命名,根据特殊keygetsetreplace上做特殊操作,或者根据命令中的flag等做特殊处理,或者扩充stat命令等,都是可以的,我们暂时就考虑修改特殊的键值做特殊处理。

要做一个完备的既支持ascii命令又支持binary命令的兼容memcached还是有一点点麻烦的,我暂时也没有太多需求,所以就仅支持了ascii命令,其实也是考虑支持ascii的客户端更多,各种语言的支持mc的客户端数不胜数,但大多只支持ascii命令。由于我之前为了测试服务器框架效率,做过一个支持ascii命令的memcached兼容版本,因此拿过来直接使用太方便了,这个版本的实现其实很容易,如果有一个较好的框架代码的话基本上在一天之内可做完,当然要做到很好可能需要多花一些时间,我现在做的也不是特好,要完全取代memcached使用还是有些差距,主要是一些过期机制等没完全实现,虽然速度上比标准mc版本还要快一点,呵呵,因为暂时的确是不需要这些过期机制,所以也没打算这个版本实现,其他功能基本上都有。

以后准备将这个memcached解码部分作为一个单独的解析器,和支持其他协议一样,换上这个解析那就支持mc协议了,还是很方便的,以后有空还是要做个支持binary协议的,以便可以更高效的解决问题。

想到server能支持Memcached协议真是好啊,客户端基本只要用个libmemcached就好了,多服务器分布,容错,多份数据啥的都有现成的解决方案,只要把server做稳定了就基本ok了,对咱这种小团队来说再合适不过了,节省了很多开发维护成本啊,现在内存这么便宜,部署几个点实在是很easy的问题。

 

 

posted @ 2011-01-23 17:13 袁斌 阅读(2039) | 评论 (1)编辑 收藏

 

关于内存数据库

 

最近要将一些数据放到内存里面做很高的并发操作,考虑了很多方案,

1、 简单点使用map hash_map等自己管理。

2、 sqlite内存表。

3、 fastdb内存数据库。

4、 ExtremeDbTimesTen等。

比较测试了一下123,发现还是自己实现速度最快,比fastdb模式快3-5倍,fastdb模式比sqlite内存表模式快10倍左右,由于自己实现不具有典型通用性,多线程下访问效率会下降,要管理多线程下各种更新查找等还是比较麻烦的,所以在13方案之间纠结。

为了使得决策更好一些,暂时还没做决定,顺便到万方等上面搜索了一些论文来看,看来看去看得真来气啊,虽然都叫内存数据库但各种实现的都有,有用gdbm来做的,有直接map管理的,有hash管理数据的,有t树管理的,有数组队列管理的,有的明显就是个不大变的东西还弄个啥事务的,靠,刚刚居然还看到一篇鸟文《电网监控系统实时数据库的设计与实现》里面的测试居然是1000条,插入时间80毫秒,真可笑啊,区区这么点数据也好意思测,还要花80毫秒,还自以为很快,这个速度至少可提高1000倍以上啊,这帮垃圾,写的啥鸟文章,研究个屁啊。

看完这十来篇论文,俺的思绪又回到1999年,当年我给别人优化过一个电信计费的软件(看的论文里面有好几篇讲电信计费的),当时有个朋友的朋友拿了个需求过来,7000万条记录,原来计算费单要花十几个小时吧,我帮他改了下,十来分钟就算完了,朋友很满意,当时的做法很简单,就是弄了个mmtable,大体就是跟map类似的东西吧,那个时候map还没流行起来,俺也不知道,所以就自己弄了个内存表,内部基本就是二分查找了,那个时候我对hash都不大熟悉,B树之类的算法刚接触也不会用,就这么个东西当时的电脑也只要花十来分钟,我估计就算是那个老程序放在现在的普通台式机上要不了几秒钟就可算完。也不知道这么几千万条记录的小需求怎么在这帮人眼里就成了什么海量数据,对俺来说跟玩似的,区区几千万嘛,不过是俺拿来测试用的。

去年中做了个md5 hash反查的东西,数据都是几百亿到几万亿的,后来的效果就是一个文件可存万亿记录,一次查询平均1.2IO,即使全放在SATA磁盘上也就十来毫秒而已。

区区几千万条记录咋就叫什么海量数据呢,海量个毛啊,内存都放得下的叫什么海量,现在服务器动不动都是几十G内存,区区千万根本算不上什么,查询定位都可到微妙了,1秒插入至少千万条了,居然还看到1000条插入的测试,真是不得不佩服国内这帮垃圾研究生的水平,也不知道这种论文咋就能通过审查,只能得出结论他们的老师也都是猪。

         骂归骂自己的问题还需要继续努力,对咱目前的需求来说自己管理数据,即使一个线程都搞得定,因为不过区区几个表,几十万条记录而已,不过这种10年前咱就会的技术还真是拿不出手,怎么的也得做得更好一点,呵呵,继续研究吧,多线程下内存数据库,从概念上看的确是个很有吸引力的东西,要是性能跟得上,其实在很多地方可以取代普通的数据结构用法了,可以大大减少编程难度,甚至我在想如果有个支持事务的内存数据库,之前设计的cad类软件的undo/redo都可以用事务来实现,完全可以抛弃先前设计的复杂结构,其实这种东西即使不用内存数据库就算是用个sqlite都完全能搞定,唉,往事不堪回首啊,看来数据库方面的确得多花功夫,特别是多线程和分布式模式下的内存数据库。

 

 

posted @ 2011-01-21 13:37 袁斌 阅读(8837) | 评论 (8)编辑 收藏

 

 

最近给自己换了个老板,忙了一段时间,所以有几个月没写博客,今后还是要争取多写啊,呵呵。

 

换来新地方,第一件大的事情就是修改后端架构和通信协议,架构也设计得很普通,因为这边的业务不需要太过复杂的后端,所以就简单设计了一下,基本是参照web的模型,符合我一贯的向web学习的思想,弄了个gate管理入口,相当于web下的webserver,后端其他服务器挂在该gate下,相当于web模型下的appserver,或者fastcgi模型的fastcgi进程,gate上管理连接、合法性检测、登录、加密、压缩、缓存。Gate和后端通信本来想参照fastcgi协议,但看了之后觉得fastcgi协议还是复杂了,所以就设计了一个更简单的协议,gate和后端server之间可传递key:value型数据对,value不局限于字符串,可以是任意数据,这样基本满足了当前的需求,第一版放上去之后也运行良好,到今天也基本持续稳定运行快一个月了,没出过什么事情。由于在gate这边缓冲了job管理,所以后端server升级很方便,随时可关闭更新,gate会在窗口时间内将未执行完成的任务重新提交,有此功能可放心大胆的升级后端,这个月这样的工作做了几次,在架构修改之前这样的事情几乎是不敢做的,因为一旦升级所有用户全部断开连接,而现在用户则基本无感觉。Gate上的缓存层为后端减少了一些压力,这个缓存是按照请求的md5key做的,并根据协议配置时效,有此cache后端大多数服务可不设计缓存或降低缓存设计的复杂度。Gate上针对敏感数据统一做了加密处理,主要是辛辛苦苦整理的数据不能轻易让竞争对手窃去了,呵呵。Gate也做了压缩,现在是针对>=128长度的包进行压缩,使用了qlz,压缩效率还是很不错的,速度很快。目前gate后端挂接的既有win上的server也有linux上的server,这是一开始就这么规划的,现在看来当初的目的达到了,混合发挥各自的优势,有的项目在原有系统上跑得好好的,没必要重新开发嘛。

 

协议设计上本来我是计划二进制混合json格式,以二进制为主,但尝试了一个协议之后发现,这边的小伙子们对直接操纵内存普遍技术不过关,他们大多是从java开始的,后来才学习c,对字符串用得很熟练,权衡之下采用了json为主,混合二进制为辅的方案,这样修改之后的协议和他们之前使用的xml类似,就是更小更紧凑一点,使用方法上很类似,从现在的效果看还行,使用json格式为主的协议当然不能跟使用pb之类的相比,解析效率上大约单线程每秒解析20来万10obj的对象,速度上不算太快但也不算太慢,对付一秒至多几万数据包的应用来说还是够的,因为现在cpu计算能力普遍过剩,使用json的另个好处就是增删字段很方便,各个版本之间不需要太考虑版本的问题,要是全用二进制格式就要麻烦很多了,在使用压缩之后,目前的json格式协议比之前的xml协议减少了2/3的带宽使用,总体效果还是可以的。使用json调试也很方便,我提供了一个工具,写后端的就直接用该工具按照json格式收发数据,无需等client开发好了再去做后端,之后做client也很方便,请求发过去之后返回来的就是标准的json格式数据,同样的解析方法,每个不同的应用就按照不同的格式处理下即可,和web等模块交互也很方便,这可算是额外的好处了。

 

总之,虽然json格式存储效率和解析效率跟二进制方式还差半个量级到一个量级,但合理使用还是可以的,特别是跟xml相比优势很明显,权衡使用吧,当然追求极致效率可能还是用pb之类的更合适一些,或者自己设计tlv格式。

 

posted @ 2011-01-11 13:33 袁斌 阅读(2507) | 评论 (3)编辑 收藏

昨天去见两个老乡,多年的朋友同学,也是搞技术的,大家都在上海,只是交流不是太多,聊起我做过的一些东西,他觉得不大相信,我说我写的远程控制程序全dll组成,所有模块可热升级,包括主模块,主模块小于20k,他似乎难于相信,我跟他说这个程序还是2001年做的,他就更难相信了。后来又说起我最近做的那个云计算的价格查询,他也很难相信底下是云计算,由于没带机器也没法给他看后台服务器,所以我估计他最后还是半信半疑吧。上周另一个朋友说他们老板有个项目十来个人做了3年,一直做不稳定,我说给我一段时间我肯定能把他整稳定,后来给他看了我之前做的一些东西,游戏等,似乎他还在怀疑我的能力,这几个其实都算是对我有些了解的朋友了,看来我还是宣传得太少啊。我知道大家都对出身大公司的人有种崇拜,我等一直在小公司混的人没什么人瞧得上,可是我又不能跟他们说哪年哪月,我到某个公司转了下,看了某某写的代码,你所崇拜的人不过如此,哪个工程里面写了个败笔 等等

可能我身边也就合作伙伴、曾经的老板、同事、敌人知道我到底什么水平,2000年的时候就带队做几十万行的项目,连续做了几个,为他们申请软件企业奠定基础。云计算的价格查询,3周完成,带2个客户端的棋牌游戏带了两个朋友一起半年完成,全部模块接口化,模块可单独升级。Netdongle,一个masterslave多重保护的网络验证系统,支持由控制端上传dlldb等,也就一个月完成。这些程序上线之后就几乎不用修改,一直稳定运行哦,一般的程序要做到第一版版本出去就几乎不出错是很难的,没有一定功力的人是做不到的。

昨天吹了下牛,我说windows下应用层的软件基本没有做不出来的,或许牛吹得有点大,那朋友惊讶了一下。

今年湿疹治好之后发现自己战斗力提升很多,之前做事情总觉得差一口气,精力不济,现在觉得精力充沛,酒也能喝了,活也能干了,速度也快了,也敢出去跟别人交流了,之前一直自卑,没病真好啊。

以后要多宣传,多吹牛啊!

posted @ 2010-10-03 14:26 袁斌 阅读(633) | 评论 (0)编辑 收藏

07年我写了一篇文章叫《我的网络模块设计》,姑且叫那个为第一版吧,由于持续对网络模块进行改进,所以现在的实现和当时有很大改变,加上上层应用越来越多,又经过了几年时间考验,现在的实现方式比之前的更灵活更有效率,也因为最近看了一些人做网络程序多年竟毫无建树,一直要用别人写的网络模块,所以有感而写此文,为了使得此文不受上一篇《我的网络模块设计》的影响,我决定写之前不看原来的文章,所以此文跟原文那篇文章可能没有太多相似性。
 一个基本的网络模块,无非就是管理N个连接,快速处理每个连接的收发数据、消息等,所谓好的网路模块,无非就是稳定、高效、灵活,下面分几部分来写:
 一、 连接管理
 之所以首先写连接管理,是因为连接管理是核心,也是最难的地方,我写第一个网络库之前,搜索过很多当时可以找到的例子工程,当时几乎找不到可稳定运行的工程,当然更找不到好的,于是摸索前进,期间对连接管理使用了各种方法,从最早一个cs(临界区CriticalSection,我简称cs),recv send都用这个cs,到后来send用一个cs,recv用一个cs,用多个的时候还出过错,最后使用一个cs+一个原子值ref管理一个连接,每个连接send的时候用cs,recv的时候用ref,如果该连接的消息要跨线程异步执行,也使用ref,如此较简单的解决了连接管理的问题。
 同样使用生存期管理方法,也有人用智能指针,虽然原理和我直接操纵生存期一样,但实现方法毕竟不同,不过我为了让实现依赖少一些没有引入智能指针。
 当然我后来也发现很多人不是用这种方法,如有些人就id来管理连接,每个连接分个id,其他操作全部用id,每次对连接的调用先翻译一下,如果id找得到映射目标就调用,否则就说明该连接不存在了,这种方法简单只是不直接,多了个查找过程,另外查找的时候可能还需要全局锁(这依赖于连接数据组织)。
 也有人使用一个线程管理连接,其他所有与该连接有关的生存期问题全部到该线程处理,这样也是可行的,只是需要做一个较好的包装,如果包装好上层调用方便,如果包装不好,可能上层调用就有一些约束。
 虽然各种方法都有人使用,但我一直选择直接的生存期管理方法,其实内部实现的时候还是有很多优化措施的,减少了大量addref、release的调用,进一步提高了效率。
 二、 线程组
 我最初做网络库的时候还不是很清楚上层如何使用这个库,后来在上面做了几个应用之后慢慢有了更多想法,最近的网络库是设计了这么几组线程:io线程组、同步线程组、异步线程组、时钟线程组、log线程组,每组线程都可开可关,就算io线程组也是可关的,这只是为了整个库更灵活适用性更广泛,如只用同步线程组或异步线程组仅将这个线程组当一个消息队列使用。
 Io线程组就是处理io收发的,listen recv send 以及解密解压缩都是在这组线程,一般这组线程会开2个或2*cpu个。
 同步线程组,一般这组线程开1个,用来处理logic。
 异步线程组,这组线程根据需要开0个或n个,简单应用无db等慢速操作的应用不开,有很多db等慢速操作的可以开很多个。
 时钟线程组,一般不开或开1个。
 Log线程组,一般开1个,主要为了避免其他线程调用WriteLog的时候被磁盘io阻塞,所以弄了一个log线程。
 其实还有一个主线程,我的每组线程(包括主线程)都支持事件和定时器,io线程、同步线程、异步线程组、时钟线程组、甚至log线程组都支持事件和定时器,到去年我还只是让每组线程都支持事件,今年为了更好的使用时钟我给每组线程设计了定时器,现在定时器线程组有点鸡肋的味道,一般是用不上专门的定时器线程组,不过我还没有将它删掉,主要在我的设计里面,它和同步异步线程组一样,都只是一组线程,如果必要的时候可以将它用作同步线程或者异步线程组,所以继续保留了它的存在。
 这几组线程之间都是可互发消息的,所以一个逻辑要异步到别的线程执行是非常方便的,只要调用一下PostXXEvent(TlsInfo *ptls, DWORD dwEvent, WPARAM wParam, LPARAM lParam);我凭借这个设计使得这套网络库几乎可以适用上层各种应用,不管是非常简单的网络应用还是复杂的,一框打尽。对最简单的,一个io线程搞定,其他线程全关,对于复杂的io线程+同步+异步+log全开。
 三、 内存池
 内存池其实没有想象中的那么神秘,当然如果要让一个网络程序持续7*24小时稳定高效运行,内存池几乎必不可少的,内存池的作用首先是减少内存碎片,其次是为了提高速度,我想这两点很容易想明白的,关于内存池我之前写了系列文章,可参考我的博客:
 
《内存池之引言》 http://blog.csdn.net/oldworm/archive/2010/02/04/5288985.aspx
 《单线程内存池》 http://blog.csdn.net/oldworm/archive/2010/02/04/5289003.aspx
 《多线程内存池》 http://blog.csdn.net/oldworm/archive/2010/02/04/5289006.aspx
 《dlmalloc、nedmalloc》 http://blog.csdn.net/oldworm/archive/2010/02/04/5289010.aspx
 《线程关联内存池》 http://blog.csdn.net/oldworm/archive/2010/02/04/5289015.aspx
 《线程关联内存池再提速》 http://blog.csdn.net/oldworm/archive/2010/02/04/5289018.aspx
 
四、 定时器
 关于定时器,上面讲线程组的时候已经讲过,我现在的设计是每个线程(包括主线程)都支持定时器,调用方法都是一样的,回调函数形式也是一样的,由于定时器放到各组线程里面,所以减少了线程之间的切换,提高了效率。
 关于定时器,可参考《定时器模块改造》 http://blog.csdn.net/oldworm/archive/2010/09/11/5877425.aspx
 
五、 包格式
 关于包格式可参考《常用cs程序自定义数据包描述》 http://blog.csdn.net/oldworm/archive/2010/03/24/5413013.aspx
 
六、 Buffer
 之前的文章其实我一直没有提过我的buffer,其实我的buffer设计是很灵活的,现在它和pool也是有些关联的,我的poolset其实底下就是按照各种不同大小的buffer预设的尺寸。Buffer我设计为循环式,不允许回绕,包含
 Char *pbase 块基址
 Char *pread 当前读指针
 Char *pwrite 当前写指针
 DWORD tag;
 Buffer *next;
 Capacity 总分配尺寸,上面分配的时候可能只是指定了19,但实际可能分配的是32个字节,所以内部用的时候要根据capacity来最大限度的利用缓冲区。
 Buffer分配还利用了一个技巧,事实上分配的时候是一次分配一个需要的大缓冲,前面为Buffer自身的数据,后面为数据部分,pbase指向数据部分,这样处理减少了一次分配,我估计很多人都在用这个技巧。
 Pwrite总是不会小于pread的,但pread可能和pbase不一样,仅当后面空余空间不够用的时候才可能会移动数据,否则数据不会移动。
 WSARecv的时候我是这么处理的,如果首次获取了一个包的一部分,但buffer中还有足够的空间放下包的剩余部分,我不会再分配一个buffer去recv,而是直接用原buffer指定一个合适的偏移和size去WSARecv,这样可以最大限度的减少复制。
 刚才还有朋友问到我recv的层次组织,我的网络库里面是这样组织的,OnRecv是个虚函数,最基础的IocpClient的OnRecv只处理数据而不解析格式,IocpClientMsg就会认识默认的一种包格式,这个类的OnRecv会将m_recvbuf中的数据组织为msg,并尽可能的一次返回更多个msg,回调OnMsg函数,由上层决定该消息在哪个线程处理,这样我认为是最灵活的,如果是个很小的server,可能直接就在io线程里面处理了,也可postevent到同步线程处理,亦可PostEvent到异步线程处理。
 
七、 TLSINFO
 TlsInfo顾名思义就是每个线程关联的一组数据,暂时我还没有看到别人这么设计,也许我设计得有些复杂了,在这个数据里面有一些常用的和该线程相关的数据,如该线程的分配基、步长,用这两个参数可让每个线程制造出唯一序列,还有常用pool的地址,如tm_pool *p1k; tm_pool *p2k;… 这样设计使得要分配的时候直接取tm_pool,最大限度的发挥了分配速度,还有一些常规参量long c; long d; DWORD a; DWORD b;… 这几个值可理解为栈内值,其实为了减少上层调用复杂度的,如我将一个连接的包从io线程PostEvent到同步线程处理,PostEvent首参数就是tlsinfo,PostEvent会根据tlsinfo里面的一个内部值决定是不是要调用addref,因为我有个地方预增了2,所以大多数情况下在io发到其他线程的时候是无需调用addref的,提高了效率,tlsinfo里的其他一些值上层应用可使用,用在逻辑处理等情况下。
 
八、 性能分析
 *nix下有很多知名的网络库,但在win下特别是使用iocp的库里面,一直就没有一个能作为基准的库,即使asio也因为出来太晚不为大多数人熟悉而不能成为基准库,libevent接iocp由于采用0 buffer模拟所以也没有发挥出足够的性能,对比spserver我比它快70%左右,我总在想要是微软能将他那个iocp的例子写得更好一点就好了,至少学的人有一个更高一点的基础,而不至于让http://www.codeproject.com/KB/IP/iocp_server_client.aspx这样的垃圾代码都能成为很多人的样板。
 
九、 杂谈
 为了写好一个win下稳定高效的网络库,我07年的时候几乎搜遍了那个时间段之前所有能找到的iocp例子,还包括通过朋友等途径看到的如snda等网络库,可惜真没找到好的,大多数例子是只要多线程发起几千个连接不断发送数据马上就死了,偶尔几个不死的(包括snda的)只要随机连接并断开就会产生句柄泄漏,关闭所有连接之后句柄并不关闭等,也就是说这些例子连基本的生存期管理都没搞定,能通过生存期管理并且不死的只有有限的几个,可惜性能又太差,杯具啊。
 早年写网络库的时候也加入了sodme在google上建的那个群,当时群还是很热闹的,可惜大多数人都是摸索,所以很多问题只是讨论却从无定论,没有谁能说服别人,也没有人可轻易被说服,要是现在或许有一些很有经验的人,可惜那个群由于GFW现在虽能访问也不大活跃了。
 最近看到有些写网络程序7年甚至更久的人还在用libevent、ace等感想很复杂,可悲的是那些人还没意识到用一个库和写一个库有多大的区别,可能那些人一辈子也认识不到写一个库比用一个库难多少,那些人以为这些库基本会用了,让他自己去写也基本是照这个模式,不会有什么突破,就无需自己动手了,悲哀啊。当然,要写一个稳定的网络库需要耗费很多时间,特别是要写一个能和知名库性能接近或更好的库,更是要费神费力,没点耐心和持久力是不可能做好的。在中文领域随便查什么稍有些名气的代码,总是能找到很多剖析类文章,可原创的东西总是很少,也不知道那些大侠怎么搞的,什么都能剖析可怎么总写不出什么像样的东西呢。
 其实本来没有打算写这篇文章,可能是看了陈硕的muduo才使得我有了写出来的冲动,大概是受到他的开源鼓励吧。
 谨以此文记录本人最近3年对网络模块的修改并简短总结。

 

posted @ 2010-10-03 14:25 袁斌 阅读(3227) | 评论 (5)编辑 收藏

我的IOCP网络模块设计

 

为了设计一个稳定易用高效的iocp网络模块,我前前后后花了好几个月的时间,也曾阅读过网上很多资料和代码,但是非常遗憾,能找到的资料一般都说得很含糊,很少有具体的,能找到的代码离真正能商用的网络模块差得太远,大多只是演示一下最基本的功能,而且大多是有很多问题的,主要问题如下:

1、 很多代码没有处理一次仅发送成功部分数据的情况。

2、 几乎没有找到能正确管理所有资源的代码。

3、 大多没有采用用pool,有的甚至画蛇添足用什么map查找对应客户端,没有充分使用perhandle, perio

4、 接收发送数据大多拷贝太多次数。

5、 接收管理大多很低效,没有充分发挥iocp能力。

6、 几乎都没有涉及上层如何处理逻辑,也没有提供相应解决方案(如合并io线程处理或单独逻辑线程)。

7、 大多没有分离流数据和包数据。

问题还有很多,就不一一列出来了,有一定设计经验的人应该有同感。要真正解决这些问题也不是那么容易的,特别是在win下用iocp的时候资源释放是个麻烦的问题,我在资源管理上花了很多时间,起初也犯了很多错误,后来在减少同步对象上又花了不少时间(起初client用了两个同步对象,后来减少为1个)。下面我就我所设计的网络模块的各个部分进行简单的讲解

一、内存管理。

内存管理是采用池模式,设计了一个基础池类,可以管理某固定大小的池

class CBufferPool

{

       

        void *newobj();

        void delobj(void *pbuf);

       

};

在基础池类上提供了一个模板的对象池

template <class T>

class CObjPool : public CBufferPool

{

public:

        T *newobj()

        {

                void *p = CBufferPool::newobj();

                T *pt = new(p) T;

                return pt;

        }

        void delobj(T* pt)

        {

                pt->~T();

                CBufferPool::delobj(pt);

        }

};

 

在基础池的基础上定义了一个简单的通用池

class CMemoryPool

{

private:

        CBufferPool bp[N];

};

通用池是由N个不同大小的基础池组成的,分配的时候圆整到合适的相近基础池并由基础池分配。

最后还提供了一个内存分配适配器类,从该类派生的类都支持内存池分配。

class t_alloc

{

public:

        static void *operator new(size_t size)

        {

                return CMemoryPool::instance().newobj(size);

        }

        static void operator delete(void *p, size_t size)

        {

                CMemoryPool::instance().delobj(p, size);

        }

};

根据测试CMempool分配速度比CObjpool<>稍微慢一点点,所以我在用的时候就直接用t_alloc类派生,而不是用对象池,这是个风格问题,也许有很多人喜欢用更高效一点的objpool方式,但这个并不大碍。

 

在网络模块中OVERLAPPED派生类就要用池进行分配,还有CIocpClient也要用池分配,再就是CBlockBuffer也是从池分配的。

如下定义:

struct IOCP_ACCEPTDATA : public IOCP_RECVDATA, public t_alloc

class CIocpClient : public t_alloc

 

二、数据缓冲区。

数据缓冲区CBlockBuffer为环形,大小不固定,随便分配多少,主要有以下几个元素:

Char *pbase;           //环形首部

Char *pread;           //当前读指针

Char *pwrite;          //当前写指针

Int nCapacity;         //缓冲区大小

Long nRef;              //关联计数器

用这种形式管理缓冲区有很多好处,发送数据的时候如果只发送了部分数据只要修改pread指针即可,不用移动数据,接收数据并处理的时候如果只处理了部分数据也只要修改pread指针即可,有新数据到达后直接写到pwrite并修改pwrite指针,不用多次拷贝数据。nRef关联计数还可处理一个包发给N个人的问题,如果要给N个人发送相同的包,只要分配一个缓冲区,并设置nRefN就可以不用复制N份。

 

三、收发缓冲区管理

发送缓冲区

我把CIocpClient的发送数据设计为一个CBlockBuffer 的队列,如果队列内有多个则WSASend的时候一次发送多个,如果只有一个则仅发送一个,CIocpClient发送函数提供了两个,分别是:

Bool SendData(char *pdata, int len);

Bool SendData(CBlockBuffer *pbuffer);

第一个函数会检测发送链的最后一个数据块能否容纳发送数据,如果能复制到最后一个块,如果不能则分配一个CBlockBuffer挂到发送链最后面,当然这个里面要处理同步。

 

接收缓冲区

接收管理是比较简单的,只有一个CBlockBufferWSARecv的时候直接指向CBlockBuffer->pwrite,所以如果块大小合适的话基本上是不用拼包的,如果一次没有收到一个完整的数据包,并且块还有足够空间容纳剩余空间,那么再提交一个WSARecv让起始缓冲指向CBlockBuffer->pwrite如此则收到一个完整数据包的过程都不用重新拼包,收到一个完整数据包之后可以调用虚函数让上层进行处理。

IocpClient层其实是不支持数据包的,在这个层次只有流的概念,这个后面会专门讲解。

 

四、IocpServer的接入部分管理

我把IocpServer设计为可以支持打开多个监听端口,对每个监听端口接入用户后调用IocpServer的虚函数分配客户端:

        virtual CIocpClient *CreateNewClient(int nServerPort)

分配客户端之后会调用IocpClient的函数 virtual void OnInitialize();分配内部接收和发送缓冲区,这样就可以根据来自不同监听端口的客户端分配不同的缓冲区和其他资源。

 

Accept其实是个可以有很多选择的,最简单的做法可以用一个线程+accept,当然这个不是高效的,也可以采用多个线程的领导者-追随者模式+accept实现,还可以是一个线程+WSAAccept,或者多个线程的领导者-追随者模式+WSAAccept模式,也可以采用AcceptEx模式,我是采用AcceptEx模式做的,做法是有接入后投递一个AcceptEx,接入后重复利用此OVERLAPPED再投递,这样即使管理大量连接也只有起初的几十个连接会分配 OVERLAPPED后面的都是重复利用前面分配的结构,不会导致再度分配。

 

IocpServer还提供了一个虚函数

        virtual bool CanAccept(const char *pip, int port){return true;}

来管理是否接入某个ip:port 的连接,如果不接入直接会关闭该连接并重复利用此前分配的WSASocket

 

五、资源管理

Iocp网络模块最难的就是这个了,什么时候客户端关闭或服务器主动关闭某个连接并收回资源,这是最难处理的问题,我尝试了几种做法,最后是采用计数器管理模式,具体做法是这样的:

CIocpClient2个计数变量

        volatile long m_nSending;              //是否正发送中

        volatile long m_nRef;                     //发送接收关联字

m_nSending表示是否有数据已WSASend中没有返回

m_nRef表示WSASendWSARecv有效调用未返回和

在合适的位置调用

        inline void AddRef(const char *psource);

        inline void Release(const char *psource);

增引用计数和释放引用计数

        if(InterlockedDecrement(&m_nRef)<=0)

        {

                //glog.print("iocpclient %p Release %s ref %d\r\n", this, psource, m_nRef);

                m_server->DelClient(this);

        }

当引用计数减少到0的时候删除客户端(其实是将内存返回给内存池)。

 

六、锁使用

锁的使用至关重要,多了效率低下,少了不能解决问题,用多少个锁在什么粒度上用锁也是这个模块的关键所在。

IocpClient有一个锁      DECLARE_SIGNEDLOCK_NAME(send);        //发送同步锁

这个锁是用来控制发送数据链管理的,该锁和前面提到的volatile long m_nSending;共同配合管理发送数据链。

可能有人会说recv怎么没有锁同步,是的,recv的确没有锁,recv不用锁是为了最大限度提高效率,如果和发送共一个锁则很多问题可以简化,但没有充分发挥iocp的效率。Recv接收数据后就调用OnReceive虚函数进行处理。可以直接io线程内部处理,也可以提交到某个队列由独立的逻辑线程处理。具体如何使用完全由使用者决定,底层不做任何限制。

 

七、服务器定时器管理

服务器定义了如下定时器函数,利用系统提供的时钟队列进行管理。

        bool AddTimer(int uniqueid, DWORD dueTime, DWORD period, ULONG nflags=WT_EXECUTEINTIMERTHREAD);

        bool ChangeTimer(int uniqueid, DWORD dueTime, DWORD period);

        bool DelTimer(int uniqueid);

        //获取Timers数量

        int GetTimerCount() const;

        TimerIterator GetFirstTimerIterator();

        TimerNode *GetNextTimer(TimerIterator &it);

        bool IsValidTimer(TimerIterator it)

设计思路是给每个定时器分配一个独立的id,根据id可修改定时器的首次触发时间和后续每次触发时间,可根据id删除定时器,也可遍历定时器。定时器时间单位为毫秒。

 

八、模块类结构

模块中最重要的就是两个类CIocpClientCIocpServer,其他有几个类从这两个类派生,图示如下:

 

图表 1

 

图表 2

 

CIocpClient是完全流式的,没有包概念。CIocpMsgClientCIocpClient派生,内部支持包概念:

class CIocpMsgClient : public CIocpClient

{

        virtual void OnDataTooLong(){};

        virtual void OnMsg(PKHEAD *ph){};

 

        bool SendMsg(WORD mtype, WORD stype, const char *pdata, int length);

};

 

template <class TYPE>

class CIocpMsgClientT : public CIocpMsgClient

{

        void AddMsg(DWORD id, CBFN pfn);

        BOOL DelMsg(DWORD id);

};

CIocpMsgClientT模板类支持内嵌入式定义,如在

CMyDoc中可这样定义

CIocpMsgClientT<CMyDoc> client;

后面可以调用client.AddMsg(UMSG_LOGIN, OnLogin);关联一个类成员函数作为消息处理函数,使用很方便。

 

CIocpServerT定义很简单,从CIocpServer派生,重载了CreateNewClient函数

template <class TClient>

class CIocpServerT : public CIocpServer

{

public:

        //如果CIocpClient派生了则也需要重载下面的函数,这里可以根据nServerPort分配不同的CIocpClient派生类

        virtual CIocpClient *CreateNewClient(int nServerPort)

        {

                CIocpClient *pclient = new TClient;

                return pclient;

        }

};

 

八、应用举例

 

class CMyClient : public CIocpMsgClient

{

public:

        CMyClient() : CIocpMsgClient()

        {

        }

        virtual ~CMyClient()

{

}

        virtual void OnConnect()

{

Printf(“用户连接%s:%d连接到服务器\r\n”, GetPeerAddr().ip(),GetPeerAddr().port());

}

        virtual void OnClose()

{

Printf(“用户%s:%d关闭连接\r\n”, GetPeerAddr().ip(),GetPeerAddr().port());

}

        virtual void OnMsg(PKHEAD *phead)

{

        SendData((const char *)phead, phead->len+PKHEADLEN);

}

        virtual void OnSend(DWORD dwbyte)

{

Printf(“成功发送%d个字符\r\n”, dwbyte);

}

        virtual void OnInitialize()

{

        m_sendbuf = newbuf(1024);

        m_recvbuf = newbuf(4096);

}

 

        friend class CMyServer;

 

};

 

class CMyServer : public CIocpServer

{

public:

        CMyServer() : CIocpServer

        {

        }

 

        virtual void OnConnect(CIocpClient *pclient)

{

                printf("%p : %d 远端用户%s:%d连接到本服务器.\r\n", pclient, pclient->m_socket,

                        pclient->GetPeerAddr().ip(), pclient->GetPeerAddr().port());

}

        virtual void OnClose(CIocpClient *pclient)

{

        printf("%p : %d 远端用户%s:%d退出.\r\n", pclient, pclient->m_socket,

                        pclient->GetPeerAddr().ip(), pclient->GetPeerAddr().port());

}

        virtual void OnTimer(int uniqueid)

{

If(uniqueid == 10)

{

}

Else if(uniqueid == 60)

{

}

}

//这里可以根据nServerPort分配不同的CIocpClient派生类

        virtual CIocpClient *CreateNewClient(int nServerPort)

        {

        //      If(nServerPort == ?)

//             

                CIocpClient *pclient = new CMyClient;

                return pclient;

        }

 

};

 

Int main(int argc, char *argv[])

{

        CMyServer server;

 

        server.AddTimer(60, 10000, 60000);

        server.AddTimer(10, 10000, 60000);

 

        //第二个参数为0表示使用默认cpu*2io线程,>0表示使用该数目的io线程。

//第三个参数为0表示使用默认cpu*4个逻辑线程,如果为-1表示不使用逻辑线程,逻辑在io线程内计算。>0则表示使用该数目的逻辑线程

        server.StartServer("1000;2000;4000", 0, 0);

}

 

从示例可看出,对使用该网络模块的人来说非常简单,只要派生两个类,集中精力处理消息函数即可,其他内容内部全部包装了。

 

九、后记

我研究iocp大概在2005年初,前一个版本的网络模块是用多线程+异步事件来做的,iocp网络模块基本成型在2005年中,后来又持续进行了一些改进,2005底进入稳定期,2006年又做了一些大的改动,后来又持续进行了一些小的改进,目前该模块作为服务程序框架已经在很多项目中稳定运行了1年半左右的时间。在此感谢大宝、Chost ChengSunway等众多网友,是你们的讨论给了我灵感和持续改进的动力,也是你们的讨论给了我把这些写出来的决心。若此文能给后来者们一点点启示我将甚感欣慰,若有错误欢迎批评指正。

 

oldworm

oldworm@21cn.com

2007.9.24

posted @ 2010-10-03 14:25 袁斌 阅读(893) | 评论 (0)编辑 收藏

It行业从业十几年,虽然接触的人并不多,但算上网络上有点交往的人,也不算太少,阅读过无数代码,很容易得出这样的感悟,国内it行业能说的人太多,能做的人太少,能说大意就是能说会道,说起来头头是道,从架构到体系到模块到接口都能说得很专业,但实施起来就不行,不但架构做不好,接口定义不清,就连小小模块也未见得可搞定,这样的人实在是太多,之前一直以为一个项目组多几个人总能加快点速度,现在终于明白,一个项目组加几个不合适的人不但不能加速反而要降速,甚至直接导致项目开发失控、失败。关键模块如果让一个不称职的人去负责,最终该模块可能需要耗费核心人员更多时间去修改,甚至要重写,轻则导致项目延时,重则导致项目失败。

实施一个成功的it项目(只说技术不说市场),概括起来就是一句话:找合适的人做合适的项目。说起来容易做起来难啊,每个人都有他的领域,如果找来一个擅长a领域的让他做b领域的项目也未必做得好,虽然有的人学习能力超强,但总归是需要时间积累经验的。见过听说过很多开发失败的例子,莫不如此,曾帮人家优化一个电信计费项目,原实施的人只会用数据库,所有的计算全用数据库实现,速度比其对手慢一个数量级以上,将计算需要的数据预装入内存,之后全在内存里面查找计算,速度一下提高了上百倍,修改后速度领先其对手好几倍,其实这个修改很容易,只要几天时间就搞定了,还包括熟悉他们的数据及规则。 还帮别人看过一个棋牌的项目,原项目组十几个人搞了1年多,整了40多万行代码,结果bug不断,一直不能稳定运行,项目组无人能搞得定,我看了之后下的结论是重写,他们傻眼了,还以为改一个项目的时间肯定要少,毕竟写了那么多代码,他们那些外行哪里知道,修改一个漏洞无数的工程哪有重写快啊,这是典型的找了一群不恰当的人做了一个不恰当的项目,几百万投入打了水漂,要是让一个有能力的人设计把关,他那棋牌项目100万足够开发得出来了。

刚毕业那会做项目的时候带过几个水平较差的手下(都是俺领导招的人),他们最常说的话是“优化”,我把某某地方“优化”了一下,呵呵,外行要听到这个还以为真的是优化呢,其实很多时候他只是改写了一下,是不是算优化还值得商量,大多数时候都算不上优化,有的时候还改错引入了更多bug。对高手来说更喜欢说重构,我把某某模块重新设计了一下,以前模块有哪些缺陷,重新设计一下之后这些缺陷就不存在了,还有某某好处,等等,高手着眼于大局,低手只能看到一个小角落,高手优化关键之处,低手优化无关紧要之处。看豆瓣网技术发展,就是一个不断重构的过程,看qq98年开始到现在的蜂鸟内核,大的重构就3次了,完全重写3次了。最近迅雷终于官方承认v6开发失败,我之前在群里表达过我的观点,我估计他们v6是开发失败了,所以一直以v5顶着,现在等到v7出来终于承认了,这种自诩市值超100亿美刀的公司这么长时间的开发还能失败,就不要说小公司小团队了,见光死的项目很多,未见光死的项目更多。

国内真正懂得开发的老板很少,大多数老板觉得一个人开5w 10w的月薪太高了,这个待遇找1w的能找好几个呢,其实他们不知道,it行业一个重要的人能顶100个普通的人,甚至不可比较,因为一个普通的人去做一个项目可能根本不能完成,成功为010这个比值是无穷大啊,可惜等老板们失败了几个项目之后才能悟出这个道理,it项目做得好和做不好的差别不是差一点的问题,而是10的问题。俗人总是忙忙碌碌,天天维护自己前一天制造的bug,看起来很敬业,高手总是懒懒散散,因为100天后可能出现的问题都已经了然于胸,于是整天看起来无所事事时,不懂的老板很可能青睐于前者而打压后者。也正因为能说的人太多,懂行的老板又太少,所以使得整个行业充斥着浮躁和急功近利,很多关键职位其实只是个鹦鹉在顶着,有能力的人被压制,悲哀啊。千里马难找,伯乐更难找啊。

posted @ 2010-10-03 14:24 袁斌 阅读(810) | 评论 (2)编辑 收藏

仅列出标题
共4页: 1 2 3 4