- /*
- QQ: 2#4#2#1#0#6#7#6#4 #表示为空
- Mail: lin_style#foxmail.com #替换成@
- */
核心,我的并行思路
整体拓扑图
代码执行模块层次
核心,我的并行思路
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的启动初始程序。
整个大致的工作流程就是:在接收方面,网络层收到数据后,统一放到队列里,然后由逻辑层解析,有结果返回时直接由逻辑层发送。
这样设计的原因有二:
- 接收的时候,是一个创建各种任务的抽象过程。自然,肯定有即时任务,非即时任务这样的各种任务属性。因此创建一个队列进行同步的转换是非常必要的。
- 发送的时候,我不进行一个队列的中间过渡,而是选择了交给逻辑层直接发送。虽然耦合度高了点,但是简单性和效率高了很多。首先,发送本身就是个即时的过程,没有优先级之分(起码在这个版本里没有);其次,发送的数据没有附加任何的事件属性,完全是个二进制流的级别,再经过一次队列的转换没什么必要。
- /*
- QQ: 2#4#2#1#0#6#7#6#4 #表示为空
- Mail: lin_style#foxmail.com #替换成@
- /
行为
难点
邮局路由流程图
邮局路由详细图
实例代码目录说明
算法的示例代码
行为
对于客户端:
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的个数产生对应的线程来绑定不同的端口.反正上文的方法是无锁的,跑得肯定畅快。而这些端口的信息当然是交给更新服务器给客户端,客户端也是根据一个概率来选择连接。
- /*
- VS2008下编译通过
- 如有BUG和错误,请给在下一个消息,感激不尽
- QQ: 2#4#2#1#0#6#7#6#4 #表示为空
- Mail: lin_style#foxmail.com #替换成@
- */
- // 0xtiger_Rand.cpp : 定义控制台应用程序的入口点。
- //
- #include "stdafx.h"
- //
- //测试的次数
- const int TEST_COUNT = 40000;
- //
- //服务器台数
- const int SERVER_NUMBER_OF = 5;
- //
- //服务器单台最大人数
- const int SERVER_PEOPLE_SIZE_MAX = 25000;
- //
- //
- const int PEOPLE_PRICISION_PROPORTION = 1000;
- //
- //总人数
- int ServerSumPeople=0;
- //
- //服务器信息结构体
- //int ServerInfo[SERVER_NUMBER_OF];
- int ServerInfo[SERVER_NUMBER_OF];
- //
- //服务器人数的比例存放
- int ServerProportion[SERVER_NUMBER_OF];
- //
- //服务器人数计算比例概率的偏移值
- int ServerProportionOffset[SERVER_NUMBER_OF];
- //
- //记录被选了多少次
- int ServerSelectRecord[SERVER_NUMBER_OF];
- void Tracer_ServerSelectRecord()
- {
- cout<<"server select record"<<endl;
- for(int i=0; i<SERVER_NUMBER_OF; ++i)
- {
- cout<<"num "<<i<<":"<<ServerSelectRecord[i]<<endl;
- }
- cout<<"*************end*************"<<endl<<endl;
- }
- void Tracer_ServerInfo()
- {
- cout<<"server pepole"<<endl;
- cout<<"now sum people:"<<ServerSumPeople<<endl;
- cout<<"max people size of a server:"<<SERVER_PEOPLE_SIZE_MAX<<endl;
- for(int i=0; i<SERVER_NUMBER_OF; ++i)
- {
- cout<<"num "<<i<<":"<<ServerInfo[i]<<endl;
- }
- cout<<"*************end*************"<<endl<<endl;
- }
- void Tracer_ServerProportion()
- {
- int i;
- cout<<"server Proportion"<<endl;
- cout<<"max people_pricision_proportion:"<<PEOPLE_PRICISION_PROPORTION<<endl;
- for(i=0; i<SERVER_NUMBER_OF; ++i)
- {
- cout<<"num "<<i<<":"<<ServerProportion[i]<<endl;
- }
- cout<<"max people_pricision_proportion offset:"<<endl;
- for(i=0; i<SERVER_NUMBER_OF; ++i)
- {
- cout<<"num "<<i<<":"<<ServerProportionOffset[i]<<endl;
- }
- cout<<"*************end*************"<<endl<<endl;
- }
- void InitServerInfo()
- {
- ServerInfo[0] = 1;
- ServerInfo[1] = 2;
- ServerInfo[2] = 500;
- ServerInfo[3] = 0;
- ServerInfo[4] = 0;
- int i;
- for(i=0; i<SERVER_NUMBER_OF; ++i)
- {
- ServerSumPeople+=ServerInfo[i];
- }
- }
- //
- //计算比例
- void CtrlProportion()
- {
- int i;
- int nSumServerProportion=0;
- double d;
- for(i=0; i<SERVER_NUMBER_OF; ++i)
- {
- //
- //判断是否超出单台上限
- if( SERVER_PEOPLE_SIZE_MAX<ServerInfo[i] )
- {
- ServerProportion[i] = 0;
- }
- else
- {
- ServerProportion[i] = (SERVER_PEOPLE_SIZE_MAX-ServerInfo[i]) / (double)SERVER_PEOPLE_SIZE_MAX * PEOPLE_PRICISION_PROPORTION;
- }
- nSumServerProportion += ServerProportion[i];
- }
- for(i=0; i<SERVER_NUMBER_OF; ++i)
- {
- ServerProportion[i] = ServerProportion[i] / (double)nSumServerProportion * PEOPLE_PRICISION_PROPORTION;
- }
- ServerProportionOffset[0]=ServerProportion[0];
- for(i=1; i<SERVER_NUMBER_OF; ++i)
- {
- ServerProportionOffset[i] = ServerProportionOffset[i-1]+ServerProportion[i];
- }
- }
- int GetRandObject(int nRandBase)
- {
- for(int i=0; i<SERVER_NUMBER_OF; ++i)
- {
- int nBegin = ServerProportionOffset[i] - ServerProportion[i];
- int nEnd = ServerProportionOffset[i];
- if( nRandBase>=nBegin&& nRandBase<nEnd )
- {
- return i;
- }
- }
- return SERVER_NUMBER_OF-1;
- }
- int _tmain(int argc, _TCHAR* argv[])
- {
- srand( (unsigned)time( NULL ) );
- InitServerInfo();
- CtrlProportion();
- Tracer_ServerInfo();
- Tracer_ServerProportion();
- int i;
- for(i=0; i<TEST_COUNT; ++i)
- {
- int nRecord;
- //
- //rand 0-32767
- nRecord = GetRandObject( rand()%PEOPLE_PRICISION_PROPORTION );
- ServerSelectRecord[nRecord]++;
- ServerInfo[nRecord]++;
- ServerSumPeople++;
- //CtrlProportion(); //是否每次都进行比例纠正
- }
- Tracer_ServerSelectRecord();
- cout<<"now server people:"<<endl;
- Tracer_ServerInfo();
- return 0;
- }
- /*
- out put:
- 测试一:
- 在注释掉这句情况下//是否每次都进行比例纠正
- 测试次数为10000次,每台上限为2500次。(因为算法以上限数进行比例计算,超出则失衡)
- 初始人数
- ServerInfo[0] = 1;
- ServerInfo[1] = 2;
- ServerInfo[2] = 500;
- ServerInfo[3] = 0;
- ServerInfo[4] = 0;
- 得出结果
- now server people:
- server pepole
- now sum people:10503
- max people size of a server:2500
- num 0:2086
- num 1:2151
- num 2:2132
- num 3:2116
- num 4:2018
- 虽然最高值偏差到140人,差不多是5%-7%偏差(我没计算错吧),具体还跟rand()这个值有关。不过我已经非常满意
- 这样的分布了。
- 测试二:
- 开启注释的//是否每次都进行比例纠正
- 测试次数改为20000,进行超出测试
- now server people:
- server pepole
- now sum people:20503
- max people size of a server:2500
- num 0:2498
- num 1:2498
- num 2:2498
- num 3:2498
- num 4:10511
- 发现千分之一的人数误差。因为超出的默认都在最后一台所以人数偏大
- */
- QQ: 2#4#2#1#0#6#7#6#4 #表示为空
- 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是否变动,如果一致则转发。
- Struct
- {
- int nBinIndex; //发给哪个bin
- unsigned int nPlaySocket; //客户端套接字
- unsigned int nPlayIP; //客户端IP
- int nPlayPort; //客户端port;
- };
简单示例图如下:
客户端向邮局服务器发送的
bin服务器向邮局服务器发送的
邮局服务器流程图
邮局服务器详细图
安全
在接受内部的连接时,需要进行IP绑定等“写死”操作
客户端来源防劫持(bin中处理,这里略提):需验证每个协议体中的IP+PORT信息