游戏服务器的总体框架

Cpp代码  收藏代码
  1. /* 
  2. QQ: 2#4#2#1#0#6#7#6#4    #表示为空 
  3. Mail: lin_style#foxmail.com    #替换成@ 
  4. */  
 

核心,我的并行思路

整体拓扑图

代码执行模块层次

 

核心,我的并行思路

21:31 2009-12-18
    昨晚睡觉的时候,又仔细的考虑了下采取的整个框架模型。前提是要充分利用多核和分布。
方法一:把整个游戏看成一个场景,多线程+锁的肆意执行。想都不用想,代价何其巨大和复杂,抛弃
方法二:为了解决这种异步,加入一个任务队列,并且指定一个线程只能执行几个场景。也就是网络收包还是共同的,场景执行分开了。这样的瓶颈是任务队列需要网络线程、场景执行线程的资源竞争;并且还需要维护一个场景和玩家的关系表,同时可能造成空取。即取得的包不是自己所属的场景。
方法三:既然方法二已经把场景执行线程分开,那何不把网络的连接也分开呢?具体方法如下:
先给若干个场景编号指定好逻辑线程,接着再分配一个网络接包线程,把这个看成是一个完整的游戏服务端。在N核的机器上/不同的机器上,你可以启动N个这样的完整程序,分别指定好不同的场景编号,亲缘到各自的CPU数上。这样就成了一个完整的游戏世界。(也就是吧CPU切成单核单核的应用)然后你要维护的就是,当玩家进行场景切换时,如果场景不在当前的服务端里,改怎么进行跳转,这也就是此次要解决的分布式难题
    那么登录模块也可以这么做。一个CPU对应着一个bin,该bin包括一个完成端口的线程(为了防止阻塞等意外可以多个),一个逻辑处理线程,然后一个已经连接的套接字队列。那么,你就得为客户端单独配置一个登陆接口的文件,指定好IP和各个端口。当然,这件事可以交给更新服务器去做。
2009-12-24
今天又机会看了下big world的引擎结构图。发现主体的架构是差不多的。让我兴奋了一把。但是big world的邮局数据转移,让我大开了眼界,毕竟以前都是自个瞎琢磨。
在我的结构里,最麻烦的就是场景切换时不在一个线程里的数据转移。而BW得做法是,在服务端和客户端的中间设置一排中间服务器,专门用来用户数据的海量转发。这样就不涉及到用户数据转移的问题。而且实现起来也确实直观,决定采用。
 

整体拓扑图


        逐个介绍下每个部件的作用: 
        Login邮局分配: 非常关键的路由。它负责邮局服务器的监控,知道邮局的负载量;负责帮助客户端对邮局的寻址工作。客户端与邮局分配服务器采取的是UDP的连接。原因有二:

  • 这是一个非常小的通讯交互,客户端发个请求,服务端发个回答,整个开销甚至低于一个TCP的建立
  • 服务端寻址的时候非常的简单(事实也是如此,按我的方法仅仅只要一个随机和一个邮局服务器台数的循环量)。即时UDP的出错概率,在1秒内也能做到(1秒/包来回时间)的重试次数,更重要的是,UDP在高并发上有着无限的可能。

         邮局服务器: 进行客户端和BIN之间的消息转发。

  • 接受客户端的高并发连接
  • 接受BIN的连接
  • 主动向邮局分配服务器连接注册自己 ,理论上可以做到邮局服务器动态的增加

       Bin1..Bin4:主程序: 你只要为他提供好场景编号,就可以像一个独立的游戏程序跑动。尽量将一些最即时的信息(比如打斗)放在这里面运算。 
       DPC: 用来控制非即时性的消息。比如物品的邮寄,聊天消息的转发,数据存储等。 

 

代码执行模块层次

层次上分为网络层,队列层,和逻辑层。以及一个main的启动初始程序。
整个大致的工作流程就是:在接收方面,网络层收到数据后,统一放到队列里,然后由逻辑层解析,有结果返回时直接由逻辑层发送。
这样设计的原因有二:

  • 接收的时候,是一个创建各种任务的抽象过程。自然,肯定有即时任务,非即时任务这样的各种任务属性。因此创建一个队列进行同步的转换是非常必要的。
  • 发送的时候,我不进行一个队列的中间过渡,而是选择了交给逻辑层直接发送。虽然耦合度高了点,但是简单性和效率高了很多。首先,发送本身就是个即时的过程,没有优先级之分(起码在这个版本里没有);其次,发送的数据没有附加任何的事件属性,完全是个二进制流的级别,再经过一次队列的转换没什么必要。

