基於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, 內容可以設置超時更新

應用場景

game

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));
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章