网络模型(2)

    技术2025-09-30  33

    文章出处:http://www.cppblog.com/CppExplore/archive/2008/03/21/45061.html

    作者: CppExplore

     

    本章主要列举服务器程序的各种网络模型,示例程序以及性能对比后面再写。

    一、分类依据。服务器的网络模型分类主要依据以下几点(1)是否阻塞方式处理请求,是否多路复用,使用哪种多路复用函数(2)是否多线程,多线程间如何组织(3)是否多进程,多进程的切入点一般都是accept函数前

    二、分类。首先根据是否多路复用分为三大类:

    (1)阻塞式模型(2)多路复用模型(3)实时信号模型

     

    三、详细分类。

    1、阻塞式模型根据是否多线程分四类:

    (1)  单线程处理。

        实现可以参见http://www.cppblog.com/CppExplore/archive/2008/03/14/44509.html后面的示例代码。(2)  一个请求一个线程。    主线程阻塞在accept处,新连接到来,实时生成线程处理新连接。受限于进程的线程数,以及实时创建线程的开销过多线程后上下文切换的开销该模型也就是有学习上的价值

    (3)  预派生一定数量线程,并且所有线程阻塞在accept处

        该模型与下面的(4)类似与线程的领导者 / 追随者模型。

        传统的看法认为多进程(linux上线程仍然是进程方式)同时阻塞在accept处,当新连接到来时会有“惊群”现象发生,即所有都被激活,之后有一个获取连接描述符返回,其它再次转为睡眠。linux从2.2.9版本开始就不再存在这个问题,只会有一个被激活,其它平台依旧可能有这个问题,甚至是不支持所有进程直接在accept阻塞。(4)  预派生一定数量线程,并且所有线程阻塞在accept前的线程锁处。

        一次只有一个线程能阻塞在accept处。避免不支持所有线程阻塞在accept,并避免惊群问题。特别是当前linux2.6的线程库下,模型(3)没有存在的价值了。另有文件锁方式,不具有通用性,并且效率也不高,不再单独列举。

    (5)  主线程处理accept,预派生多个线程 ( 线程池 ) 处理连接。

        类似与线程的半同步 / 半异步模型。

        主线程的accept返回后,将clientfd放入预派生线程的线程消息队列,线程池读取线程消息队列处理clientfd。主线程只处理accept,可以快速返回继续调用accept,可以避免连接爆发情况的拒绝连接问题,另加大线程消息队列的长度,可以有效减少线程消息队列处的系统调用次数。

    (6) 预派生多线程阻塞在accept处,每个线程又有预派生线程专门处理连接。    (3) 和(4) / (5) 的复合体。

     

        经测试,(5) 中的accept线程处理能力非常强,远远大于业务线程,并发10000的连接数也毫无影响,因此该模型没有实际意义 (?)

     总结: 就前五模型而言,性能最好的是模型(5)。 模型(3) / (4) 可以一定程度上改善模型(1)的处理性能,处理爆发繁忙的连接,仍然不理想

         阻塞式模型应为读的阻塞性,容易受到攻击,一个死链接(建立连接但是不发送数据的连接)就可以导致业务线程死掉。因此内部服务器的交互可以采用这类模型,对外的服务器不合适。优先(5)(?),然后是(4),然后是(1),其它不考虑

     

    2、多路复用模型根据多路复用点、是否多线程分类:

        以下各个模型依据选用 select / poll / epoll 又细分为3类。下面个别术语采用select中的,仅为说明。

    (1)  accept函数在多路复用函数之前,主线程在accpt处阻塞,多个从线程在多路复用函数处阻塞。主线程和从线程通过管道通讯,主线程通过管道依次将连接的clientfd写入对应从线程管道,从线程把管道的读端pipefd作为fd_set的第一个描述符,如pipefd可读,则读数据,根据预定义格式分解出clientfd放入fd_set,如果clientfd可读,则read之后处理业务。    此方法可以避免select的fd_set上限限制,具体机器上select可以支持多少个描述符,可以通过打印sizeof(fd_set)查看,我机器上是512字节,则支持512×8=4096个(注:一个描述符占1bit)。为了支持多余4096的连接数,此模型下就可以创建多个从线程分别多路复用,主线程accept后平均放入(顺序循环)各个线程的管道中。创建5个从线程以其对应管道,就可以支持2w的连接,足够了。另一方面相对与单线程的select,单一连接可读的时候,还可以减少循环扫描fd_set的次数。单线程下要扫描所有fd_set(如果再最后),该模型下,只需要扫描所在线程的fd_set就可。(2)  accept函数在多路复用函数之前,与(1)的差别在于,主线程不直接与从线程通过管道通讯,而是将获取的fd放入另一缓存线程的线程消息队列,缓存线程读消息队列,然后通过管道与从线程通讯目的在主线程中减少系统调用,加快accept的处理,避免连接爆发情况下的拒绝连接

    (3)  多路复用函数在accept之前多路复用函数返回,如果可读的是serverfd,则accept,其他则read,后处理业务,这是多路复用通用的模型,也是经典的reactor模型

    (4)  连接在单线程中处理。

        以上(1)(2)(3)都可以在检测到cliendfd可读的时候,把描述符写入另一线程(也可以是线程池)的线程消息队列,另一线程(或线程池)负责read,后处理业务。

    (5)  业务线程独立,下面的网络层读取结束后通知业务线程。

        以上(1)(2)(3)(4)中都可以将业务线程(可以是线程池)独立,事先告之(1)、(2)、(3)、(4)中read所在线程(上面1、2、4都可以是线程池),需要读取的字符串结束标志或者需要读取的字符串个数,读取结束,则将clientfd/buffer指针放入业务线程的线程消息队列,业务线程读取消息队列处理业务。这也就是经典的proactor模拟

    总结: 模型(1)是拓展select处理能力不错选择;模型(2)是模型(1)在爆发连接下的调整版本;模型(3)是经典的reactor,epoll在该模型下性能就已经很好,而select/poll仍然存在爆发连接的拒绝连接情况模型(4)(5)则是方便业务处理,对模型(3)进行多线程调整的版本带有复杂业务处理的情况下推荐模型(5) 。根据测试显示,使用epoll的时候,模型(1)(2)相对(3)没有明显的性能优势,(1)由于主线程两次的系统调用,反而性能下降

    3、实时信号模型:

        使用fcntl的F_SETSIG操作,把描述符可读的信号由不可靠的SIGIO(SYSTEM V)或者SIGPOLL(BSD)换成可靠信号。即可成为替代多路复用的方式。优于select/poll,特别是在大量死连接存在的情况下,但不及epoll

     

    四、多进程的参与的方式

    (1)fork模型。fork后所有进程直接在accept阻塞。以上主线程在accept阻塞的都可以在accept前fork为多进程。同样面临惊群问题。(2)fork模型。fork后所有进程阻塞在accept前的线程锁处。同线程中一样避免不支持所有进程直接阻塞在accept或者惊群问题,所有进程阻塞在共享内存上实现的线程互斥锁。(3)业务和网络层分离为不同进程模型。这个模型可能是受unix简单哲学的影响,一个进程完成一件事情,复杂的事情通过多个进程结合管道完成。我见过进程方式的商业协议栈实现。自己暂时还没有写该模型的示例程序测试对比性能。(4)均衡负载模型。起多个进程绑定到不同的服务端口,前端部署lvs等均衡负载系统,暴露一个网络地址,后端映射到不同的进程,实现可扩展的多进程方案。总结:个人认为(1)(2)没什么意义。(3)暂不评价。(4)则是均衡负载方案,和以上所有方案不冲突。以上模型的代码示例以及性能对比后面给出。

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

    最新回复(0)