Cpp代码  收藏代码
  1. /*  
  2.    QQ: 2#4#2#1#0#6#7#6#4    #表示为空  
  3.    Mail: lin_style#foxmail.com    #替换成@   
  4. /  
 

行为
难点

邮局路由流程图
邮局路由详细图
实例代码目录说明

算法的示例代码


行为

对于客户端:
1.    接受客户端的短连接
2.    返回给客户端一个密匙
对于邮局服务器:
1.    接受邮局服务器的主动连接并记录,理论上可以动态
2.    接受邮局服务器的定时更新
3.    筛选出邮局服务器的负载信息返回
 

难点 
粗略思考下,有这么几个纠结的地方
1.    如何做到每次都是返回最小的服务器结构
2.    客户端每次请求都是并行的(udp线程根据CPU数量启动,可以视为一个资源竞争)
3.    服务器资源的更新会不会和第一条产生资源竞争
       当然,你也可以把这块做成单线程,直接进行一个排序后算出,也非常的简单。但是我的观点是,东西总是越做越极限,虽然简单不一定高效的方法可以解决,但是还有更简单更高效更新一层的东西来等你挖掘。当然,前提是有时间。
      设计的几个方案都无可避免的要发生资源竞争。最终采取的如下,非常的优雅,不管多少并行下都不带锁。其核心毫不夸张的说只有5行左右。那就是“概率”。
      举一个简单的例子,假设有4台机器,分别是400,300,200,100人。那么它们的比例就是4:3:2:1,那么在负载均衡的情况下,假设投入 1000人,那么每台机器分配到的人数应该是100,200,300,400人。如果我们事先分配好这些比例,并且给出一个按此比例的随机函数,是不是可以非常轻松的解决所谓的高并发锁的困扰呢?比如随机1000次的实验,在0的位置上出现100次,1的位置上出现200次。。。。当然可以!再设想一下,假如我们不要求每个服务器的负载都是非常的精确话,以下的伪码就可以表现出一个流程:

      接收到客户请求,fun 随机返回一个概率比例值,send
      接受到更新请求,fun 比例重新设置

注意,在“fun比例重新设置”函数中是不加锁的,每台服务器都有对应的维护对象;而“fun随便返回一个概率比率值”函数,虽然要根据服务器人数进行重新设置比例,但是这些精度的损耗可以忽略不计(更新人数时仅仅是一个赋值过程)。
      我给出的demo中采取的是rand函数(取值范围是0-32767)。关于这个函数的缺陷有如下:

  • 假如你求余10000的话,你会发现前面几千的概率非常高
  • 加入你求余1000的话,你会发现前面700多的概率非常高

       原因很简单,最大值不是你求余数的倍数。虽然这些可以忽略,但我还是做了处理。在进行比例计算的时候,最后一个值会有一些误差(就比如10/3这样的整取),当随机到这些忽略值时我们默认给最后一台。而rand的这种缺陷,恰好使得767(我取了 1000的精度)以后的值概率较低,互相弥补了下。
      在UDP的这一块,根据CPU的个数产生对应的线程来绑定不同的端口.反正上文的方法是无锁的,跑得肯定畅快。而这些端口的信息当然是交给更新服务器给客户端,客户端也是根据一个概率来选择连接。


邮局路由流程图 


该程序里虽然用到了2个协议UDP和TCP,但是执行的动作都很简单。TCP负责在指定时间内更新自己服务器信息,UDP负责反馈这些信息给用户。在采用UDP上,我从这几个方面考虑:

  • 需求上,客户单只需要获得一个要连接的信息包即可,那么发起的动作只是简单的请求-接受这么个回合。即使UDP包出错,那么在1秒内完成这样的回合可以是十个左右(最佳情况),即时不是,那延迟个2-3秒,从登陆这个需求来说也是完全可以的。
  • 效率上,只是这一个简单的回合,建立起一个TCP花费的效率都比其高,更重要的是为这样的小回合再进行一个机器部署不合算,并且也很容易在打规模登陆的时候宕掉。UDP,无限的并发可能。即时处理不过来,也仍然屹立不倒。

邮局路由详细图

 

 

 

 

实例代码目录说明

以上的代码目录是邮局路由图,也大致体现了上文所说的框架大体样貌。因为整个流转的流程是这样的:
先来简单说下各个目录里的文件:
源文件/ 
PostofficeRoute.cpp:是个main程序,启动各种线程和网络库。其中UDP是根据CPU的数量来自动创建线程,能达到最高效使用。启动完毕后,就没main的什么事了,要做的只是等待各个线程的返回。
CRoutePublic.cpp:是一些main里公用的函数,比如拦截一些退出键,取得系统信息等
CConfigManager.cpp:配置文件
Standalone/ 
CRouteRand.cpp:一个随机的比例抽取对象,内容详见上一篇

下面是最主要的三个目录
Bridge/ 抽像NetWork和Logic之间的接口,因为邮局路由比较简单,所以没做队列的中间转换。
CUserLogicBridge/ CUserNetBridge:用户的网络接口和逻辑接口。网络接口包含了一些比如端口信息,sockaddr_in的结构信息等等,而逻辑接口里包含了对象的内存地址等等信息。这两个接口主要是为了一些信息的冗余和预留。

Logic/ 
CWorldRouteClient.cpp:网络数据提交至此的一个逻辑处理。该类里主要是一个成员函数的数组,根据协议的编号来实行自动跳转执行

NetWord/ 
CPublicSocket.cpp: 目前只包括一些协议正确与否的检测
CRouteClientUDP.cpp:包含一个UDP的网络处理程序和若干个跳转逻辑
CRouteServerTCP.cpp:同上

接下来模拟下数据流,当收到一个客户端UDP的请求后:
UDP的线程之一收到请求后,进行CPublicSocket的检测,检测通过,取到该连接的逻辑接口和网络接口,加上协议然后交给CWorldRouteClient.cpp的跳转函数到具体的实现函数里执行。

 

算法的示例代码

      在UDP的这一块,根据CPU的个数产生对应的线程来绑定不同的端口.反正上文的方法是无锁的,跑得肯定畅快。而这些端口的信息当然是交给更新服务器给客户端,客户端也是根据一个概率来选择连接。

 

Cpp代码  收藏代码
  1. /* 
  2.     VS2008下编译通过 
  3.  
  4.     如有BUG和错误,请给在下一个消息,感激不尽 
  5.     QQ: 2#4#2#1#0#6#7#6#4    #表示为空 
  6.     Mail: lin_style#foxmail.com    #替换成@  
  7. */  
  8.   
  9. // 0xtiger_Rand.cpp : 定义控制台应用程序的入口点。  
  10. //  
  11.   
  12. #include "stdafx.h"  
  13.   
  14. //  
  15. //测试的次数  
  16. const int TEST_COUNT = 40000;  
  17. //  
  18. //服务器台数  
  19. const int SERVER_NUMBER_OF = 5;  
  20. //  
  21. //服务器单台最大人数  
  22. const int SERVER_PEOPLE_SIZE_MAX = 25000;  
  23. //  
  24. //  
  25. const int PEOPLE_PRICISION_PROPORTION = 1000;  
  26.   
  27. //  
  28. //总人数  
  29. int ServerSumPeople=0;  
  30. //  
  31. //服务器信息结构体  
  32. //int ServerInfo[SERVER_NUMBER_OF];  
  33. int ServerInfo[SERVER_NUMBER_OF];  
  34. //  
  35. //服务器人数的比例存放  
  36. int ServerProportion[SERVER_NUMBER_OF];  
  37. //  
  38. //服务器人数计算比例概率的偏移值  
  39. int ServerProportionOffset[SERVER_NUMBER_OF];  
  40. //  
  41. //记录被选了多少次  
  42. int ServerSelectRecord[SERVER_NUMBER_OF];  
  43.   
  44. void Tracer_ServerSelectRecord()  
  45. {  
  46.     cout<<"server select record"<<endl;  
  47.   
  48.     for(int i=0; i<SERVER_NUMBER_OF; ++i)  
  49.     {  
  50.         cout<<"num "<<i<<":"<<ServerSelectRecord[i]<<endl;  
  51.     }  
  52.   
  53.     cout<<"*************end*************"<<endl<<endl;  
  54. }  
  55.   
  56. void Tracer_ServerInfo()  
  57. {  
  58.     cout<<"server pepole"<<endl;  
  59.   
  60.     cout<<"now sum people:"<<ServerSumPeople<<endl;  
  61.   
  62.     cout<<"max people size of a server:"<<SERVER_PEOPLE_SIZE_MAX<<endl;  
  63.   
  64.     for(int i=0; i<SERVER_NUMBER_OF; ++i)  
  65.     {  
  66.         cout<<"num "<<i<<":"<<ServerInfo[i]<<endl;  
  67.     }  
  68.   
  69.     cout<<"*************end*************"<<endl<<endl;  
  70. }  
  71.   
  72. void Tracer_ServerProportion()  
  73. {  
  74.     int i;  
  75.     cout<<"server Proportion"<<endl;  
  76.   
  77.     cout<<"max people_pricision_proportion:"<<PEOPLE_PRICISION_PROPORTION<<endl;  
  78.   
  79.     for(i=0; i<SERVER_NUMBER_OF; ++i)  
  80.     {  
  81.         cout<<"num "<<i<<":"<<ServerProportion[i]<<endl;  
  82.     }  
  83.   
  84.     cout<<"max people_pricision_proportion offset:"<<endl;  
  85.     for(i=0; i<SERVER_NUMBER_OF; ++i)  
  86.     {  
  87.         cout<<"num "<<i<<":"<<ServerProportionOffset[i]<<endl;  
  88.     }  
  89.   
  90.     cout<<"*************end*************"<<endl<<endl;      
  91. }  
  92.   
  93. void InitServerInfo()  
  94. {  
  95.     ServerInfo[0] = 1;  
  96.     ServerInfo[1] = 2;  
  97.     ServerInfo[2] = 500;  
  98.     ServerInfo[3] = 0;  
  99.     ServerInfo[4] = 0;  
  100.   
  101.     int i;  
  102.     for(i=0; i<SERVER_NUMBER_OF; ++i)  
  103.     {  
  104.         ServerSumPeople+=ServerInfo[i];  
  105.     }  
  106. }  
  107.   
  108. //  
  109. //计算比例   
  110. void CtrlProportion()  
  111. {  
  112.     int i;  
  113.     int nSumServerProportion=0;  
  114.     double d;  
  115.       
  116.     for(i=0; i<SERVER_NUMBER_OF; ++i)  
  117.     {         
  118.         //  
  119.         //判断是否超出单台上限  
  120.         if( SERVER_PEOPLE_SIZE_MAX<ServerInfo[i] )  
  121.         {  
  122.             ServerProportion[i] = 0;  
  123.         }  
  124.         else  
  125.         {                 
  126.             ServerProportion[i] = (SERVER_PEOPLE_SIZE_MAX-ServerInfo[i]) / (double)SERVER_PEOPLE_SIZE_MAX * PEOPLE_PRICISION_PROPORTION;          
  127.         }  
  128.   
  129.         nSumServerProportion += ServerProportion[i];  
  130.     }  
  131.   
  132.     for(i=0; i<SERVER_NUMBER_OF; ++i)  
  133.     {         
  134.         ServerProportion[i] = ServerProportion[i] / (double)nSumServerProportion * PEOPLE_PRICISION_PROPORTION;  
  135.     }  
  136.   
  137.     ServerProportionOffset[0]=ServerProportion[0];  
  138.     for(i=1; i<SERVER_NUMBER_OF; ++i)  
  139.     {  
  140.         ServerProportionOffset[i] = ServerProportionOffset[i-1]+ServerProportion[i];  
  141.     }  
  142.   
  143. }  
  144.    
  145. int GetRandObject(int nRandBase)  
  146. {  
  147.     for(int i=0; i<SERVER_NUMBER_OF; ++i)  
  148.     {  
  149.         int nBegin = ServerProportionOffset[i] - ServerProportion[i];  
  150.         int nEnd = ServerProportionOffset[i];  
  151.         if( nRandBase>=nBegin&& nRandBase<nEnd )  
  152.         {  
  153.             return i;  
  154.         }  
  155.     }  
  156.   
  157.     return SERVER_NUMBER_OF-1;  
  158. }  
  159.   
  160. int _tmain(int argc, _TCHAR* argv[])  
  161. {  
  162.     srand( (unsigned)time( NULL ) );  
  163.     InitServerInfo();  
  164.     CtrlProportion();  
  165.   
  166.     Tracer_ServerInfo();  
  167.     Tracer_ServerProportion();  
  168.   
  169.     int i;  
  170.     for(i=0; i<TEST_COUNT; ++i)  
  171.     {  
  172.         int nRecord;  
  173.         //  
  174.         //rand 0-32767  
  175.         nRecord = GetRandObject( rand()%PEOPLE_PRICISION_PROPORTION );  
  176.         ServerSelectRecord[nRecord]++;  
  177.         ServerInfo[nRecord]++;  
  178.         ServerSumPeople++;  
  179.   
  180.         //CtrlProportion();    //是否每次都进行比例纠正  
  181.     }  
  182.   
  183.   
  184.     Tracer_ServerSelectRecord();  
  185.   
  186.     cout<<"now server people:"<<endl;  
  187.     Tracer_ServerInfo();  
  188.   
  189.     return 0;  
  190. }  
  191.   
  192. /* 
  193. out put: 
  194. 测试一: 
  195. 在注释掉这句情况下//是否每次都进行比例纠正 
  196.  
  197. 测试次数为10000次,每台上限为2500次。(因为算法以上限数进行比例计算,超出则失衡) 
  198. 初始人数 
  199.     ServerInfo[0] = 1; 
  200.     ServerInfo[1] = 2; 
  201.     ServerInfo[2] = 500; 
  202.     ServerInfo[3] = 0; 
  203.     ServerInfo[4] = 0; 
  204. 得出结果 
  205. now server people: 
  206. server pepole 
  207. now sum people:10503 
  208. max people size of a server:2500 
  209. num 0:2086 
  210. num 1:2151 
  211. num 2:2132 
  212. num 3:2116 
  213. num 4:2018 
  214. 虽然最高值偏差到140人,差不多是5%-7%偏差(我没计算错吧),具体还跟rand()这个值有关。不过我已经非常满意 
  215. 这样的分布了。 
  216.  
  217. 测试二: 
  218. 开启注释的//是否每次都进行比例纠正 
  219. 测试次数改为20000,进行超出测试 
  220. now server people: 
  221. server pepole 
  222. now sum people:20503 
  223. max people size of a server:2500 
  224. num 0:2498 
  225. num 1:2498 
  226. num 2:2498 
  227. num 3:2498 
  228. num 4:10511 
  229. 发现千分之一的人数误差。因为超出的默认都在最后一台所以人数偏大 
  230. */  
 

 

 

