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, 內容可以設置超時更新
應用場景
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));
}
}
}