基于swoole的轻量级socket框架(含协程版数据库/缓存连接池)

ycsocket

基于 swoole 和 ycdatabase 的 websocket 框架,各位可以自己扩展到 TCP/UDP,HTTP。

在ycsocket 中,采用的是全协程化,全池化的数据库、缓存IO,对于IO密集型型的应用,能够支撑较高并发。

如果希望项目同时能够支持计算密集型,我建议可以把耗时的计算过程,通过zephir写成PHP扩展,zephir是phalcon框架的基础语言, 可以解释成php扩展,非常高效,也是一个面向对象的语言。

项目github地址:https://github.com/caohao-php/ycsocket

文档暂时未写全,后续有时间了再完善。

环境:
PHP7+
ext-orm //一个C语言扩展的ORM,本框架协程数据库需要该扩展支持,https://github.com/swoole/ext-orm
swoole 4.0 +

我写推送后端的时候写的

客户端是一个聊天窗口

支持 Redis 协程线程池,源码位于 system/RedisPool,支持失败自动重连

支持 MySQL 协程连接池, 带ORM,源码位于 system/MySQLPool,支持失败自动重连,需要安装ORM扩展ext-orm

支持共享内存 entity, 内容可以设置超时更新

应用场景

基于swoole的轻量级socket框架(含协程版数据库/缓存连接池)_第1张图片

Redis Pool

class RedisPool {
    const POOL_SIZE = 10;
    protected $host;
    protected $port;
    protected $pool;
    protected $logger;
    static private $instances;
    static public function instance($redis_name) {
        if (!isset(self::$instances[$redis_name])) {
            global $util_redis_conf;
            if (!isset($util_redis_conf[$redis_name]['host'])) {
                throw new RuntimeException("Loader::redis:  redis config not exist");
            }
            $pool_size = isset($util_redis_conf[$redis_name]['pool_size']) ? intval($util_redis_conf[$redis_name]['pool_size']) : RedisPool::POOL_SIZE;
            $pool_size = $pool_size <= 0 ? RedisPool::POOL_SIZE : $pool_size;
            self::$instances[$redis_name] = new RedisPool($util_redis_conf[$redis_name]['host'], $util_redis_conf[$redis_name]['port'], $pool_size);
        }
        return self::$instances[$redis_name];
    }
    /**
     * RedisPool constructor.
     * @param int $size 连接池的尺寸
     */
    function __construct($host, $port, $size) {
        $this->logger = new Logger(array('file_name' => 'redis_log'));
        $this->host = $host;
        $this->port = $port;
        $this->pool = new Swoole\Coroutine\Channel($size);
        for ($i = 0; $i < $size; $i++) {
            $redis = new Swoole\Coroutine\Redis();
            $res = $redis->connect($host, $port);
            if ($res) {
                $this->pool->push($redis);
            } else {
                throw new RuntimeException("Redis connect error [$host] [$port]");
            }
        }
    }
    public function __call($func, $args) {
        $ret = null;
        try {
            $redis = $this->pool->pop();
            $ret = call_user_func_array(array($redis, $func), $args);
            if ($ret === false) {
                $this->logger->LogError("redis reconnect [{$this->host}][{$this->port}]");
                //重连一次
                $redis->close();
                $res = $redis->connect($this->host, $this->port);
                if (!$res) {
                    throw new RuntimeException("Redis reconnect error [{$this->host}][{$this->port}]");
                }
                $ret = call_user_func_array(array($redis, $func), $args);
                if ($ret === false) {
                    throw new RuntimeException("redis error after reconnect");
                }
            }
            $this->pool->push($redis);
        } catch (Exception $e) {
            $this->pool->push($redis);
            $this->logger->LogError("Redis catch exception [".$e->getMessage()."] [$func]");
            throw new RuntimeException("Redis catch exception [".$e->getMessage()."] [$func]");
        }
        return $ret;
    }
}

MySQL Pool

class MySQLPool {
    const POOL_SIZE = 10;

    protected $pool;
    protected $logger;
    static private $instances;

    var $host = '';
    var $username = '';
    var $password = '';
    var $dbname = '';
    var $port = 3306;
    var $pconnect = FALSE;
    var $db_debug = FALSE;
    var $char_set = 'utf8';
    var $dbcollat = 'utf8_general_ci';

    static public function instance($params) {
        if (!isset(self::$instances[$params])) {

            $params = empty($params) ? 'default' : $params;

            global $util_db_config;

            if (! isset($util_db_config[$params])) {
                throw new RuntimeException("You have specified an invalid database connection group.");
            }

            $config = $util_db_config[$params];

            $pool_size = isset($config['pool_size']) ? intval($config['pool_size']) : MySQLPool::POOL_SIZE;
            $pool_size = $pool_size <= 0 ? MySQLPool::POOL_SIZE : $pool_size;

            self::$instances[$params] = new MySQLPool($config, $pool_size);
        }

        return self::$instances[$params];
    }

    /**
     * MySQLPool constructor.
     * @param int $size 连接池的尺寸
     */
    function __construct($params, $size) {
        $this->logger = new Logger(array('file_name' => 'mysql_log'));

        foreach ($params as $key => $val) {
            $this->$key = $val;
        }

        $this->ycdb = new ycdb(["unix_socket" => ""]);

        $this->pool = new Swoole\Coroutine\Channel($size);

        for ($i = 0; $i < $size; $i++) {
            $mysql = new Swoole\Coroutine\MySQL();

            $ret = $this->connect($mysql);

            if ($ret) {
                $this->pool->push($mysql);
                $this->query("SET NAMES '".$this->char_set."' COLLATE '".$this->dbcollat."'");
            } else {
                throw new RuntimeException("MySQL connect error host={$this->host}, port={$this->port}, user={$this->username}, database={$this->dbname}, errno=[" . $mysql->errno . "], error=[" . $mysql->error . "]");
            }
        }
    }

