2005年08月31日

本文作者:sodme
本文出处:http://blog.csdn.net/sodme
声明:本文可以不经作者同意任意转载,但任何对本文的引用都须注明作者、出处及此声明信息。谢谢!!

  在网络应用中,“负载均衡”已经不能算是什么新鲜话题了,从硬件到软件,也都有了很多的方法来实现负载均衡。我们这里讨论的负载均衡,并不是指依靠DNS转向或其它硬件设备等所作的负载均衡,而是指在应用层所作的负载均衡。

  一般而言,只有在大型在线系统当中才有必要引入负载均衡,那么,多大的系统才能被称为大型系统呢?比如动辄同时在线数十万的网络游戏,比如同时在线数在10万以上的WEB应用,这些我们都可以理解为大型系统,这本身就是一个宽泛的概念。

  设计再好的服务器程序,其单个程序所能承载的同时访问量也是有限的,面对一个庞大且日益增长的网络用户群,如何让我们的架构能适应未来海量用户访问,这自然就牵涉到了负载均衡问题。支持百万级以上的大型在线系统,它的架构核心就是如何将“百万”这么大的一个同时在线量分摊到每个单独的服务器程序上去。真正的逻辑处理应该是在这最终的底层的服务器程序(如QQ游戏平台的游戏房间服务器)上的,而在此之前所存在的那些服务器,都可以被称为“引路者”,它们的作用就是将客户端一步步引导到这最终的负责真正逻辑的底层服务器上去,我们计算“百万级在线”所需要的服务器数量,也是首先考虑这底层的逻辑服务器单个可承载的客户端连接量。

  比如:按上篇我们所分析QQ游戏架构而言,假设每个服务器程序最高支持2W的用户在线(假设一台机子只运行一个服务器程序),那么实现150万的在线量至少需要多少台服务器呢?如果算得简单一点的话,就应该是:150/2=75台。当然,这样算起来,可能并不能代表真正的服务器数量,因为除了这底层的服务器外,还要包括登录/账号服务器以及大厅服务器。但是,由于登录/账号服务器和大厅服务器,它们与客户端的连接都属于短连接(即:取得所需要信息后,客户端与服务器即断开连接),所以,客户端给这两类服务器带来的压力相比于长连接(即:客户端与服务器始终保持连接)而言就要轻得多,它们的压力主要在处理瞬间的并发访问上。

  “短连接”,是实现应用层负载均衡的基本手段!!!如果客户端要始终与登录/账号服务器以及大厅服务器保持连接,那么这样作的分层架构将是无意义的,这也没有办法从根本上解决用户量不断增长与服务器数量有限之间的矛盾。

  当然,短连接之所以可以被使用并能维护正常的游戏逻辑,是因为在玩家看不到的地方,服务器与服务器之间进行了大量的数据同步操作。如果一个玩家没有登录到登录服务器上去而是直接连接进了游戏房间服务器并试图进行游戏,那么,由于游戏房间服务器与大厅服务器和登录/账号服务器之间都会有针对于玩家登录的逻辑维护,游戏房间服务器会检测出来该玩家之前并没有到登录服务器进行必要的账号验证工作,它便会将玩家踢下线。由此看来,各服务器之间的数据同步,又是实现负载均衡的又一必要条件了。

  服务器之间的数据同步问题,依据应用的不同,也会呈现不同的实现方案。比如,我们在处理玩家登录这个问题上。我们首先可以向玩家开放一些默认的登录服务器(服务器的IP及PORT信息),当玩家连接到当前的登录服务器后,由该服务器首先判断自己同时连接的玩家是不是超过了自定义的上限,如果是,由向与该服务器连接着的“登录服务器管理者”(一般是一个内部的服务器,不直接向玩家开放)申请仲裁,由“登录服务器管理者”根据当前各登录服务器的负载情况选择一个新的服务器IP和PORT信息传给客户端,客户端收到这个IP和PORT信息之后重定向连接到这个新的登录服务器上去,完成后续的登录验证过程。

  这种方案的一个特点是,在面向玩家的一侧,会提供一个外部访问接口,而在服务器集群的内部,会提供一个“服务器管理者”及时记录各登录服务器的负载情况以便客户端需要重定向时根据策略选择一个新的登录接口给客户端。

  采用分布式结构的好处是可以有效分摊整个系统的压力,但是,不足点就是对于全局信息的索引将会变得比较困难,因为每个单独的底层逻辑服务器上都只是存放了自己这一个服务器上的用户数据,它没有办法查找到其它服务器上的用户数据。解决这个问题,简单一点的作法,就是在集群内部,由一个中介者,提供一个全局的玩家列表。这个全局列表,根据需要,可以直接放在“服务器管理者”上,也可以存放在数据库中。

  对于逻辑相对独立的应用,全局列表的使用机会其实并不多,最主要的作用就是用来检测玩家是不是重复登录了。但如果有其它的某些应用,要使用这样的全局列表,就会使数据同步显得比较复杂。比如,我们在超大无缝地图的MMORPG里,如果允许跨服操作(如跨服战斗、跨服交易等)的话,这时的数据同步将会变得异常复杂,也容易使处理逻辑出现不可预测性。

  我认为,对于休闲平台而言,QQ游戏的架构已经是比较合理的,也可以称之为休闲平台的标准架构了。那么,MMORPG一般的架构是什么样的呢?

  MMORPG一般是把整个游戏分成若干个游戏世界组,每个组内其实就是一个单独的游戏世界。而不同的组之间,其数据皆是相互独立的,并不象QQ休闲平台一样所有的用户都会有一个集中的数据存放点,MMORPG的游戏数据是按服务器组的不同而各自存放的。玩家在登录QQ游戏时,QQ游戏相关的服务器会自动为玩家的登录进行负载均衡,选择相对不忙的服务器为其执行用户验证并最终让用户选择进入哪一个游戏房间。但是,玩家在登录MMORPG时,却没有这样的自动负载均衡,一般是由玩家人为地去选择要进入哪一个服务器组,之所以这样,是因为各服务器组之间的数据是不相通的。其实,细致想来,MMORPG的服务器架构思想与休闲平台的架构思想有异曲同工之妙,MMORPG的思想是:可以为玩家无限地开独立的游戏世界(即服务器组),以满足大型玩家在线;而休闲平台的思想则是:可以为玩家无限地开游戏房间以满足大量玩家在线。这两种应用,可以无限开的都是“具有完整游戏性的游戏世界”,对于MMORPG而言,它的一个完整的游戏地图就是一个整体的“游戏世界”,而对于休闲平台,它的一个游戏房间就可以描述为一个“游戏世界”。如果MMORPG作成了休闲平台那样的全服皆通,也不是不可以,但随之而来的,就是要解决众多跨服问题,比如:好友、组队、帮派等等的问题,所有在传统MMORPG里所定义的这些玩家组织形式的规则可能都会因为“全服皆通”而改变。

  架构的选择是多样性的,确实没有一种可以称得上是最好的所谓的架构,适合于当前项目的,不一定就适合于另一个项目。针对于特定的应用,会灵活选用不同的架构。但有一点,是可以说的:不管你如何架构,你所要作的就是--要以尽可能简单的方案实现尽可能的稳定、高效!

  <全文完>

