swoole协程+zephir纯php开发大型RPG微信小游戏(已开源)

Yvonne ·
更新时间:2024-11-13
· 771 次阅读

概述

我在一年前,帮助朋友构建游戏RPG后台的时候,思考如何兼顾开发效率和性能,最终想到了 php + swoole协程 + swoole_orm + zephir ,微信小游戏搜索:“剑的传说”

swoole协程有着极高的IO并发能力

swoole_orm 是我开发的php 扩展,有着非常高的性能、sql安全性和开发效率,开源地址为: https://github.com/swoole/ext-orm

游戏的战斗部分完全用 zephir 来实现,宣称像写php一样写php扩展,能做到同时兼顾性能和开发效率,(zephir 代码有机会我再开源出来,目前时机不成熟,游戏还比较火热)

后台框架开源地址:https://github.com/caohao-php/ycsocket

在这里插入图片描述

仅展示部分zephir代码:
在这里插入图片描述

代码结构 ———————————————— |--- server.php //启动入口 |--- system //框架系统代码 |--- application //业务代码 |----- config //配置目录 |----- controller //控制器目录 |------ Game.php //Game控制器 |----- dao //数据层 |----- library //公用类库 |----- service //业务层 请求路由 webSocket.send('{"c":"game","m":"ver", "userid":123593}');

输入参数为json, 根据 c 和 m 参数,路由到 controller/Game.php 下 verAction 函数。路由逻辑在 Application->run() 方法中,
路由之前,首先会调用 Filter::auth($params) 对参数验签,我们可以在该函数中加入自己的签名验证逻辑。

//system/Application.php class Application { public function run(& $params, $clientInfo) { $ret = Filter::auth($params); if ($ret != 0) { return $ret; } foreach ($params as $k => $v) { $params[$k] = trim($v); } $controller = ucfirst($params['c']); $action = $params['m'] . "Action"; $class_name = $controller . "Controller"; try { $obj = new $class_name($params, $clientInfo); if (!method_exists($obj, $action)) { unset($obj); show_404("$controller/$action"); return $this->response_error(3, "route error"); } $ret = $obj->$action(); unset($obj); return $ret; } catch (Exception $e) { unset($obj); if ($e instanceof LogicException) { //业务异常 $errorcode = $e->getCode() == 0 ? 8 : $e->getCode(); return $this->response_error($errorcode, $e->getMessage()); } else if ($e->getMessage() != 'swoole exit.') { Logger::error("Catch An Exception File=[" . $e->getFile() . "|" . $e->getLine() . "] Code=[" . $e->getCode() . "], Message=[" . $e->getMessage() . "]", "exception_log"); echo "Catch An Exception \n"; echo "File:" . $e->getFile() . "\n"; echo "Line:" . $e->getLine() . "\n"; echo "Code:" . $e->getCode() . "\n"; echo "Message:" . $e->getMessage() . "\n"; return $this->response_error(99, "system exception"); } else { echo "swoole exit.\n"; return $this->response_error(99, "application exit"); } } } ... } 控制器Controller

所有控制器位于:application/controllers 目录下,继承自SuperController,父类SuperController 的构造函数中会调用$this->init()函数,所以你的控制器如果有初始化任务,请写在 init 函数里。

提供4个返回函数:
response_error 返回报错信息给自己
response_success_to_all 返回数据给当前所有玩家,例如世界聊天
response_success_to_me 返回数据给自己
response_success_to_uids 返回数据给指定uid,在 server.php ,数据接入的时候,我们会将 uid 绑定到 socket fd 上面去

//server.php $uid = intval($input['userid']); if($uid > 0) { Connector::set_fd($uid, $ws); } class GameController extends SuperController { var $game_service; var $userinfo_service; public function init() { $this->userinfo_service = $this->loader->service('UserinfoService'); $this->game_service = $this->loader->service('GameService'); $this->util_log = $this->loader->logger('game_log'); } //聊天接口 public function chatAction() { $userId = $this->params['userid']; $type = intval($this->params['type']); //0-世界 1-私聊 $token = $this->params['token']; $nickname = $this->params['nickname']; $avatar_url = $this->params['avatar_url']; $content = $this->params['content']; $to_userid = intval($this->params['to_userid']); $this->userinfo_service->getZoneUserAndAuth($userId, $token); if (empty($content)) { return $this->response_error(13342339, '内容不能为空'); } $result = array(); $result['userid'] = $userId; $result['type'] = $type; $result['nickname'] = $nickname; $result['avatar_url'] = $avatar_url; $result['gender'] = $this->params['gender']; $result['vip_level'] = $this->params['vip_level']; $result['lv'] = $this->params['lv']; $result['content'] = $content; if ($type == 0) { return $this->response_success_to_all($result); } else if ($type == 1) { return $this->response_success_to_uids([$userId, $to_userid], $result); } } } 过滤验签

