现在的位置: 首页 > 综合 > 正文

说说游戏服务器

2013年10月06日 ⁄ 综合 ⁄ 共 4572字 ⁄ 字号 评论关闭

注:此帖来源于CSDN游戏开发论坛,作者是gantleman,非常感谢他无私的分享了自己的开发经验。以下是他的技术博客,欢迎大家支持:

http://blog.csdn.net/gantleman

 

原帖地址:http://topic.csdn.net/u/20100617/21/d9b20c80-6e95-4922-acaf-eddfcf54aaed.html

 

正文:

这是一个纯技术贴,以我做的mansterSLS作为基础,讲述该如何做一个mmorpg游戏服务器。最近心情不好写到重新入职为止。

所有最新的代码可以在https://sourceforge.net/projects/monstersls/ 目录code,svn下载。

说到网游服务器是最近火起来的,但最初的网游服务器几乎和单机游戏一样的古老那就是mudos。mudos是一个通过文字交流的古老的逻辑系统,有点类似聊天机器人,可以根据不同的客户端命令做出相应的回复,使用户维持在一个自我虚幻的环境,跟着故事情节的发展得到许多不同的命运体验。

游戏服务器处理游戏的逻辑部分,为啥不是在客户端呢?原因是游戏是一个完整的世界,所有事件都发生在这个世界里。既然是一个完整的世界那么这个世界发生的事件就要持续完整。这样所有事件都要记录在服务器供理论上所有客户端去查询。另一原因就是防止客户端篡改游戏世界的数据。请注意wow的客户没有加壳和任何通常的防止黑客的手段为什么呢?因为所有游戏世界的数据和行为都是发生在服务器。客户端的指令要经过逻辑判断,不合逻辑的指令会被判失败。一个简单的例子普通攻击指令执行过程大致是,客户端通过服务器查询看到某人,给服务器发送攻击指令,服务器计算攻击的结果,给客户端和他攻击的目标发送结果,并在服务端保存对用的数据。这里无论通过任何客户端都可以给服务器发送指令甚至是模拟器。服务器会先判断一系列的先决条件,例如攻击距离,对方是否在线等等。通过判断后计算攻击结果,这里一系列的过程客户端是无法更改的全部在服务端完成,并把这些数据共享给对应的客户端。这里我们可以看到在客户修改这些数值没有任何意义,服务判断的依据不是客户端上保存的数据。

到这里我们了解了游戏服务器就是一个大的数据库,这个数据库的特征描述如下:

1,高度并发的读取和写入。

2,数据字段不固定,可能在任何时刻增加或减少。

3,不需要模糊查询的操作,所有数据查询都是对象为基础。

等等剩下的还没有想到。

不过单从上面几点商用数据库可以pass了,即使号称对象数据库的PostgreSQL也不行,数据库的一大弊病就是为了模糊查询需要所有对象共享数据字段,这样即使某个数据不使用某个字段也要共享。会浪费大量的数据空间。游戏里面对象之间的字段也完全没有共性可言。

例如npc是一个对象,椅子也是一个对象,如果按hp,mp排序,要买椅子是hp,mp是零,要么是一个非常大的数值。因为椅子就没有hp,mp。如果npc和椅子放在数据库一个表里面椅子就要浪费mp,hp的两个段值。