本文作者:sodme
本文出处:http://blog.csdn.net/sodme
声明:本文可以不经作者同意任意转载,但任何对本文的引用都须注明作者、出处及此声明信息。谢谢!!

  要了解此篇文章中引用的本人写的另一篇文章,请到以下地址:
  http://blog.csdn.net/sodme/archive/2004/12/12/213995.aspx
  以上的这篇文章是早在去年的时候写的了,当时正在作休闲平台,一直在想着如何实现一个可扩充的支持百万人在线的游戏平台,后来思路有了,就写了那篇总结。文章的意思,重点在于阐述一个百万级在线的系统是如何实施的,倒没真正认真地考察过QQ游戏到底是不是那样实现的。

  近日在与业内人士讨论时,提到QQ游戏的实现方式并不是我原来所想的那样,于是,今天又认真抓了一下QQ游戏的包,结果确如这位兄弟所言,QQ游戏的架构与我当初所设想的那个架构相差确实不小。下面,我重新给出QQ百万级在线的技术实现方案,并以此展开,谈谈大型在线系统中的负载均衡机制的设计。

  从QQ游戏的登录及游戏过程来看,QQ游戏中,也至少分为三类服务器。它们是:
  第一层:登陆/账号服务器(Login Server),负责验证用户身份、向客户端传送初始信息,从QQ聊天软件的封包常识来看,这些初始信息可能包括“会话密钥”此类的信息,以后客户端与后续服务器的通信就使用此会话密钥进行身份验证和信息加密;
  第二层:大厅服务器(估且这么叫吧, Game Hall Server),负责向客户端传递当前游戏中的所有房间信息,这些房间信息包括:各房间的连接IP,PORT,各房间的当前在线人数,房间名称等等。
  第三层:游戏逻辑服务器(Game Logic Server),负责处理房间逻辑及房间内的桌子逻辑。

  从静态的表述来看,以上的三层结构似乎与我以前写的那篇文章相比并没有太大的区别,事实上,重点是它的工作流程,QQ游戏的通信流程与我以前的设想可谓大相径庭,其设计思想和技术水平确实非常优秀。具体来说,QQ游戏的通信过程是这样的:

  1.由Client向Login Server发送账号及密码等登录消息,Login Server根据校验结果返回相应信息。可以设想的是,如果Login Server通过了Client的验证,那么它会通知其它Game Hall Server或将通过验证的消息以及会话密钥放在Game Hall Server也可以取到的地方。总之,Login Server与Game Hall Server之间是可以共享这个校验成功消息的。一旦Client收到了Login Server返回成功校验的消息后,Login Server会主动断开与Client的连接,以腾出socket资源。Login Server的IP信息,是存放在QQGame\config\QQSvrInfo.ini里的。

  2.Client收到Login Server的校验成功等消息后,开始根据事先选定的游戏大厅入口登录游戏大厅,各个游戏大厅Game Hall Server的IP及Port信息,是存放在QQGame\Dirconfig.ini里的。Game Hall Server收到客户端Client的登录消息后,会根据一定的策略决定是否接受Client的登录,如果当前的Game Hall Server已经到了上限或暂时不能处理当前玩家登录消息,则由Game Hall Server发消息给Client,以让Client重定向到另外的Game Hall Server登录。重定向的IP及端口信息,本地没有保存,是通过数据包或一定的算法得到的。如果当前的Game Hall Server接受了该玩家的登录消息后,会向该Client发送房间目录信息,这些信息的内容我上面已经提到。目录等消息发送完毕后,Game Hall Server即断开与Client的连接,以腾出socket资源。在此后的时间里,Client每隔30分钟会重新连接Game Hall Server并向其索要最新的房间目录信息及在线人数信息。

  3.Client根据列出的房间列表,选择某个房间进入游戏。根据我的抓包结果分析,QQ游戏,并不是给每一个游戏房间都分配了一个单独的端口进行处理。在QQ游戏里,有很多房间是共用的同一个IP和同一个端口。比如,在斗地主一区,前50个房间,用的都是同一个IP和Port信息。这意味着,这些房间,在QQ游戏的服务器上,事实上,可能是同一个程序在处理!!!QQ游戏房间的人数上限是400人,不难推算,QQ游戏单个服务器程序的用户承载量是2万,即QQ的一个游戏逻辑服务器程序最多可同时与2万个玩家保持TCP连接并保证游戏效率和品质,更重要的是,这样可以为腾讯省多少money呀!!!哇哦!QQ确实很牛。以2万的在线数还能保持这么好的游戏品质,确实不容易!QQ游戏的单个服务器程序,管理的不再只是逻辑意义上的单个房间,而可能是许多逻辑意义上的房间。其实,对于服务器而言,它就是一个大区服务器或大区服务器的一部分,我们可以把它理解为一个庞大的游戏地图,它实现的也是分块处理。而对于每一张桌子上的打牌逻辑,则是有一个统一的处理流程,50个房间的50*100张桌子全由这一个服务器程序进行处理(我不知道QQ游戏的具体打牌逻辑是如何设计的,我想很有可能也是分区域的,分块的)。当然,以上这些只是服务器作的事,针对于客户端而言,客户端只是在表现上,将一个个房间单独罗列了出来,这样作,是为便于玩家进行游戏以及减少服务器的开销,把这个大区中的每400人放在一个集合内进行处理(比如聊天信息,“向400人广播”和“向2万人广播”,这是完全不同的两个概念)。

  4.需要特别说明的一点。进入QQ游戏房间后,直到点击某个位置坐下打开另一个程序界面,客户端的程序,没有再创建新的socket,而仍然使用原来大厅房间客户端跟游戏逻辑服务器交互用的socket。也就是说,这是两个进程共用的同一个socket!不要小看这一点。如果你在创建桌子客户端程序后又新建了一个新的socket与游戏逻辑服务器进行通信,那么由此带来的玩家进入、退出、逃跑等消息会带来非常麻烦的数据同步问题,俺在刚开始的时候就深受其害。而一旦共用了同一个socket后,你如果退出桌子,服务器不涉及释放socket的问题,所以,这里就少了很多的数据同步问题。关于多个进程如何共享同一个socket的问题,请去google以下内容:WSADuplicateSocket。

  以上便是我根据最新的QQ游戏抓包结果分析得到的QQ游戏的通信流程,当然,这个流程更多的是客户端如何与服务器之间交互的,却没有涉及到服务器彼此之间是如何通信和作数据同步的。关于服务器之间的通信流程,我们只能基于自己的经验和猜想,得出以下想法:

  1.Login Server与Game Hall Server之前的通信问题。Login Server是负责用户验证的,一旦验证通过之后,它要设法让Game Hall Server知道这个消息。它们之前实现信息交流的途径,我想可能有这样几条:a. Login Server将通过验证的用户存放到临时数据库中;b. Login Server将验证通过的用户存放在内存中,当然,这个信息,应该是全局可访问的,就是说所有QQ的Game Hall Server都可以通过服务器之间的数据包通信去获得这样的信息。

  2.Game Hall Server的最新房间目录信息的取得。这个信息,是全局的,也就是整个游戏中,只保留一个目录。它的信息来源,可以由底层的房间服务器逐级报上来,报给谁?我认为就如保存的全局登录列表一样,它报给保存全局登录列表的那个服务器或数据库。

  3.在QQ游戏中,同一类型的游戏,无法打开两上以上的游戏房间。这个信息的判定,可以根据全局信息来判定。

  以上关于服务器之间如何通信的内容,均属于个人猜想,QQ到底怎么作的,恐怕只有等大家中的某一位进了腾讯之后才知道了。呵呵。不过,有一点是可以肯定的,在整个服务器架构中,应该有一个地方是专门保存了全局的登录玩家列表,只有这样才能保证玩家不会重复登录以及进入多个相同类型的房间。

  在前面的描述中,我曾经提到过一个问题:当登录当前Game Hall Server不成功时,QQ游戏服务器会选择让客户端重定向到另位的服务器去登录,事实上,QQ聊天服务器和MSN服务器的登录也是类似的,它也存在登录重定向问题。

  那么,这就引出了另外的问题,由谁来作这个策略选择?以及由谁来提供这样的选择资源?这样的处理,便是负责负载均衡的服务器的处理范围了。由QQ游戏的通信过程分析派生出来的针对负责均衡及百万级在线系统的更进一步讨论,将在下篇文章中继续。

  在此,特别感谢网友tilly及某位不便透露姓名的网友的讨论,是你们让我决定认真再抓一次包探个究竟。

  <未完待续>

