【服务器架构】服务器框架与redis
来源:互联网 发布:网络除草是什么意思 编辑:程序博客网 时间:2024/05/21 13:22
折腾了好久,终于可以开始正式项目开发了。
之前的这段日子,我们陷落在公司的股权分配问题中,纠结于到底需要几个人到位才启动;更是反复讨论,到底应该做个怎样的游戏。林林总总,终于,在已经到位的几位同学的摩拳擦掌中,叮当决定自己挂帅开始干了。
就这么不到十个人,空旷的办公室,跟我们起先想像的情况不太一样。尤其是主策划还没有落定。我说,叮当,你好歹也是一资深游戏玩家,带了这么多年的游戏部,跟了这么多成功的项目,没吃过猪肉总见过猪跑吧,我就不相信你干着会比别人差。若不是我必须盯着程序实现这块,我都想自己做主策划了。不过有你干,我放心。
主策划的位置,咱们可以先空着,前期工作不能延。产品经理代理一下游戏主策划的位置也是创业公司必须的;正如我这挂牌的 CTO ,除了负责系统架构以外,也得兼个程序员做实现嘛。
经过两天对项目计划表的讨论后,我们今天就算正式开工了。
游戏是怎样的?半保密,不在这里多写,怕大家骂。再说了,这个东西现在说是怎样的,一两年后肯定有变化,多说无益,多解释无益。简单说呢,就是一个战斗系统类似魔兽世界,但系统核心玩法差异很大,更为轻松,更偏重 PvP 的 MMORPG 。为什么是这样一个东西,不想解释,反正不是拍脑袋想出来的。
既然是开发笔记,就写写现在在做些啥,打算怎样做下去。
我们先集体玩了一周魔兽世界,虽然大部分人已经是 wow 老玩家,但还是有一两个人玩的比较少,大家一起熟悉一下。主要是熟悉战斗系统,还有美术风格的感觉。培养点兴趣,对未来我们自己做的东西至少在表层上大家都有点感觉,有爱。
瞥开美术和策划的计划不谈,主要写写程序的计划。
在 Closed Beta 前,有 8 个历程碑,到一期截至,就开始有内部运行的版本。这次完全抛弃以往在网易积累的任何一点成品,全部从零开始做。但是,对于程序员来说,一切都在脑子里,其实工作量并不大。有机会重头来,恐怕是大部分程序员梦寐以求的机会。
当然,我们购买了一套 3d engine (不介绍,不解释),起点高一些。服务器也会尽量用一些成熟的开源库。
一期打算实现基本的用户登陆,场景漫游,包括在场景中做各种跑、跳、走、骑乘等动作。
一期程序员三人:
云风:负责总体规划,总体协议设计,以及部分服务器模块的实现。
怪物公司:负责客户端的设计与实现。
蜗牛:负责服务器的细节设计与实现。
吵架是我们的传统,自今天就开始了。按照惯例,我无法说服项目组认可我的所有设计和技术选择。不过大家妥协的结果是,先按我的想法做,一期雏形在下周末前完成,根据实现过程中遇到的问题,我们在修正甚至全部重构目前的设计。一周半时间的代价是目前我们可以承受的。
ps. 程序员就是这么一种奇怪的生物。好的程序员都有自己独立的思想。对自己实现或即将实现的代码有爱。按照别人的思想去实现是件无比痛苦的事情,会觉得在浪费自己的生命。所以,大部分有活力的项目开始都是一个人建立起来的。在大公司,好多老程序员都喜欢招聘所谓有潜力的新人,认为他们白纸一张,好塑造。说到底就是听话。但事实的结果一般是,要么培养出来一个庸才,无法担当;要么,在技术选择上最终分道扬镳。我总是对他们说,想想你愿不愿意总听着别人的意见干活?如果你不愿意,那么就别指挥别人干。自己不愿意做的事情,就别让别人帮你做。
我的设计草稿是这样的:
整个游戏系统(一期工程需要的)大体由这样一些部分组成:
客户端,运行在玩家的终端上,用一条 TCP 连接和系统通讯。它接入游戏服务器网关。
网关,负责汇总所有客户端,大体上和我很多年前的想法一致 ,虽然会有一些实现上的小改变,但思想没有根本变化。
客户代理(Agent) ,运行在网关的后端,在逻辑上,每个客户端都有一个 agent 负责翻译和转发客户端发来的请求,以及回应。众多 agent 可以实现在同一个进程中,也可以是多个不同的进程里。可以用脚本虚拟机的形式跑,也可以是别的形式,这些都不太重要。这一次,我们最大可能使用独立的 lua state 来实现单个 agent。
数据服务,保存玩家数据,场景数据等。前端用自己的代码和协议同系统其它部分沟通,后端这次想采用 redis 做储存。
场景管理器,用于管理静态场景和动态副本。
若干场景服务器,用于玩家在里面做漫游。
网关之后的服务是相互信任的,并组成一个虚拟网络可以相互通讯。通讯底层暂时采用 zeromq ,通讯协议采用 google protobuf 。
客户端到 agent 的通讯协议与网关后各个逻辑结点之间的通讯协议将隔离,分开设计。甚至不保证采用一致的技术实现方案,以及协议设计风格。
这里的设计关键在于 Agent 的设计,大部分的工作量是围绕它而来的。
单个 Agent 的工作流程代表了项目一期的结果会展现的东西,大体上逻辑流程如下:
等待用户认证
把认证系统交给认证服务器认证,失败则返回 1 重试。
从数据库获取用户数据(一期数据很少,仅有默认的用户名,用户形象) ,并转发给客户端。
取得用户所在场景号,从场景管理器取得场景服务的位置,并申请加入场景。
从场景服务器,同步环境。
转发用户在场景中漫游的请求,并同步场景的实时数据。
一旦客户端发生异常或得到推出消息,通知场景服务器离开。
这里,和历史上我们的游戏服务器设计有一个小的不同。我认为,用户的角色在场景中的位置,动作状态,甚至以后的战斗数值状态,都是属于场景数据的一部分,而不是用户数据的一部分。
用户数据中,和场景有关的只包括他当前所属的场景。由场景自己来保存角色在场景中的状态数据。简单而具体的说,类似网易历史上的西游系列,玩家的坐标是玩家的属性之一,是会在持久化时存放在玩家数据里的。经过我这两年的思考,我觉得这种设计方法是有问题的。
从我现在的直觉上来说,虚拟角色的属性不应包括角色的地理位置信息。那是角色的外在约束。而场景更应该拥有在它其中的 PC 和 NPC 的位置以及各种状态。PC 下线只应该认为是 PC 处于一种特殊状态(不被周围的 PC/NPC 所见或影响),而并没有脱离这个场景。所以我更愿意把 PC 下线定义成 detach ,而上线则是 attach 的操作。在这点上,场景,作为一个实体,拥有它自己的数据逻辑。
agent 相关的协议粗略的设计如下:
- auth 登陆认证
- user/pass 取得 userid ,返回成功或各种失败
- userdb
- 用 userid 取得 avatar list
- 用 avatar 取得 avatar data (包括场景名)
- scene manager
- 用 场景名 取得 scene id
- 用 scene id 取得场景状态,返回允许进入或各种拥堵状态
- scene
- 把 avatar attach 到场景
- 取得 avatar 的场景上下文
- 把 avatar detach 出场景
- avatar 设置坐标,速度,行为(跑,走,站立),方向
- avatar 骑乘状态改变
- avatar 做特定动作
好吧,未来一周的工作任务就是这些了。需要明天再做细化整理。然后就是实现了。
按照我们一期项目的需求,昨天我简单设计了数据库里的数据格式。数据库采用的是 Redis ,我把它看成一个远端的数据结构保存设备。它提供基本的 Key-Value 储存功能,没有层级表。如果需要两层结构,可以在 Value 里保存一组 Hashes 。
这是我第一次实战使用 Redis ,没有什么经验。不过类似的设施几年前自己实现过,区别不大。经过这几年,有了 Redis 这个开源项目,就不需要重造轮子了。但其模式还是比较熟悉的。也就是说,是按我历史经验来使用 Redis 。
一期项目需要比较简单,不打算把数据拆分到不同的数据服务器上。但为日后的拆分需求做好设计准备。以后有需要,可以按 Key 的前缀把数据分到不同的位置。例如,account 信息是最可能独立出去的,因为它和具体游戏无关。
用户系统使用 email 来做用户名,但在数据库中的唯一标识是一个 uid 。用户应该允许修改登陆名(用户很可能更换 email)。用户的身份识别是用 id 来定位的。所以,在数据库中就应该有如下几组 Key :
- account:count id
- account:userlist set(id)
- account:email:[email] id
这里,account:userlist 对应的 value 是一个 set ,里面存放了所有存在的 user id 。用于遍历所有的 user 。这个暂时可能用不上,而且当用户量相当大的时候可能有问题。不过暂时不用考虑这么多问题,等以后改进。
account:count 是一个计数器,可以用来生成唯一 id 。
account:email:[email] 用来标示每个注册的 account 的登陆名。[email] 指登陆用 email 地址。
这里,email 内可能也存在符号 ":" ,为了回避这个问题,许多对 email 进行编码。我的方案是,将字母数字 @ . _ 之外的字符编码为 %XX 的形式。用 lua 干这件事情非常简单:
local function _encode(str) return string.format("%%%02X",string.byte(str))endfunction emailEncode(str) return string.gsub(str,"([^%w_@.])",_encode)end
当然,解码回来也很简单
local function _decode(str) return string.char(tonumber(str,16))endfunction emailDecode(str) return string.gsub(str,"%%(%w%w)",_decode)end
之后,就是 account 下每个 id 的数据:
- account:[id]:version number
- account:[id]:email string
- account:[id]:password string // md5(password..salt)
- account:[id]:nickname string
- account:[id]:lastlogin hashes
- ip string
- time string
- account:[id]:history list(string)
- account:[id]:available enum(open/locked/delete)
其中,密码不想保存为明文。因为任何可能的数据泄露都会导致用户的损失,我也不想任何人看到用户的密码。所以采用 md5(password .. salt) 的风格。
md5 运算前,加一个 salt 后缀,是因为单纯的文本 md5 值也是有数据库可查的。
lastlogin 下保存了用户最后一次登陆的信息,使用了一张 hashes 表,因为这些信息在未来会进一步扩充。
history 保存了用户登陆的所有历史记录,用一个 string 链表记录。
用户删除自己的账户时,不想把数据从数据库删除,只想在 available 下做一个标记。
考虑到数据库内数据结构有可能发生变化,所以加了 version 域做版本标识。
我不想让各种服务可以直接读写这份数据,所以,会单独写一个认证服务器做处理。
认证服务器提供三项服务:
用户注册
用户名 密码 认证 (用于 ssl 连接上的 web 服务)
用户名 密码 挑战式认证 (用于 client 的认证服务)
下面是基本的场景服务用的数据:
- account:[id]:avatars set(id)
- avatar:count id
- avatar:[id]:version number
- avatar:[id]:account id
- avatar:[id]:scene string
- avatar:[id]:available enum(open/delete)
- avatar:[id]:data hashes
- name string
- figure string
- world:scene hashes
- [name] id
- scene:count id
- scene:[id]:name string
- scene:[id]:available enum(open/close/delete)
- scene:[id]:info hashes
- time string
- pc number
- scene:[id]:pc hashes
- [id] enum[online/offline]
- scene:[id]:pc:[id] hashes
- status string
用户账号下可以有许多游戏角色,列表放在 account:[id]:avatars 下。
每个角色也拥有一个唯一 id 。这个 id 原则上和 account id 是独立体系,但是为了人类好区格,avatar:count 的起点和 account:count 不同。
角色所在场景记录一个字符串的场景名 avatar:[id]:scene ,角色的其它各种数据放在一个 hashes 里。
所有的场景索引方在 world:scene 下。如果日后有多个世界,可以采用 world:[id]:scene 。但目前不必考虑。
scene 下面的所有 pc 的在线状态放在 scene:[id]:pc hashes 中,pc 离线也把它的 id 记录在内,只有 pc 转移场景才移除。
每个 PC 的位置状态信息记录在 scene:[id]:pc:[id] 中,第一个 id 是 scene 的 id ,第二个则是 PC 的 avatar id 。
btw. 这是一份草稿,虽然思考不周,但足够满足项目一期的需求。当然许多欠考虑的地方也并非是考虑不到,而是希望尽量简单,以满足一期需求为目的。这个日后修改的代价并不大。
最后吐槽一下 Redis 的 Windows 版。办公室的 Linux 服务器还没有装好,我暂时在 Windows 下做开发。取了一份 google 搜到的非官方 Redis 的 Windows 版 。为了图方便,使用的是 luajit ffi 去调用 hiredis 的 dll 。一开始怎么都搞不定。建立不了 socket 连接,出错码也取不到。
对比了源代码,发现修改版把 C Struct 结构改了,前面增加了几个域,而我以 hiredis 官方标准来定义的接口。
改好后,能够正确取出出错码了。发现万恶的 Windows socks api 需要调用 WSAStartup 才可以用。而 hiredis 的 Windows 修改版居然没有去调用。让我大费周折才改好,前后折腾了一个多小时。
再吐槽一下 hiredis 的 API 设计,居然依赖 C Struct 的布局。良好的 C 库的接口设计不会这么干的吧。比如 lua ,又比如 zmq 。唉,用这种东西有点小不爽。不过比 C++ 库还是好太多了。
我记得是前天看到的这个。
然后花了点时间去琢磨redis,感觉效率异常的高效。
我现在是这样。
数据落地的时候, 先根据mysql的结构入一份 mysql 一份序列化到 redis 。create的时候, 先去redis找,如果找不到。直接去 mysql读。
把所有数据反序列化出来 也就<5ms 而已。
而之前 去mysql 读取各种 邮件, 道具, 好友。。。基本要> 50 ms
- 【服务器架构】服务器框架与redis
- 服务器设计与架构
- redis 服务器启动与关闭
- 服务器架构与“军事战争”
- laravel 框架 服务器部署 连接其他服务器 redis 和 mysql
- 淘宝服务器架构框架图,简单实现....
- 淘宝服务器架构框架图,简单实现....
- 一步一步架构 react 服务器渲染同构框架
- redis服务器
- Redis 服务器
- redis服务器
- Redis服务器
- Redis 服务器
- Redis服务器
- Redis 服务器
- Web服务器与Web框架
- 服务器架构
- 服务器架构
- Group-only ACL的一些知识
- 肥黄金价格刚回家就哭了
- --数组元素插入有两种方式
- adb device找不到设备
- 创建试点(访问控制对象)
- 【服务器架构】服务器框架与redis
- 且学且生活
- 关于读写锁
- eclipse打开当前文件所在文件夹的两种方法
- 【SDnoip夏令营】【day7&&总结】
- 编写ruby扩展库
- [OpenGL ES 01]OpenGL ES之初体验
- VC中宽字符串换行(WCHAR字符串换行)
- 人同意让他鱼同一条