Cpp代码  收藏代码
  1. QQ: 2#4#2#1#0#6#7#6#4    #表示为空    
  2. Mail: lin_style#foxmail.com    #替换成@   

行为
难点
邮局服务器流程图
邮局服务器详细图
安全

 

 

行为 
邮局服务器控制着两个对象,客户端和bin程序。

接收客户端的协议,根据协议中的信息找到bin,把信息转发向该bin
接收bin的协议,根据协议中的信息找到客户端,把信息转发向该客户端

难点 
       首先是连接对应。需要有3方的连接通讯
自己作为服务器(完成端口模型): 接受游戏客户端的连接,接受bin的连接
作为客户端:连接邮局路由,提供信息

那何BIN程序之间的通讯呢?我这里把BIN程序当成一个游戏客户端来通讯。原因有2

  • 虽然BIN和邮局服务器的通讯时双向的,但是邮局服务器是作为一个主要的中转站,有在其处登记过的才算是一个签约客户的关系,这样比较符合逻辑上的理解和功能的归纳
  • 如果将邮局服务器作为客户端进行发起连接,除了BIN服务器外,可能还有其他类型的服务器程序(比如一些全局的服务器),还要额外的对这些套接字和线程进行维护。


       其次是数据的对应。客户端和BIN都需要一个编号标识。需要知道当前客户端在那个BIN程序里。如果是做成实时的表示BIN里用户的集合情况。比如收到一个信息,获取用户名,然后再去查找这个用户在那个BIN里,接着找到BIN的套接字,最后发送。必然会非常的繁琐和复杂,其中还涉及到许多增删的动作。
       最后采取如下:(保留套接字信息)
       在三方(
 客户端,邮局,BIN )中,客户端做为独立,不参与信息冗余。邮局对连接至的BIN建立起套接字数组,客户端发送时,带上该数组索引信息,邮局进行查询后直接转发给BIN;在转发中带上客户端套接字的值和ip,port。
BIN发送时,将邮局发送的客户端原样转发,邮局收到后进行验证客户端IP是否变动,如果一致则转发。

Cpp代码  收藏代码
  1. Struct  
  2. {  
  3.     int         nBinIndex;               //发给哪个bin  
  4.     unsigned  int        nPlaySocket;  //客户端套接字  
  5.     unsigned  int     nPlayIP;         //客户端IP  
  6.     int         nPlayPort;             //客户端port;  
  7. };  

 简单示例图如下:

客户端向邮局服务器发送的

 

 

bin服务器向邮局服务器发送的

 

邮局服务器流程图


邮局服务器详细图


安全

在接受内部的连接时,需要进行IP绑定等“写死”操作
客户端来源防劫持(bin中处理,这里略提):需验证每个协议体中的IP+PORT信息
 

 

 



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值