本文作者:sodme
本文出处:http://blog.csdn.net/sodme
声明:本文可以不经作者同意任意转载,但请保留文章开始前的作者、出处及声明信息。谢谢。

  由于个人工作的关系,接触高性能服务器的研发已经有一段时间了,在没有接触这个话题之前,我也和许多人一样,认为服务器的设计无非就是用一下winsock,调用调用函数那么简单。当亲自完成了一个在win平台上能承载上万连接的测试模型后,才知道,原来,作高性能服务器是这么有挑战性。你不仅需要在细支末节上对模型进行精雕细琢,更需要在总体架构方面进行权衡选择。这两方面,都会影响到你的服务器性能的正常发挥。作为对“细枝末节”方面的介绍,请参照我之前写的系列文章“完成端口之性能优化”。而从此篇文章开始的系列文章,将就一个完整的高性能服务器模型进行介绍,而对于服务器集群架构方面,则不在此系列的讨论范围,那将是未来数周我将要完成的事。有朋友跟我说,希望我能提供底层模型的完整代码或可供使用的动态链接库及LIB文件。对于这个要求,我现在还在考虑之中。确实,有了代码,要学得更快一点,但由于模型里牵涉到一定的版权问题,所以,我会考虑将其进行必要的修改,而后再考虑是否向大家提供源代码。不过,可以肯定的是,对于LIB及DLL的要求,我想我会尽快提交上来的。

  所谓的“高性能”,我想不外乎两个方面:
  1、处理的并发请求要尽可能地多,具体表现为同一时间内同时连接的客户端数量;
  2、数据包的吞吐量要尽可能地大,具体表现为单位时间内服务器的收、发数据量。

  win平台下,IOCP(完成端口)是处理大量并发连接的最佳处理方案,所以,此后讨论的文章,也是基于这个模型的。有关一个独立的IOCP模型到底是如何工作的,请大家下载微软的SDK,在那个例子里,有基本的IOCP示例。但那仅仅是个示例,于实际的应用中,还需要作很多工作。阅读以下的内容时,最好具备以下条件:知道IOCP的基本概念,以及它所调用的几个必备函数(Create、Get、Post函数等)的使用。注:为嫌麻烦,这几个函数,我采用的都是简单叫法,完整的函数名请查看完成端口相关资料。

  为了使大家能有一个宏观概念的把握,我先介绍一下基于三层结构的服务器通信底层模型。对于软件架构而言,没有最好之说,放在不同的应用环境其表现都可能千差万别,所以,我不保证这个架构是能让所有人都看着舒服的,我只保证,在我当前的项目应用中,这个架构充分考虑了“性能”和“可扩展性”两者的兼顾,让我在很容易地开发新的服务器时,仍然可以享受到很好的性能。

  一、三层架构图及简单说明

  三层架构图如下:
         CIOCPServer
         ||
         ||
      CCutomHPServer
         ||
         ||
       CTestServer

  以后我们要讨论的就是这个看上去并不复杂的三层架构图,下面就此架构图中的主要类进行简要说明。

  CIOCPServer:
  完成端口服务器基本通信类,它使用winnt/2000/xp平台特有完成端口特性,对通信模型进行封装,向它的派生类提供以下基本扩展接口(可被重载的虚函数):

  1、有客户端连接时的处理接口;
  2、客户端断开时的处理接口;
  3、从客户端收完数据后的处理接口;
  4、向客户端发送完数据后的处理接口;
  5、网络通信及服务器处理出现错误时的处理接口。

  CCustomHPServer:
  典型的高性能服务器类,CIOCPServer是其基类之一(之一?难道还有另外的基类,回答是:当然,呵呵,别急,后面会介绍),此类在CIOCPServer的基础上,封装了三个数据队列及三类处理线程,介绍如下:

  1、接收数据包队列及接收线程:用于存放刚收到的数据包,此数据包还没有进行逻辑意义上的拆解,接收线程从此队列中取出数据包,并将其形成一个逻辑意义上完整的数据包加入到“处理数据包队列”中;

  2、处理数据包队列及逻辑处理线程:已经拆解成了逻辑意义上的数据包,逻辑处理线程对此类数据包进行逻辑解析,这里就是服务器的主要逻辑部分,有的数据包在处理完成后,可能是需要向客户端返回处理结果的,此时就需要逻辑线程在处理完成后将返回结果的数据包放入“发送数据包队列”中;

  3、发送数据包队列及发送线程:待发送的数据包队列,由发送线程根据数据包里的客户端套接字发送给特定客户端。

  CTestServer:
  此类是一个测试类,主要用于演示如何在CCustomHPServer的基础上派生一个真正的应用服务器,并用于说明它需要重载实现CCustomHPServer的哪些重要虚函数。

  基于以上的结构,我们的服务器通信模型,可以一层一层地实现,一层一层的测试。在CIOCPServer中,它本身是不带有任何数据队列的,所有的网络数据都是即来即处理,没有保存数据,实现的是即时响应。在CIOCPServer里,有两类重要线程:AcceptThread线程和WorkerThread线程。其中,AccetpThread线程使用Accept或AcceptEx函数来接收客户端的连接请求,并实现客户端socket与完成端口句柄的绑定,“有客户端连接时的处理接口”就是在这里封装的;而WorkerThread线程是我们在完成端口中常说的“工作者线程”,它由get函数触发工作,除“有客户端连接时的处理接口”之外的其它接口,都在这里进行封装。

  在真正实现一个高性能服务器模型时,我们可能需要逐层地加以实现,这样作,一是因为测试起来要简单一些,二是因为在我们逐层实现时可以清楚地知道每一层在实现时的效率是什么样的,这样就有利于我们找出提高效率的突破口。可以先从CIOCPServer类开始,实现一个简单的ECHO服务器,即:回显服务器。不用维护数据包队列,对客户端发过来的数据,即时返回给客户端。作完这一层,属于完成端口该作的事基本上就作完了,它包括:有大量客户端连接时的客户端连接队列维护,客户端连接、断开、发送、接收数据时的事件处理及线程同步。根据我的经验,在处理底层的客户端断开事件时是一个难点,新手往往会在接收到断开事件时直接释放掉当前客户端对象,但比较好的作法是使用引用计数机制,而不是直接释放。另外,在“释放”这一点上,我也有自己的一个看法,即:客户端对象最好不要释放,而把它放入闲置队列或者关闭原来的socket之后,再重新生成一个新的socket让它与原客户端对象关联,把它作为一个新的客户端对象使用,这样就避免了频繁的客户端对象的建立与释放,当然,这样作的前提是在接受客户端连接方面最好使用AcceptEx函数而不是Accept。

  这篇文章里,仅就模型的总体结构进行了介绍,关于这三个类的具体实现及片段代码,会在下篇文章里介绍,请继续关注,也请有兴趣的朋友能在我的Blog里反馈你们的意见。

  <未完待续>

