高性能服务器程序开发(1)

类别:编程语言 点击:0 评论:0 推荐:

   这几天一直忙着调程序,今天程序调通了,下班后就把计划中的这文档的前面一部分写出来。


作者:PPP    [email protected]

未经过许可,拒绝任何转载,作者保留一切权利。



                                                                    一、前言

       即使在硬件性能越来越高以及其成本越来越低的今天,开发高性能服务器程序仍然具有相当现实的意义。但是,开发管理大规模客户连接的高性能服务器程序,从来都是一个挑战,开发一个支持100个客户连接的服务器程序很容易,但是要支持1000个用户连接或者10000个用户连接呢?绝对比很多人想像中要难得多。
       很久以来就有写这篇文档的念头了,本人从今年毕业以来一直从事网络服务器程序开发,学到很多东西,也积累了一些经验,作为总结,把这些东西以及自己的想法写出来,与大家共享。
      本文不会涉及到详细的技术细节,也不会提供示例代码。
      首先这篇文章的讨论有一个原则:一切以性能为前提。性能!性能是第一位。下面的所有讨论都是以这个原则为前提的。当然,这只是我个人的看法:服务器程序以性能为首。这个出发点不一定正确,我就遇到过很多朋友的反对,他们认为要以可扩充性等等为首要原则,他们甚至可以接受为了可扩充性等而牺牲性能。但是我这文章里,只会有为了性能牺牲灵活性或者可扩充性等。
       另外,本文讨论的只是一个纯粹的服务器程序的网络模块,也可以叫着网络引擎,不涉及到任何业务层应用。

                                                             二、程序模型

       首先,作为一个纯粹的网络模块,他应该是独立的,与业务无关的,一切网络细节应该向上层的业务层隐藏起来。换句话说,这应该是一个通用的TCPSERVER,任何基于TCP的应用都要能在这个模块上跑起来。如果不能保证程序做到这点,那么就是失败。这个TCPSERVER至少要实现如下功能:
   1:抵御基于网络层的恶意攻击
      这里的网络层不是指TCP/IP协议中的网络层,而是相对与上层业务层来说的,也就是这个TCPSERVER。比如一个客户端连接上来后,不收发数据,就是一个死连接。或者一个连接一建立上来,马上又断开,不断地重复。对于这种恶意攻击,TCPSERVER要能够有效的抵御,静悄悄地处理掉。保证提交到上面业务层的每一个连接都是有效连接。至于那些基于业务层的恶意攻击或者基于TCP/IP协议中网络层的SYN FLOOD攻击等,这就不是TCPSERVER所能防御的了。
   2:数据包的解析
      下面TCPSERVER至少要保证,每次提交到上面业务层的包是一个完整的业务包(或者叫逻辑包),分包解包对于业务层来说是透明的。
   3: 数据包的发送
      TCPSERVER应该保证,对业务层投递的合法数据包独立地进行发送。也就是说,比如上面业务层投递了1个60K或者100K的数据包,TCPSERVER应该根据数据包的大小以及IO策略等对这60K或100K数据选择最合理的方式分次发送出去。
   4:错误处理以及资源回收
      TCPSERVER应该向上层提供一个安全的错误处理机制,当某个SOCKET连接出错后,上层只会收到一次错误通知,并且在错误通知提交后,上层不会再收到任何有关此连接的后续信息,保证上层对此连接对象资源的正确清除。这在那种一个连接上同时异步收发和非阻塞收发的应用来说,是非常重要的一点。

       以上四点是最基本的功能,也是最重要的功能,包括了连接的建立,数据的收发和连接的断开处理。另外还需要考虑的如流量控制等等。
       前面我已经说了,我的出发点就是:高效。为了高效可以在这个程序模型里牺牲别的东西,比如:牺牲平台移植性。我一直强烈反对在网络模块上坚持使用可移植性。对于网络底层支撑模块来说,可移植性为0,除非用select模型。事实上,如果用了线程池和select模型的组合,同样失去了可移植性,WINDOWS的线程库和LINUX的线程库是不同的。所以首先就要牺牲可移植性,针对所在平台选择最高效的IO模型。在WINDOWS下,首选当然是IOCP,LINUX下现在有EPOLL,freeBSD下有KQUEUE。好的IO模型是开发一个高性能服务器程序的关键,一旦选择了IO模型,基本上可移植性也就为零。然后和可移植性相关的的就是:在程序中尽量使用操作系统本身提供的函数。一般这些函数都是针对所在的操作系统优化过的。关于这一点,我一直坚持的观点就是:针对特定的应用特定的平台进行优化。TCPSERVER的结构要通用,也就是说对于业务层来说,他要通吃。而实现上,则不考虑通用,完全充分地利用操作系统本身的优化。
       另外,程序的开发语言选择上,我强烈推荐用C。简单高效是我们的目标,而C正好适合这一点,用C++来写网络底层,不对路,用不上。因为一个高效的TCPSERVER,代码不会超过4000行,一般2000-3000行。这么小的程序中,根本不用考虑任何OO可重用继承或者多态这些东西,完全是浪费。至于上面的业务层,我则是觉得用C++来写比较合适。


                                                            三、数据拷贝

        数据拷贝是非常耗时的操作。如何减少程序中的数据拷贝是我们首先要考虑的问题,减少数据拷贝一个很简单的方法就是:用缓冲区描述符(buffer descriptor) 来代替单纯的缓冲区指针。比如我们设计如下一个缓冲区描述符:
