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 on 2010-10-03 14:25 袁斌 阅读(3213) 评论(5)  编辑 收藏 引用 所属分类: win32游戏开发

Feedback

# re: 我的网络模块设计第二版  回复  更多评论   

2010-10-03 14:54 by true
杂谈中的几句似乎有些言重了,技术以实用为本,应该允许百家争鸣,很多时候使用libevent,ace是因为他们在网络库开发方面,已经或多或少的成为了标准,容易为大家接受,况且在整个系统架构方面,网络库本身已经越来越不重要了。

# re: 我的网络模块设计第二版  回复  更多评论   

2010-10-03 23:10 by 袁斌
@true
谢谢批评,虚心接受。

# re: 我的网络模块设计第二版  回复  更多评论   

2010-10-04 09:28 by cppexplore
顶贴支持!

# re: 我的网络模块设计第二版  回复  更多评论   

2010-10-05 20:30 by 饭中淹
不错,深有同感。
不过自己做库,也有个很严重的问题,要想突破自己,也是比较困难的。
我自己维护了一个类似STL的库,一个网络库,还有很多杂七杂八的东西。很多次重构之后,很多架构依然还存在着,只是不断的修修补补。有时候想推翻重来,却总是因为各种原因而放弃或者失败了。
可能做项目的时候,不适合去做库的推翻重来。
不过有时候做项目时,偶尔会来一些灵感,突然获得一个能够推翻之前库里的东西的想法,但是却迟迟无法更新到库里面。因为心里在害怕,没有大量测试的代码,会导致库的不稳定。

# re: 我的网络模块设计第二版  回复  更多评论   

2010-10-10 20:05 by Avlee
@true
未见得,特别是ACE,大部分时候是不错的,就是有时候(偶尔)会造成系统崩溃,几乎不留下任何痕迹,很难查找原因。

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理