一年之前,朋友开游戏公司,让我帮忙设计RPG游戏的后台架构,我思考如何兼顾开发效率和性能,最终想到了 php + swoole协程 + swoole_orm + zephir ,微信小游戏搜索:“剑的传说”
swoole协程有着极高的IO并发能力
swoole_orm 是我开发的php 扩展,有着非常高的性能、sql安全性和开发效率,开源地址为: https://github.com/swoole/ext-orm
游戏的战斗部分完全用 zephir 来实现,宣称像写php一样写php扩展,能做到同时兼顾性能和开发效率,整个技能过程属于计算密集型运算,如果用PHP实现,对CPU的压力非常大,PHP不擅长计算密集型应用,尤其在协程情况下,太高的CPU占用会导致其它协程饿死,尽管swoole支持抢占式协程,所以这里采用zephir将类php语言转化为c语言扩展,性能媲美c语言,而且开发速度快。(zephir 代码有机会我再开源出来,目前时机不成熟,游戏还比较火热)
后台框架开源地址:https://github.com/caohao-php/ycsocket
仅展示部分zephir代码,整个技能过程属于计算密集型运算,如果用PHP实现,对CPU的压力非常大,PHP不擅长计算密集型应用,尤其在协程情况下,太高的CPU占用会导致其它协程饿死,尽管swoole支持抢占式协程,所以这里采用zephir将类php语言转化为c语言扩展,性能媲美c语言,而且开发速度快。
伤害计算:
————————————————
|--- 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");
}
}
}
...
}
所有控制器位于: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);
}
...
}
所有与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);
第三方类库都存在于 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");
}
}
/**
* 根据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);