struct buff_desptor
{
  u_int   buff_len;
  char  * buf;
  u_short ref_count;
  char    buffer[BUFFER_SIZE];  
};
这样的设计在对缓冲区只要进行只读操作的时候非常有用,当某个线程需要读取缓冲区里的数据,只要简单的对引用计数进行一次递增,然后就拿着缓冲区去用,用完后对引用计数进行递减,省略掉了以前的memcopy。看过《TCP/IP协议详解》V2的人应该有还有印象,书的开头就是对那个BUF结构进行介绍,那个结构设计得确实很巧妙,其实我们也可以把这东西引进到我们的程序中来。事实上,WINDOWS下的网络协议栈里也有类似的设计,开发过NDIS IM程序的朋友应该很熟悉这两个东西:NDIS_PACKET和NDIS_BUFFER这两个东西,下面就是示意图。

单包情况:



多包情况:



这里的NDIS_BUFFER就是上面我说的缓冲区描述符,而NDIS_PACKET我们叫他为包描述符,一个包描述符可能包括一个或者多个缓冲区描述符。这样从队列里取包或者把包放到队列里的时候,可以减少很多开销。想像一个如下的设计:一个队列里有5个包,这5个包由5个包描述符来描述,5个包描述符组成一个队列,然后每个包描述符就是一个链表头。这和单纯的用缓冲区或者用缓冲区描述符相比,在存取数据的时候会减少很多时间。当然,这些是在网络驱动程序中用到的,不会设计到引用计数这些。我们用的时候,需要在他们的基础上对这些结构进行一些适当的改造来满足我们的应用。
      接下来对数据拷贝的优化设计到了程序的结构和与上层业务层之间的交互,一般来说,TCPSERVER向上面业务层提交数据的时候,需要将数据COPY给业务层层传递进来的缓冲区,当业务层发送数据的时候,又需要将业务层的数据COPY到地层,这对于每秒几万次IO的服务器程序来说,是非常昂贵的开销,所以我们可以考虑在上层和下层之间共享缓冲区,这需要很好的设计技巧,对于实现的具体方法这里就不探讨,不过我可以给大家保证一点就是:绝对可以实现。
       最后,还有一个优化手段:对memcopy进行优化。不管怎样来做,对于一个程序来说,数据拷贝始终是无法避免的。那么在这个时候,我们就要对数据拷贝的本身进行优化。一般来说,通用的memcpy效率比较折中,不是最高的。在一些多媒体处理中,为了对memcpy进行优化,都开发一个叫fastmemcpy的函数,有兴趣可以到GOOGLE上去搜索一下,有用AT&T汇编实现的,有用8086汇编实现的。至于这个优化效率,对于特定的应用来说,还是比较有效,大家可以看看这里的测试:http://www.blogcn.com/User8/flier_lu/index.html?id=1577430&run=.0A2F3E7


未完待续。后续内容预告:
4:上下文切换
5:同步以及同步方式
6:内存池和内存池
7:数据库网关和连接池
8:缓冲区和缓冲区策略
9:IO策略
10:双向链表以及HASH 链表 和B TREE的使用
11:编译器优化

本文地址:http://com.8s8s.com/it/it25390.htm