本文作者:sodme
本文出处:http://blog.csdn.net/sodme
声明:本文可不经作者同意,任意被转载、引用、复制,但任何对本文的引用都必须注明本文作者,出处以及本行声明信息。谢谢。

  作为WIN平台下同时管理数千个连接的最为高效的网络模型,完成端口已经被越来越多的人认识和熟悉。通常情况下,一种经典的完成端口使用模式是:
  (1)创建完成端口,并在指定端口开始监听;
  (2)创建接受连接线程,用accept或acceptEx接受客户端连接;
  (3)创建工作者线程,处理客户端的数据收发。
  
  众所周知,CreateIoCompletionPort函数,有两个作用,一是“创建”一个完成端口,二是将一个socket与已经创建的完成端口句柄相“绑定”,绑定之后,基于该socket的收、发、断开等事件都可以被完成端口感知。一般情况下,较为正常的思维状态下,CreateIoCompletionPort的绑定是选在accept函数执行以后或acceptEx函数完成之时与套接字相绑定。但是,这并不说明CreateIoCompletionPort函数就不能进行其它形式的绑定。

  事实上,CreateIoCompletionPort关心的只是一个套接字,它并不关心这个套接字到底是通过accept而来的,还是用来connect的。也就是说,它并不关心当前的这个socket是用于接受客户端连接的,还是用来连接其它服务器的。那就是说,CreateIoCompletionPort函数,也可以用来绑定一个连接到其它服务器的客户端socket。

  这个问题的提出,是我在设计网关服务器时。

  网关服务器,承担的主要工作就是两个:向内,负责客户端数据包的分发;向外,负责把内部服务器所有的返回数据包统一通过网关发送出去。网关服务器上,我创建了两个IOCP,一个用来向外接受玩家与网关的数据交换,另一个用来与内部的服务器进行数据转发。由于本人较懒,所以想在内部的那个服务器上,也采用IOCP模型来作。

  在网关上负责向内部服务器提供数据转发的这个IOCP,必须要能正确识别哪一个内部服务器使用的是哪一个套接字。这样,内部服务器与网关服务器之间的套接字确认就有两种方式。一种方式,是由内部服务器连接到网关服务器上,随后向网关服务器发送一个注册包,告诉网关服务器当前的这个socket是哪个内部服务器;另一种方式,是由网关服务器先创建若干个指定的socket,然后用这些socket去连接指定类型的内部服务器。前面,我已经说过,IOCP可以绑定的套接字,不仅是可以被accept来的套接字,也可以是connect其它服务器的套接字。所以,我选用了后一种方式。这样在网关服务器对内的IOCP的逻辑处理上,就少了一项判定和一个包的逻辑解析。当网关流量非常巨大时,每节省一项判定或一种分支,效率和资源都会得到更有效的利用,高性能从哪里来?就是从这样的一点一滴的改进中来。到目前为止,我能测试到的网关服务器流量上限是每秒10M到12M,据说,局域网的某些HUB给每台机子的带宽只分了10M左右的出口带宽,所以,我无法确定如果把局域网的交换机换成电信级的,数据交换量会不会有一个更大的提高,不久,公司会进一个这样的电信级交换机,到时可以作进一步的测试。

  过段时间,我会在我的BLOG(http://blog.csdn.net/sodme)上公布我的IOCP底层通信模型的架构设计以及一个通用的高性能服务器模型架构,希望能与各位同仁共同研究高性能服务器研发问题。

本文作者:sodme 本文出处:http://blog.csdn.net/sodme
版权声明:本文可以不经作者同意任意转载,但转载时烦请保留文章开始前两行的版权、作者及出处信息。

对于初次使用IOCP进行高性能服务器开发的朋友来说,可能会经常遇到一些莫名其妙的错误,让自己无从下手。为此,我将利用此篇文章对IOCP开发中的常见问题予以集中记录并持续添加,并附上我的处理建议,以供大家参考。

1、在程序创建监听套接字时,使用socket函数创建一个套接字时,总是报“INVALID_SOCKET”错误?
原因:出现此问题的原因,很可能是因为没有正确执行WSAStartUp函数引起的;
解决方法:请检查,是否使用WSAStartUp对winsock进行了初始化工作?如果进行了初始化,请检查初始化是否成功?

2、使用WSASend或WSARecv投递相应的发送或接收请求后,始终没有收到相应的GET函数完成返回通知?
原因:出现此问题的原因,绝大多数是因为函数参数没有进行正确的赋值。
解决方法:在执行wsasend和wsarecv操作前,请先将overlapped结构体使用memset进行清零。一个正确的调用格式如下:
发送操作
  DWORD ByteSend=0;
  DWORD Flags=0;
  int tmpResult=0;
     ……
 PPerHandleData tmpData;
 ……
 memset(&(tmpData->Overlapped), ‘\0′, sizeof(OVERLAPPED));//将overlapped结构清空
 tmpData->Statu = ssSend;
    tmpResult = WSASend(tmpData->socket, &(tmpData->WSASendBuffer), 1,
   &ByteSend,
   Flags,
   &(tmpData->Overlapped),
   NULL);

接收操作
  DWORD byteRecv=0;
  DWORD Flags=0;
  int tmpResult=0;
  ……
  PPerHandleData myHandlData; 
  …… 
  memset(&(myHandlData->Overlapped), ‘\0′, sizeof(OVERLAPPED));
  memset(myHandlData->RecvBuffer, ‘\0′, CLIENT_BUFFER_SIZE);
  myHandlData->WSARecvBuffer.buf = myHandlData->RecvBuffer;
  myHandlData->WSARecvBuffer.len = CLIENT_BUFFER_SIZE;
  myHandlData->socket = myClient->m_ClientSocket;
  myHandlData->Statu = ssRecv;

  tmpResult = WSARecv(myHandlData->socket, &(myHandlData->WSARecvBuffer), 1, (LPDWORD)&byteRecv, (LPDWORD)&Flags, (LPWSAOVERLAPPED)&(myHandlData->Overlapped), 0);


3、当投递了一个WSARecv或WSASend请求后,总是返回“ERROR_IO_PENDING”错误?
原因:“ERROR_IO_PENDING”,表示的是WSARecv或WSASend操作正在执行中,还没有执行完毕。
解决方法:此错误可以直接忽略,如果参数设置正确,当操作完成时,系统会通过GET函数返回执行的形式来通知发送或接收操作已经完成。

4、为什么投递一个WSASend操作后,在Get函数中,获得发送完成事件的通知时,截获到的发送出去的数据却不是刚刚投递的数据?
原因:在投递WSASend时,会要求你指定数据缓冲区的地址,如果你此处的地址是通过new动态分配的,并且在投递过WSASend之后随之即delete了此空间,则可能会发生你的描述的这个现象。我的理解是:当WSASend操作还没有真正完成时,如果释放了数据缓冲区,那么WSASend将会因为缓冲区的释放而造成没有正确投递缓冲区内的数据,也就是说,WSASend在投递一个数据时,并没有即刻把数据拷到投递缓冲区去,如果是在WSASend完成后再释放此空间则不会发生这种情况。以上,只是偶的猜想,除调试结果外,到现在还没有获得其它信息加以证实。

本文作者:sodme 本文出处:http://blog.csdn.net/sodme
版权声明:本文可以不经作者同意任意转载,但转载时烦请保留文章开始前两行的版权、作者及出处信息。

  前面有朋友对本系列文章的题目提出质疑,说:这恐怕不能算是性能优化吧?我要指出的是,本系列文章中提到的优化并不仅仅是某段具体的代码优化,当然这种东西肯定会有,但优化绝不仅仅是这些方面,我这里提到的优化还包括更多的关于模型架构方面的考量。

  上次我提到,在模型里,引入“池”的概念可以有效改善服务器效率。对于完成端口来说,它处理的是成千上万个的客户端连接。在单一客户端连接的情况下,偶尔的多余操作可能并没有给你的系统带来什么不良影响,但这在形如使用完成端口构建的高性能服务器上是绝对应该避免的,不然,你会发现用了完成端口说不定还没有用其它模型来得高效。另外,需要指出的是,完成端口并不是万能模型,有的地方可以考虑用,而有的地方则完全没必要用,至于完成端口在网游服务器模型里的具体使用,我会在另外的文章里提及。

  “池”的概念的引入,最主要的是想让服务器在运行时,维护一个相对静态的数据存储空间,并在这个相对静态的空间上进行相关操作。但是,尽管我们千方百计在诸如此类的地方引入“池”,还是不可避免地要遇到这样的几个问题:拼包操作时的数据移动,内存数据的拷贝,socket与客户端对象的一一对应和定位等。下面,将会针对这三方面介绍有关的优化细节。

  为讨论的方便,在此引入几个状态常量:
  stAccept:表示处于连接建立状态;
  stRecv:表示处于数据接收状态;
  stSend:表示处于数据发送状态。
  注:本文及后续文章,会简称“GetQueuedCompletionStatus”函数为“Get”函数。

  现在我们要讨论的是当Get函数返回时,处于stRecv状态时的数据包拼装问题。我们知道,TCP是流协议,它的数据包大小并不一定就是我们期望的发送或接收时的逻辑包大小,每次发送或接收的大小到底是多少,是根据网络的实际状况来决定的。一个在逻辑意义上完整的包,可能会被TCP分为两次进行发送,第一次发送前半部,第二次发送后半部,由此便带来了不完整数据包的拼装问题。那么,要实现数据包的拼装,必须要有一个地方,可以把两个半截的包放到一起,然后从首部根据逻辑包里的大小定义字段取出相应长度的逻辑包。“把两个半截的包放到一起”,这个操作,就涉及到了数据拷贝问题。在实际的应用中,有两种拼装方案可供选择:第一种方案,是将新收到的后半部分数据包复制到前半部分数据包的末尾,形成一个连续的数据包空间,在此空间的基础上进行拼装操作;第二种方案,不用把新收到的后半部分数据包复制到前半部分的末尾,而是在现有两个缓冲区的基础上直接进行拼装操作,当从前半部分搜索到截开的位置时,将指针直接指向新包首部,取走一个完整逻辑意义的包。

  不论是哪种形式的拼装,可能都会或早或晚地牵涉到剩余数据的拷贝转移问题。第一种方案的情况下,会执行memcpy将新收到的数据包,复制到前半部分数据包的后面;第二种情况,尽管不会首先执行memcpy执行复制,但当执行了“取走所有完整逻辑包”的操作后,缓冲区里可能会残留一个新的不完整的数据包,此时仍需要执行memcpy操作将剩余的这个不完整的数据包复制到原来的前半部分数据包所在的缓冲区,以跟其后续的不完整数据包完成下一步的拼装操作。两种方案的效率,比较起来,第二种方案执行memcpy时所复制的数据内容可能要小得多,所以,相对来说,第二种方案的效率要高一点。

  当然,如果只就数据包的拼装问题而言,我们也完全可以避免数据拷贝操作,方法就是:使用环形缓冲区。环形缓冲区的实现还是比较简单的,具体的代码我这里不会贴出,只介绍它的基本思想。学过数据结构的人都会知道一种叫作“环形队列”的东西(不知道的,请使用GOOGLE搜索),我们这里所说的环形缓冲区正是具体这种特征的接收缓冲区。在服务器的接收事件里,当我们处理完了一次从缓冲区里取走所有完整逻辑包的操作后,可能会在缓冲区里遗留下来新的不完整包。使用了环形缓冲区后,就可以不将数据重新复制到缓冲区首部以等待后续数据的拼装,可以根据记录下的队列首部和队列尾部指针进行下一次的拼包操作。环形缓冲区,在IOCP的处理中,甚至在其它需要高效率处理数据收发的网络模型的接收事件处理中,是一种应该被广泛采用的优化方案。

  memcpy函数的优化,在google上可以搜索到相关主题,用的比较多的优化函数是fastmemcpy,本处给出有关memcpy函数优化的两个连接地址:
http://www.blogcn.com/user8/flier_lu/blog/1577430.html
http://www.blogcn.com/user8/flier_lu/blog/1577440.html
请有兴趣的朋友认真阅读一下这两篇文章。优化的核心思想是:根据系统硬件体系架构,来确定最优的数据拷贝方案。根据以上给出的两个连接地址,memcpy会得到较为明显的优化效果。

  对于网络层来说,每个连接到服务器的客户端唯一标识就是它的socket值,但是,很多情况下,我们需要一个所谓的客户端对象与这个socket能够一一对应,即:以socket值来确定唯一的客户端对象与它对应。于是,我们很自然地会想到使用STL里的map完成这种映射,在实际的使用时,会通过map的查找功能根据socket值取出相应的客户端对象。这样的作法,是较为普遍的作法。但是,尽管map对它的查找算法进行了相应的优化,这个查找也还是要花费一定时间的。如果我们仅仅是要在IOCP的模型里通过GET函数返回时,确定当前返回的socket所代表的那个客户端对象,我们完全可以对投递的overlapped扩展结构进行相应的设置来通过GET函数返回的overlapped扩展结构来直接定位这个客户端对象。

  我的overlapped结构是这样的:
  struct  Per_IO_Data {
  OVERLAPPED  ov;
  …..
  CIOCPClient*  iocpClient;
  …..
  }
  大家可以看到,在我的扩展overlapped结构中,我引入了一个客户端对象指针:iocpClient,每次在投递一个WSASend或WSARecv请求时,都会顺带着这个客户端对象指针。这样,当WSASend或WSARecv操作完成时,就可以通过GET函数执行后返回的Per_IO_Data结构取得这个客户端对象指针,从而就省去了根据socket值进行map查找的步骤。在持续的数据收发下,如果在GET函数里频繁地进行map查找,势必会对性能有较大的影响,而通过传递客户端指针到扩展overlapped结构中实现客户端对象的直接定位则大大节省了查找的时间开销,无疑在性能优化方面又作出了一个重大改进。

  <未完待续>

2005年08月30日

本文作者:sodme 本文出处:http://blog.csdn.net/sodme
版权声明:本文可以不经作者同意任意转载,但转载时烦请保留文章开始前两行的版权、作者及出处信息。

  完成端口的主要优点在哪里?

  完成端口的最大优点在于其管理海量连接时的处理效率,通过操作系统内核的相关机制完成IO处理的高效率。注意:完成端口的优点在于管理连接量的巨大,而不是传输数据量的巨大。在这种场合最适合用完成端口:连接量巨大,且每个连接上收发的数据包容易比较小,通常只有几K甚至不到1K的字节。

  既然完成端口处理的是海量连接问题,那么我们对完成端口的优化则也应该首先放在海量连接的相关管理上。为此,我们引入“池”的概念。

  在完成端口的设计中,“池”几乎是必须采用的原则。这里的“池”包含有多个方面,分为:线程池,内存池,连接池等等。下面,将逐一介绍这些“池”的含义及用途,稍后的文章里,如果可能的话,我会给出采用了“池”和未采用“池”两种情况下的效率对比,你将会发现“池”是多么地可爱。

  在大型在线系统中,数据空间的频繁创建和释放是相当占用系统资源的。为此,在数据空间的管理上,我们引入了“内存池”。

  大家知道,在每一次的wsasend和wsarecv中,我们都要投递一个结构体变量。这个结构体变量的创建和释放,可以有多种方式。

  方式一:即用即创建,即每次执行wsasend和wsarecv时都声明一个新的结构体空间,GET完成后在工作者线程中销毁;

  
方式二:只有每出现一个新的连接时,我们才随新连接建立一个新的结构体空间,将它与新的客户端socket绑定在一起,只有当客户端socket关闭时才将它与客户端对象一起销毁;

  方式三:建立一定量的结构体空间,并将其统一放入一个空闲队列,不管何时执行wsasend和wsarecv,都先从空闲队列里取一个结构体空间来用,用完后,将其放回空闲队列。

  从执行的效率及实现的便捷性方面考虑,建议采用方式3的形式管理这些空闲结构体空间。采用方式3的难点在于,到底创建多少个结构体空间才是比较合适的?这可能取决于你在自己服务器上作的完成端口的处理效率,如果完成端口的处理效率比较高,那么需要的队列长度可能就比较小,如果完成端口的处理效率低,那么队列长度就要大一点。

  下面说说“连接池”。我们知道,使用传统的accept接收客户端的一个连接后,此函数会返回一个创建成功的客户端socket,也就是说,此函数自己完成了socket的创建工作。非常好的是,windows给我们提供了另一个函数,允许我们在接受连接之前就事先创建一个socket,使之于接受的连接相关联,这个函数就是acceptEx,在acceptEx的参数中,SOCKET类型的参数值是我们事先用socket函数创建完成的一个socket,在调用acceptEx时,把这个已经创建好的socket传给acceptEx。说到这里,明白的人可能已经偷笑了--既然这样,那岂不是允许我们在接受连接之前就创建好一大堆的socket等着acceptEx来用吗?这就省去了临时创建一个socket的系统开销呀?呵呵,事实确实如此。在实际操作中,我们可以事先创建好一大堆的socket,然后接受客户端连接的函数使用acceptEx。这一大堆的socket,我们可以形象地称之为“socket连接池”(当然,也许在更多的场合,我们听到的是“数据库连接池”的概念),这个名字是我自己取的,听着觉得不舒服的兄弟,干脆就叫它“socket池”吧,哈哈。

  线程池的概念,相信作过多线程的朋友都会有所耳闻。在此处的文字中,就不对它再多加介绍了,感兴趣的朋友可以自己去GOOGLE一下。我这里说的线程池,不仅仅指由完成端口自己负责维护的那个工作者线程池,还指我们建立在完成端口模型基础之上的线程池。这个线程池里又具体分为执行逻辑线程池和发送线程池。当然,也有人建议执行逻辑线程只要一条就行了,没必要用多条,而且同步会非常麻烦。我赞同这种说法以,但前提是,这样的设计,要求你对服务器功能的划分要相当合理,否则会让这一条线程累得够呛。

  未完待续。


  因为实在忍受不了CSDN上BLOG的服务质量,所以很早之前就已经在物色有没有一个更好的blog空间,去过blogchina,也去过blogger,但在那些东方写东西,总有点孤家寡人的感觉,难有共鸣之人。找来找去,还是决定落在donews中,虽然这里不完全是一个技术社区(它可能首先是一个媒体人的社区),但对于我个人而言,毕竟不是那种闷下头只顾作技术的人,对于业界的新鲜事,还是时刻在关注的。

  总体感觉,虽然donews的blog也有服务器不可用的时候,但至少比CSDN好多了,所以,还是感谢donews,也感谢刘韧。

  从开始写Blog的时候起,我写Blog的目的,简单用一句话概括:真实记录开发历程,以文会友,真心希望能与各位网友坦诚交流相关的开发经验。目前,我的主要工作内容是:Win平台下的高性能服务器开发,主要工作领域是:3D MMORPG网络游戏服务器开发。

  国内技术社区的交流,长久以来都被谩骂声所淹没,对于提出不同意见的人,我有两种处理方法:
  1、真诚就技术细节展开交流的,我热烈欢迎并耐心与之讨论;
  2、不说具体问题或说不出具体问题的,只在那里瞎扯蛋,随意发表观点的,删。

  我自认向来不是一个心胸非常宽广的人,对于不友好的讨论,我也没必要恭敬如宾。只要大家都是热心就技术展开讨论的,我是欢迎的。当然,首先,我保证我的出发点是:真诚地希望从技术角度多交流。如果你认为我的文章和观点有错误,我希望你能不吝指出来,而不是象个大牛似的只简单地扔下只言片语便选择沉默,那会让我认为你自己也未必搞得清问题所在。