initial 2.0.0-a3 commit

This commit is contained in:
jerry 2020-10-03 23:00:18 +08:00
parent f91d24aaaa
commit da584e0542
28 changed files with 580 additions and 225 deletions

View File

@ -31,7 +31,7 @@
"symfony/console": "^5.1",
"symfony/polyfill-ctype": "^1.18",
"zhamao/connection-manager": "^1.0",
"zhamao/console": "^1.0",
"zhamao/console": "*@dev",
"zhamao/config": "^1.0",
"zhamao/request": "^1.0",
"symfony/routing": "^5.1"
@ -61,5 +61,11 @@
},
"require-dev": {
"phpunit/phpunit": "^9.3"
}
},
"repositories": [
{
"type": "path",
"url": "/Users/jerry/project/git-project/zhamao-console"
}
]
}

View File

@ -1,4 +1,6 @@
<?php
/** @noinspection PhpFullyQualifiedNameUsageInspection */
/** @noinspection PhpComposerExtensionStubsInspection */
global $config;
/** bind host */
@ -25,7 +27,7 @@ $config['crash_dir'] = $config['zm_data'] . 'crash/';
/** 对应swoole的server->set参数 */
$config['swoole'] = [
'log_file' => $config['crash_dir'] . 'swoole_error.log',
'worker_num' => 2,
'worker_num' => 8,
'dispatch_mode' => 2,
'max_coroutine' => 30000,
//'task_worker_num' => 4,
@ -37,7 +39,8 @@ $config['light_cache'] = [
"status" => true,
"size" => 2048, //最多允许储存的条数需要2的倍数
"max_strlen" => 4096, //单行字符串最大长度需要2的倍数
"hash_conflict_proportion" => 0.6 //Hash冲突率越大越好但是需要的内存更多
"hash_conflict_proportion" => 0.6, //Hash冲突率越大越好但是需要的内存更多
"persistence_path" => $config['zm_data']."_cache.json"
];
/** MySQL数据库连接信息host留空则启动时不创建sql连接池 */

View File

@ -2,7 +2,7 @@
namespace Module\Example;
use ZM\Annotation\Swoole\OnStart;
use ZM\Annotation\Swoole\OnWorkerStart;
use ZM\Annotation\Swoole\OnTick;
use ZM\ConnectionManager\ConnectionObject;
use ZM\Console\Console;
@ -10,6 +10,8 @@ use ZM\Annotation\CQ\CQCommand;
use ZM\Annotation\Http\Middleware;
use ZM\Annotation\Http\RequestMapping;
use ZM\Annotation\Swoole\SwooleEvent;
use ZM\Store\LightCache;
use ZM\Store\ZMBuf;
use ZM\Utils\ZMUtil;
/**
@ -72,9 +74,9 @@ class Hello
/**
* 中间件测试的一个示例函数
* @RequestMapping("/httpTimer")
* @Middleware("timer")
*/
public function timer() {
ZMBuf::atomic("_tmp_2")->add(1);
return "This page is used as testing TimerMiddleware! Do not use it in production.";
}

View File

@ -7,6 +7,7 @@ use Co;
use ZM\ConnectionManager\ConnectionObject;
use ZM\Console\Console;
use ZM\Event\EventHandler;
use ZM\Store\LightCache;
use ZM\Store\ZMBuf;
trait CQAPI
@ -26,27 +27,20 @@ trait CQAPI
}
public function processWebsocketAPI($connection, $reply, $function = null) {
public function processWebsocketAPI($connection, $reply, $function = false) {
$api_id = ZMBuf::atomic("wait_msg_id")->get();
$reply["echo"] = $api_id;
ZMBuf::atomic("wait_msg_id")->add(1);
EventHandler::callCQAPISend($reply, $connection);
if (is_callable($function)) {
ZMBuf::appendKey("sent_api", $api_id, [
"data" => $reply,
"time" => microtime(true),
"func" => $function,
"self_id" => $connection->getOption("connect_id")
]);
} elseif ($function === true) {
ZMBuf::appendKey("sent_api", $api_id, [
if ($function === true) {
LightCache::set("sent_api_".$api_id, [
"data" => $reply,
"time" => microtime(true),
"coroutine" => Co::getuid(),
"self_id" => $connection->getOption("connect_id")
]);
} else {
ZMBuf::appendKey("sent_api", $api_id, [
LightCache::set("sent_api_".$api_id, [
"data" => $reply,
"time" => microtime(true),
"self_id" => $connection->getOption("connect_id")
@ -56,8 +50,8 @@ trait CQAPI
if (server()->push($connection->getFd(), json_encode($reply))) {
if ($function === true) {
Co::suspend();
$data = ZMBuf::get("sent_api")[$api_id];
ZMBuf::unsetByValue("sent_api", $reply["echo"]);
$data = LightCache::get("sent_api_".$api_id);
LightCache::unset("sent_api_".$api_id);
return isset($data['result']) ? $data['result'] : null;
}
return true;
@ -69,10 +63,10 @@ trait CQAPI
"data" => null,
"self_id" => $connection->getOption("connect_id")
];
$s = ZMBuf::get("sent_api")[$reply["echo"]];
$s = LightCache::get("sent_api_".$reply["echo"]);
if (($s["func"] ?? null) !== null)
call_user_func($s["func"], $response, $reply);
ZMBuf::unsetByValue("sent_api", $reply["echo"]);
LightCache::unset("sent_api_".$reply["echo"]);
if ($function === true) return $response;
return false;
}

View File

@ -7,12 +7,12 @@ use Doctrine\Common\Annotations\Annotation\Target;
use ZM\Annotation\AnnotationBase;
/**
* Class OnStart
* Class OnWorkerStart
* @package ZM\Annotation\Swoole
* @Annotation
* @Target("ALL")
*/
class OnStart extends AnnotationBase
class OnWorkerStart extends AnnotationBase
{
/**
* @var int

View File

@ -4,6 +4,8 @@
namespace ZM\Command;
use Swoole\Atomic;
use Swoole\Coroutine;
use Swoole\Http\Request;
use Swoole\Http\Response;
use Swoole\Http\Server;
@ -15,6 +17,7 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use ZM\Config\ZMConfig;
use ZM\Console\Console;
use ZM\Store\ZMBuf;
use ZM\Utils\HttpUtil;
class PureHttpCommand extends Command
@ -37,9 +40,15 @@ class PureHttpCommand extends Command
$host = $input->getOption("host") ?? $global["host"];
$port = $input->getOption("port") ?? $global["port"];
$server = new Server($host, $port);
Console::init(2, $server);
$server->set(ZMConfig::get("global", "swoole"));
Console::init(0, $server);
ZMBuf::$atomics["request"] = [];
for ($i = 0; $i < 32; ++$i) {
ZMBuf::$atomics["request"][$i] = new Atomic(0);
}
$index = ["index.html", "index.htm"];
$server->on("request", function (Request $request, Response $response) use ($input, $index){
$server->on("request", function (Request $request, Response $response) use ($input, $index, $server) {
ZMBuf::$atomics["request"][$server->worker_id]->add(1);
HttpUtil::handleStaticPage(
$request->server["request_uri"],
$response,
@ -47,10 +56,16 @@ class PureHttpCommand extends Command
"document_root" => realpath($input->getArgument('dir') ?? '.'),
"document_index" => $index
]);
echo "\r".Coroutine::stats()["coroutine_peak_num"];
});
$server->on("start", function($server){
Process::signal(SIGINT, function () use ($server){
$server->on("start", function ($server) {
Process::signal(SIGINT, function () use ($server) {
Console::warning("Server interrupted by keyboard.");
for ($i = 0; $i < 32; ++$i) {
$num = ZMBuf::$atomics["request"][$i]->get();
if($num != 0)
echo "[$i]: ".$num."\n";
}
$server->shutdown();
$server->stop();
});

View File

@ -7,7 +7,6 @@ namespace ZM;
use Exception;
use Symfony\Component\Console\Command\Command;
use TypeError;
use ZM\Command\BuildCommand;
use ZM\Command\InitCommand;
use ZM\Command\PureHttpCommand;
use ZM\Command\RunServerCommand;
@ -25,6 +24,7 @@ class ConsoleApplication extends Application
}
public function initEnv() {
$this->selfCheck();
$this->addCommands([
new RunServerCommand(), //运行主服务的指令控制器
new InitCommand(), //初始化用的用于项目初始化和phar初始化
@ -77,4 +77,16 @@ class ConsoleApplication extends Application
die("{$e->getMessage()} at {$e->getFile()}({$e->getLine()})");
}
}
private function selfCheck() {
if (!extension_loaded("swoole")) die("Can not find swoole extension.\n");
if (version_compare(SWOOLE_VERSION, "4.4.13") == -1) die("You must install swoole version >= 4.4.13 !");
//if (!extension_loaded("gd")) die("Can not find gd extension.\n");
if (!extension_loaded("sockets")) die("Can not find sockets extension.\n");
if (substr(PHP_VERSION, 0, 1) != "7") die("PHP >=7 required.\n");
//if (!function_exists("curl_exec")) die("Can not find curl extension.\n");
//if (!class_exists("ZipArchive")) die("Can not find Zip extension.\n");
//if (!file_exists(CRASH_DIR . "last_error.log")) die("Can not find log file.\n");
return true;
}
}

View File

@ -14,6 +14,7 @@ use ZM\Exception\InvalidArgumentException;
use ZM\Exception\WaitTimeoutException;
use ZM\Http\Response;
use ZM\API\ZMRobot;
use ZM\Store\LightCache;
use ZM\Store\ZMBuf;
class Context implements ContextInterface
@ -153,17 +154,17 @@ class Context implements ContextInterface
if ($hang["message_type"] == "group" || $hang["message_type"] == "discuss") {
$hang[$hang["message_type"] . "_id"] = $this->getData()[$this->getData()["message_type"] . "_id"];
}
ZMBuf::appendKey("wait_api", $api_id, $hang);
LightCache::set("wait_api_".$api_id, $hang);
$id = swoole_timer_after($timeout * 1000, function () use ($api_id, $timeout_prompt) {
$r = ZMBuf::get("wait_api")[$api_id] ?? null;
if ($r !== null) {
$r = LightCache::get("wait_api_".$api_id);
if (is_array($r)) {
Co::resume($r["coroutine"]);
}
});
Co::suspend();
$sess = ZMBuf::get("wait_api")[$api_id];
ZMBuf::unsetByValue("wait_api", $api_id);
$sess = LightCache::get("wait_api_".$api_id);
LightCache::unset("wait_api_".$api_id);
$result = $sess["result"];
if (isset($id)) swoole_timer_clear($id);
if ($result === null) throw new WaitTimeoutException($this, $timeout_prompt);

View File

@ -1,4 +1,4 @@
<?php
<?php /** @noinspection PhpComposerExtensionStubsInspection */
namespace ZM\DB;
@ -10,7 +10,6 @@ use ZM\Console\Console;
use ZM\Store\ZMBuf;
use PDOException;
use PDOStatement;
use Swoole\Coroutine;
use Swoole\Database\PDOStatementProxy;
use ZM\Exception\DbException;
@ -63,9 +62,6 @@ class DB
* @throws DbException
*/
public static function unprepared($line) {
if (ZMBuf::get("sql_log") === true) {
$starttime = microtime(true);
}
try {
$conn = ZMBuf::$sql_pool->get();
if ($conn === false) {
@ -76,13 +72,6 @@ class DB
ZMBuf::$sql_pool->put($conn);
return $result;
} catch (DBException $e) {
if (ZMBuf::get("sql_log") === true) {
$log =
"[" . date("Y-m-d H:i:s") .
" " . round(microtime(true) - $starttime, 5) .
"] " . $line . " (Error:" . $e->getMessage() . ")\n";
Coroutine::writeFile(CRASH_DIR . "sql.log", $log, FILE_APPEND);
}
Console::warning($e->getMessage());
throw $e;
}
@ -96,9 +85,6 @@ class DB
* @throws DbException
*/
public static function rawQuery(string $line, $params = [], $fetch_mode = ZM_DEFAULT_FETCH_MODE) {
if (ZMBuf::get("sql_log") === true) {
$starttime = microtime(true);
}
Console::debug("MySQL: ".$line." | ". implode(", ", $params));
try {
$conn = ZMBuf::$sql_pool->get();
@ -126,23 +112,9 @@ class DB
//echo json_encode(debug_backtrace(), 128 | 256);
}
ZMBuf::$sql_pool->put($conn);
if (ZMBuf::get("sql_log") === true) {
$log =
"[" . date("Y-m-d H:i:s") .
" " . round(microtime(true) - $starttime, 4) .
"] " . $line . " " . json_encode($params, JSON_UNESCAPED_UNICODE) . "\n";
Coroutine::writeFile(CRASH_DIR . "sql.log", $log, FILE_APPEND);
}
return $ps->fetchAll($fetch_mode);
}
} catch (DbException $e) {
if (ZMBuf::get("sql_log") === true) {
$log =
"[" . date("Y-m-d H:i:s") .
" " . round(microtime(true) - $starttime, 4) .
"] " . $line . " " . json_encode($params, JSON_UNESCAPED_UNICODE) . " (Error:" . $e->getMessage() . ")\n";
Coroutine::writeFile(CRASH_DIR . "sql.log", $log, FILE_APPEND);
}
if(mb_strpos($e->getMessage(), "has gone away") !== false) {
zm_sleep(0.2);
Console::warning("Gone away of MySQL! retrying!");
@ -151,13 +123,6 @@ class DB
Console::warning($e->getMessage());
throw $e;
} catch (PDOException $e) {
if (ZMBuf::get("sql_log") === true) {
$log =
"[" . date("Y-m-d H:i:s") .
" " . round(microtime(true) - $starttime, 4) .
"] " . $line . " " . json_encode($params, JSON_UNESCAPED_UNICODE) . " (Error:" . $e->getMessage() . ")\n";
Coroutine::writeFile(CRASH_DIR . "sql.log", $log, FILE_APPEND);
}
if(mb_strpos($e->getMessage(), "has gone away") !== false) {
zm_sleep(0.2);
Console::warning("Gone away of MySQL! retrying!");

View File

@ -85,7 +85,6 @@ class MessageEvent
}
/**
* @throws AnnotationException
* @noinspection PhpRedundantCatchClauseInspection
*/
public function onActivate() {
@ -122,7 +121,7 @@ class MessageEvent
EventDispatcher::interrupt();
});
$r = $dispatcher->dispatchEvents($word);
if ($r === false) return;
if ($r === null) return;
//分发CQMessage事件
$msg_dispatcher = new EventDispatcher(CQMessage::class);

View File

@ -7,7 +7,6 @@ namespace ZM\Event;
use Doctrine\Common\Annotations\AnnotationException;
use Exception;
use ZM\Annotation\AnnotationBase;
use ZM\Annotation\Interfaces\Rule;
use ZM\Exception\InterruptException;
use ZM\Utils\ZMUtil;
@ -20,6 +19,9 @@ class EventDispatcher
/** @var null|callable */
private $return_func = null;
/**
* @throws InterruptException
*/
public static function interrupt() {
throw new InterruptException('interrupt');
}
@ -46,12 +48,20 @@ class EventDispatcher
}
return true;
} catch (InterruptException $e) {
return false;
return null;
} catch (AnnotationException $e) {
return false;
}
}
/**
* @param AnnotationBase|null $v
* @param null $rule_func
* @param mixed ...$params
* @return bool
* @throws AnnotationException
* @throws InterruptException
*/
public function dispatchEvent(?AnnotationBase $v, $rule_func = null, ...$params) {
$q_c = $v->class;
$q_f = $v->method;

View File

@ -22,6 +22,7 @@ use ZM\Annotation\Http\MiddlewareClass;
use ZM\Context\Context;
use ZM\Http\MiddlewareInterface;
use ZM\Http\Response;
use ZM\Store\LightCache;
use ZM\Store\ZMBuf;
use ZM\Utils\ZMUtil;
@ -158,8 +159,8 @@ class EventHandler
$status = $req["status"];
$retcode = $req["retcode"];
$data = $req["data"];
if (isset($req["echo"]) && ZMBuf::array_key_exists("sent_api", $req["echo"])) {
$origin = ZMBuf::get("sent_api")[$req["echo"]];
if (isset($req["echo"]) && LightCache::isset("sent_api_" . $req["echo"])) {
$origin = LightCache::get("sent_api_" . $req["echo"]);
$self_id = $origin["self_id"];
$response = [
"status" => $status,
@ -188,12 +189,12 @@ class EventHandler
if (($origin["func"] ?? null) !== null) {
call_user_func($origin["func"], $response, $origin["data"]);
} elseif (($origin["coroutine"] ?? false) !== false) {
$p = ZMBuf::get("sent_api");
$p[$req["echo"]]["result"] = $response;
ZMBuf::set("sent_api", $p);
$r = LightCache::get("sent_api_" . $req["echo"]);
$r["result"] = $response;
LightCache::set("sent_api_" . $req["echo"], $r);
Co::resume($origin['coroutine']);
}
ZMBuf::unsetByValue("sent_api", $req["echo"]);
LightCache::unset("sent_api_" . $req["echo"]);
}
}

View File

@ -16,7 +16,7 @@ use Swoole\Process;
use Swoole\Timer;
use ZM\Annotation\AnnotationParser;
use ZM\Annotation\Http\RequestMapping;
use ZM\Annotation\Swoole\OnStart;
use ZM\Annotation\Swoole\OnWorkerStart;
use ZM\Annotation\Swoole\SwooleEvent;
use ZM\Config\ZMConfig;
use ZM\ConnectionManager\ManagerGM;
@ -54,21 +54,23 @@ class ServerEventHandler
try {
Terminal::executeCommand($var, $r);
} catch (Exception $e) {
Console::error("Uncaught exception ".get_class($e).": ".$e->getMessage()." at ".$e->getFile()."(".$e->getLine().")");
Console::error("Uncaught exception " . get_class($e) . ": " . $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")");
} catch (Error $e) {
Console::error("Uncaught error ".get_class($e).": ".$e->getMessage()." at ".$e->getFile()."(".$e->getLine().")");
Console::error("Uncaught error " . get_class($e) . ": " . $e->getMessage() . " at " . $e->getFile() . "(" . $e->getLine() . ")");
}
});
}
Process::signal(SIGINT, function () use ($r) {
echo "\r";
Console::warning("Server interrupted by keyboard on Master.");
if ((Framework::$server->inotify ?? null) !== null)
/** @noinspection PhpUndefinedFieldInspection */ Event::del(Framework::$server->inotify);
ZMUtil::stop();
});
if(Framework::$argv["watch"]) {
if (Framework::$argv["watch"]) {
if (extension_loaded('inotify')) {
Console::warning("Enabled File watcher, do not use in production.");
/** @noinspection PhpUndefinedFieldInspection */
Framework::$server->inotify = $fd = inotify_init();
$this->addWatcher(DataProvider::getWorkingDir() . "/src", $fd);
Event::add($fd, function () use ($fd) {
@ -117,7 +119,7 @@ class ServerEventHandler
if ($error["type"] != 0) {
Console::error("Internal fatal error: " . $error["message"] . " at " . $error["file"] . "({$error["line"]})");
}
DataProvider::saveBuffer();
//DataProvider::saveBuffer();
/** @var Server $server */
if (server() === null) $server->shutdown();
else server()->shutdown();
@ -125,7 +127,7 @@ class ServerEventHandler
Console::info("Worker #{$server->worker_id} 启动中");
Framework::$server = $server;
ZMBuf::resetCache(); //清空变量缓存
//ZMBuf::resetCache(); //清空变量缓存
//ZMBuf::set("wait_start", []); //添加队列在workerStart运行完成前先让其他协程等待执行
foreach ($server->connections as $v) {
$server->close($v);
@ -193,12 +195,12 @@ class ServerEventHandler
EventManager::registerTimerTick(); //启动计时器
//ZMBuf::unsetCache("wait_start");
set_coroutine_params(["server" => $server, "worker_id" => $worker_id]);
$dispatcher = new EventDispatcher(OnStart::class);
$dispatcher = new EventDispatcher(OnWorkerStart::class);
$dispatcher->setRuleFunction(function ($v) {
return server()->worker_id === $v->worker_id || $v->worker_id === -1;
});
$dispatcher->dispatchEvents($server, $worker_id);
Console::debug("@OnStart 执行完毕");
Console::debug("@OnWorkerStart 执行完毕");
} catch (Exception $e) {
Console::error("Worker加载出错停止服务");
Console::error($e->getMessage() . "\n" . $e->getTraceAsString());
@ -289,9 +291,9 @@ class ServerEventHandler
try {
$no_interrupt = $dis->dispatchEvents($request, $response);
if (!$no_interrupt) {
if ($no_interrupt !== null) {
$result = HttpUtil::parseUri($request, $response, $request->server["request_uri"], $node, $params);
if (!$result) {
if ($result === true) {
ctx()->setCache("params", $params);
$dispatcher = new EventDispatcher(RequestMapping::class);
$div = new RequestMapping();
@ -300,11 +302,13 @@ class ServerEventHandler
$div->method = $node["method"];
$div->request_method = $node["request_method"];
$div->class = $node["class"];
//Console::success("正在执行路由:".$node["method"]);
$r = $dispatcher->dispatchEvent($div, null, $params, $request, $response);
if (is_string($r) && !$response->isEnd()) $response->end($r);
}
}
if (!$response->isEnd()) {
//Console::warning('返回了404');
HttpUtil::responseCodePage($response, 404);
}
} catch (Exception $e) {
@ -380,9 +384,10 @@ class ServerEventHandler
* @param $fd
*/
public function onClose($server, $fd) {
Console::debug("Calling Swoole \"close\" event from fd=" . $fd);
unset(Context::$context[Co::getCid()]);
$conn = ManagerGM::get($fd);
if ($conn === null) return;
Console::debug("Calling Swoole \"close\" event from fd=" . $fd);
set_coroutine_params(["server" => $server, "connection" => $conn, "fd" => $fd]);
$dispatcher = new EventDispatcher(SwooleEvent::class);
$dispatcher->setRuleFunction(function ($v) {

View File

@ -65,8 +65,6 @@ class Framework
} catch (ConnectionManager\TableException $e) {
die($e->getMessage());
}
//start swoole Framework
$this->selfCheck();
try {
self::$server = new Server(ZMConfig::get("global", "host"), ZMConfig::get("global", "port"));
$this->server_set = ZMConfig::get("global", "swoole");
@ -118,6 +116,7 @@ class Framework
"size" => 2048,
"max_strlen" => 4096,
"hash_conflict_proportion" => 0.6,
"persistence_path" => realpath(DataProvider::getDataFolder()."_cache.json")
]);
self::$server->start();
} catch (Exception $e) {
@ -127,18 +126,6 @@ class Framework
}
}
private function selfCheck() {
if (!extension_loaded("swoole")) die("Can not find swoole extension.\n");
if (version_compare(SWOOLE_VERSION, "4.4.13") == -1) die("You must install swoole version >= 4.4.13 !");
//if (!extension_loaded("gd")) die("Can not find gd extension.\n");
if (!extension_loaded("sockets")) die("Can not find sockets extension.\n");
if (substr(PHP_VERSION, 0, 1) != "7") die("PHP >=7 required.\n");
//if (!function_exists("curl_exec")) die("Can not find curl extension.\n");
//if (!class_exists("ZipArchive")) die("Can not find Zip extension.\n");
//if (!file_exists(CRASH_DIR . "last_error.log")) die("Can not find log file.\n");
return true;
}
/**
* 从全局配置文件里读取注入系统事件的类
* @throws ReflectionException

View File

@ -4,10 +4,9 @@
namespace ZM\Http;
use ZM\Config\ZMConfig;
use ZM\Console\Console;
use Framework\ZMBuf;
use ZM\Utils\HttpUtil;
use ZM\Utils\ZMUtil;
class StaticFileHandler
{
@ -23,7 +22,7 @@ class StaticFileHandler
} else {
if(is_file($full_path)) {
$exp = strtolower(pathinfo($full_path)['extension'] ?? "unknown");
$response->setHeader("Content-Type", ZMBuf::config("file_header")[$exp] ?? "application/octet-stream");
$response->setHeader("Content-Type", ZMConfig::get("file_header")[$exp] ?? "application/octet-stream");
$response->end(file_get_contents($full_path));
return true;
}

View File

@ -5,7 +5,8 @@ namespace ZM\Module;
/**
* Class QQBot
* @package ZM\Plugins
* @package ZM\Module
* @ExternalModule("qqbot")
*/
class QQBot
{

View File

@ -6,6 +6,7 @@ namespace ZM\Store;
use Exception;
use Swoole\Table;
use ZM\Console\Console;
class LightCache
{
@ -14,13 +15,31 @@ class LightCache
private static $config = [];
public static $last_error = '';
public static function init($config) {
self::$config = $config;
self::$kv_table = new Table($config["size"], $config["hash_conflict_proportion"]);
self::$kv_table->column("value", Table::TYPE_STRING, $config["max_strlen"]);
self::$kv_table->column("expire", Table::TYPE_INT);
self::$kv_table->column("data_type", Table::TYPE_STRING, 12);
return self::$kv_table->create();
$result = self::$kv_table->create();
if ($result === true && isset($config["persistence_path"])) {
$r = json_decode(file_get_contents($config["persistence_path"]), true);
if ($r === null) $r = [];
foreach ($r as $k => $v) {
$write = self::set($k, $v);
Console::debug("Writing LightCache: " . $k);
if ($write === false) {
self::$last_error = '可能是由于 Hash 冲突过多导致动态空间无法分配内存';
return false;
}
}
}
if ($result === false) {
self::$last_error = '系统内存不足,申请失败';
}
return $result;
}
/**
@ -56,12 +75,14 @@ class LightCache
*/
public static function set(string $key, $value, int $expire = -1) {
if (self::$kv_table === null) throw new Exception("not initialized LightCache");
if (is_array($value) || is_int($value)) {
if (is_array($value)) {
$value = json_encode($value, JSON_UNESCAPED_UNICODE);
if (strlen($value) >= self::$config["max_strlen"]) return false;
$data_type = "json";
} elseif (is_string($value)) {
$data_type = "";
} elseif (is_int($value)) {
$data_type = "int";
} else {
throw new Exception("Only can set string, array and int");
}
@ -97,20 +118,31 @@ class LightCache
$r = [];
$del = [];
foreach (self::$kv_table as $k => $v) {
if ($v["expire"] <= time()) {
$del[]=$k;
if ($v["expire"] <= time() && $v["expire"] >= 0) {
$del[] = $k;
continue;
}
$r[$k] = self::parseGet($v);
}
foreach($del as $v) {
foreach ($del as $v) {
self::unset($v);
}
return $r;
}
public static function savePersistence() {
$r = [];
foreach (self::$kv_table as $k => $v) {
if ($v["expire"] === -2) {
$r[$k] = self::parseGet($v);
}
}
$r = file_put_contents(self::$config["persistence_path"], json_encode($r, 128 | 256));
if($r === false) Console::error("Not saved, please check your \"persistence_path\"!");
}
private static function checkExpire($key) {
if (($expire = self::$kv_table->get($key, "expire")) !== -1) {
if (($expire = self::$kv_table->get($key, "expire")) >= 0) {
if ($expire <= time()) {
self::$kv_table->del($key);
}
@ -120,6 +152,7 @@ class LightCache
private static function parseGet($r) {
switch ($r["data_type"]) {
case "json":
case "int":
return json_decode($r["value"], true);
case "":
default:

View File

@ -0,0 +1,9 @@
<?php
namespace ZM\Store\Redis;
class Redis
{
}

View File

@ -15,68 +15,20 @@ use ZM\Config\ZMConfig;
class ZMBuf
{
//读写的缓存数据需要在worker_num = 1下才能正常使用
/** @var mixed[] ZMBuf的data */
private static $cache = [];
//Swoole SQL连接池多进程下每个进程一个连接池
/** @var PDOPool */
static $sql_pool = null;//保存sql连接池的类
/** @var array Http请求uri路径根节点 */
public static $req_mapping_node;
/** @var array 事件注解的绑定对 */
public static $events = [];
// 下面的有用,上面的没用了
/** @var Atomic[] */
public static $atomics;
public static $instance = [];
public static $context_class = [];
public static $terminal = null;
static function get($name, $default = null) {
return self::$cache[$name] ?? $default;
}
static function set($name, $value) {
self::$cache[$name] = $value;
}
static function append($name, $value) {
self::$cache[$name][] = $value;
}
static function appendKey($name, $key, $value) {
self::$cache[$name][$key] = $value;
}
static function appendKeyInKey($name, $key, $value) {
self::$cache[$name][$key][] = $value;
}
static function unsetCache($name) {
unset(self::$cache[$name]);
}
static function unsetByValue($name, $vale) {
$key = array_search($vale, self::$cache[$name]);
array_splice(self::$cache[$name], $key, 1);
}
static function isset($name) {
return isset(self::$cache[$name]);
}
static function array_key_exists($name, $key) {
return isset(self::$cache[$name][$key]);
}
static function in_array($name, $val) {
return in_array($val, self::$cache[$name]);
}
public static function resetCache() {
self::$cache = [];
self::$instance = [];
}
/**
* 初始化atomic计数器
*/
@ -86,6 +38,9 @@ class ZMBuf
}
self::$atomics["stop_signal"] = new Atomic(0);
self::$atomics["wait_msg_id"] = new Atomic(0);
for($i = 0; $i < 10; ++$i) {
self::$atomics["_tmp_".$i] = new Atomic(0);
}
}
/**

View File

@ -5,9 +5,6 @@ namespace ZM\Utils;
use ZM\Config\ZMConfig;
use ZM\Console\Console;
use ZM\Annotation\Swoole\OnSave;
use ZM\Store\ZMBuf;
class DataProvider
{
@ -24,57 +21,10 @@ class DataProvider
return null;
}
public static function getDataConfig() {
return CONFIG_DIR;
}
public static function addSaveBuffer($buf_name, $sub_folder = null) {
$name = ($sub_folder ?? "") . "/" . $buf_name . ".json";
self::$buffer_list[$buf_name] = $name;
Console::debug("Added " . $buf_name . " at $sub_folder");
ZMBuf::set($buf_name, self::getJsonData($name));
}
public static function saveBuffer() {
$head = Console::setColor(date("[H:i:s] ") . "[V] Saving buffer......", "blue");
if (Console::getLevel() >= 3)
echo $head;
foreach (self::$buffer_list as $k => $v) {
Console::debug("Saving " . $k . " to " . $v);
self::setJsonData($v, ZMBuf::get($k));
}
foreach (ZMBuf::$events[OnSave::class] ?? [] as $v) {
$c = $v->class;
$method = $v->method;
$class = new $c();
Console::debug("Calling @OnSave: $c -> $method");
$class->$method();
}
if (Console::getLevel() >= 3)
echo Console::setColor("saved", "blue") . PHP_EOL;
}
public static function getFrameworkLink() {
return ZMConfig::get("global", "http_reverse_link");
}
public static function getJsonData(string $string) {
if (!file_exists(self::getDataConfig() . $string)) return [];
return json_decode(file_get_contents(self::getDataConfig() . $string), true);
}
public static function setJsonData($filename, array $args) {
$pathinfo = pathinfo($filename);
if (!is_dir(self::getDataConfig() . $pathinfo["dirname"])) {
Console::debug("Making Directory: " . self::getDataConfig() . $pathinfo["dirname"]);
mkdir(self::getDataConfig() . $pathinfo["dirname"]);
}
$r = file_put_contents(self::getDataConfig() . $filename, json_encode($args, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_BIGINT_AS_STRING));
if ($r === false) {
Console::warning("无法保存文件: " . $filename);
}
}
public static function getDataFolder() {
return ZM_DATA;
}

View File

@ -52,10 +52,10 @@ class HttpUtil
}
if (ZMConfig::get("global", "static_file_server")["status"]) {
HttpUtil::handleStaticPage($request->server["request_uri"], $response);
return true;
return null;
}
}
return false;
return !isset($node["route"]) ? false : true;
}
public static function getHttpCodePage(int $http_code) {

View File

@ -5,6 +5,7 @@ namespace ZM\Utils;
use Exception;
use Psy\Shell;
use Swoole\Event;
use ZM\Console\Console;
use ZM\Framework;
@ -35,8 +36,9 @@ class Terminal
$class->$function_name();
return true;
case 'psysh':
if (Framework::$argv["disable-coroutine"])
eval(\Psy\sh());
if (Framework::$argv["disable-coroutine"]) {
(new Shell())->run();
}
else
Console::error("Only \"--disable-coroutine\" mode can use psysh!!!");
return true;

View File

@ -30,13 +30,14 @@ class ZMUtil
public static function reload($delay = 800) {
Console::info(Console::setColor("Reloading server...", "gold"));
usleep($delay * 1000);
foreach (ZMBuf::get("wait_api", []) as $k => $v) {
if ($v["result"] === null) Co::resume($v["coroutine"]);
foreach (LightCache::getAll() as $k => $v) {
if (mb_substr($k, 0, 8) == "wait_api")
if ($v["result"] === null) Co::resume($v["coroutine"]);
}
foreach (server()->connections as $v) {
server()->close($v);
}
DataProvider::saveBuffer();
//DataProvider::saveBuffer();
Timer::clearAll();
server()->reload();
}

View File

@ -0,0 +1,256 @@
<?php
namespace ZMTest\Mock;
use Swoole\Http\Request;
use Swoole\WebSocket\Frame;
use Swoole\WebSocket\Server;
use ZM\API\ZMRobot;
use ZM\ConnectionManager\ConnectionObject;
use ZM\Context\ContextInterface;
use ZM\Http\Response;
class Context implements ContextInterface
{
/**
* Context constructor.
* @param $cid
*/
public function __construct($cid) { }
/**
* @return Server
*/
public function getServer() {
// TODO: Implement getServer() method.
}
/**
* @return Frame
*/
public function getFrame() {
// TODO: Implement getFrame() method.
}
/**
* @return mixed
*/
public function getData() {
// TODO: Implement getData() method.
}
/**
* @param $data
* @return mixed
*/
public function setData($data) {
// TODO: Implement setData() method.
}
/**
* @return ConnectionObject
*/
public function getConnection() {
// TODO: Implement getConnection() method.
}
/**
* @return int|null
*/
public function getFd() {
// TODO: Implement getFd() method.
}
/**
* @return int
*/
public function getCid() {
// TODO: Implement getCid() method.
}
/**
* @return Response
*/
public function getResponse() {
// TODO: Implement getResponse() method.
}
/**
* @return Request
*/
public function getRequest() {
// TODO: Implement getRequest() method.
}
/**
* @return ZMRobot
*/
public function getRobot() {
// TODO: Implement getRobot() method.
}
/**
* @return mixed
*/
public function getUserId() {
// TODO: Implement getUserId() method.
}
/**
* @return mixed
*/
public function getGroupId() {
// TODO: Implement getGroupId() method.
}
/**
* @return mixed
*/
public function getDiscussId() {
// TODO: Implement getDiscussId() method.
}
/**
* @return string
*/
public function getMessageType() {
// TODO: Implement getMessageType() method.
}
/**
* @return mixed
*/
public function getRobotId() {
// TODO: Implement getRobotId() method.
}
/**
* @return mixed
*/
public function getMessage() {
// TODO: Implement getMessage() method.
}
/**
* @param $msg
* @return mixed
*/
public function setMessage($msg) {
// TODO: Implement setMessage() method.
}
/**
* @param $id
* @return mixed
*/
public function setUserId($id) {
// TODO: Implement setUserId() method.
}
/**
* @param $id
* @return mixed
*/
public function setGroupId($id) {
// TODO: Implement setGroupId() method.
}
/**
* @param $id
* @return mixed
*/
public function setDiscussId($id) {
// TODO: Implement setDiscussId() method.
}
/**
* @param $type
* @return mixed
*/
public function setMessageType($type) {
// TODO: Implement setMessageType() method.
}
/**
* @return mixed
*/
public function getCQResponse() {
// TODO: Implement getCQResponse() method.
}
/**
* @param $msg
* @param bool $yield
* @return mixed
*/
public function reply($msg, $yield = false) {
echo $msg.PHP_EOL;
// TODO: Implement reply() method.
}
/**
* @param $msg
* @param bool $yield
* @return mixed
*/
public function finalReply($msg, $yield = false) {
// TODO: Implement finalReply() method.
}
/**
* @param string $prompt
* @param int $timeout
* @param string $timeout_prompt
* @return mixed
*/
public function waitMessage($prompt = "", $timeout = 600, $timeout_prompt = "") {
// TODO: Implement waitMessage() method.
}
/**
* @param $arg
* @param $mode
* @param $prompt_msg
* @return mixed
*/
public function getArgs(&$arg, $mode, $prompt_msg) {
$r = $arg;
array_shift($r);
return array_shift($r);
// TODO: Implement getArgs() method.
}
/**
* @param $key
* @param $value
* @return mixed
*/
public function setCache($key, $value) {
// TODO: Implement setCache() method.
}
/**
* @param $key
* @return mixed
*/
public function getCache($key) {
// TODO: Implement getCache() method.
}
/**
* @return mixed
*/
public function cloneFromParent() {
// TODO: Implement cloneFromParent() method.
}
/**
* @return mixed
*/
public function copy() {
// TODO: Implement copy() method.
}
}

114
test/ZMTest/Mock/global.php Normal file
View File

@ -0,0 +1,114 @@
<?php
/** @noinspection PhpFullyQualifiedNameUsageInspection */
/** @noinspection PhpComposerExtensionStubsInspection */
global $config;
/** bind host */
$config['host'] = '0.0.0.0';
/** bind port */
$config['port'] = 20001;
/** 框架开到公网或外部的HTTP访问链接通过 DataProvider::getFrameworkLink() 获取 */
$config['http_reverse_link'] = "http://127.0.0.1:" . $config['port'];
/** 框架是否启动debug模式 */
$config['debug_mode'] = false;
/** 存放框架内文件数据的目录 */
$config['zm_data'] = realpath(__DIR__ . "/../") . '/zm_data/';
/** 存放各个模块配置文件的目录 */
$config['config_dir'] = $config['zm_data'] . 'config/';
/** 存放崩溃和运行日志的目录 */
$config['crash_dir'] = $config['zm_data'] . 'crash/';
/** 对应swoole的server->set参数 */
$config['swoole'] = [
'log_file' => $config['crash_dir'] . 'swoole_error.log',
'worker_num' => 8,
'dispatch_mode' => 2,
'max_coroutine' => 30000,
//'task_worker_num' => 4,
//'task_enable_coroutine' => true
];
/** 轻量字符串缓存,默认开启 */
$config['light_cache'] = [
"status" => true,
"size" => 2048, //最多允许储存的条数需要2的倍数
"max_strlen" => 4096, //单行字符串最大长度需要2的倍数
"hash_conflict_proportion" => 0.6 //Hash冲突率越大越好但是需要的内存更多
];
/** MySQL数据库连接信息host留空则启动时不创建sql连接池 */
$config['sql_config'] = [
'sql_host' => '',
'sql_port' => 3306,
'sql_username' => 'name',
'sql_database' => 'db_name',
'sql_password' => '',
'sql_enable_cache' => true,
'sql_reset_cache' => '0300',
'sql_options' => [
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_EMULATE_PREPARES => false
],
'sql_no_exception' => false,
'sql_default_fetch_mode' => PDO::FETCH_ASSOC // added in 1.5.6
];
/** CQHTTP连接约定的token */
$config["access_token"] = "";
/** HTTP服务器固定请求头的返回 */
$config['http_header'] = [
'X-Powered-By' => 'zhamao-framework',
'Content-Type' => 'text/html; charset=utf-8'
];
/** HTTP服务器在指定状态码下回复的页面默认 */
$config['http_default_code_page'] = [
'404' => '404.html'
];
/** zhamao-framework在框架启动时初始化的atomic们 */
$config['init_atomics'] = [
//'custom_atomic_name' => 0, //自定义添加的Atomic
];
/** 终端日志显示等级0-4 */
$config["info_level"] = 2;
/** 自动保存计时器的缓存保存时间(秒) */
$config['auto_save_interval'] = 900;
/** 上下文接口类 implemented from ContextInterface */
$config['context_class'] = \ZMTest\Mock\Context::class;
/** 静态文件访问 */
$config['static_file_server'] = [
'status' => false,
'document_root' => realpath(__DIR__ . "/../") . '/resources/html',
'document_index' => [
'index.html'
]
];
/** 注册 Swoole Server 事件注解的类列表 */
$config['server_event_handler_class'] = [
\ZM\Event\ServerEventHandler::class,
];
/** 注册自定义指令的类 */
$config['command_register_class'] = [
//\Custom\Command\CustomCommand::class
];
/** 服务器启用的外部第三方和内部插件 */
$config['plugins'] = [
'qqbot' => true, // QQ机器人事件解析器如果取消此项则默认为 true 开启状态,否则你手动填写 false 才会关闭
];
return $config;

View File

@ -9,7 +9,7 @@ use Module\Example\Hello;
use PHPUnit\Framework\TestCase;
use ReflectionException;
use ZM\Annotation\AnnotationParser;
use ZM\Annotation\Swoole\OnStart;
use ZM\Annotation\Swoole\OnWorkerStart;
use ZM\Console\Console;
class AnnotationParserRegisterTest extends TestCase
@ -34,8 +34,8 @@ class AnnotationParserRegisterTest extends TestCase
public function testAnnotation() {
ob_start();
$gen = $this->parser->generateAnnotationEvents();
$m = $gen[OnStart::class][0]->method;
$class = $gen[OnStart::class][0]->class;
$m = $gen[OnWorkerStart::class][0]->method;
$class = $gen[OnWorkerStart::class][0]->class;
$c = new $class();
try {
$c->$m();

View File

@ -0,0 +1,27 @@
<?php
namespace ZMTest\Testing;
use Module\Example\Hello;
use PHPUnit\Framework\TestCase;
use ZM\Config\ZMConfig;
use ZM\Utils\ZMUtil;
class ModuleTest extends TestCase
{
protected function setUp(): void {
ZMConfig::setDirectory(realpath(__DIR__."/../Mock"));
set_coroutine_params([]);
require_once __DIR__ . '/../../../src/ZM/global_defines.php';
}
public function testCtx() {
$r = ZMUtil::getModInstance(Hello::class);
ob_start();
$r->randNum(["随机数", "1", "5"]);
$out = ob_get_clean();
$this->assertEquals("随机数是1\n", $out);
}
}

View File

@ -25,7 +25,15 @@ $route = new Route(
// ...
$routes->add('date', $route);
$routes->add('date', new Route(
'/archive/{month}', // path
array('controller' => 'showArchive', 'asd' => 'feswf'), // default values
array('month' => '[0-9]{4}-[0-9]{2}', 'subdomain' => 'www|m'), // requirements
array(), // options
'', // host
array(), // schemes
array() // methods
));
$route = new Route('/archive/test');