application/Filter.php , 在 auth 中写入验签方法,所有接口都会在这里校验, 所有GET、POST等参数放在 $params 里。

class Filter { //验签过程 public static function auth(& $params) { /* if($auth_error == false) { //验签失败 return self::response_error(123, "auth error"); } */ //验签成功 return 0; } public static function response_error($code, $message) { $data = array("code" => $code, "msg" => $message); $result['send_user'] = "me"; $result['msg'] = json_encode($data); return $result; } } 加载器

通过 Loader 加载器可以加载业务层,dao层,公共库,日志、配置等对象, Logger 为日志类。

$this->game_service = $this->loader->service('GameService'); $this->game_dao = $this->loader->dao("GameDao"); $this->util_log = $this->loader->logger('game_log'); $this->util_lib = $this->loader->library('Utillib'); $this->conf = $this->loader->config('config'); 业务层

通过 $this->game_service = $this->loader->service(‘GameService’); 去加载业务层。
Service 继承自 SuperService,在 init() 函数里面实现对象初始化内容。

class GameService extends SuperService { public function init() { parent::init(); $this->game_dao = $this->loader->dao("GameDao"); $this->userinfo_service = $this->loader->dao("UserinfoService"); $this->util_log = $this->loader->logger('game_log'); } //用户充值 public function get_user_vip_contents($userid) { $data = $this->game_dao->get_user_vip_contents($userid); if (empty($data['content'])) { $content = array(); $content['leiji_xiaofei'] = 0; //累计消费 $content['leiji_chong'] = 0; //累计充值 $content['jijin']['status'] = 0; //是否购买成长基金 0-未购买 1-已购买 $this->game_dao->insert_user_vip_contents($userid, $content); } else { $content = json_decode($data['content'], true); } return $content; } //更新充值信息 public function update_user_vip_contents($userid, $content) { return $this->game_dao->update_user_vip_contents($userid, $content); } ... } Dao层

所有与Redis、MySQL等等存储介质打交道的逻辑,最好都放在Dao层,
dao对象通过 $this->game_dao = $this->loader->dao(“GameDao”); 加载。
Dao层继承自 SuperDao,在 init() 函数里面实现对象初始化内容。
SuperDao 提供了许多快速操作数据库的方法,如果你需要用到 SuperDao 的快速操作数据库的函数,
你最好指定以下数据库、缓存配置,因为默认他们是 default, 这些配置位于application/config 目录下的 database.php 和 redis.php 中。

$this->redis_name = “default”;
$this->db_name = “default”;

class GameDao extends SuperDao { public function init() { $this->db_name = "game"; $this->util_log = $this->loader->logger('game_log'); } //user_vip_contents 表 public function get_user_vip_contents($userid) { $key = 'pre_vip_contents_' . $userid; $data = $this->get_one_table_data('user_vip_contents', ['user_id' => $userid], $key); return $data; } public function insert_user_vip_contents($userid, $content) { $key = 'pre_vip_contents_' . $userid; return $this->insert_table('user_vip_contents', ['user_id' => $userid, 'content' => json_encode($content)], $key); } ... } //数据库配置 database.php $util_db_config['default']['host'] = '127.0.0.1'; $util_db_config['default']['username'] = 'test'; $util_db_config['default']['password'] = 'test'; $util_db_config['default']['dbname'] = 'user'; $util_db_config['default']['char_set'] = 'utf8'; $util_db_config['default']['dbcollat'] = 'utf8_general_ci'; $util_db_config['default']['pool_size'] = 10; //redis配置 redis.php $util_redis_conf['userinfo']['host'] = '127.0.0.1'; $util_redis_conf['userinfo']['port'] = 6381; $util_redis_conf['userinfo']['auth'] = 'o01nc7vgd65xa'; //使用方法 MySQLPool::instance('default')->query($sql); MySQLPool::instance('default')->get($table, $where, $column); RedisPool::instance('userinfo')->set('test', 123); RedisPool::instance('userinfo')->expire('test', 86400); library库

第三方类库都存在于 application/library 目录下 ,通过$this->utillib = $this->loader->library(“Utillib”); 实例化。

日志

日志可以通过 loader 实例化,实例化的日志会打印有请求参数和客户端IP等信息,也可以用得静态函数,不过静态函数无法获取则请求参数或者客户端IP等信息。

日志路径在 server.php 中配置,记得把 /data/app/logs 的权限设置高些,define(‘LOG_PATH’, ‘/data/app/logs/super_server’); //日志目录

日志分如下5个级别:
const DEBUG = ‘DEBUG’; /* 级别为 1 , 调试日志, 当 DEBUG = 1 的时候才会打印调试 /
const INFO = ‘INFO’; /
级别为 2 , 应用信息记录, 与业务相关, 这里可以添加统计信息 /
const NOTICE = ‘NOTICE’; /
级别为 3 , 提示日志, 用户不当操作,或者恶意刷频等行为,比INFO级别高,但是不需要报告*/
const WARN = ‘WARN’; /* 级别为 4 , 警告, 应该在这个时候进行一些修复性的工作,系统可以继续运行下去 /
const ERROR = ‘ERROR’; /
级别为 5 , 错误, 可以进行一些修复性的工作,但无法确定系统会正常的工作下去,系统在以后的某个阶段, 很可能因为当前的这个问题,导致一个无法修复的错误(例如宕机),但也可能一直工作到停止有不出现严重问题 */

class GameService extends SuperService { public function init() { parent::init(); $this->util_log = $this->loader->logger('game_log'); } public funciton test() { $this->util_log->LogInfo("info test"); $this->util_log->LogNotice("notice test"); $this->util_log->LogWarn("warning test"); $this->util_log->LogError("error test"); } public funciton static_test() { Logger::info("static info test"); Logger::notice("static notice test"); Logger::warn("static warning test"); Logger::error("static error test"); } } 附录 - CoreModel 中的辅助极速开发函数(不关心可以跳过) /** * 根据key获取表记录 * @param string redis_key redis 缓存键值 */ public function hget_redis($redis_key, $field); /** * 设置 redis 值 * @param string redis_key redis 缓存键值, 可空, 非空时清理键值缓存 * @param array data 表数据 * @param int redis_expire redis 缓存到期时长(秒) * @param boolean set_empty_flag 是否缓存空值,如果缓存空值,在表记录更新之后,一定记得清理空值标记缓存 */ public function hset_redis($redis_key, $field, $data, $redis_expire = 600, $set_empty_flag = true); /** * 根据key获取表记录 * @param string redis_key redis 缓存键值 */ public function get_redis($redis_key) /** * 设置 redis 值 * @param string redis_key redis 缓存键值, 可空, 非空时清理键值缓存 * @param array data 表数据 * @param int redis_expire redis 缓存到期时长(秒) * @param boolean set_empty_flag 是否缓存空值,如果缓存空值,在表记录更新之后,一定记得清理空值标记缓存 */ public function set_redis($redis_key, $data, $redis_expire = 600, $set_empty_flag = true); /** * 清理记录缓存 * @param string redis_key redis 缓存键值 */ public function clear_redis_cache($redis_key = ""); /** * 插入表记录 * @param string table 表名 * @param array data 表数据 * @param string redis_key redis 缓存键值, 可空, 非空时清理键值缓存 */ public function insert_table($table, $data, $redis_key = ""); /** * 更新表记录 * @param string table 表名 * @param array where 查询条件 * @param array data 更新数据 * @param string redis_key redis 缓存键值, 可空, 非空时清理键值缓存 */ public function update_table($table, $where, $data, $redis_key = ""); /** * 替换表记录 * @param string table 表名 * @param array data 替换数据 * @param string redis_key redis 缓存键值, 可空, 非空时清理键值缓存 */ public function replace_table($table, $data, $redis_key = ""); /** * 删除表记录 * @param string table 表名 * @param array where 查询条件 * @param string redis_key redis缓存键值, 可空, 非空时清理键值缓存 */ public function delete_table($table, $where, $redis_key = ""); /** * 获取表数据 * @param string table 表名 * @param array where 查询条件 * @param string redis_key redis 缓存键值, 可空, 非空时清理键值缓存 * @param int redis_expire redis 缓存到期时长(秒) * @param string $column 数据库表字段,可空 * @param boolean set_empty_flag 是否将空值写入缓存,防止数据库击穿,默认为是 */ public function get_table_data($table, $where = array(), $redis_key = "", $redis_expire = 600, $column = "*", $set_empty_flag = true); /** * 获取一条表数据 * @param string table 表名 * @param array where 查询条件 * @param string redis_key redis 缓存键值, 可空, 非空时清理键值缓存 * @param int redis_expire redis 缓存到期时长(秒) * @param string $column 数据库表字段,可空 * @param boolean set_empty_flag 是否将空值写入缓存,防止数据库击穿,默认为是 */ public function get_one_table_data($table, $where, $redis_key = "", $redis_expire = 600, $column = "*", $set_empty_flag = true);
作者:smallhow



开源 rpg swoole php开发 小游戏 PHP

需要 登录 后方可回复, 如果你还没有账号请 注册新账号