系列二:游戏服务器的邮局路由

 /*
    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
发现千分之一的人数误差。因为超出的默认都在最后一台所以人数偏大
*/
 

猜你喜欢

转载自lin-style.iteye.com/blog/571309