C/C++ code
typedef struct dobject { map<stringstring> data; ///for share, only net object using uintptr_t index; intptr_t x,y,z; size_t stamp; list<uintptr_t> view; void* psp; }DOBJECT, *PDOBJECT;



上面代码是我在游戏里对象的定义,由一个动态增加的map段存储游戏里的逻辑数据。

下面讲到的数据和其他的系统相关会在稍后说到其他系统的时候详细介绍

例如mp,hp,物品,任务等等,

index是socket的索引在讲解网络系统的时候会讲到,

x,y,z是对象在空间的坐标,过段时间会加上方向。

stamp,是时间戳为移动加的。

view,是当前可见对象列表。

psp,是当前对象在空间矩阵的位置。

C/C++ code
typedef struct tobject {; public: uintptr_t id; PDOBJECT data; pthread_mutex_t hmutex; }TOBJECT, *PTOBJECT;



这个结构是我称为cell的结构,是一个静态数组结构的一部分。静态数组结构在服务器被普遍使用目的是通过一个全局地址指针可以快速查到相应的数据结构。因为地址指针是全局静态分配的任何情况下都不会访问失败,是对线程下交换数据最快的方式。我们看到这个结构有3部分组成,第一个是id是一个自增加整数值,每次给cell赋值新的对象都会加一,以区分被销毁的旧对象。humtex是互斥锁为了安全的访问cell内的数据对象,data指向的就是对象。

一个对象的创建的过程如下

C/C++ code
uintptr_t OBJECT::ObNew(char* obid, uintptr_t index) { PDOBJECT ob = new DOBJECT; ob->index = index; ob->psp =0; ob->stamp = 0///add ob to g_obmap uintptr_t nob=0, no=0for (size_t cursor = 0; cursor < g_maxobj; cursor++) { LOCK(g_obmap[cursor].hmutex, OBJECT::ObNew); if (NULL == g_obmap[cursor].data) { g_obmap[cursor].data = ob; nob = (uintptr_t)&(g_obmap[cursor]); sprintf(obid, "%p#%u"&g_obmap[cursor],++g_obmap[cursor].id); no = 1; } UNLOCK(g_obmap[cursor].hmutex, OBJECT::ObNew); if (no)break; } return(uintptr_t)nob; }



第一行new一个新对象,为什么不用malloc呢?因为我要使用map!对不起,我对C的狂热还不深。接下来给这个对象赋初始值,然后循环全局的cell,找到一个最近为空的cell,把刚刚创造的对象放到cell里面。使用前要锁住cell对象,lock和unlock是两个宏。

C/C++ code
///if lock or unlock fail all is unacceptable #define LOCK(mutex, fun) if(pthread_mutex_lock(&mutex) != 0)/ {/ Logf(#fun "lock fail/r/n");/ exit(0);/ } #define UNLOCK(mutex, fun) if(pthread_mutex_unlock(&mutex) != 0)/ {/ Logf(#fun "unlock fail/r/n");/ exit(0);/ }



调用pthread_mutex_lock锁住互斥锁,你也许会问你的工程不是vc的吗?这个函数是linux?这是为了避免使用#define宏定义夸平台,所有的代码尽量使用标准C和C++语法避免,因为线程是平台相关的使用了一个了跨平台的win32版本的pthread库方便代码移植到lnux。如果这两个函数失败会写log后结束程序也就是这两个函数在运行过程中不能失败。


接下来我们看

C/C++ code
sprintf(obid, "%p#%u"&g_obmap[cursor], ++g_obmap[cursor].id);



这个是在lua中使用的对象表示,在lua中对象是由地址和id组成的字符串,Lua部分我们稍后在讲。

C/C++ code
int OBJECT::ObDelete(const char* obid) { PTOBJECT tob = GET_OBJ(obid); int ret = 0; LOCK(tob->hmutex, OBJECT::ObDelete); if(NULL != tob->data && tob->id == GET_IND(obid)) { if (tob->data->index) CloseIndex(tob->data->index); delete tob->data; tob->data = NULL; ret = 1; } UNLOCK(tob->hmutex, OBJECT::ObDelete); returnret; }



这段代码讲的是对象的销毁,过程和创建相反根据对象字符串得到对象的地址调用cell锁,锁住对象判断cell对象内data是否为空和对象的id是否和当前cell的id是否相同,满足条件的话判断对象是否是一个网络连接对象,如是连接对象调用CloseIndex关闭连接,删除对象并把cell设置成空。

今天到这,以后不定期继续。

 

昨天迷迷糊糊的居然写了那么,我是做c++起步,c#和java对我来说太慢了。主线就是我开始考虑写服务器时最先想到最难得问题开始说的。对于一个游戏服务器可以多线程并发并快速读写数据是最优先考虑的问题。对于多线程并发最先考虑的是什么?加锁的位置,ok在游戏服务器最小的数据单位是什么呢?对象,ok!当我们一个线程得到一个对象并对这个对象加锁读取对象的数据。虽然游戏服务器可能有几百万个对象但一个行为受影响的对象只会有那么区区几个。这样无论任何时间点上相互等待的对象被限制在一定范围内了。这样多线程服务器就不会出现很多个线程等待一个线程的情况。捎带还要说下多线程的问题,开几个线程比较好呢?我的建议是运行平台有多少个cpu就开多少个线程,开多了,多余的线程要等待,开少了cpu会有富裕。

到目前我们知道了服务器是一个跟我们平时认识的数据库不太一样的数据库,如果有的比较的话客户端对服务器的命令就是数据库的存储过程。运行的过程大致是客户端发出对一些对象的操作指令,服务器锁住对象,操作对象的值,将指令运行的结果返回并分发到客户端。

下面要讲网络了,首先我使用的是select模型,这个模型不是最快的,但对于并发1万个链接来说每次的循环所占的cpu也算很低了。而且这个网络模型可以跨平台使用。使用了一个map用自增加一index做索引查找socket,因为socket反复创建会重复,关掉客户端链接后发送队列可能会有这个socket的数据还没有发送,清理队列操作太耗时,用一个永远不重复的索引可以辨别sokcet是否已经释放,如果释放就不发送了。下面的代码是用户socket的建立和删除。

C/C++ code
BOOL CreateSocketInformation(SOCKET s) { LPSOCKET_INFORMATION SI=&SocketArray[TotalSockets]; DEBUGOUT("Accepted socket number %d/r/n", s); SI->Socket = s; SI->index = PashSToS(SI->Socket); memset(SI->Obid, 0128); ObNew(SI->Obid, SI->index); if (0 == SI->index 

抱歉!评论已关闭.