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

概述

一年之前,朋友开游戏公司,让我帮忙设计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

swoole协程+zephir纯php开发大型RPG微信小游戏(已开源)_第1张图片

仅展示部分zephir代码,整个技能过程属于计算密集型运算,如果用PHP实现,对CPU的压力非常大,PHP不擅长计算密集型应用,尤其在协程情况下,太高的CPU占用会导致其它协程饿死,尽管swoole支持抢占式协程,所以这里采用zephir将类php语言转化为c语言扩展,性能媲美c语言,而且开发速度快。
swoole协程+zephir纯php开发大型RPG微信小游戏(已开源)_第2张图片
伤害计算:swoole协程+zephir纯php开发大型RPG微信小游戏(已开源)_第3张图片

代码结构

———————————————— 
|--- 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);

你可能感兴趣的:(PHP,扩展,高并发)