    function insert($table = '', $data = NULL) {
        if (empty($table) || empty($data) || !is_array($data)) {
            throw new RuntimeException("insert_table_or_data_must_be_set");
        }

        $ret = $this->ycdb->insert_sql($table, $data);
        if (empty($ret) || $ret == -1) {
            throw new RuntimeException("insert_sql error [$table][".json_encode($data)."]");
        }

        $sql = $ret['query'];
        $map = $ret['map'];
        $sql = str_replace(array_keys($map), "?", $sql);
        $ret = $this->query($sql, array_values($map), $mysql);
        if (!empty($ret)) {
            return $mysql->insert_id;
        } else {
            return intval($ret);
        }
    }

    function replace($table = '', $data = NULL) {
        if (empty($table) || empty($data) || !is_array($data)) {
            throw new RuntimeException("replace_table_or_data_must_be_set");
        }

        $ret = $this->ycdb->replace_sql($table, $data);
        if (empty($ret) || $ret == -1) {
            throw new RuntimeException("replace_sql error [$table][".json_encode($data)."]");
        }

        $sql = $ret['query'];
        $map = $ret['map'];
        $sql = str_replace(array_keys($map), "?", $sql);
        $ret = $this->query($sql, array_values($map));
        return $ret;
    }

    function update($table = '', $where = NULL, $data = NULL) {
        if (empty($table) || empty($where) || empty($data) || !is_array($data)) {
            throw new RuntimeException("update_table_or_data_must_be_set");
        }

        $ret = $this->ycdb->update_sql($table, $data, $where);
        if (empty($ret) || $ret == -1) {
            throw new RuntimeException("update_sql error [$table][".json_encode($data)."][".json_encode($where)."]");
        }

        $sql = $ret['query'];
        $map = $ret['map'];
        $sql = str_replace(array_keys($map), "?", $sql);
        $ret = $this->query($sql, array_values($map));
        return $ret;
    }

    function delete($table = '', $where = NULL) {
        if (empty($table) || empty($where)) {
            throw new RuntimeException("delete_table_or_where_must_be_set");
        }

        $ret = $this->ycdb->delete_sql($table, $where);
        if (empty($ret) || $ret == -1) {
            throw new RuntimeException("replace_sql error [$table][".json_encode($where)."]");
        }

        $sql = $ret['query'];
        $map = $ret['map'];
        $sql = str_replace(array_keys($map), "?", $sql);
        $ret = $this->query($sql, array_values($map));
        return $ret;
    }

    function get($table = '', $where = array(), $columns = "*") {
        if (empty($table) || empty($columns)) {
            throw new RuntimeException("select_table_or_columns_must_be_set");
        }

        $ret = $this->ycdb->select_sql($table, $columns, $where);
        if (empty($ret) || $ret == -1) {
            throw new RuntimeException("select_sql error [$table][".json_encode($where)."][".json_encode($columns)."]");
        }

        $sql = $ret['query'];
        $map = $ret['map'];

        $sql = str_replace(array_keys($map), "?", $sql);
        $ret = $this->query($sql, array_values($map));
        return $ret;
    }

    function get_one($table = '', $where = array(), $columns = "*") {
        if (empty($table) || empty($columns)) {
            throw new RuntimeException("select_table_or_columns_must_be_set");
        }

        $where['LIMIT'] = 1;
        $ret = $this->get($table, $where, $columns);
        if (empty($ret) || !is_array($ret)) {
            return array();
        }

        return $ret[0];
    }

    private function connect(& $mysql, $reconn = false) {
        if ($reconn) {
            $mysql->close();
        }

        $options = array();
        $options['host'] = $this->host;
        $options['port'] = intval($this->port) == 0 ? 3306 : intval($this->port);
        $options['user'] = $this->username;
        $options['password'] = $this->password;
        $options['database'] = $this->dbname;
        $ret = $mysql->connect($options);
        return $ret;
    }

    function real_query(& $mysql, & $sql, & $map) {
        if (empty($map)) {
            return $mysql->query($sql);
        } else {
            $stmt = $mysql->prepare($sql);

            if ($stmt == false) {
                return false;
            } else {
                return $stmt->execute($map);
            }
        }
    }

    function query($sql, $map = null, & $mysql = null) {
        if (empty($sql)) {
            throw new RuntimeException("input_empty_query_sql");
        }

        try {
            $mysql = $this->pool->pop();
            $ret = $this->real_query($mysql, $sql, $map);

            if ($ret === false) {
                $this->logger->LogError("MySQL QUERY FAIL [".$mysql->errno."][".$mysql->error."], sql=[{$sql}], map=[".json_encode($map)."]");

                if ($mysql->errno == 2006 || $mysql->errno == 2013) {
                    //重连MySQL
                    $ret = $this->connect($mysql, true);
                    if ($ret) {
                        $ret = $this->real_query($mysql, $sql, $map);
                    } else {
                        throw new RuntimeException("reconnect fail: [" . $mysql->errno . "][" . $mysql->error . "], host={$this->host}, port={$this->port}, user={$this->username}, database={$this->dbname}");
                    }
                }
            }

            if ($ret === false) {
                throw new RuntimeException($mysql->errno . "|" . $mysql->error);
            }

            $this->pool->push($mysql);
            return $ret;
        } catch (Exception $e) {
            $this->pool->push($mysql);
            $this->logger->LogError("MySQL catch exception [".$e->getMessage()."], sql=[{$sql}], map=".json_encode($map));
            throw new RuntimeException("MySQL catch exception [".$e->getMessage()."], sql=[{$sql}], map=".json_encode($map));
        }
    }
}

你可能感兴趣的:(PHP,架构)