1、 前言
最近在看网上下载的石器时代Server的代码,上周看完saac(帐号服务器)后觉得里面有些东西值得借鉴和思考。写本文的目的只是在于抛砖引玉,和各位开发游戏的同志们进行交流。
石器时代是日本人开发的网络游戏,服务器运行在linux下。代码中的注释大部分是日文,所以看的过程中有点痛苦!手上的Server代码全部都是用C写的,整体上分为2部分:gmsv和saac。按照我的理解,gmsv全称应该是game server,是和客户端打交道的游戏服务器;saac全称是stoneage account server,是用于操作后台数据的帐号服务器。目前只看完了saac部分,只能输出这部分的总结,后续看完gmsv后再写对应的心得。
2、saac框架
saac主要包括五部分功能:mail(邮件)、family(家族)、char(角色)、PK、chatroom(聊天室),lock是用于操作帐号相关数据时加锁。可能是当时玩网游的人不是很多,服务器的压力不大,并且当时的机器硬件条件有限,导致saac采用的是单进程实现。这也意味着上面5个功能系统实现时并不是真正设计意义上的子系统划分,采用的是功能函数族,互相之间耦合度较紧。另外,mail、family、char、PK、mission的数据全部都是存储在文件中,所以文件数据读写占了不小的篇幅。
gmsv与saac通信的协议为字符串协议,前2个参数固定为消息id(msgid)、请求消息名称(funcname),后面为该消息的参数,参数个数不定。
3、main详解
saac在main函数中加载完数据和配置文件后,创建了2个监听套结字:mainsockfd和worksockfd。
main主循环流程:
在main函数的while循环中mainsockfd不断接受gmsv发起的连接,接收请求消息内容后调用函数saacproto_ServerDispatchMessage进行分发处理。处理是根据消息中第2个参数funcname调用saacproto_funcname_recv进行请求处理,处理完成后调用saacproto_funcname_send发送响应结果。实际上saacproto_funcname_recv会调用funcnam对应功能子系统的簇函数进行数据操作,如果数据涉及到用户帐号,如char、family等操作前需要向lock部分请求加锁,只有加锁成功后才能对数据进行操作。
worksockfd用于接收saac内部的请求消息,调用该请求对应的回调函数,如charLoadCallback(角色数据加载)、charListCallback(列举用户帐号所有角色)、charDeleteCallback(删除角色)等。当初看到这个worksockfd时,以为会启动一个单独的进程进行这些操作,结果没想到还是在一个进程里做。
4、 套接字缓冲区
saac管理连接使用的数据结构是connection,定义如下:
struct connection { int use; //该连接是否在使用 int fd; //用于该连接通信的socket int mbtop_ri; //读取缓冲区的起始index int mbtop_wi; //写入缓冲区的起始index struct sockaddr_in remoteaddr; //客户端的IP地址 int closed_by_remote; //连接是否有客户端关闭 };
套接字的缓冲区结构体是membuf,定义如下:
struct membuf { int use; //该缓冲区对应得buf是否正在使用 char buf[512]; //存储内容 int len; //缓冲区实际存储内容的长度 int next; //下一个buf的index };
使用时的定义如下,把某个连接的缓冲区以单向链表组织起来,而查找空闲的缓冲区则是以数组的方式进行遍历。
struct membuf *mb;
struct connection *con;
使用中的连接及缓冲区的关系如下:
connection上对应的连接接收到数据时,把接收到的数据存储在mb[mbtop_ri]起始的缓冲区里。需要发送数据响应时,把待发送的数据存入mb[mbtop_wi]起始的缓冲区里,读取后再进行发送。这种缓冲区比较适用于对一些请求不需要立即处理和回复响应时,可以把响应结果先存储在connection对应的读/写缓冲区里,待主循环运行一定次数,空闲时可以对连接进行批量处理。
5、内存数据表
石器时代只是在储存角色方面使用了mysql数据库,因此,对于其它需要操作且数据量较小的指定格式类型的数据采用了内存数据表的结构进行处理。这种结构能处理的数据格式有2种:第1种数据有3列,分别是key(键值),intval(整数值), charval(字符串);第2种数据只有2列,是key和charvalue。大致思想是:所有表的记录都保存在一个记录链表中,每张表保存一个hash节点的双向链表,每条记录根据key计算hash值后找到对应的hash节点,节点中保存对应记录在记录链表中的索引。
表的结构定义如下:
struct table { int use; //标志表目前是否被使用 DBTYPE type; //表类型:对应上面提到的2种数据类型 char name[32]; //表名 int num; //表中的记录条数 int toplinkindex; //首条记录在记录链表中的index struct hashentry *hashtable; //hash节点链表 int hashsize; // hash节点链表的长度 int updated; //标志表记录内容是否需要更新到文件中 int ent_finder; //用于插入新节点时的起始查找位置 }; struct hashentry { char key[KEY_MAX]; int use; int dbind; //对应dbentry结点的index int prev; int next; }; struct dbentry { int use; int ivalue; //整数值 int prev; int next; char key[KEY_MAX]; char charvalue[CHARVALUE_MAX]; //字符串 };
程序中定义了
struct dbentry *master_buf;作为记录链表,
struct table dbt[MAXTABLE];储存MAXTABLE张表。
实际操作数据时,内存数据表具体结构如下: