diff --git a/.gitignore b/.gitignore index 65d831f..1a29d44 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ -**/*.o +.ccls-cache +.vscode +debug +release +stage +compile_commands.json diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..6baa161 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,100 @@ +cmake_minimum_required(VERSION 3.13) +# ----------------------------------------------------------------------------- +# 默认编译器 +# ----------------------------------------------------------------------------- +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Debug) +endif(NOT CMAKE_BUILD_TYPE) +set(CLANG_GCC_TOOLCHAIN "" CACHE PATH "gcc toolchain install prefix, used with clang --gcc-toolchain option") +if (NOT (CLANG_GCC_TOOLCHAIN STREQUAL "")) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --gcc-toolchain=${CLANG_GCC_TOOLCHAIN}") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --gcc-toolchain=${CLANG_GCC_TOOLCHAIN}") +endif() +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +# ----------------------------------------------------------------------------- +# 项目源文件 +# ----------------------------------------------------------------------------- +project(FLAME) +execute_process(COMMAND find ${CMAKE_SOURCE_DIR}/src -name *.cpp + COMMAND tr "\n" ";" + OUTPUT_VARIABLE SOURCES) +add_library(FLAME SHARED ${SOURCES}) +# ----------------------------------------------------------------------------- +# 依赖项目录 +# ----------------------------------------------------------------------------- +set(VENDOR_BOOST /data/vendor/boost-1.70.0) +set(VENDOR_PARSER /data/vendor/parser-1.0.0) +set(VENDOR_LLTOML /data/vendor/lltoml-1.0.0) +set(VENDOR_HIREDIS /data/vendor/hiredis-0.14.0) +set(VENDOR_AMQP /data/vendor/amqpcpp-4.1.5) +set(VENDOR_OPENSSL /data/vendor/openssl-1.1.1c) +set(VENDOR_MONGODB /data/vendor/mongoc-1.14.0) +set(VENDOR_RDKAFKA /data/vendor/rdkafka-1.1.0) +set(VENDOR_PHP /data/server/php-7.2.19) +set(VENDOR_PHPEXT /data/vendor/phpext-2.2.0) +set(VENDOR_CARES /data/vendor/cares-1.15.0) +set(VENDOR_NGHTTP2 /data/vendor/nghttp2-1.39.1) +set(VENDOR_CURL /data/vendor/curl-7.65.0) +set(VENDOR_MARIA /data/vendor/mariac-3.1.2) + +execute_process(COMMAND ${VENDOR_PHP}/bin/php-config --includes + COMMAND sed "s/ *-I/;/g" + OUTPUT_VARIABLE VENDOR_PHP_INCLUDES + OUTPUT_STRIP_TRAILING_WHITESPACE) + +# ----------------------------------------------------------------------------- +# 编译选项 +# ----------------------------------------------------------------------------- +target_compile_options(FLAME BEFORE + PRIVATE "-std=c++17" + PRIVATE "-pthread") +target_link_options(FLAME BEFORE + PRIVATE "-static-libstdc++" + PRIVATE "-pthread") +set_target_properties(FLAME PROPERTIES + PREFIX "" + OUTPUT_NAME "flame") +# 包含路径 +target_include_directories(FLAME SYSTEM PRIVATE + ${VENDOR_PARSER}/include + ${VENDOR_LLTOML}/include + ${VENDOR_HIREDIS}/include + ${VENDOR_AMQP}/include + ${VENDOR_RDKAFKA}/include + ${VENDOR_MONGODB}/include/libmongoc-1.0 + ${VENDOR_MONGODB}/include/libbson-1.0 + ${VENDOR_MARIA}/include + ${VENDOR_NGHTTP2}/include + ${VENDOR_CURL}/include + ${VENDOR_PHPEXT}/include + ${VENDOR_PHP_INCLUDES} + ${VENDOR_OPENSSL}/include + ${VENDOR_BOOST}/include +) +# 链接库 +target_link_libraries(FLAME + ${VENDOR_HIREDIS}/lib/libhiredis.a + ${VENDOR_LLTOML}/lib/liblltoml.a + ${VENDOR_PHPEXT}/lib/libphpext.a + ${VENDOR_MARIA}/lib/mariadb/libmariadbclient.a + ${VENDOR_MONGODB}/lib/libmongoc-static-1.0.a + ${VENDOR_MONGODB}/lib/libbson-static-1.0.a + ${VENDOR_AMQP}/lib/libamqpcpp.a + ${VENDOR_RDKAFKA}/lib/librdkafka.a + ${VENDOR_BOOST}/lib/libboost_program_options.a + ${VENDOR_BOOST}/lib/libboost_context.a + ${VENDOR_BOOST}/lib/libboost_system.a + ${VENDOR_BOOST}/lib/libboost_thread.a + ${VENDOR_BOOST}/lib/libboost_filesystem.a + ${VENDOR_CURL}/lib/libcurl.a + ${VENDOR_NGHTTP2}/lib/libnghttp2.a + ${VENDOR_CARES}/lib/libcares.a + ${VENDOR_OPENSSL}/lib/libssl.a + ${VENDOR_OPENSSL}/lib/libcrypto.a + resolv + z + dl + m + nsl + rt +) diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..d72693d --- /dev/null +++ b/LICENCE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2017-2019 terrywh + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/Makefile b/Makefile deleted file mode 100644 index 2a8223f..0000000 --- a/Makefile +++ /dev/null @@ -1,38 +0,0 @@ -ROOT_TERRYWH=/data/wuhao/cpdocs/github.com/terrywh - -EXTENSION_NAME=flame -EXTENSION_VERSION=0.1.0 - -ROOT_PROJECT=${ROOT_TERRYWH}/php-${EXTENSION_NAME} - -VENDOR_LIBRARY= ${ROOT_TERRYWH}/libphpext/libphpext.a /data/vendor/boost/lib/libboost_system.a -lpthread - -PHP=php -PHP_CONFIG=php-config - -CXX?=g++ -CXXFLAGS?= -g -O0 -INCLUDE=-I${ROOT_TERRYWH}/libphpext `${PHP_CONFIG} --includes` -LIBRARY=${VENDOR_LIBRARY} - -# SOURCES=$(wildcard src/*.cpp) $(wildcard src/**/*.cpp) $(wildcard src/**/**/*.cpp) -SOURCES=src/extension.cpp src/core.cpp src/net/init.cpp src/net/udp_socket.cpp src/net/tcp_socket.cpp src/net/tcp_server.cpp -OBJECTS=$(SOURCES:%.cpp=%.o) - -EXTENSION=${EXTENSION_NAME}.so - -.PHONY: install clean - -# 暂时先将 libphpext.a 作为依赖(库还不稳定) -${EXTENSION}: ${OBJECTS} ${ROOT_TERRYWH}/libphpext/libphpext.a - ${CXX} -shared ${OBJECTS} ${LIBRARY} -Wl,-rpath='$$ORIGIN/' -Wl,-rpath='/usr/local/gcc6/lib64/' -o ${EXTENSION_NAME}.so -%.o: %.cpp - ${CXX} -std=c++11 -fPIC -DEXTENSION_NAME=\"${EXTENSION_NAME}\" -DEXTENSION_VERSION=\"${EXTENSION_VERSION}\" ${CXXFLAGS} ${INCLUDE} -c $^ -o $@ - -clean: - rm -f ${EXTENSION} ${OBJECTS} -install: ${EXTENSION} - cp -f ${EXTENSION} `${PHP_CONFIG} --extension-dir` - -test: - ${PHP} udp/server.php diff --git a/README.md b/README.md index 75a1e88..2a2855e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,232 @@ -#### 依赖 -##### c++ 11 -* -std=c+11 -##### boost -* system -* asio +### FLAME +为解决 PHP 被认为“并发处理能力不足”的问题,我最初在业务中试用 [Swoole](https://www.swoole.com/) 框架;估计由于使用的是 Swoole 相对早期的版本,在实际业务中出现了各式各样的问题(很多都是内存问题);同时很多同学对 Swoole 中提供的进程使用形式,都感觉把握比较吃力; +于是在各方支持下,开发了 FLAME 框架: + +* 基于协程机制的异步化网络底层( TCP / UDP / HTTP); +* 简单的多进程模式 ( SO_REUSEPORT ); +* 基于协程的队列机制( 类似 Go 的 Channel); +* 多种驱动异步化支持( MySQL / Redis / MongoDB / RabbitMQ / Kafka ); + +同时为了简化日常应用开发成本,及很多重复性工作,框架还提供了: +* 平滑起停; +* 多进程日志、登等级过滤、文件重载; +* 数据库连接池机制; +* 异步进程; +* 等等(BSON / SNAPPY / INTERFACE)... + +目前,FLAME 已经使用在了公司内各种业务线;实际使用中,框架在**并行处理性能**方面表现十分**突出**,并在持续的改进、完善下,框架在**稳定性**方面也有了**不错**的表现; +不过,由于时间、能力问题,未能详尽的测试整个框架的各项功能,可能还存在一些问题、缺陷;感谢大家的包容、反馈与帮助(大礼参拜) + +### 示例 +以下代码实现了几个简单的 HTTP 接口,展示了框架的基本使用方法: + +``` PHP +before(function($req, $res) { // 前置处理器(HOOK) + $req->data["before"] = flame\time\now(); // 记录请求开始时间 + }) + ->get("/hello", function($req, $res) { // 路径处理器 + // 简单响应方式 + $res->status = 200; + $res->body = "world"; + }) + ->post("/hello/world", function($req, $res) { // 路径处理器 + // Transfer-Encoding: Chunked + $res->write_header(200); + $res->write("CHUNKED RESPONSE:") + $res->write($res->body); + $res->end(); + }) + ->after(function($req, $res, $r) { + // 后置处理器(HOOK) + flame\log\trace($req->method, $req->path // 请求时长日志记录 + , "in", (flame\time\now() - $req->data["before"]), "ms"); + if(!$r) { + $res->status = 404; + $res->file(__DIR__."/404.html"); // 响应文件 + } + }); + $server->run(); +}); +// 启动(调度) +flame\run(); +``` + +### 功能 + +* **core** - 核心,框架初始化设置,协程启动,协程队列,协程锁等; +* **time** - 时间相关,协程休眠,当前时间等; +> 毫秒级时间戳及缓存机制; +* **log** - 简单日志记录功能: +> 多进程统一日志记录; +> 支持日志登记过滤设置; +> 支持通过信号进行日志重载; +* **os** - 操作系统相关信息获取,异步进程启停操作等; +> 获取本地网卡地址; +* **tcp** - 封装 TCP 相关服务端客户端功能; +* **udp** - 封装 UDP 相关服务端客户端功能; +* **http** - 简单的 HTTP 客户端/服务端封装; +> 客户端支持长连 Keep-Alive 及相关连接数控制; +> 客户端支持 HTTPS 及 HTTP/2 协议部分功能; +> 客户端暂不支持 form-data/multipart 形式的请求; +> 服务器**不支持** HTTPS 可考虑使用 NGINX 等进行反向代理; +* **mysql** - 简单 MySQL 客户端: +> 提供部分简化方法,如 `insert/delete/one` 等, 自动进行 ESCAPE 转义; +> 内部使用连接池形式自动连接复用; +> 支持事务; +* **redis** - 简单 Redis 客户端: +> 内部使用连接池形式自动连接复用; +> 暂不支持 SUBSCRIBE 相关指令; +* **mongodb** - 简单 MongoDB 客户端: +> 提供部分简化方法,如 `insert/delete/one/count` 等; +> 内部使用连接池自动连接复用; +* **rabbitmq** - 简单 RabbitMQ 生产消费支持: +> 支持使用协程进行"并行"消费; +* **kafka** - 简单 Kafka 生产消费支持: +> 仅支持 Kafka 0.10+ 群组消费形式; +> 支持使用协程进行"并行"消费; +* **hash** - 提供了若干哈希算法; +* **encoding** - 提供了若干编码、序列化函数; +* **compress** - 提供了若干压缩算法; + +### 说明 +* [API 文档](https://github.com/terrywh/php-flame/tree/master/doc) +* [常见问题](https://github.com/terrywh/php-flame/wiki/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98) +* 自动完成(IDE提示)- 目录 `/doc` 下使用 PHP Doc 语法定义和说明 API 接口,可挂接 IDE 自动完成功能; + * VSCode/ (php-intelephense) + > ``` Bash + > ln -s /path/to/php-flame/doc /path/to/extensions/bmewburn.vscode-intelephense-client-x.x.xx/node_modules/intelephense/lib/stub/flame + > ``` + * PhpStorm + > [Configuring Include Paths](https://www.jetbrains.com/help/phpstorm/configuring-include-paths.html#Configuring_Include_Paths.xml) + +### 其他 +
依赖项编译安装,仅供参考 +

+ +#### boost +``` Bash +./bootstrap.sh --prefix=/data/vendor/boost-1.70.0 +./b2 --prefix=/data/vendor/boost-1.70.0 cxxflags="-fPIC" variant=release link=static threading=multi install +``` + +#### cpp-parser +``` Bash +make install +``` + +#### lltoml +``` Bash +CFLAGS="-O2 -DNDEBUG" CXXFLAGS="-O2 -DNDEBUG" make +make install +``` + +#### hiredis +``` Bash +CC=gcc make +PREFIX=/data/vendor/hiredis-0.14.0 make install +# 未提供禁用动态库选项 +# rm /data/vendor/hiredis-0.14.0/lib/*.so* +``` + +#### openssl +``` Bash +CC=gcc CXX=g++ ./Configure no-shared --prefix=/data/vendor/openssl-1.1.1c linux-x86_64 +make && make install +``` + +#### AMQP-CPP +``` Bash +mkdir stage && cd stage +CC=gcc CXX=g++ CXXFLAGS="-fPIC -I/data/vendor/openssl-1.1.1c/include" LDFLAGS="-L/data/vendor/openssl-1.1.c/lib" cmake -DCMAKE_INSTALL_PREFIX=/data/vendor/amqpcpp-4.1.5 -DCMAKE_BUILD_TYPE=Release -DAMQP-CPP_LINUX_TCP=ON ../ +make && make install +``` + + + +#### mongoc-driver +``` Bash +mkdir stage && cd stage +CC=gcc CXX=g++ CFLAGS="-fPIC" LDFLAGS="-pthread -ldl" PKG_CONFIG_PATH=/data/vendor/openssl-1.1.1c/lib/pkgconfig cmake -DCMAKE_INSTALL_PREFIX=/data/vendor/mongoc-1.14.0 -DCMAKE_INSTALL_LIBDIR=lib -DCMAKE_BUILD_TYPE=Release -DENABLE_STATIC=ON -DENABLE_SASL=OFF -DENABLE_SHM_COUNTERS=OFF -DENABLE_TESTS=OFF -DENABLE_EXAMPLES=OFF -DENABLE_AUTOMATIC_INIT_AND_CLEANUP=OFF ../ +make && make install +# 未提供 ENABLE_SHARED=OFF 或类似选项 +# rm /data/vendor/mongoc-1.14.0/lib/*.so* +``` + +#### rdkafka +``` Bash +CC=gcc CXX=g++ PKG_CONFIG_PATH=/data/vendor/openssl-1.1.1c/lib/pkgconfig ./configure --prefix=/data/vendor/rdkafka-1.1.0 --disable-sasl +make && make install +# rm /data/vendor/rdkafka-1.1.0/lib/*.so* +cp src/snappy.h /data/vendor/rdkafka-1.1.0/include/librdkafka/ +cp src/rdmurmur2.h /data/vendor/rdkafka-1.1.0/include/librdkafka/ +cp src/xxhash.h /data/vendor/rdkafka-1.1.0/include/librdkafka/ +``` + +#### PHP +``` Bash +CC=gcc CXX=g++ CFLAGS="-pthread" ./configure --prefix=/data/server/php-7.2.19 --with-config-file-path=/data/server/php-7.2.19/etc --disable-simplexml --disable-xml --disable-xmlreader --disable-xmlwriter --with-readline --enable-mbstring --without-pear --with-zlib --with-openssl=/data/vendor/openssl-1.1.1c --build=x86_64-linux-gnu +make && make install +``` + + + +#### libphpext +``` Bash +CXXFLAGS="-O2 -DNDEBUG" make +make install +``` + +#### c-ares +``` Bash +mkdir stage && cd stage +CC=gcc CXX=g++ cmake -DCARES_SHARED=OFF -DCARES_STATIC=ON -DCARES_STATIC_PIC=ON -DCMAKE_INSTALL_PREFIX=/data/vendor/cares-1.15.0 -DCMAKE_BUILD_TYPE=Release ../ +make && make install +``` + +#### nghttp2 +``` Bash +mkdir stage && cd stage +CC=gcc CXX=g++ CFLAGS="-fPIC" CXXFLAGS="-fPIC" PKG_CONFIG_PATH="/data/vendor/cares-1.15.0/lib/pkgconfig:/data/vendor/openssl-1.1.1c/lib/pkgconfig" cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/data/vendor/nghttp2-1.39.1 -DENABLE_LIB_ONLY=ON -DENABLE_SHARED_LIB=OFF -DENABLE_STATIC_LIB=ON -DCMAKE_INSTALL_LIBDIR:PATH=lib ../ +make && make install +``` + +#### curl +``` Bash +mkdir stage && cd stage +# quote: cmake for curl is poorly maintained +# segfault @7.65.1 +CC=gcc CXX=g++ CFLAGS=-fPIC CPPFLAGS=-fPIC ./configure --with-ssl=/data/vendor/openssl-1.1.1c --enable-ares=/data/vendor/cares-1.15.0 --with-nghttp2=/data/vendor/nghttp2-1.39.1 --enable-shared=no --enable-static --enable-ipv6 --without-brotli --without-libidn2 --without-libidn --without-librtmp --disable-unix-sockets --disable-ftp --disable-ldap --disable-ldaps --disable-rtsp --disable-dict --disable-file --disable-telnet --disable-tftp --disable-pop3 --disable-imap --disable-smb --disable-smtp --disable-gopher --without-libpsl --prefix=/data/vendor/curl-7.65.0 +make && make install +``` + +#### maria-connector-c +``` Bash +mkdir stage && cd stage +CC=gcc CXX=g++ CFLAGS="-pthread" CXXFLAGS="-pthread" PKG_CONFIG_PATH=/data/vendor/openssl-1.1.1c/lib/pkgconfig:/data/vendor/curl-7.65.0/lib/pkgconfig cmake -DCMAKE_BUILD_TYPE=Release -DCLIENT_PLUGIN_SHA256_PASSWORD=STATIC -DCLIENT_PLUGIN_CACHING_SHA2_PASSWORD=STATIC -DCMAKE_INSTALL_PREFIX=/data/vendor/mariac-3.1.2 ../ +make && make install +# rm /data/vendor/mariac-3.1.2/lib/mariadb/*.so* +``` + +

+
+ +### 版权声明 +本软件使用 MIT 许可协议; diff --git a/doc/compress.php b/doc/compress.php new file mode 100644 index 0000000..460f9b4 --- /dev/null +++ b/doc/compress.php @@ -0,0 +1,17 @@ + ["b" => 123]]; + * var_dump(flame\get($a, "a.b")); // 123 + * @return mixed + * 注意: + * 1. 数字下标将被当作文本处理; + */ +function get($array, $keys) {} +/** + * 设置数组的层级键值 + * @example + * $a = []; + * flame\set($a, "a.b", 123); + * var_dump($a); // ["a" => ["b" => 123]] + * 注意: + * 1. 数字下标将被当作文本处理; + */ +function set(&$array, $keys, $val) {} +/** + * 获取一个当前协程 ID 标识 + */ +function co_id():int { + return 123456; +} +/** + * 获取运行中的协程数量 + */ +function co_count():int { + return 8; +} +/** + * 框架调度, 上述协程会在框架开始调度运行后启动 + * 注意:协程异步调度需要 run() 才能启动执行; + */ +function run() {} +/** + * 从若干个队列中选择(等待)一个有数据队列 + * @return 若所有通道已关闭, 返回 null; 否则返回一个有数据的通道, 即: 可以无等待 pop() + */ +function select(queue $q1, $q2/*, ...*/):queue { + return new queue(); +} +/** + * 向指定工作进程发送消息通知 + * @param int $target_worker 目标工作进程编号,1 ~ FLAME_MAX_WORKERS;当前进程可读取环境变量 FLAME_CUR_WORKER 获取编号; + * @param mixed $data 消息数据,自动进行 JSON 序列化传输; + * 注意: + * 对象类型数据进行 JSON 序列化可能丢失数据细节而无法还原; + * 目标进程将会收到 `message`(自动 JSON 反序列化数据)回调; + * @see flame\on("message", $cb); + */ +function send(int $target_worker, $data) { + +} +/** + * 为指定事件添加处理回调(函数) + * @param string $event 目前消息存在以下两种: + * * "exception" - 当协程发生未捕获异常, 执行对应的回调,并记录错误信息(随后进程会退出),用户可在此回调进行错误报告或报警; + * * "quit" - 退出消息, 用户可在此回调停止各种服务,如停止 HTTP 服务器 / 关闭 MySQL 连接等; + * * "message" - 消息通知,(启动内部协程)接收来自各子进程之间的相互通讯数据;回调函数接收一个不限类型参数; + * @see flame\send() + * @param callable 回调函数 + */ +function on(string $event, callable $cb) {} +/** + * 删除指定事件的所有处理回调(函数) + * 注意: + * 由于 `message` 事件内部启动协程,阻止了程序的停止;须取消对应回调,使该内部协程停止后,进程才可以正常退出; + */ +function off(string $event) {} +/** + * 用于在用户处理流程中退出 + * 注意: + * 1. 使用 PHP 内置 exit() 进行退出可能导致框架提供的清理、退出机制无效;(请示用本函数代替) + * 2. 调用本函数主动退出进程,不会触发上述 `on("quit", $cb)` 注册的回调; + * 3. 父子进程模式,非停止流程,当 $exit_code 不为 0 时表示异常退出,父进程会(1s ~ 3s 后)将当前工作进程重启; + */ +function quit(int $exit_code = 0) {} +/** + * 协程型队列 + */ +class queue { + /** + * @param int $max 队列容量, 若已放入数据达到此数量, push() 将"阻塞"(等待消费); + */ + function __construct($max = 1) {} + /** + * 放入; 若向已关闭的队列放入, 将抛出异常; + */ + function push($v) {} + /** + * 取出 + */ + function pop() {} + /** + * 关闭 (将唤醒阻塞在取出 pop() 的协程); + * 原则上仅能在生产者方向关闭队列; + */ + function close() {} + /** + * 队列是否已关闭 + */ + function is_closed(): bool { + return false; + } +} +/** + * 协程互斥量(锁) + */ +class mutex { + /** + * 构建一个协程式 mutex 对象 + */ + function __construct() {} + /** + * 加锁 + */ + function lock() {} + /** + * 解锁 + */ + function unlock() {} +} +/** + * 协程锁(使用上述 mutex 构建自动加锁解锁流程) + */ +class guard { + /** + * 构建守护并锁定 mutex + * @param mutex $mutex 实际保护使用的 mutex 对象 + */ + function __construct(mutex $mutex) {} + /** + * 解锁 mutex 并销毁守护 + */ + function __destruct() {} +} diff --git a/doc/encoding.php b/doc/encoding.php new file mode 100644 index 0000000..8d8f37f --- /dev/null +++ b/doc/encoding.php @@ -0,0 +1,17 @@ + "close" 防止连接复用; + * @var array + */ + public $header; + /** + * @var array + */ + public $cookie; + /** + * @var mixed + */ + public $body; + /** + * 构建请求, 可选的指定请求体 `$body` (将自动设置为 POST 请求方法)及超时时间 `$timeout` + */ + function __construct(string $url, $body = null, int $timeout = 3000) {} + /** + * @param string $cert_file 证书文件路径 + * @param string $pkey_file 密钥文件路径 + * @param string $pkey_pass 密钥密码 + */ + function ssl_pem(string $cert_file, string $pkey_file = "", string $pkey_pass = "") {} + /** + * 请使用 client_request::SSL_VERIFY_* 相关常量进行设置(可按位组合) + */ + function ssl_verify(int $verify) {} + /** + * 请选择使用 client_request::HTTP_VERSION_* 常量设置版本 + */ + function http_version(int $version) {} +} +/** + * 客户端响应对象,执行请求后得到 + */ +class client_response { + /** + * 对应请求实际的 HTTP 版本 + * @var string + */ + public $version; + /** + * 响应码 + * @var int + */ + public $status; + /** + * 注意: 目前使用了数组形式, 切暂不支持多个同名 Header 数据的访问; + * @var array + */ + public $header; + /** + * @var mixed 以下几种 Content-Type 将自动进行解析并生成关联数组: + * * "application/json" + * * "application/x-www-form-urlencoded" + * * "multipart/form-data" + * 其他类型保持文本的原始数据(与 $raw_body 相同); + */ + public $body; + /** + * 原始请求体数据 + * @var string + */ + public $raw_body; +} +/** + * HTTP 服务器 + */ +class server { + /** + * 服务端本地监听地址 + */ + public $address; + /** + * 创建服务器并绑定地址 + */ + function __construct(string $address) {} + /** + * 每次 HTTP 请求在执行对应 path 的处理器前, 先执行下述设置的回调; + * 回调形式如下: + * function callback(server_request $req, server_response $res, bool $match) {} + * 其中 $match 参数标识此请求是否匹配了定义的 path 处理器; + * 当 callback 函数返回 false 时, 将**不再**执行后续处理器 + */ + function before(callable $cb):server { + return $this; + } + /** + * 每次 HTTP 请求在执行对应 path 处理器后, 会执行下述设置的回调; + * 回调形式同 before(); + * 注意: 若 before 回调或 path 回调返回了 false, 此 after 回调将不再执行; + */ + function after(callable $cb):server { + return $this; + } + /** + * 设置一个处理 "PUT $path HTTP/1.1" 请求的路径 path 处理器; + * 注意: 当 before 回调返回 false 时, 此处理器将不再被执行; + */ + function put(string $path, callable $cb):server { + return $this; + } + /** + * 设置一个处理 "DELETE $path HTTP/1.1" 请求的路径 path 处理器; + * 注意: 当 before 回调返回 false 时, 此处理器将不再被执行; + */ + function delete(string $path, callable $cb):server { + return $this; + } + /** + * 设置一个处理 "POST $path HTTP/1.1" 请求的路径 path 处理器; + * 注意: 当 before 回调返回 false 时, 此处理器将不再被执行; + */ + function post(string $path, callable $cb):server { + return $this; + } + /** + * 设置一个处理 "PATCH $path HTTP/1.1" 请求的路径 path 处理器; + * 注意: 当 before 回调返回 false 时, 此处理器将不再被执行; + */ + function patch(string $path, callable $cb):server { + return $this; + } + /** + * 设置一个处理 "GET $path HTTP/1.1" 请求的路径 path 处理器; + * 注意: 当 before 回调返回 false 时, 此处理器将不再被执行; + */ + function get(string $path, callable $cb):server { + return $this; + } + /** + * 设置一个处理 "HEAD $path HTTP/1.1" 请求的路径 path 处理器; + * 注意: 当 before 回调返回 false 时, 此处理器将不再被执行; + */ + function head(string $path, callable $cb):server { + return $this; + } + /** + * 设置一个处理 "OPTIONS $path HTTP/1.1" 请求的路径 path 处理器; + * 注意: 当 before 回调返回 false 时, 此处理器将不再被执行; + */ + function options(string $path, callable $cb):server { + return $this; + } + /** + * 启动服务器, 监听请求并执行对应回调 + * 注意: 运行服务器将阻塞当前协程; + */ + function run() {} + /** + * 停止服务器, 不再接收新的连接; 上述 run() 阻塞协程将恢复执行; + * 注意: 正在处理中的请求不收影响(继续处理); + */ + function close() {} +} +/** + * 服务端请求对象(由 server 对应回调获得) + */ +class server_request { + /** + * 请求方法(大写) + * @var string + */ + public $method = "GET"; + /** + * 请求路径(不包含查询参数) + * @var string + */ + public $path = "/"; + /** + * 请求参数, 使用 PHP 内置 parse_str() 函数解析得到; + * @var array + */ + public $query; + /** + * 请求头; + * @var array + * 注意:所有头信息字段名被转换为**小写**形式; + * 注意:由于目前使用数组形式,暂不支持同名字段; + */ + public $header; + /** + * 请求 Cookie + * @var array + */ + public $cookie; + /** + * 请求体, 以下几种 Content-Type 时自动解析为关联数组: + * * "application/json" + * * "application/x-www-form-urlencoded" + * * "multipart/form-data" + * 其他类型时与 $raw_body 相同; + * @var mixed + */ + public $body; + /** + * 原始请求体 + * @var string + */ + public $raw_body; + /** + * 上传文件,形如: + * [ + * "file_field_1"=> + * [ + * "content-type" => "mime_type", + * ... // other header field lowercased if available + * "filename" => "original_file_name", + * "size" => file_data_size, + * "data" => "file_data", // content of the uploaded file + * ], + * "file_filed_2"=> + * ... + * ] + * 若无上传文件时可能为空 + * @var array + */ + public $file; + /** + * 用户可设置的请求关联数据; + * @example 在 before 回调中从 Cookie 中提取用户信息放在 $req->data["user"] 中, 后续处理器即可访问 $req->data["user"] 获取户信息; + * @var array + */ + public $data; +} + +/** + * 服务端响应对象(由 server 对应处理回调获得)提供接口向客户端返回数据 + * 默认状态下 HTTP/1.1 使用 长连接(保持连接);可使用 Connection: close 头禁止; + * 向进程发送 SIGUSR1 信号时将会在 长短连 状态切换(短连接状态自动附加 Connection: close 头信息); + */ +class server_response { + /** + * 响应状态码, 可以直接设置; 也可以在 Transfer-Encoding: chunked 模式时使用 write_header 指定; + * @var int + */ + public $status = 200; + /** + * 响应头信息, Key/Val 形式 + * @var array + * 注意:一般情况下 Key/Val 均为字符串,其他类型会被转换为字符串输出; + */ + public $header; + // public $cookie; + /** + * 响应体, 用于在 Content-Length: xxx 响应模式下返回数据; + * @var mixed 任意数据, 实际响应会序列化此数据; 以下几种 Content-Type 存在内置的序列化: + * * "application/json" - 使用 json_encode 进行序列化; + * * "application/x-www-form-urlencoded" - 使用 http_build_query 进行序列化; + * 其他类型强制转换为文本类型后返回响应; + */ + public $body; + /** + * 设置返回的 Set-Cookie 项; + * 参数功能与 PHP 内置 set_cookie 函数类似; + */ + function set_cookie(string $name, string $value = null, int $expire = 0, string $path = "/", string $domain = "", bool $secure = false, bool $http_only = false) {} + /** + * 返回响应头, 用于启用 Transfer-Encoding: chunked 模式, 准备持续响应数据; + * @param int $status 当参数存在时, 覆盖上述 $res->status 属性响应码; + * + * Transfer-Encoding: chunked 模式可用于 EventSource 等持续响应; + */ + function write_header($status = 0) {} + /** + * 在 Transfer-Encoding: chunked 模式下, 返回一个 chunk 数据块; + */ + function write(string $chunk) {} + /** + * 在 Transfer-Encoding: chunked 模式下, 结束响应过程 (返回最后一个结束块); + * @param string $chunk 可选, 调用上述 write() 返回一个数据块; + */ + function end(string $chunk = null) {} + /** + * 响应一个文件的内容; 一般需要自行设置对应文件的 Content-Type 以保证浏览器能正常渲染; + * 下述参数中的 $path 将会被正常化处理, 以保证其不超出 $root 指定的根目录范围; + * 注意: 此功能一般仅用于调试或少量文件访问使用, 大量生产环境请考虑使用 nginx 等代为处理静态文件; + */ + function file(string $root, string $path) {} +} diff --git a/doc/kafka.php b/doc/kafka.php new file mode 100644 index 0000000..6c8fb3d --- /dev/null +++ b/doc/kafka.php @@ -0,0 +1,119 @@ + 1, // 插入数量 + * "ok" => 1, // 插入结果 + * ... + * ) + */ + function insert(array $data, bool $ordered = true):array { + return []; + } + function insert_many(array $data, bool $ordered = true):array { + return []; + } + function insert_one(array $data):array { + return []; + } + /** + * 执行删除 + * @return array 形式同 insert() 类似: + * array( + * "n" => 1, // 删除数量 + * "ok" => 1, // 删除结果 + * ... + * ) + */ + function delete(array $query, int $limit = 0):array { + return []; + } + function delete_many(array $query, int $limit = 0):array { + return []; + } + function delete_one(array $query):array { + return []; + } + /** + * 执行更新动作 + * @param array $update 一般使用类似如下形式进行更新设置: + * ['$set'=>['a'=>'b'],'$inc'=>['c'=>2]] + * @return array 形式同 insert() 类似: + * array( + * "n" => 1, // 更新数量 + * "ok" => 1, // 更新结果 + * ... + * ) + */ + function update(array $query, array $update, $upsert = false):array { + return []; + } + function update_many(array $query, array $update, $upsert = false):array { + return []; + } + function update_one(array $query, array $update, $upsert = false):array { + return []; + } + /** + * 执行指定查询,返回游标对象 + * @param array $sort 一般使用如下形式表达排序: + * ['a'=>1,'b'=>-1] // a 字段升序,b 字段降序 + * @param mixed $limit 可以为 int 设置 `limit` 值,或 array 设置 `[$skip, $limit]` + */ + function find(array $query, array $projection = null, array $sort = null, $limit = null):cursor { + return new cursor(); + } + function find_many(array $query, array $projection = null, array $sort = null, $limit = null):cursor { + return new cursor(); + } + + /** + * 查询并返回单个文档 + * @param array $sort 一般使用如下形式表达排序: + * ['a'=>1,'b'=>-1] // a 字段升序,b 字段降序 + */ + function find_one(array $query, array $sort = null): array { + return []; + } + function one(array $query, array $sort = null): array { + return []; + } + /** + * 查询并返回单个文档的指定字段值 + * @param array $sort 一般使用如下形式表达排序: + * ['a'=>1,'b'=>-1] // a 字段升序,b 字段降序 + * @return mixed 字段类型映射请参见顶部说明; + */ + function get(array $query, string $field, array $sort = null) { + return null; + } + /** + * 查询并返回匹配文档数量 + * @param mixed $limit integer/array 请参见 find 函数对应 $limit 说明 + */ + function count(array $query, $limit): int { + return 0; + } + /** + * 执行指定聚合操作,返回游标对象; + * 请参考 https://docs.mongodb.com/master/reference/operator/aggregation-pipeline/ 编写; + */ + function aggregate(array $pipeline, array $options = []): cursor { + return new cursor(); + } + /** + * 查找满足条件的第一个文档,删除并将其返回 + */ + function find_and_delete(array $query, array $sort = null, boolean $upsert = false, $fields = null): array { + return []; + } + /** + * 查找满足条件的第一个文档,对其进行更新,并返回 + * @param $new boolean 返回更新后的文档 + */ + function find_and_update(array $query, array $update, array $sort = null, $upsert = false, array $fields = null, $new = false) { + return []; + } +} +/** + * 游标对象 + */ +class cursor { + /** + * 读取当前 cursor 返回一个文档; 若读取已完成, 返回 null; + * @return mixed array/null + */ + function fetch_row():array { + return []; + } + /** + * 读取当前 cursor 返回所有文档 (二维); 若读取已完成, 返回 null; + * @return mixed array/null + */ + function fetch_all(): array { + return []; + } +} +/** + * 对应 MongoDB 日期时间字段,毫秒级精度 + */ +class date_time implements \JsonSerializable { + /** + * 构建日期时间对象 + * @param int $milliseconds 指定毫秒级时间,默认 null 使用当前时间 + */ + function __construct(int $milliseconds = null) {} + function __toString() {} + function __toDateTime(): \DateTime { + return new \DateTime(); + } + function __debugInfo() {} + function jsonSerialize(): array { + return []; + } + /** + * 返回秒级时间戳 + */ + function unix(): int { + return time(); + } + /** + * 返回毫秒级时间戳 + */ + function unix_ms(): int { + return intval(microtime(true) * 1000); + } + /** + * 返回标准的 YYYY-MM-DD hh:mm:ss 文本形式的时间 + */ + function iso(): string { + return "2019-03-31 21:40:25"; + } +} +/** + * 对应 MongoDB 对象ID + */ +class object_id implements \JsonSerializable { + /** + * 构建 + * @param string $object_id 从文本形式构建(恢复)一个对象,默认 null,构建一个新的对象 + */ + function __construct(string $object_id = null) {} + function __toString() {} + function __toDateTime(): \DateTime { + return new \DateTime(); + } + function __debugInfo() {} + function jsonSerialize(): array { + return []; + } + /** + * 秒级时间戳 + */ + function unix(): int { + return time(); + } + /** + * 当前对象与另一 object_id 对象是否**值相等** + */ + function equal(object_id $oid): bool { + return false; + } +} diff --git a/doc/mysql.php b/doc/mysql.php new file mode 100644 index 0000000..024f61b --- /dev/null +++ b/doc/mysql.php @@ -0,0 +1,265 @@ + 字符集 + * * "auth" => 认证方式,默认 "mysql_native_password" 可用 "caching_sha2_password"(MySQL 8.0 服务器默认) / "sha256_parssword" + * * "proxy" => 当前地址是否是代理服务(已提供连接复用机制) "1" - 是,已提供连接复用机制 (禁用框架提供的复用重置机制), "0" - 否,未提供(默认) + * 注意:若服务端配置默认字符集与上述字符集不同,且未指定 proxy=1 参数,那么: + * 每次连接使用(复用)连接重置,字符集被恢复为服务器默认,框架将再次进行字符集设置(这里可能会产生少量额外消耗); + * 注意:若指定了 proxy 参数框架不再进行任何”连接复用“的清理工作(CONNECTION_RESET / CHANGE_USER / CHARSET_RESET); + * @return client 客户端对象 + */ +function connect($url): client { + return new client(); +} + +/** + * WHERE 字句语法规则: + * + * @example + * $where = ["a"=>"1", "b"=>[2,"3","4"], "c"=>null, "d"=>["{!=}"=>5]]; + * // " WHERE (`a`='1' AND `b` IN ('2','3','4') AND `c` IS NULL AND `d`!='5')" + * @example + * $where = ["{OR}"=>["a"=>["{!=}"=>1], "b"=>["{><}"=>[1, 10, 3]], "c"=>["{~}"=>"aaa%"]]]; + * // $sql == " WHERE (`a`!='1' OR `b` NOT BETWEEN 1 AND 10 OR `c` LIKE 'aaa%')" + * @example + * $where = "`a`=1"; + * // $sql == " WHERE `a`=1"; + * + * @param 特殊的符号均以 "{}" 进行包裹, 存在以下可用项: + * * `{NOT}` / `{!}` - 逻辑非, 对逻辑子句取反, 生成形式: `NOT (.........)`; + * * `{OR}` / `{||}` - 逻辑或, 对逻辑子句进行逻辑或拼接, 生成形式: `... OR ... OR ...`; + * * `{AND}` / `{&&}` - 逻辑与, 对逻辑子句进行逻辑与拼接 (默认拼接方式), 生成形式: `... AND ... AND ...`; + * * `{!=}` - 不等, 生成形式: `...!='...'` / ` ... IS NOT NULL`; + * * `{>}` - 大于, 生成形式: `...>...`; + * * `{<}` - 小于, 生成形式: `...<...`; + * * `{>=}` - 大于等于, 生成形式: `...>=...`; + * * `{<=}` - 小于等于, 生成形式: `...<=...`; + * * `{<>}` - 区间内, 生成形式: `... BETWEEN ... AND ...`, 目标数组至少存在两个数值; + * * `{><}` - 区间外, 生成形式: `... NOT BETWEEN ... AND ...`, 目标数组至少存在两个数值; + * * `{~}` - 模糊正匹配, 生成形式: `... LIKE ...`; + * * `{!~}` - 模糊非匹配, 生成形式: `... NOT LIKE ...`; + */ + +/** + * ORDER 子句语法规则: + * @example + * $order = "`a` ASC, `b` DESC"; + * // $sql == " ORDER BY `a` ASC, `b` DESC"); + * @example + * $order = ["a"=>1, "b"=>-1, "c"=>true, "d"=>false, "e"=>"ASC", "f"=>"DESC"]; + * // $sql == " ORDER BY `a` ASC, `b` DESC, `c` ASC, `d` DESC, `e` ASC, `f` DESC" + * + * @param + * * 当 value 位正数或 `true` 时, 生成 `ASC`; 否则生成 `DESC`; + * * 当 value 为文本时, 直接拼接; + */ + +/** + * LIMIT 子句语法规则: + * @example + * $limit = 10; + * // $sql == " LIMIT 10" + * @example + * $limit = [10,10]; + * // $sql == " LIMIT 10, 10" + * @example + * $limit = "20, 300"; + * // $sql == " LIMIT 20, 300" + * + * @param + * * 当 $limit 值位文本或数值时, 直接拼接; + * * 当 $limit 为数组时, 拼接前两个值; + */ + +/** + * MySQL 客户端(内部使用连接池) + */ +class client { + /** + * 将数据进行转移,防止可能出现的 SQL 注入等问题; + */ + function escape($value):string { + return "escaped string"; + } + /** + * 返回事务对象 (绑定在一个连接上) + */ + function begin_tx(): ?tx { + return new tx(); + } + /** + * 执行制定的 SQL 查询, 并返回结果 + * @param string $sql + * @return mixed SELECT 型语句返回结果集对象 `result`; 否则返回关联数组, 一般包含 affected_rows / insert_id 两项; + */ + function query(string $sql): result { + return new result(); + } + /** + * 返回最近一次执行的 SQL 语句 + * 注意:由于协程的“并行”(穿插)执行,本函数返回的语句可能与当前上下问代码不对应; + */ + function last_query(): string { + return "last executed sql"; + } + /** + * 向指定表插入一行或多行数据 (自动进行 ESCAPE 转义) + * @param string $table 表名 + * @param array $data 待插入数据, 多行关联数组插入多行数据(以首行 KEY 做字段名); 普通关联数组插入一行数据; + * @return array 关联数组, 一般包含 affected_rows / insert_id 两项; + */ + function insert(string $table, array $data): result { + return new result(); + } + /** + * 从指定表格删除匹配的数据 + * @param string $table 表名 + * @param mixed $where WHERE 子句, 请参考上文;(数组形式描述将自动进行 ESCAPE 转义) + * @param mixed $order ORDER 子句, 请参考上文;(数组形式描述将自动进行 ESCAPE 转义) + * @param mixed $limit LIMIT 子句, 请参考上文;(数组形式描述将自动进行 ESCAPE 转义) + * @return array 关联数组, 一般包含 affected_rows / insert_id 两项; + */ + function delete(string $table, $where, $order = null, $limit = null): result { + return new result(); + } + /** + * 更新指定表 + * @param string $table 表名 + * @param mixed $where WHERE 子句, 请参考上文; + * @param mixed $modify 当 $modify 为字符串时, 直接拼接 "UPDATE ... SET $modify", 否则按惯例数组进行 KEY = VAL 拼接; + * @param mixed $order ORDER 子句, 请参考上文; + * @param mixed $limit LIMIT 子句, 请参考上文; + * @return array 关联数组, 一般包含 affected_rows / insert_id 两项; + */ + function update(string $table, $where, $modify, $order = null, $limit = null): result { + return new result(); + } + /** + * 从指定表筛选获取数据 + * @param string $table 表名 + * @param mixed $fields 待选取字段, 为数组时其元素表示各个字段; 文本时直接拼接在 "SELECT $fields FROM $table"; + * 关联数组的 KEY 表示对应函数, 仅可用于简单函数调用, 例如: + * "SUM" => "a" + * 表示: + * SUM(`a`) + * @param mixed $where WHERE 子句, 请参考上文; + * @param mixed $order ORDER 子句, 请参考上文; + * @param mixed $limit LIMIT 子句, 请参考上文; + */ + function select(string $table, $fields, $where, $order = null, $limit = null): result { + return new result(); + } + /** + * 从指定表筛选获取一条数据, 并立即返回该行数据 + */ + function one(string $table, $where, $order = null): array { + return []; + } + /** + * 从指定表获取一行数据的指定字段值 + * @return mixed 字段类型映射请参见顶部说明 + */ + function get(string $table, string $field, array $where, $order) { + return null; + } + /** + * 获取服务端版本 + */ + function server_version():string { + return "5.5.5"; + } +}; + +/** + * 事务对象, 与 client 对象接口基本相同 + */ +class tx { + /** + * 提交当前事务 + */ + function commit() {} + /** + * 回滚当前事务 + */ + function rollback() {} + /** + * @see client::query() + * @return mixed cursor/array + */ + function query(string $sql) {} + /** + * @see client::insert() + */ + function insert(string $table, array $data): array { + return []; + } + /** + * @see client::delete() + */ + function delete(string $table, $where, $order, $limit): array { + return []; + } + /** + * @see client::update() + */ + function update(string $table, $where, $modify, $order = null, $limit = null): array { + return []; + } + /** + * @see client::select() + */ + function select(string $table, $fields, $where, $order = null, $limit = null): result { + return new result(); + } + /** + * @see client::one() + */ + function one(string $table, $where, $order = null): array { + return []; + } + /** + * @see client::get() + * @return mixed 字段类型映射参见顶部说明; + */ + function get(string $table, string $field, $where, $order = null) { + return null; + } +}; +/** + * 结果集 + */ +class result { + /** + * @var 结果集包含的记录行数 + */ + public $fetched_rows; + /** + * 读取下一行 + * @return 下一行数据关联数组; + * 若读取已完成, 返回 NULL + */ + function fetch_row():?array { + return []; + } + /** + * 读取 (剩余) 全部行 + * @return 二维数组, 可能为空数组; + * 若读取已完成, 返回 NULL + */ + function fetch_all():?array { + return []; + } +}; diff --git a/doc/os.php b/doc/os.php new file mode 100644 index 0000000..75c889d --- /dev/null +++ b/doc/os.php @@ -0,0 +1,98 @@ + + * array(1) { + * [0]=> + * array(2) { + * ["family"]=> + * string(4) "IPv4" + * ["address"]=> + * string(9) "127.0.0.1" + * } + * } + * ["eth0"]=> + * array(1) { + * [0]=> + * array(2) { + * ["family"]=> + * string(4) "IPv4" + * ["address"]=> + * string(13) "10.110.16.197" + * } + * } + * } + */ +function interfaces():array { + return []; +} +/** + * 异步启动进程 + * @return 进程对象 + */ +function spawn(string $command, array $argv = [], array $options = []):process { + return new process(); +} +/** + * 调用上述 spawn() 异步启动进程, 并等待其结束, 返回进程标准输出 + * @param array $options 目前可用的选项如下: + * * "cwd" - string - 工作路径; + * * "env" - array - 环境变量,K/V 结构文本; + * @return string 进程标准输出内容 + */ +function exec(string $command, array $argv, array $options = []):string { + return "output of the process"; +} + +/** + * 进程对象 + */ +class process { + /** + * 向进程发送指定信号 + */ + function kill(int $signal = SIGTERM) {} + /** + * 等待进程结束 + */ + function wait() {} + /** + * 获取进程标准输出(若进程还未结束需要等待) + */ + function stdout():string { + return "stdout output"; + } + /** + * 获取进程错误输出(若进程还未结束需要等待) + */ + function stderr():string { + return "stderr output"; + } +} \ No newline at end of file diff --git a/doc/rabbitmq.php b/doc/rabbitmq.php new file mode 100644 index 0000000..3b8bc2e --- /dev/null +++ b/doc/rabbitmq.php @@ -0,0 +1,110 @@ +get("xxx")->set("xxx","yyy")->exec(); + */ + function __call($name, $argv): tx { + return $this; + } + /** + * 执行目前提交的命令并带回所有响应返回 + * @return array 返回数据项与命令一一对应 + */ + function exec():array { + return []; + } +} + + + +$r = new client(); diff --git a/doc/tcp.php b/doc/tcp.php new file mode 100644 index 0000000..d8502d0 --- /dev/null +++ b/doc/tcp.php @@ -0,0 +1,61 @@ +on_packet = function($packet, $remote_addr) { -// echo $packet, " ", $remote_addr, " ", posix_getpid(), "\n"; -// }; -// $proc->add($svr); -// $app->add($proc); -// } -// $app->run(); - -function server1() { - mill\sleep(1000); - echo "ccccc\n"; - $server = new mill\udp\server("127.0.0.1", 6696); - $server->listen(); - echo "dddddd\n"; - while($packet = $server->recv()) { - echo posix_getpid(), " ", $server->remote_addr, " => ", $packet,"\n"; - } - $server->close(); -} - -function server2() { - $server = new mill\udp\server("127.0.0.1", 6697); - $server->listen(); - while($packet = $server->recv()) { - echo posix_getpid(), " ", $server->remote_addr, " => ", $packet,"\n"; - } - - $server->close(); -} - -function tcp1() { - $socket = mill\tcp\client(); - $socket->recv(); - $socket->send(); -} - -function http1() { - $task_queue = []; - mill\go(function() use($task_queue) { - // ....... - array_push($task_queue, "task"); - }); - for($i=0;$i<100;++$i) { - mill\go(function() use($task_queue) { - $taks = array_pop($task_queue); - $client = mill\http\client(); - $client->get("http://aaaaaa/", ["a"=>$task], ["header"=>[]]); - $client->post("http://bbbbbbb/", ["a"=>"b"], []); - $response = $client->request("GET", "URL", ["a"=>"b"], []); - $response->body - if($response->header["Content-type"] === "text/plain") { - - } - $client->get("key", function($err, $value) { - - }); - $value = $client->get("key"); - }); - } -} - -function main() { - echo "11111\n"; - // mill\go(server1); - // echo "22222\n"; - // mill\go(server2); - // file_get_contents(); - // echo "33333\n"; - mill\sleep(1000000); -} - -mill\run(main, ["worker_count"=>1]); - -// yield target; -// -// -// 1. target -> object of Generator -// 2. target -> function() {} definition, if function return object of Generator -> 1. -// 3. target -> evaluate other diff --git a/src/core.cpp b/src/core.cpp deleted file mode 100644 index 906525f..0000000 --- a/src/core.cpp +++ /dev/null @@ -1,169 +0,0 @@ -#include "vendor.h" -#include "core.h" -//#include "zend_generators.h" - -boost::asio::io_service* core::io_ = nullptr; - -php::value core::error_to_exception(const boost::system::error_code& err) { - php::object ex = php::object::create("Exception"); - ex.call("__construct", err.message(), err.value()); - return std::move(ex); -} -// 为了提高 sleep 函数的效率,考虑复用少量 timer -static std::vector timers; - -void core::init(php::extension_entry& extension) { - extension.on_module_startup(core::module_startup); - extension.on_module_shutdown(core::module_shutdown); - - extension.add("flame\\go"); - extension.add("flame\\run"); - extension.add("flame\\sleep"); - extension.add("flame\\fork"); -} -bool core::module_startup(php::extension_entry& extension) { - core::io_ = new boost::asio::io_service(); - for(int i=0;i<16;++i) { - timers.push_back(new boost::asio::deadline_timer(core::io())); - } - return true; -} -bool core::module_shutdown(php::extension_entry& extension) { - while(!timers.empty()) { - delete timers.back(); - timers.pop_back(); - } - delete core::io_; - return true; -} -// 核心调度 -static void generator_runner(php::object g) { - - if(EG(exception)) { - core::io().stop(); - return ; - } - if(!g.scall("valid").is_true()) { - /*if(exception) {*/ - //core::io().stop(); - /*}*/ - //zend_object* og = g; - //zend_generator* zg = (zend_generator)og; - /*std::printf("exception: %08x\n", EG(exception));*/ - return; - } - core::io().post([g] () mutable { - php::value v = g.call("current"); - if(v.is_callable()) { - php::callable cb = v; - // 传入的 函数 用于将异步执行结果回馈到 "协程" 中 - cb(php::value([g] (php::parameters& params) mutable -> php::value { - int len = params.length(); - if(len == 0) { - g.call("next"); - generator_runner(g); - return nullptr; - } - // 首个参数表达错误信息 - if(params[0].is_empty()) { // 没有错误 - if(len > 1) { // 有回调数据 - g.call("send", params[1]); - }else{ - g.call("next"); - } - generator_runner(g); - }else if(params[0].is_instance_of("Exception")) { // 内置 exception - g.call("throw", params[0]); - generator_runner(g); - }else{ // 其他错误信息 - php::object ex = php::object::create("Exception"); - ex.call("__construct", params[0].to_string()); - g.call("throw", std::move(ex)); - generator_runner(g); - } - return nullptr; - })); - }else{ - g.call("send", v); - generator_runner(g); - } - }); -} -// 所谓“协程” -php::value core::go(php::parameters& params) { - php::value r; - if(params[0].is_callable()) { - php::callable c = params[0]; - r = c(); - }else{ - r = params[0]; - } - if(r.is_object() && r.is_instance_of("Generator")) { - generator_runner(r); - return nullptr; - }else{ - return std::move(r); - } - return nullptr; -} -static bool started = false; -// 程序启动 -php::value core::run(php::parameters& params) { - started = true; - if(params.length() > 0) { - go(params); - } - core::io().run(); - return nullptr; -} -// sleep 对 timer 进行预分配的重用优化,实际上这个流程可能不会有太实际的优化效果, -// 这里的优化更多的是对后面其他对象、函数实现的一种示范 -php::value core::sleep(php::parameters& params) { - int duration = params[0]; - - return php::value([duration] (php::parameters& params) -> php::value { - std::shared_ptr timer; - - if(timers.empty()) { // 没有空闲的预分配 timer 需要创建新的 - timer.reset(new boost::asio::deadline_timer(core::io())); - }else{ // 重复使用这些 timer - timer.reset(timers.back(), [] (boost::asio::deadline_timer *timer) { - timers.push_back(timer); // 用完放回去 - }); - timers.pop_back(); - } - - timer->expires_from_now(boost::posix_time::milliseconds(duration)); - php::callable done = params[0]; - timer->async_wait([timer, done] (const boost::system::error_code& ec) mutable { - if(ec) { - done(ec.message()); - }else{ - done(nullptr); - } - }); - return nullptr; - }); -} - -static bool forked = false; -php::value core::_fork(php::parameters& params) { -#ifndef SO_REUSEPORT - throw php::exception("failed to fork: SO_REUSEPORT needed"); -#endif - if(started) { - throw php::exception("failed to fork: already running"); - } - if(forked) { - throw php::exception("failed to fork: already forked"); - } - forked = true; - int count = params[0]; - if(count <= 0) { - count = 1; - } - for(int i=0;i co) + : co_(co) { } + coroutine_handler(const coroutine_handler& ch) = default; + virtual ~coroutine_handler() { + + } + coroutine_handler& operator[](boost::system::error_code& error) { + error_ = &error; + return *this; + } + coroutine_handler& operator[](std::size_t& size) { + size_ = &size; + return *this; + } + + void operator()(const boost::system::error_code& error, std::size_t size = 0); + + inline bool operator !() { + return !co_; + } + + inline operator bool() { + return !!co_; + } + + void reset() { + co_.reset(); + size_ = nullptr; + error_= nullptr; + } + + void reset(std::shared_ptr co) { + co_ = co; + size_ = nullptr; + error_= nullptr; + } + void reset(coroutine_handler& ch) { + co_ = ch.co_; + size_ = nullptr; + error_= nullptr; + } + + void suspend(); + void resume(); + + inline void error(const boost::system::error_code& error) { + if(error_) *error_ = error; + } + inline void size(std::size_t sz) { + if(size_) *size_ = sz; + } +public: + std::size_t* size_ = nullptr; + boost::system::error_code* error_ = nullptr; + std::shared_ptr co_; + +private: + +}; + +class coroutine: public std::enable_shared_from_this { +public: + coroutine(boost::asio::executor ex) + : ex_(ex) { + + } + virtual ~coroutine() { + + } + + template + static void start(boost::asio::executor ex, Handler&& fn); + + virtual void suspend() { + c2_ = std::move(c2_).resume(); + } + virtual void resume() { + boost::asio::post(ex_, [co = shared_from_this()] () { + co->c1_ = std::move(co->c1_).resume(); + }); + } +protected: + boost::context::fiber c1_; + boost::context::fiber c2_; + boost::asio::executor ex_; + + friend class coroutine_handler; +}; + +template +void coroutine::start(boost::asio::executor ex, Handler&& fn) { + auto co = std::make_shared(ex); + + boost::asio::post(co->ex_, [co, fn] () mutable { + co->c1_ = boost::context::fiber([co, gd = boost::asio::make_work_guard(co->ex_), fn] (boost::context::fiber&& c2) mutable { + co->c2_ = std::move(c2); + fn(coroutine_handler{co}); + return std::move(co->c2_); + }); + co->c1_ = std::move(co->c1_).resume(); + }); +} + +inline void coroutine_handler::operator()(const boost::system::error_code& error, std::size_t size) { + if(error_) *error_ = error; + if(size_) *size_ = size; + co_->resume(); +} + +inline void coroutine_handler::suspend() { + co_->suspend(); +} + +inline void coroutine_handler::resume() { + co_->resume(); +} + +namespace boost::asio { + template <> + class async_result<::coroutine_handler, void (boost::system::error_code error, std::size_t size)> { + public: + explicit async_result(::coroutine_handler& ch) : ch_(ch), size_(0) { + ch_.size_ = &size_; + } + using completion_handler_type = ::coroutine_handler; + using return_type = std::size_t; + return_type get() { + ch_.suspend(); + return size_; + } + private: + ::coroutine_handler &ch_; + std::size_t size_; + }; + + template <> + class async_result<::coroutine_handler, void (boost::system::error_code error)> { + public: + explicit async_result(::coroutine_handler& ch) : ch_(ch) { + } + using completion_handler_type = ::coroutine_handler; + using return_type = void; + void get() { + ch_.suspend(); + } + private: + ::coroutine_handler &ch_; + }; +} // namespace boost::asio diff --git a/src/coroutine_mutex.h b/src/coroutine_mutex.h new file mode 100644 index 0000000..7948f0c --- /dev/null +++ b/src/coroutine_mutex.h @@ -0,0 +1,49 @@ +#pragma once +#include "coroutine.h" + +class coroutine_mutex { +public: + coroutine_mutex() + : mt_(false) { + + } + + void lock(coroutine_handler& ch) { + while(mt_) { + cm_.insert(&ch); + ch.suspend(); + } + mt_ = true; + } + + bool try_lock() { + if (!mt_) { + mt_ = true; + return true; + } + return false; + } + + void unlock() { + while(!cm_.empty()) { + auto ch = cm_.extract(cm_.begin()).value(); + ch->resume(); + } + mt_ = false; + } +private: + std::set cm_; + bool mt_; +}; +class coroutine_guard { +public: + coroutine_guard(coroutine_mutex& cm, coroutine_handler& ch) + : cm_(cm) { + cm_.lock(ch); + } + ~coroutine_guard() { + cm_.unlock(); + } +private: + coroutine_mutex cm_; +}; diff --git a/src/coroutine_queue.h b/src/coroutine_queue.h new file mode 100644 index 0000000..6bf6826 --- /dev/null +++ b/src/coroutine_queue.h @@ -0,0 +1,80 @@ +#pragma once +#include "coroutine.h" + +template +class coroutine_queue: private boost::noncopyable { +public: + coroutine_queue(std::size_t n = 1) + : n_(n) + , closed_(false) { + } + + // !!!! 生产者进行关闭 !!!! + void close() { + closed_ = true; + while (!c_.empty()) { + auto ch = c_.extract(c_.begin()).value(); + ch->resume(); + } + } + bool is_closed() { + return q_.empty() && closed_; + } + + void push(const T& t, coroutine_handler& ch) { + if (closed_) throw php::exception(zend_ce_error_exception + , "Failed to push queue: already closed" + , -1); + if (q_.size() >= n_) { + p_.insert(&ch); + ch.suspend(); + } + q_.push_back(t); + while(!q_.empty() && !c_.empty()) { + auto ch = c_.extract(c_.begin()).value(); + ch->resume(); + } + } + + std::optional pop(coroutine_handler& ch) { + for(;;) { + if (!q_.empty()) break; // 有数据消费 + else if (closed_) return std::optional(); // 无数据关闭 + else { // 无数据等待 + c_.insert(&ch); + ch.suspend(); + } + } + T t = q_.front(); + q_.pop_front(); + + while (!p_.empty()) { + auto ch = p_.extract(p_.begin()).value(); + ch->resume(); + } + return std::optional(t); + } + +/* private: */ + std::size_t n_; + std::list q_; + std::set c_; // 消费者 + std::set p_; // 生产者 + bool closed_; +}; + +template +std::shared_ptr < coroutine_queue > select_queue(std::vector < std::shared_ptr> > queues, coroutine_handler &ch) { +TRY_ALL: + bool all_closed = true; + for(auto i=queues.begin();i!=queues.end();++i) { + if (!(*i)->q_.empty()) return *i; + else if (!(*i)->closed_) all_closed = false; + } + if (all_closed) return std::shared_ptr< coroutine_queue >(nullptr); + for(auto i=queues.begin();i!=queues.end();++i) (*i)->c_.insert(&ch); + ch.suspend(); + for (auto i = queues.begin(); i != queues.end(); ++i) (*i)->c_.erase(&ch); + goto TRY_ALL; +} + diff --git a/src/extension.cpp b/src/extension.cpp deleted file mode 100644 index 2505cfb..0000000 --- a/src/extension.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include "vendor.h" -#include "core.h" -#include "net/init.h" -#include "net_mill/http/init.h" - -extern "C" { - ZEND_DLEXPORT zend_module_entry* get_module() { - static php::extension_entry extension(EXTENSION_NAME, EXTENSION_VERSION); - core::init(extension); - net::init(extension); - net::http::init(extension); - return extension; - } -} diff --git a/src/flame/compress/compress.cpp b/src/flame/compress/compress.cpp new file mode 100644 index 0000000..5407e38 --- /dev/null +++ b/src/flame/compress/compress.cpp @@ -0,0 +1,46 @@ +#include "compress.h" +extern "C" { +#include +} + +namespace flame::compress { + + static php::value snappy_compress(php::parameters& params) { + php::string data = params[0].to_string(); + struct iovec iov_in = { + .iov_base = data.data(), + .iov_len = data.size(), + }; + std::size_t len = rd_kafka_snappy_max_compressed_length(data.size()); + php::string out(len); + struct iovec iov_out = { + .iov_base = out.data(), + .iov_len = 0xffffffff, + }; + struct snappy_env env; + rd_kafka_snappy_init_env(&env); + rd_kafka_snappy_compress_iov(&env, &iov_in, 1, data.size(), &iov_out); + rd_kafka_snappy_free_env(&env); + out.shrink(iov_out.iov_len); + return std::move(out); + } + + static php::value snappy_uncompress(php::parameters& params) { + php::string data = params[0].to_string(); + std::size_t len; + rd_kafka_snappy_uncompressed_length(data.c_str(), data.size(), &len); + php::string out(len); + rd_kafka_snappy_uncompress(data.c_str(), data.size(), out.data()); + return std::move(out); + } + + void declare(php::extension_entry &ext) { + ext + .function("flame\\compress\\snappy_compress", { + {"data", php::TYPE::STRING}, + }) + .function("flame\\compress\\snappy_uncompress", { + {"data", php::TYPE::STRING}, + }); + } +} diff --git a/src/flame/compress/compress.h b/src/flame/compress/compress.h new file mode 100644 index 0000000..480e4cb --- /dev/null +++ b/src/flame/compress/compress.h @@ -0,0 +1,6 @@ +#pragma once +#include "../../vendor.h" + +namespace flame::compress { + void declare(php::extension_entry &ext); +} // namespace flame::compress diff --git a/src/flame/controller.cpp b/src/flame/controller.cpp new file mode 100644 index 0000000..18b2821 --- /dev/null +++ b/src/flame/controller.cpp @@ -0,0 +1,73 @@ +#include "controller.h" +#include "worker.h" +// #include "../logger_master.h" +// #include "../logger_worker.h" + +namespace flame { + // 全局控制器 + std::unique_ptr gcontroller; + + controller::controller() + : type(process_type::UNKNOWN) + , env(boost::this_process::environment()) + , status(STATUS_UNKNOWN) + , evnt_cb(new std::multimap()) { + + worker_size = std::atoi(env["FLAME_MAX_WORKERS"].to_string().c_str()); + worker_size = std::min(std::max((int)worker_size, 0), 256); + // FLAME_MAX_WORKERS 环境变量会被继承, 故此处顺序须先检测子进程 + if (env.count("FLAME_CUR_WORKER") > 0) { + type = process_type::WORKER; + worker_idx = std::atoi(env["FLAME_CUR_WORKER"].to_string().c_str()); + } + else if (worker_size > 0) type = process_type::MASTER; + else { // 单进程模式 + worker_size = 0; + type = process_type::WORKER; + } + mthread_id = std::this_thread::get_id(); + } + + controller *controller::on_init(std::function fn) { + init_cb.push_back(fn); + return this; + } + + void controller::init(php::array options) { + for (auto fn : init_cb) fn(options); + } + + controller* controller::on_stop(std::function fn) { + stop_cb.push_back(fn); + return this; + } + + void controller::stop() { + for (auto fn : stop_cb) fn(); + delete evnt_cb; + } + + controller* controller::add_event(const std::string& event, php::callable cb) { + evnt_cb->insert({event, cb}); + return this; + } + + controller* controller::del_event(const std::string& event) { + evnt_cb->erase(event); + return this; + } + + std::size_t controller::cnt_event(const std::string& event) { + return evnt_cb->count(event); + } + + void controller::event(const std::string& event, std::vector params) { + auto ft = evnt_cb->equal_range(event); + for(auto i=ft.first; i!=ft.second; ++i) i->second.call(params); + } + + void controller::event(const std::string& event) { + auto ft = evnt_cb->equal_range(event); + for(auto i=ft.first; i!=ft.second; ++i) i->second.call(); + } +} diff --git a/src/flame/controller.h b/src/flame/controller.h new file mode 100644 index 0000000..2b9aa8d --- /dev/null +++ b/src/flame/controller.h @@ -0,0 +1,52 @@ +#pragma once +#include "../vendor.h" + +namespace flame { + + class controller { + public: + boost::asio::io_context context_x; + boost::asio::io_context context_y; + boost::asio::io_context context_z; + enum class process_type { + UNKNOWN = 0, + MASTER = 1, + WORKER = 2, + } type; + boost::process::environment env; + enum status_t { + STATUS_UNKNOWN = 0x00, + STATUS_INITIALIZED = 0x01, + STATUS_CLOSING = 0x02, + STATUS_QUITING = 0x04, + STATUS_RSETING = 0x08, + STATUS_EXCEPTION = 0x10, + STATUS_RUN = 0x20, + STATUS_CLOSECONN = 0x40, + }; + int status; + std::uint8_t worker_idx; + std::size_t worker_size; + std::size_t worker_quit; // 多进程退出超时时间 + std::thread::id mthread_id; + private: + std::list> init_cb; + std::list> stop_cb; + std::multimap* evnt_cb; // 防止 PHP 提前回收, 使用堆容器 + public: + controller(); + controller(const controller& c) = delete; + controller *on_init(std::function fn); + void init(php::array options); + controller* on_stop(std::function fn); + void stop(); + controller* add_event(const std::string& event, php::callable cb); + controller* del_event(const std::string& event); + std::size_t cnt_event(const std::string& event); + void event(const std::string& event, std::vector params); + void event(const std::string& event); + }; + + extern std::unique_ptr gcontroller; + +} \ No newline at end of file diff --git a/src/flame/coroutine.cpp b/src/flame/coroutine.cpp new file mode 100644 index 0000000..334d012 --- /dev/null +++ b/src/flame/coroutine.cpp @@ -0,0 +1,99 @@ +#include "controller.h" +#include "coroutine.h" +#include "../util.h" + +static void coroutine_php_save_context(flame::coroutine::php_context_t &ctx) { + ctx.vm_stack = EG(vm_stack); + ctx.vm_stack_top = EG(vm_stack_top); + ctx.vm_stack_end = EG(vm_stack_end); + // ctx.scope = EG(fake_scope); + ctx.current_execute_data = EG(current_execute_data); + ctx.exception = EG(exception); + ctx.exception_class = EG(exception_class); + ctx.error_handling = EG(error_handling); +} + +static void coroutine_php_restore_context(flame::coroutine::php_context_t &ctx) { + EG(vm_stack) = ctx.vm_stack; + EG(vm_stack_top) = ctx.vm_stack_top; + EG(vm_stack_end) = ctx.vm_stack_end; + // EG(fake_scope) = ctx.scope; + EG(current_execute_data) = ctx.current_execute_data; + EG(exception) = ctx.exception; + EG(exception_class) = ctx.exception_class; + EG(error_handling) = ctx.error_handling; +} +// 参考 zend_execute.c +static zend_vm_stack coroutine_php_vm_stack_new_page(size_t size, zend_vm_stack prev) { + zend_vm_stack page = (zend_vm_stack)emalloc(size); + + page->top = ZEND_VM_STACK_ELEMENTS(page); + page->end = (zval *)((char *)page + size); + page->prev = prev; + return page; +} + +// 参考 zend_execute.c +static void coroutine_php_vm_stack_init(void) { + EG(current_execute_data) = nullptr; + EG(error_handling) = EH_NORMAL; + EG(exception_class) = nullptr; + EG(exception) = nullptr; + + EG(vm_stack) = coroutine_php_vm_stack_new_page(4 * sizeof(zval) * 1024, NULL); + EG(vm_stack)->top++; + EG(vm_stack_top) = EG(vm_stack)->top; + EG(vm_stack_end) = EG(vm_stack)->end; +} + +namespace flame { + + unsigned int coroutine::count = 0; + std::shared_ptr coroutine::current; + coroutine::php_context_t coroutine::gctx_; + + void coroutine::start(php::callable fn) { + ++coroutine::count; + coroutine_php_save_context(coroutine::gctx_); + auto co = std::make_shared(); + + boost::asio::post(co->ex_, [co, fn] () mutable { + co->c1_ = boost::context::fiber([co, gd = boost::asio::make_work_guard(co->ex_), fn] (boost::context::fiber&& c2) mutable { + co->c2_ = std::move(c2); + + coroutine_php_vm_stack_init(); + coroutine::current = co; + + fn.call(); // 产生 PHP 进栈 操作,才能进行捕获异常 + fn = nullptr; + + coroutine::current.reset(); + zend_vm_stack_destroy(); + coroutine_php_restore_context(coroutine::gctx_); + + --coroutine::count; + + return std::move(co->c2_); + }); + co->c1_ = std::move(co->c1_).resume(); + }); + } + + void coroutine::suspend() { + coroutine_php_save_context(cctx_); + coroutine_php_restore_context(coroutine::gctx_); + // coroutine::current.reset(); + c2_ = std::move(c2_).resume(); + } + + void coroutine::resume() { + auto co = std::static_pointer_cast(shared_from_this()); + boost::asio::post(ex_, [co] () { + coroutine_php_save_context(coroutine::gctx_); + coroutine_php_restore_context(co->cctx_); + coroutine::current = co; + co->c1_ = std::move(co->c1_).resume(); + }); + } + +} diff --git a/src/flame/coroutine.h b/src/flame/coroutine.h new file mode 100644 index 0000000..5066eae --- /dev/null +++ b/src/flame/coroutine.h @@ -0,0 +1,106 @@ +#pragma once +#include "../vendor.h" +#include "../coroutine.h" +#include "controller.h" + +namespace flame { + + class coroutine : public ::coroutine { + public: + struct php_context_t { + zend_vm_stack vm_stack; + zval *vm_stack_top; + zval *vm_stack_end; + zend_class_entry *scope; + zend_execute_data *current_execute_data; + + zend_object * exception; + zend_error_handling_t error_handling; + zend_class_entry * exception_class; + }; + static void start(php::callable fn); + static unsigned int count; + static std::shared_ptr current; + + coroutine() + : ::coroutine(gcontroller->context_x.get_executor()) { + + } + void suspend(); + void resume(); + protected: + static php_context_t gctx_; + php_context_t cctx_; + + friend class coroutine_handler; + }; + + class coroutine_handler: public ::coroutine_handler { + public: + coroutine_handler() + : ::coroutine_handler() { + + } + coroutine_handler(const coroutine_handler& ch) = default; + coroutine_handler(std::shared_ptr co) + : ::coroutine_handler(co) { + + } + ~coroutine_handler() { + + } + + void reset() { + co_.reset(); + } + void reset(std::shared_ptr co) { + co_ = co; + } + void reset(const coroutine_handler& ch) { + co_ = ch.co_; + } + + coroutine_handler& operator[](boost::system::error_code& error) { + error_ = &error; + return *this; + } + coroutine_handler& operator[](std::size_t& size) { + size_ = &size; + return *this; + } + }; + +} + +namespace boost::asio { + template <> + class async_result { + public: + explicit async_result(flame::coroutine_handler& ch) : ch_(ch), size_(0) { + ch_.operator[](size_); + } + using completion_handler_type = flame::coroutine_handler; + using return_type = std::size_t; + return_type get() { + ch_.suspend(); + return size_; + } + private: + flame::coroutine_handler &ch_; + std::size_t size_; + }; + + template <> + class async_result { + public: + explicit async_result(flame::coroutine_handler& ch) : ch_(ch) { + } + using completion_handler_type = flame::coroutine_handler; + using return_type = void; + void get() { + ch_.suspend(); + } + private: + flame::coroutine_handler &ch_; + }; +} // namespace boost::asio diff --git a/src/flame/encoding/encoding.cpp b/src/flame/encoding/encoding.cpp new file mode 100644 index 0000000..c35b5be --- /dev/null +++ b/src/flame/encoding/encoding.cpp @@ -0,0 +1,32 @@ +#include "encoding.h" +#include "../mongodb/mongodb.h" + +namespace flame::encoding { + static php::value bson_encode(php::parameters ¶ms) { + if (params[0].type_of(php::TYPE::ARRAY)) { + auto doc = flame::mongodb::array2bson(params[0]); + return php::string((const char*)bson_get_data(doc.get()), doc->len); + } + else throw php::exception(zend_ce_type_error + , "Failed to encode: typeof 'array' required" + , -1); + } + static php::value bson_decode(php::parameters ¶ms) { + php::string data = params[0].to_string(); + bson_t* doc = bson_new_from_data((const uint8_t*)data.data(), data.size()); + if (doc == nullptr) return nullptr; + php::array rv = flame::mongodb::bson2array(doc); + bson_destroy(doc); + return std::move(rv); + } + + void declare(php::extension_entry &ext) { + ext + .function("flame\\encoding\\bson_encode", { + {"data", php::TYPE::ARRAY}, + }) + .function("flame\\encoding\\bson_decode", { + {"data", php::TYPE::STRING}, + }); + } +} diff --git a/src/flame/encoding/encoding.h b/src/flame/encoding/encoding.h new file mode 100644 index 0000000..dd66fc6 --- /dev/null +++ b/src/flame/encoding/encoding.h @@ -0,0 +1,6 @@ +#pragma once +#include "../../vendor.h" + +namespace flame::encoding { + void declare(php::extension_entry &ext); +} // namespace flame::encoding diff --git a/src/flame/flame.cpp b/src/flame/flame.cpp new file mode 100644 index 0000000..0ef5ed8 --- /dev/null +++ b/src/flame/flame.cpp @@ -0,0 +1,30 @@ +#include "../vendor.h" +#include "../util.h" +#include "controller.h" +#include "version.h" +#include "worker.h" +#include "master.h" + +extern "C" { + ZEND_DLEXPORT zend_module_entry *get_module() { + static php::extension_entry ext(EXTENSION_NAME, EXTENSION_VERSION); + std::string sapi = php::constant("PHP_SAPI"); + if (sapi != "cli") { + std::cerr << "[" << util::system_time() << "] (WARNING) FLAME disabled: SAPI='cli' mode only\n"; + return ext; + } + // 内置一个 C++ 函数包裹类 + php::class_entry class_closure("flame\\closure"); + class_closure.method<&php::closure::__invoke>("__invoke"); + ext.add(std::move(class_closure)); + // 全局控制器 + flame::gcontroller.reset(new flame::controller()); + // 扩展版本 + flame::version::declare(ext); + // 主进程与工作进程注册不同的函数实现 + if (flame::gcontroller->type == flame::controller::process_type::WORKER) flame::worker::declare(ext); + else flame::master::declare(ext); + + return ext; + } +}; diff --git a/src/flame/hash/hash.cpp b/src/flame/hash/hash.cpp new file mode 100644 index 0000000..b90ca2f --- /dev/null +++ b/src/flame/hash/hash.cpp @@ -0,0 +1,57 @@ +#include "hash.h" +#include +extern "C" { + #include + #include +} + +namespace flame::hash { + + template + static php::string dec2hex(INTEGER_TYPE dec) { + php::string hex(sizeof(INTEGER_TYPE) * 2); + sprintf(hex.data(), "%0*lx", sizeof(INTEGER_TYPE) * 2, dec); + return std::move(hex); + } + + static php::value murmur2(php::parameters& params) { + php::string data = params[0].to_string(); + std::uint32_t raw = rd_murmur2(data.c_str(), data.size()); + if (params.size() > 1 && params[1].to_boolean()) return raw; + return dec2hex(raw); + } + + static php::value xxh64(php::parameters& params) { + php::string data = params[0].to_string(); + unsigned long long seed = 0; + if (params.size() > 1) { + seed = params[1].to_integer(); + } + std::uint64_t raw = XXH64(data.c_str(), data.size(), seed); + if (params.size() > 2 && params[2].to_boolean()) return raw; + return dec2hex(raw); + } + + static php::value crc64(php::parameters& params) { + php::string data = params[0].to_string(); + boost::crc_optimal<64, 0x42F0E1EBA9EA3693, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, true, true> crc64; + crc64.process_bytes(data.c_str(), data.size()); + std::uint64_t raw = crc64.checksum(); + if (params.size() > 1 && params[1].to_boolean()) return raw; + return dec2hex(raw); + } + + void declare(php::extension_entry &ext) { + ext + .function("flame\\hash\\murmur2", { + {"data", php::TYPE::STRING}, + }) + .function("flame\\hash\\xxh64", { + {"data", php::TYPE::STRING}, + {"seed", php::TYPE::INTEGER, false, true}, + }) + .function("flame\\hash\\crc64", { + {"data", php::TYPE::STRING}, + }); + } +} diff --git a/src/flame/hash/hash.h b/src/flame/hash/hash.h new file mode 100644 index 0000000..31fac80 --- /dev/null +++ b/src/flame/hash/hash.h @@ -0,0 +1,6 @@ +#pragma once +#include "../../vendor.h" + +namespace flame::hash { + void declare(php::extension_entry &ext); +} // namespace flame::hash diff --git a/src/flame/http/_handler.cpp b/src/flame/http/_handler.cpp new file mode 100644 index 0000000..319842e --- /dev/null +++ b/src/flame/http/_handler.cpp @@ -0,0 +1,221 @@ +#include "../coroutine.h" +#include "../time/time.h" +#include "../log/logger.h" +#include "_handler.h" +#include "http.h" +#include "server.h" +#include "server_request.h" +#include "server_response.h" + +namespace flame::http { + + _handler::_handler(server *svr, tcp::socket &&sock) + : svr_ptr(svr) + , svr_obj(svr) + , socket_(std::move(sock)) { + } + + _handler::~_handler() { + } + + php::value _handler::run(php::parameters& params) { + coroutine_handler ch{coroutine::current}; + + while(true) { + if (!prepare(ch)) return nullptr; + if (!execute(ch)) return nullptr; + + // 由于错误清理了对象 或 不允许连接复用 + if (!res_ || !res_->keep_alive()) return nullptr; + } + } + + bool _handler::prepare(coroutine_handler& ch) { + boost::system::error_code error; + req_.reset(new boost::beast::http::request_parser>()); + req_->header_limit(16 * 1024); + req_->body_limit(body_max_size); + boost::beast::http::async_read(socket_, buffer_, *req_, ch[error]); + if(error) return false; + res_.reset(new boost::beast::http::message>()); + // 服务及将停止或服务器以进行关闭后,禁止连接复用 + if (gcontroller->status & controller::STATUS_CLOSECONN || svr_ptr->closed_) { + res_->keep_alive(false); + res_->set(boost::beast::http::field::connection, "close"); + }else{ + res_->keep_alive(req_->get().keep_alive()); // 默认为请求 KeepAlive 配置 + } + return true; + } + + bool _handler::execute(coroutine_handler& ch) { + php::object req = php::object(php::class_entry::entry()); + php::object res = php::object(php::class_entry::entry()); + server_request *req_ptr = static_cast(php::native(req)); + server_response *res_ptr = static_cast(php::native(res)); + + req_ptr->build_ex(req_->get()); // 将 C++ 请求对象展开到 PHP 中 + res_ptr->handler_ = shared_from_this(); + + std::string route = req.get("method"); + route.push_back(':'); + route.append(req.get("path")); + + auto ib = svr_ptr->cb_.find("before"), + ih = svr_ptr->cb_.find(route), + ia = svr_ptr->cb_.find("after"); + + try { + php::value rv = true; + if (ib != svr_ptr->cb_.end()) rv = ib->second.call({req, res, ih != svr_ptr->cb_.end()}); + if (!res_) return false; // 处理过程网络异常,下同 + if (rv.type_of(php::TYPE::NO)) goto HANDLE_SKIPPED; + if (ih != svr_ptr->cb_.end()) rv = ih->second.call({req, res}); + if (!res_) return false; + if (rv.type_of(php::TYPE::NO)) goto HANDLE_SKIPPED; + if (ia != svr_ptr->cb_.end()) rv = ia->second.call({req, res, ih != svr_ptr->cb_.end()}); + if (!res_) return false; +HANDLE_SKIPPED: + if (res_->chunked()) { + // Chunked 方式响应,但未结束: + if(!(res_ptr->status_ & res_ptr->STATUS_BODY_END)) { + write_end(res_ptr, ch); // 结束 + } + } + else { // 使用 body 属性进行响应 + write_body(res_ptr, ch); + } + return true; + /*else { + // Chunked 未结束 -> server_response::__destruct() -> finish() + }*/ + } catch(const php::exception& ex) { + gcontroller->event("exception", {ex}); // 调用用户异常回调 + // 记录错误信息 + php::object obj = ex; + log::logger_->stream() << "[" << time::iso() << "] (ERROR) Uncaught Exception in HTTP handler: " << obj.call("__toString") << std::endl; + return false; + } + } + + void _handler::write_body(server_response* res_ptr, coroutine_handler& ch) { + res_ptr->build_ex(*res_); + // Content 方式, 待结束 + php::string body = res_ptr->get("body"); + if (!body.empty()) + body = ctype_encode(res_->find(boost::beast::http::field::content_type)->value(), body); + res_->body() = body; + res_->prepare_payload(); + + boost::system::error_code error; + boost::beast::http::async_write(socket_, *res_, ch[error]); + if(error) { + req_.reset(); + res_.reset(); + } + } + + void _handler::write_head(server_response *res_ptr, coroutine_handler &ch) { + if (!res_) return; + res_ptr->build_ex(*res_); + res_->chunked(true); + res_->set(boost::beast::http::field::transfer_encoding, "chunked"); + // 服务及将停止或服务器以进行关闭后,禁止连接复用 + if (gcontroller->status & controller::STATUS_CLOSECONN || svr_ptr->closed_) { + res_->keep_alive(false); + res_->set(boost::beast::http::field::connection, "close"); + } + boost::system::error_code error; + + boost::beast::http::serializer> sr(*res_); + boost::beast::http::async_write_header(socket_, sr, ch[error]); + if (error) { + res_.reset(); + req_.reset(); + } + } + + void _handler::write_chunk(server_response *res_ptr, php::string data, coroutine_handler &ch) { + if (!res_) return; + if (!res_->chunked()) + throw php::exception(zend_ce_error_exception + , "Failed to write_chunk: 'chunked' encoding required" + , -1); + + boost::system::error_code error; + boost::asio::async_write(socket_, + boost::beast::http::make_chunk(boost::asio::const_buffer(data.c_str(), data.size())), ch[error]); + if (error) { + res_.reset(); + req_.reset(); + } + } + + void _handler::write_end(server_response* res_ptr, coroutine_handler &ch) { + if (!res_) return; + if (!res_->chunked()) throw php::exception(zend_ce_error_exception + , "Failed to write body: 'chunked' encoding required" + , -1); + boost::system::error_code error; + boost::asio::async_write(socket_, boost::beast::http::make_chunk_last(), ch[error]); + if (error) { + res_.reset(); + req_.reset(); + } + } + + void _handler::write_file(server_response *res_ptr, std::string path, coroutine_handler &ch) { + if (!res_) return; + if (!res_->chunked()) + throw php::exception(zend_ce_error_exception + , "Failed to write_file: 'chunked' encoding required" + , -1); + std::ifstream file(path); + char buffer[2048]; + std::size_t size; + boost::system::error_code error; + + while (!file.eof()) { + boost::asio::post(gcontroller->context_y, [&file, &ch, &buffer, &size, &error] () { + try { + file.read(buffer, sizeof(buffer)); + size = file.gcount(); + } catch(const std::ios_base::failure& ex) { + error.assign(ex.code().value(), boost::system::generic_category()); + } + ch.resume(); + }); + ch.suspend(); + if (error) { + res_.reset(); + req_.reset(); + throw php::exception(zend_ce_exception + , (boost::format("Failed to write_file: %s") % error.message()).str() + , error.value()); + } + boost::asio::async_write(socket_, boost::beast::http::make_chunk( boost::asio::buffer(buffer, size) ), ch[error]); + if (error) goto WRITE_ERROR; + } + boost::asio::async_write(socket_, boost::beast::http::make_chunk_last(), ch[error]); +WRITE_ERROR: + if (error) { + res_.reset(); + req_.reset(); + } + // file.close(); + } + + void _handler::finish(server_response *res_ptr, coroutine_handler &ch) { + if (res_) { + if (res_->chunked() && !(res_ptr->status_ & server_response::STATUS_BODY_END)) { + boost::system::error_code error; + boost::asio::async_write(socket_, boost::beast::http::make_chunk_last(), ch[error]); + + if (error) { + res_.reset(); + req_.reset(); + } + } + } + } +} diff --git a/src/flame/http/_handler.h b/src/flame/http/_handler.h new file mode 100644 index 0000000..c14275d --- /dev/null +++ b/src/flame/http/_handler.h @@ -0,0 +1,53 @@ +#pragma once +#include "../../vendor.h" +#include "../coroutine.h" +#include "http.h" + +namespace flame::http { + class server_response; + class server_request; + + template + class value_body; + class server; + class _handler : public std::enable_shared_from_this<_handler> { + public: + _handler(server *svr, boost::asio::ip::tcp::socket &&sock); + ~_handler(); + php::value run(php::parameters& params); + + bool prepare(coroutine_handler& ch); + bool execute(coroutine_handler& ch); + + void write_body(server_response* res_ptr, coroutine_handler& ch); + void write_head(server_response* res_ptr, coroutine_handler& ch); + void write_chunk(server_response* res_ptr, php::string data, coroutine_handler& ch); + void write_end(server_response* res_ptr, coroutine_handler& ch); + void write_file(server_response* res_ptr, std::string file, coroutine_handler& ch); + + // 由 server_response 销毁时调用 + void finish(server_response *res, coroutine_handler &ch); + + private: + server *svr_ptr; + php::object svr_obj; + boost::asio::ip::tcp::socket socket_; + + + // std::shared_ptr>> req_; + std::shared_ptr>> req_; + std::shared_ptr>> res_; + boost::beast::flat_buffer buffer_; + }; + // enum + // { + // RESPONSE_STATUS_HEADER_BUILT = 0x01, + // RESPONSE_STATUS_HEADER_SENT = 0x02, + // RESPONSE_STATUS_FINISHED = 0x04, + // RESPONSE_STATUS_DETACHED = 0x08, + + // RESPONSE_TARGET_WRITE_HEADER = 0x01, + // RESPONSE_TARGET_WRITE_CHUNK = 0x02, + // RESPONSE_TARGET_WRITE_CHUNK_LAST = 0x03, + // }; +} // namespace flame::http diff --git a/src/flame/http/client.cpp b/src/flame/http/client.cpp new file mode 100644 index 0000000..d79567e --- /dev/null +++ b/src/flame/http/client.cpp @@ -0,0 +1,215 @@ +#include "../coroutine.h" +#include "client.h" +#include "client_poll.h" +#include "client_request.h" +#include "client_response.h" +#include "../time/time.h" + +namespace flame::http { + + void client::declare(php::extension_entry& ext) { + php::class_entry class_client("flame\\http\\client"); + class_client + .method<&client::__construct>("__construct", { + {"options", php::TYPE::ARRAY, false, true}, + }) + .method<&client::exec>("exec", { + {"options", "flame\\http\\client_request"}, + }) + .method<&client::get>("get", { + {"url", php::TYPE::STRING}, + {"timeout", php::TYPE::INTEGER, false, true} + }) + .method<&client::post>("post", { + {"url", php::TYPE::STRING}, + {"body", php::TYPE::UNDEFINED}, + {"timeout", php::TYPE::INTEGER, false, true} + }) + .method<&client::put>("put", { + {"url", php::TYPE::STRING}, + {"body", php::TYPE::UNDEFINED}, + {"timeout", php::TYPE::INTEGER, false, true} + }) + .method<&client::delete_>("delete", { + {"url", php::TYPE::STRING}, + {"timeout", php::TYPE::INTEGER, false, true} + }); + ext.add(std::move(class_client)); + } + + void client::check_done() { + CURLMsg *msg; + int left; + client_response* res_; + while((msg = curl_multi_info_read(c_multi_, &left))) { + if (msg->msg == CURLMSG_DONE) { + curl_easy_getinfo(msg->easy_handle, CURLINFO_PRIVATE, &res_); + res_->c_final_ = msg->data.result; + res_->c_coro_.resume(); + } + } + } + + static const char* action_str[] = { "UNKNOWN", "CURL_POLL_IN", "CURL_POLL_OUT" , "CURL_POLL_INOUT", "CURL_POLL_REMOVE" }; + + void client::c_socket_ready_cb(const boost::system::error_code& error, client_poll* poll, curl_socket_t fd, int action) { + client* self = reinterpret_cast(poll->data); + if (error) action = CURL_CSELECT_ERR; + curl_multi_socket_action(self->c_multi_, fd, action, &self->c_still_); + self->check_done(); + if (self->c_still_ <= 0) self->c_timer_.cancel(); + } + + int client::c_socket_cb(CURL* e, curl_socket_t fd, int action, void* data, void* sock_data) { + client* self = reinterpret_cast(data); + client_poll* poll = reinterpret_cast(sock_data); + + if (action == CURL_POLL_REMOVE) { + curl_multi_assign(self->c_multi_, fd, nullptr); + } + else if (poll == nullptr) { + poll = client_poll::create_poll(gcontroller->context_x, fd, c_socket_ready_cb, self); + curl_multi_assign(self->c_multi_, fd, poll); + } + poll->async_wait(action); // 自行 delete + return 0; + } + + int client::c_timer_cb(CURLM *m, long timeout_ms, void* data) { + client* self = reinterpret_cast(data); + + self->c_timer_.cancel(); + if (timeout_ms == 0) timeout_ms = 1; + if (timeout_ms > 0) { + self->c_timer_.expires_after(std::chrono::milliseconds(timeout_ms)); + self->c_timer_.async_wait([self] (const boost::system::error_code& err) { + if (err) return; + curl_multi_socket_action(self->c_multi_, CURL_SOCKET_TIMEOUT, 0, &self->c_still_); + self->check_done(); + }); + } + return 0; + } + + client::client() + : c_timer_(gcontroller->context_x) { + c_multi_ = curl_multi_init(); + + curl_multi_setopt(c_multi_, CURLMOPT_SOCKETDATA, this); + curl_multi_setopt(c_multi_, CURLMOPT_SOCKETFUNCTION, client::c_socket_cb); + curl_multi_setopt(c_multi_, CURLMOPT_TIMERDATA, this); + curl_multi_setopt(c_multi_, CURLMOPT_TIMERFUNCTION, client::c_timer_cb); + // 7.62.x + HTTP1 PIPELINE 功能已无效 + curl_multi_setopt(c_multi_, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX/* | CURLPIPE_HTTP1*/); + // curl_multi_setopt(c_multi_, CURLMOPT_MAX_PIPELINE_LENGTH, 4L); + curl_multi_setopt(c_multi_, CURLMOPT_MAX_TOTAL_CONNECTIONS, 64L); + curl_multi_setopt(c_multi_, CURLMOPT_MAXCONNECTS, 32L); + curl_multi_setopt(c_multi_, CURLMOPT_MAX_HOST_CONNECTIONS, 16L); + } + + client::~client() { + curl_multi_cleanup(c_multi_); + } + + php::value client::__construct(php::parameters& params) { + if (params.size() > 0) { + php::array opts = params[0]; + if (opts.exists("connection_per_host")) { + long c = opts.get("connection_per_host"); + if (c < 1 || c > 512) { + curl_multi_setopt(c_multi_, CURLMOPT_MAX_TOTAL_CONNECTIONS, c * 4); + curl_multi_setopt(c_multi_, CURLMOPT_MAXCONNECTS, c * 2); + curl_multi_setopt(c_multi_, CURLMOPT_MAX_HOST_CONNECTIONS, c); + } + } + } + return nullptr; + } + + php::value client::exec_ex(const php::object& req) { + auto req_ = static_cast(php::native(req)); + if (req_->c_easy_ == nullptr) req_->c_easy_ = curl_easy_init(); + curl_easy_setopt(req_->c_easy_, CURLOPT_PIPEWAIT, 1); + req_->build_ex(); + + php::object res(php::class_entry::entry()); + auto res_ = static_cast(php::native(res)); + res_->c_easy_ = req_->c_easy_; + res_->c_coro_.reset(coroutine::current); + res_->c_head_ = php::array(4); + curl_easy_setopt(res_->c_easy_, CURLOPT_WRITEFUNCTION, client_response::c_write_cb); + curl_easy_setopt(res_->c_easy_, CURLOPT_WRITEDATA, res_); + curl_easy_setopt(res_->c_easy_, CURLOPT_HEADERFUNCTION, client_response::c_header_cb); + curl_easy_setopt(res_->c_easy_, CURLOPT_HEADERDATA, res_); + curl_easy_setopt(res_->c_easy_, CURLOPT_ERRORBUFFER, res_->c_error_); + curl_easy_setopt(res_->c_easy_, CURLOPT_PRIVATE, res_); + + curl_multi_add_handle(c_multi_, res_->c_easy_); + res_->c_coro_.suspend(); // <----- check_done + res_->build_ex(); + curl_multi_remove_handle(c_multi_, res_->c_easy_); + return std::move(res); + } + php::value client::exec(php::parameters& params) { + return exec_ex(params[0]); + } + php::value client::get(php::parameters& params) { + php::object req(php::class_entry::entry()); + req.set("method", "GET"); + req.set("url", params[0]); + req.set("header", php::array(0)); + req.set("cookie", php::array(0)); + req.set("body", nullptr); + if (params.length() > 1) { + req.set("timeout", params[1]); + } + else { + req.set("timeout", 3000); + } + return exec_ex(req); + } + php::value client::post(php::parameters& params) { + php::object req(php::class_entry::entry()); + req.set("method", "POST"); + req.set("url", params[0]); + req.set("header", php::array(0)); + req.set("cookie", php::array(0)); + req.set("body", params[1]); + if (params.length() > 2) { + req.set("timeout",params[2]); + }else{ + req.set("timeout", 3000); + } + return exec_ex(req); + } + php::value client::put(php::parameters& params) { + php::object req(php::class_entry::entry()); + req.set("method", "PUT"); + req.set("url", params[0]); + req.set("header", php::array(0)); + req.set("cookie", php::array(0)); + req.set("body", params[1]); + if (params.length() > 2) { + req.set("timeout",params[2]); + } + else { + req.set("timeout", 3000); + } + return exec_ex(req); + } + php::value client::delete_(php::parameters& params) { + php::object req(php::class_entry::entry()); + req.set("method", "DELETE"); + req.set("url", params[0]); + req.set("header", php::array(0)); + req.set("cookie", php::array(0)); + req.set("body", nullptr); + if (params.length() > 1) { + req.set("timeout", params[1]); + } + else { + req.set("timeout", 3000); + } + return exec_ex(req); + } +} diff --git a/src/flame/http/client.h b/src/flame/http/client.h new file mode 100644 index 0000000..00df8ca --- /dev/null +++ b/src/flame/http/client.h @@ -0,0 +1,33 @@ +#pragma once +#include "../../vendor.h" +#include "http.h" + +namespace flame::http { + + class client_poll; + class client: public php::class_base { + public: + static void declare(php::extension_entry& ext); + client(); + ~client(); + php::value __construct(php::parameters& params); + php::value exec(php::parameters& params); + php::value get(php::parameters& params); + php::value post(php::parameters& params); + php::value put(php::parameters& params); + php::value delete_(php::parameters& params); + private: + CURLM *c_multi_ = nullptr; + int c_still_ = 0; + boost::asio::steady_timer c_timer_; + ; + + php::value exec_ex(const php::object& req); + static int c_socket_cb(CURL* e, curl_socket_t fd, int action, void* cbp, void* data); + static int c_timer_cb(CURLM *m, long timeout_ms, void* data); + static curl_socket_t c_socket_open_cb(void* data, curlsocktype purpose, struct curl_sockaddr* address); + static int c_socket_close_cb(void* data, curl_socket_t fd); + static void c_socket_ready_cb(const boost::system::error_code& error, client_poll* poll, curl_socket_t fd, int action); + void check_done(); + }; +} diff --git a/src/flame/http/client_body.cpp b/src/flame/http/client_body.cpp new file mode 100644 index 0000000..9b1cb28 --- /dev/null +++ b/src/flame/http/client_body.cpp @@ -0,0 +1,8 @@ +#include "client_body.h" + +namespace flame::http { + void client_body::declare(php::extension_entry& ext) { + php::class_entry class_client_body("flame\\http\\client_body"); + ext.add(std::move(class_client_body)); + } +} \ No newline at end of file diff --git a/src/flame/http/client_body.h b/src/flame/http/client_body.h new file mode 100644 index 0000000..2cb3187 --- /dev/null +++ b/src/flame/http/client_body.h @@ -0,0 +1,11 @@ +#pragma once +#include "../../vendor.h" +#include "http.h" + +namespace flame::http { + + class client_body: public php::class_base { + public: + static void declare(php::extension_entry& ext); + }; +} \ No newline at end of file diff --git a/src/flame/http/client_poll.cpp b/src/flame/http/client_poll.cpp new file mode 100644 index 0000000..4a43ed2 --- /dev/null +++ b/src/flame/http/client_poll.cpp @@ -0,0 +1,106 @@ +#include "client_poll.h" +#include "http.h" +#include "client.h" + +namespace flame::http { + + std::map client_poll::pool_; + + client_poll* client_poll::create_poll(boost::asio::io_context &io, curl_socket_t fd, client_poll::poll_callback_t cb, void* data) { + int type, r; + socklen_t size = sizeof(type); + struct sockaddr addr; + r = ::getsockopt(fd, SOL_SOCKET, SO_TYPE, reinterpret_cast(&type), &size); + assert(r == 0); + size = sizeof(addr); + r = ::getsockname(fd, &addr, &size); + assert(r == 0); + client_poll* poll; + if (type == SOCK_STREAM) poll = new client_poll_tcp(io, addr.sa_family == AF_INET6 + ? boost::asio::ip::tcp::v6() : boost::asio::ip::tcp::v4(), ::dup(fd)); + else if (type == SOCK_DGRAM) poll = new client_poll_udp(io, addr.sa_family == AF_INET6 + ? boost::asio::ip::udp::v6() : boost::asio::ip::udp::v4(), ::dup(fd)); + else return nullptr; + + poll->data = data; + poll->fd_ = fd; + poll->cb_ = cb; + return poll; + } + + client_poll::client_poll() + : action_(CURL_POLL_NONE) + , ref_(0) { + + } + + client_poll::~client_poll() { + + } + + void client_poll::on_ready(const boost::system::error_code& error, int action) { + --ref_; + if(ref_ == 0 && action_ == CURL_POLL_REMOVE) { + delete this; + return; + } + if (error == boost::asio::error::operation_aborted) return; // 取消监听(销毁)或设置了新的监听 + + cb_(error, this, fd_, action); + if (!error && action_ == action) async_wait(); + } + + void client_poll::async_wait(int action) { + if (action_ != action) { + action_ = action; + async_wait(); + } + } + + client_poll_tcp::client_poll_tcp(boost::asio::io_context &io, boost::asio::ip::tcp proto, curl_socket_t fd) + : sock_(io, proto, fd) { + + } + + client_poll_tcp::~client_poll_tcp() { + + } + + void client_poll_tcp::async_wait() { + sock_.cancel(); + // 重新监听 + if (action_ == CURL_POLL_IN || action_ == CURL_POLL_INOUT) { + ++ref_; + sock_.async_wait(boost::asio::ip::tcp::socket::wait_read, std::bind(&client_poll_tcp::on_ready, this, std::placeholders::_1, action_)); + } + if (action_ == CURL_POLL_OUT || action_ == CURL_POLL_INOUT) { + ++ref_; + sock_.async_wait(boost::asio::ip::tcp::socket::wait_write, std::bind(&client_poll_tcp::on_ready, this, std::placeholders::_1, action_)); + } + if (action_ == CURL_POLL_REMOVE && ref_ == 0) delete this; + } + + client_poll_udp::client_poll_udp(boost::asio::io_context &io, boost::asio::ip::udp proto, curl_socket_t fd) + : client_poll(), sock_(io, proto, fd) { + + } + + client_poll_udp::~client_poll_udp() { + + } + + void client_poll_udp::async_wait() { + sock_.cancel(); + // 重新监听 + if (action_ == CURL_POLL_IN || action_ == CURL_POLL_INOUT) { + ++ref_; + sock_.async_wait(boost::asio::ip::tcp::socket::wait_read, std::bind(&client_poll_udp::on_ready, this, std::placeholders::_1, action_)); + } + if (action_ == CURL_POLL_OUT || action_ == CURL_POLL_INOUT) { + ++ref_; + sock_.async_wait(boost::asio::ip::tcp::socket::wait_write, std::bind(&client_poll_udp::on_ready, this, std::placeholders::_1, action_)); + } + if (action_ == CURL_POLL_REMOVE && ref_ == 0) delete this; + } + +} \ No newline at end of file diff --git a/src/flame/http/client_poll.h b/src/flame/http/client_poll.h new file mode 100644 index 0000000..0564383 --- /dev/null +++ b/src/flame/http/client_poll.h @@ -0,0 +1,44 @@ +#pragma once +#include "../../vendor.h" +#include "http.h" + +namespace flame::http { + + class client_poll { + public: + client_poll(); + typedef void (*poll_callback_t)(const boost::system::error_code& error, client_poll* poll, curl_socket_t fd, int action); + virtual ~client_poll(); + static client_poll* create_poll(boost::asio::io_context& io, curl_socket_t fd, poll_callback_t cb, void* data = nullptr); + static void destory_poll(client_poll* poll); + virtual void async_wait() = 0; + void async_wait(int action); + void* data; + protected: + static std::map pool_; + + curl_socket_t fd_; + int action_; + poll_callback_t cb_; + int ref_; + void on_ready(const boost::system::error_code& error, int action); + }; + + class client_poll_tcp: public client_poll { + public: + client_poll_tcp(boost::asio::io_context &io, boost::asio::ip::tcp proto, curl_socket_t fd); + ~client_poll_tcp(); + void async_wait() override; + private: + boost::asio::ip::tcp::socket sock_; + }; + + class client_poll_udp: public client_poll { + public: + client_poll_udp(boost::asio::io_context &io, boost::asio::ip::udp proto, curl_socket_t fd); + ~client_poll_udp(); + void async_wait() override; + private: + boost::asio::ip::udp::socket sock_; + }; +} diff --git a/src/flame/http/client_request.cpp b/src/flame/http/client_request.cpp new file mode 100644 index 0000000..1eda358 --- /dev/null +++ b/src/flame/http/client_request.cpp @@ -0,0 +1,182 @@ +#include "../coroutine.h" +#include "value_body.h" +#include "client_request.h" +#include "client_body.h" +#include "http.h" + +namespace flame::http { + + void client_request::declare(php::extension_entry& ext) { + php::class_entry class_client_request( + "flame\\http\\client_request"); + class_client_request + .constant({"HTTP_VERSION_1_0", CURL_HTTP_VERSION_1_0}) + .constant({"HTTP_VERSION_1_1", CURL_HTTP_VERSION_1_1}) + .constant({"HTTP_VERSION_2", CURL_HTTP_VERSION_2}) + .constant({"HTTP_VERSION_2_0", CURL_HTTP_VERSION_2_0}) + .constant({"HTTP_VERSION_2_TLS", CURL_HTTP_VERSION_2TLS}) + .constant({"HTTP_VERSION_2_PRI", CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE}) + .constant({"SSL_VERIFY_NONE", 0}) + .constant({"SSL_VERIFY_PEER", 1}) // CURLOPT_SSL_VERIFYPEER + .constant({"SSL_VERIFY_HOST", 2}) // CURLOPT_SSL_VERIFYHOST + .constant({"SSL_VERIFY_STATUS", 4}) // CURLOPT_SSL_VERIFYSTATUS + .constant({"SSL_VERIFY_ALL", 7}) + .property({"timeout", 3000}) + .property({"method", "GET"}) + .property({"url", nullptr}) + .property({"header", nullptr}) + .property({"cookie", nullptr}) + .property({"body", ""}) + .method<&client_request::__construct>("__construct", { + {"url", php::TYPE::STRING}, + {"body", php::TYPE::UNDEFINED, false, true}, + {"timeout", php::TYPE::INTEGER, false, true}, + }) + .method<&client_request::http_version>("http_version", { + {"version", php::TYPE::INTEGER}, + }) + .method<&client_request::ssl_pem>("ssl_pem", { + {"cert", php::TYPE::STRING}, + {"pkey", php::TYPE::STRING, false, true}, + }) + .method<&client_request::ssl_verify>("ssl_verify", { + {"verify", php::TYPE::INTEGER}, + }) + .method<&client_request::__destruct>("__destruct"); + + ext.add(std::move(class_client_request)); + } + + php::value client_request::__construct(php::parameters& params) { + if (params.length() > 2) set("timeout", params[2]); + else set("timeout", 3000); + + if (params.length() > 1 && !params[1].empty()) { + set("body", params[1]); + set("method", "POST"); + } + else set("method", "GET"); + set("url", params[0]); + set("header", php::array(0)); + set("cookie", php::array(0)); + + c_easy_ = curl_easy_init(); + return nullptr; + } + + php::value client_request::__destruct(php::parameters ¶ms) { + if (c_head_) curl_slist_free_all(c_head_); + if (c_easy_) curl_easy_cleanup(c_easy_); + return nullptr; + } + + php::value client_request::http_version(php::parameters& params) { + long v = static_cast(params[0]); + if (v <= CURL_HTTP_VERSION_NONE || v >= CURL_HTTP_VERSION_LAST) + throw php::exception(zend_ce_error_exception + , "Failed to set HTTP version: value out of range" + , -1); + + curl_easy_setopt(c_easy_, CURLOPT_HTTP_VERSION, v); + return nullptr; + } + + php::value client_request::ssl_pem(php::parameters& params) { + curl_easy_setopt(c_easy_, CURLOPT_SSLCERTTYPE, "PEM"); + php::string cert = params[0]; + curl_easy_setopt(c_easy_, CURLOPT_SSLCERT, cert.c_str()); + if (params.size() > 1 && params[1].type_of(php::TYPE::STRING)) { + php::string pkey = params[1]; + curl_easy_setopt(c_easy_, CURLOPT_SSLKEY, pkey.c_str()); + } + if (params.size() > 2 && params[2].type_of(php::TYPE::STRING)) { + php::string pass = params[2]; + curl_easy_setopt(c_easy_, CURLOPT_KEYPASSWD, pass.c_str()); + } + return nullptr; + } + + php::value client_request::ssl_verify(php::parameters& params) { + long v = static_cast(params[0]), YES = 1, NO = 0; + if (v | CURLOPT_SSL_VERIFYPEER) + curl_easy_setopt(c_easy_, CURLOPT_SSL_VERIFYPEER, YES); + else + curl_easy_setopt(c_easy_, CURLOPT_SSL_VERIFYPEER, NO); + if (v | CURLOPT_SSL_VERIFYHOST) + curl_easy_setopt(c_easy_, CURLOPT_SSL_VERIFYHOST, YES); + else + curl_easy_setopt(c_easy_, CURLOPT_SSL_VERIFYHOST, NO); + if (v | CURLOPT_SSL_VERIFYSTATUS) + curl_easy_setopt(c_easy_, CURLOPT_SSL_VERIFYSTATUS, YES); + else + curl_easy_setopt(c_easy_, CURLOPT_SSL_VERIFYSTATUS, NO); + return nullptr; + } + + void client_request::build_ex() { + long timeout = static_cast(get("timeout")); + curl_easy_setopt(c_easy_, CURLOPT_TIMEOUT_MS, timeout); + // 目标请求地址 + // --------------------------------------------------------------------------- + php::string u = get("url"); + if (!u.type_of(php::TYPE::STRING)) + throw php::exception(zend_ce_type_error + , "Failed to build client request: 'url' typeof 'string' required" + , -1); + + curl_easy_setopt(c_easy_, CURLOPT_URL, u.c_str()); + php::string m = get("method"); + curl_easy_setopt(c_easy_, CURLOPT_CUSTOMREQUEST, m.c_str()); + // 头 + // --------------------------------------------------------------------------- + if (c_head_ != nullptr) curl_slist_free_all(c_head_); + std::string ctype; + long keepalive = 1; + php::array header = get("header"); + for (auto i=header.begin(); i!=header.end(); ++i) { + php::string key = i->first.to_string(); + php::string val = i->second.to_string(); + if (strncasecmp(key.c_str(), "content-type", 12) == 0) ctype.assign(val.data(), val.size()); + else if (strncasecmp(key.c_str(), "connection", 10) == 0 + && strncasecmp(val.c_str(), "close", 5) == 0) keepalive = 0; + c_head_ = curl_slist_append(c_head_, (boost::format("%s: %s") % key % val).str().c_str()); + } + c_head_ = curl_slist_append(c_head_, "Expect: "); + curl_easy_setopt(c_easy_, CURLOPT_HTTPHEADER, c_head_); + curl_easy_setopt(c_easy_, CURLOPT_TCP_KEEPALIVE, keepalive); + // COOKIE + // --------------------------------------------------------------------------- + php::array cookie = get("cookie"); + php::buffer cookies; + for (auto i=cookie.begin(); i!=cookie.end(); ++i) { + php::string key = i->first.to_string(); + php::string val = i->second.to_string(); + val = php::url_encode(val.c_str(), val.size()); + cookies.append(key); + cookies.push_back('='); + cookies.append(val); + cookies.push_back(';'); + cookies.push_back(' '); + } + php::string cookie_str = std::move(cookies); + if (cookie_str.size() > 0) curl_easy_setopt(c_easy_, CURLOPT_COOKIE, cookie_str.c_str()); + // 体 + // --------------------------------------------------------------------------- + php::string body = get("body"); + if (ctype.empty()) ctype.assign("application/x-www-form-urlencoded", 33); + if (body.empty()) { + // TODO 空 BODY 的处理流程? + } + else if (body.instanceof(php::class_entry::entry())) { + // TODO multipart support + } + else { + body = ctype_encode(ctype, body); + // 注意: CURLOPT_POSTFIELDS 仅"引用" body 数据 + set("body", body); + // curl_easy_setopt(c_easy_, CURLOPT_POST, 1); + curl_easy_setopt(c_easy_, CURLOPT_POSTFIELDSIZE, body.size()); + curl_easy_setopt(c_easy_, CURLOPT_POSTFIELDS, body.c_str()); + } + } +} diff --git a/src/flame/http/client_request.h b/src/flame/http/client_request.h new file mode 100644 index 0000000..fd6f6dd --- /dev/null +++ b/src/flame/http/client_request.h @@ -0,0 +1,25 @@ +#pragma once +#include "../../vendor.h" +#include "../../url.h" +#include "../coroutine.h" +#include "http.h" + +namespace flame::http { + + class client_request: public php::class_base { + public: + static void declare(php::extension_entry& ext); + php::value __construct(php::parameters& params); + php::value __destruct(php::parameters& params); + php::value http_version(php::parameters& params); + php::value ssl_pem(php::parameters& params); + php::value ssl_verify(php::parameters& params); + private: + void build_ex(); + CURL* c_easy_ = nullptr; + curl_slist* c_head_ = nullptr; + + friend class client; + friend class _connection_pool; + }; +} diff --git a/src/flame/http/client_response.cpp b/src/flame/http/client_response.cpp new file mode 100644 index 0000000..9d24990 --- /dev/null +++ b/src/flame/http/client_response.cpp @@ -0,0 +1,75 @@ +#include "http.h" +#include "value_body.h" +#include "client_response.h" + +namespace flame::http { + void client_response::declare(php::extension_entry& ext) { + php::class_entry class_client_response("flame\\http\\client_response"); + class_client_response + .property({"version", "HTTP/1.0"}) + .property({"status", 3000}) + .property({"header", nullptr}) + .property({"body", nullptr}) + .property({"raw_body", ""}) + .method<&client_response::__construct>("__construct", {}, php::PRIVATE) + .method<&client_response::to_string>("__toString"); + ext.add(std::move(class_client_response)); + } + // 声明为 ZEND_ACC_PRIVATE 禁止创建(不会被调用) + php::value client_response::__construct(php::parameters& params) { + return nullptr; + } + php::value client_response::to_string(php::parameters& params) { + return get("raw_body"); + } + void client_response::build_ex() { + if (c_final_ != 0) throw php::exception(zend_ce_exception + , (boost::format("Failed to execute HTTP request: %s") % c_error_).str() + , c_final_); + // 响应码 + long rv = 0; + curl_easy_getinfo(c_easy_, CURLINFO_HTTP_VERSION, &rv); + set("version", rv == 3 ? "HTTP/2" : rv == 2 ? "HTTP/1.1" : "HTTP/1.0"); + curl_easy_getinfo(c_easy_, CURLINFO_RESPONSE_CODE, &rv); + set("status", rv); + // 响应头 + set("header", c_head_); + // 响应体 + php::string rbody = std::move(c_body_); + set("raw_body", rbody); + if (c_head_.exists("content-type")) { + php::string ctype = c_head_.get("content-type"); + std::string_view sv {ctype.c_str(), ctype.size()}; + set("body", ctype_decode(sv, rbody)); + } + else { + set("body", rbody); + } + } + + size_t client_response::c_write_cb(char *ptr, size_t size, size_t nmemb, void *data) { + client_response* self = static_cast(data); + assert(size == 1); + self->c_body_.append(ptr, nmemb); + return nmemb; + } + + size_t client_response::c_header_cb(char *buffer, size_t size, size_t nitems, void *data) { + client_response* self = static_cast(data); + size = size * nitems; + + std::string_view sv{ buffer, size }; + auto psep = sv.find_first_of(':', 1); + if (psep != sv.npos) { + auto pval = sv.find_first_not_of(' ', psep + 1); + php::string key = php::lowercase(buffer, psep); + if (pval == sv.npos) { + self->c_head_.set( key, php::string(0) ); + } + else { + self->c_head_.set( key, php::string( buffer + pval, size - pval - 2)); // trailing \r\n + } + } + return size; + } +} diff --git a/src/flame/http/client_response.h b/src/flame/http/client_response.h new file mode 100644 index 0000000..0757d66 --- /dev/null +++ b/src/flame/http/client_response.h @@ -0,0 +1,27 @@ +#pragma once +#include "../../vendor.h" +#include "../coroutine.h" +#include "http.h" + +namespace flame::http { + + class client_response: public php::class_base { + public: + static void declare(php::extension_entry& ext); + php::value to_string(php::parameters& params); + // 声明为 ZEND_ACC_PRIVATE 禁止创建(不会被调用) + php::value __construct(php::parameters& params); + private: + CURL* c_easy_; + php::array c_head_; + php::buffer c_body_; + CURLcode c_final_ = CURLE_OK; + char c_error_[CURL_ERROR_SIZE]; + coroutine_handler c_coro_; + void build_ex(); + // 接收 curl 回调(响应数据) + static size_t c_write_cb(char *ptr, size_t size, size_t nmemb, void *data); + static size_t c_header_cb(char *buffer, size_t size, size_t nitems, void *data); + friend class client; + }; +} diff --git a/src/flame/http/http.cpp b/src/flame/http/http.cpp new file mode 100644 index 0000000..3630f10 --- /dev/null +++ b/src/flame/http/http.cpp @@ -0,0 +1,168 @@ +#include "../coroutine.h" +#include "http.h" +#include "client.h" +#include "value_body.h" +#include "client_request.h" +#include "client_response.h" +#include "client_body.h" +#include "server.h" +#include "server_request.h" +#include "server_response.h" +#include "_handler.h" + +namespace flame::http { + + client* client_; + std::int64_t body_max_size = 1024 * 1024 * 1024; + + void declare(php::extension_entry& ext) { + client_ = nullptr; + ext.on_module_startup([] (php::extension_entry& ext) -> bool { + // 在框架初始化后创建全局HTTP客户端 + gcontroller->on_init([] (const php::array& opts) { + body_max_size = std::max(php::ini("post_max_size").calc(), body_max_size); + client_ = new client(); + php::parameters p(0, nullptr); + client_->__construct(p); + })->on_stop([] { + delete client_; + client_ = nullptr; + }); + return true; + }); + ext + .function("flame\\http\\get") + .function("flame\\http\\post") + .function("flame\\http\\put") + .function("flame\\http\\delete") + .function("flame\\http\\exec"); + + client_request::declare(ext); + client_response::declare(ext); + client_body::declare(ext); + client::declare(ext); + server::declare(ext); + server_request::declare(ext); + server_response::declare(ext); + } + static void init_guard() { + if (!client_) + throw php::exception(zend_ce_error_exception + , "Failed to execute HTTP request: exception or missing 'flame\\init()' ?" + , -1); + } + php::value get(php::parameters& params) { + init_guard(); + return client_->get(params); + } + php::value post(php::parameters& params) { + init_guard(); + return client_->post(params); + } + php::value put(php::parameters& params) { + init_guard(); + return client_->put(params); + } + php::value delete_(php::parameters& params) { + init_guard(); + return client_->delete_(params); + } + php::value exec(php::parameters& params) { + init_guard(); + return client_->exec(params); + } + php::string ctype_encode(std::string_view ctype, const php::value& v) { + if (v.type_of(php::TYPE::STRING)) return v; + + php::value r = v; + if (ctype.compare(0, 33, "application/x-www-form-urlencoded") == 0) { + if (r.type_of(php::TYPE::ARRAY)) { + r = php::callable("http_build_query")({v}); + } + else { + r.to_string(); + } + } + else if (ctype.compare(0, 16, "application/json") == 0) { + r = php::json_encode(r); + } + else { + r.to_string(); + } + return r; + } + php::value ctype_decode(std::string_view ctype, const php::string& v, php::array* files) { + if (ctype.compare(0, 16, "application/json") == 0) { + return php::json_decode(v); + } + else if (ctype.compare(0, 33, "application/x-www-form-urlencoded") == 0) { + php::array data(4); + php::callable("parse_str")({v, data.make_ref()}); + return data; + } + else if (ctype.compare(0, 19, "multipart/form-data") == 0) { + std::string boundary; + std::string field; + php::array meta(4); + parser::separator_parser p1('\0','\0','=','"','"',';', [&boundary, &field, &meta] (std::pair entry) { + if (entry.first == "boundary") { + boundary = std::move(entry.second); + } + else if (entry.first == "content-disposition") { + // 此项头信息没有用途 + } + else if (entry.first == "name") { + // 这里可能有不严格的地方存在,即每个 Header 项后可能都存在对应的补充字段,且这些字段名称可能重复 + // 表单字段名 + field = std::move(entry.second); + } + else { + meta.set(entry.first, entry.second); + } + }); + std::size_t begin = ctype.find_first_of(';', 19) + 1; + p1.parse(ctype.data() + begin, ctype.size() - begin); + p1.parse(";", 1); // 保证一行 K/V 结束,复用 PARSER + + php::array data(8); + parser::multipart_parser p2(boundary, [&p1, &field, &meta, &data, &files] (std::pair entry) { + if (entry.first.size() == 0) { // Part Data + if (meta.exists("filename") > 0) { + if (files) { // 接收对应文件 + // 上传文件 meta + meta["size"] = entry.second.size(); + meta["data"] = std::move(entry.second); + files->set(field, meta); + } + } + else { + data.set(field, std::move(entry.second)); + } + meta = php::array(4); + } + else { // Part Header + php::lowercase_inplace(entry.first.data(), entry.first.size()); + std::size_t end = std::string_view(entry.second.data(), entry.second.size()).find_first_of(';'); + if (end == std::string::npos) { + meta[entry.first] = std::move(entry.second); + } + else if (entry.first == "content-disposition") { + // 这里可能有不严格的地方存在,即每个 Header 项后可能都存在对应的补充字段,且这些字段名称可能重复 + std::size_t begin = end + 1; + if (entry.second.size() > begin + 2) { + p1.parse(entry.second.data() + begin, entry.second.size() - begin); + p1.parse(";", 1); // 保证一行 K/V 结束,复用 PARSER + } + } + else { + meta[entry.first] = php::string(entry.second.data(), end); + } + } + }); + p2.parse(v.data(), v.size()); + + return data; + } + else return v; + } +} diff --git a/src/flame/http/http.h b/src/flame/http/http.h new file mode 100644 index 0000000..0ff3a8e --- /dev/null +++ b/src/flame/http/http.h @@ -0,0 +1,25 @@ +#pragma once +#include "../../vendor.h" +#define BOOST_BEAST_USE_STD_STRING_VIEW +#include +#include +#include +#include + +namespace flame::http { + + class _connection_pool; + class client; + extern client* client_; + extern std::int64_t body_max_size; + + void declare(php::extension_entry& ext); + php::value get(php::parameters& params); + php::value post(php::parameters& params); + php::value put(php::parameters& params); + php::value delete_(php::parameters& params); + php::value exec(php::parameters& params); + + php::string ctype_encode(std::string_view ctype, const php::value& v); + php::value ctype_decode(std::string_view ctype, const php::string& v, php::array* file = nullptr); +} diff --git a/src/flame/http/server.cpp b/src/flame/http/server.cpp new file mode 100644 index 0000000..ec2be5d --- /dev/null +++ b/src/flame/http/server.cpp @@ -0,0 +1,156 @@ +#include "../coroutine.h" +#include "../udp/udp.h" +#include "server.h" +#include "_handler.h" + +namespace flame::http { + + void server::declare(php::extension_entry &ext) { + php::class_entry class_server("flame\\http\\server"); + class_server + .property({"address", "127.0.0.1:7678"}) + .method<&server::__construct>("__construct", { + {"address", php::TYPE::STRING}, + }) + .method<&server::before>("before", { + {"callback", php::TYPE::CALLABLE}, + }) + .method<&server::after>("after", { + {"callback", php::TYPE::CALLABLE}, + }) + .method<&server::put>("put", { + {"path", php::TYPE::STRING}, + {"callback", php::TYPE::CALLABLE}, + }) + .method<&server::delete_>("delete", { + {"path", php::TYPE::STRING}, + {"callback", php::TYPE::CALLABLE}, + }) + .method<&server::post>("post", { + {"path", php::TYPE::STRING}, + {"callback", php::TYPE::CALLABLE}, + }) + .method<&server::patch>("patch", { + {"path", php::TYPE::STRING}, + {"callback", php::TYPE::CALLABLE}, + }) + .method<&server::get>("get", { + {"path", php::TYPE::STRING}, + {"callback", php::TYPE::CALLABLE}, + }) + .method<&server::head>("head", { + {"path", php::TYPE::STRING}, + {"callback", php::TYPE::CALLABLE}, + }) + .method<&server::options>("options", { + {"path", php::TYPE::STRING}, + {"callback", php::TYPE::CALLABLE}, + }) + .method<&server::run>("run") + .method<&server::close>("close"); + ext.add(std::move(class_server)); + } + + server::server() + : accp_(gcontroller->context_x) + , sock_(gcontroller->context_x) + , closed_(false) { + + } + + typedef boost::asio::detail::socket_option::boolean reuse_port; + php::value server::__construct(php::parameters ¶ms) { + php::string str = params[0]; + auto pair = udp::addr2pair(str); + if (pair.first.empty() || pair.second.empty()) + throw php::exception(zend_ce_error_exception + , "Failed to bind tcp socket: address malformed" + , -1); + boost::asio::ip::address addr = boost::asio::ip::make_address(pair.first); + addr_.address(addr); + addr_.port(std::atoi(pair.second.c_str())); + + accp_.open(addr_.protocol()); + boost::asio::socket_base::reuse_address opt1(true); + accp_.set_option(opt1); + reuse_port opt2(true); + accp_.set_option(opt2); + + set("address", params[0]); + return nullptr; + } + + php::value server::before(php::parameters ¶ms) { + cb_["before"] = params[0]; + return this; + } + + php::value server::after(php::parameters ¶ms) { + cb_["after"] = params[0]; + return this; + } + + php::value server::put(php::parameters ¶ms) { + cb_[std::string("PUT:") + params[0].to_string()] = params[1]; + return this; + } + + php::value server::delete_(php::parameters ¶ms) { + cb_[std::string("DELETE:") + params[0].to_string()] = params[1]; + return this; + } + + php::value server::post(php::parameters ¶ms) { + cb_[std::string("POST:") + params[0].to_string()] = params[1]; + return this; + } + + php::value server::get(php::parameters ¶ms) { + cb_[std::string("GET:") + params[0].to_string()] = params[1]; + return this; + } + + php::value server::head(php::parameters ¶ms) { + cb_[std::string("HEAD:") + params[0].to_string()] = params[1]; + return this; + } + + php::value server::options(php::parameters ¶ms) { + cb_[std::string("OPTIONS:") + params[0].to_string()] = params[1]; + return this; + } + + php::value server::patch(php::parameters ¶ms) { + cb_[std::string("PATCH:") + params[0].to_string()] = params[1]; + return this; + } + + php::value server::run(php::parameters ¶ms) { + boost::system::error_code err; + accp_.bind(addr_, err); + if (err) throw php::exception(zend_ce_error_exception + , (boost::format("Failed to bind TCP socket: %s") % err.message()).str() + , err.value()); + accp_.listen(boost::asio::socket_base::max_listen_connections); + + coroutine_handler ch{coroutine::current}; + while(!closed_) { + boost::system::error_code err; + accp_.async_accept(sock_, ch[err]); + if (err == boost::asio::error::operation_aborted) break; + else if (err) throw php::exception(zend_ce_error_exception, (boost::format("Failed to accept connection: %s") % err.message()).str(), err.value()); + else coroutine::start(php::callable( // 按连接启动处理协程(目前仅支持 HTTP/1.1 按 HTTP/2 机制须按照 STREAM 启动协程) + std::bind(&_handler::run, std::make_shared<_handler>(this, std::move(sock_)), std::placeholders::_1) + )); + } + return nullptr; + } + + php::value server::close(php::parameters ¶ms) { + if (closed_) return nullptr; + closed_ = true; + accp_.cancel(); + accp_.close(); + return nullptr; + } +} // namespace flame::http diff --git a/src/flame/http/server.h b/src/flame/http/server.h new file mode 100644 index 0000000..e0b10c4 --- /dev/null +++ b/src/flame/http/server.h @@ -0,0 +1,35 @@ +#pragma once +#include "../../vendor.h" +#include "http.h" + +namespace flame::http { + class acceptor; + class server : public php::class_base { + public: + static void declare(php::extension_entry &ext); + + server(); + + php::value __construct(php::parameters ¶ms); + php::value before(php::parameters ¶ms); + php::value after(php::parameters ¶ms); + php::value put(php::parameters ¶ms); + php::value delete_(php::parameters ¶ms); + php::value post(php::parameters ¶ms); + php::value patch(php::parameters ¶ms); + php::value get(php::parameters ¶ms); + php::value head(php::parameters ¶ms); + php::value options(php::parameters ¶ms); + php::value run(php::parameters ¶ms); + php::value close(php::parameters ¶ms); + + private: + boost::asio::ip::tcp::endpoint addr_; + boost::asio::ip::tcp::acceptor accp_; + boost::asio::ip::tcp::socket sock_; + std::map cb_; + bool closed_; + + friend class _handler; + }; +} // namespace flame::http diff --git a/src/flame/http/server_request.cpp b/src/flame/http/server_request.cpp new file mode 100644 index 0000000..0695c1b --- /dev/null +++ b/src/flame/http/server_request.cpp @@ -0,0 +1,90 @@ +#include "../coroutine.h" +#include "http.h" +#include "value_body.h" +#include "server_request.h" + +namespace flame::http { + + void server_request::declare(php::extension_entry& ext) { + php::class_entry class_server_request( + "flame\\http\\server_request"); + class_server_request + .property({"method", "GET"}) + .property({"path", "/"}) + .property({"query", nullptr}) + .property({"header", nullptr}) + .property({"cookie", nullptr}) + .property({"body", nullptr}) + .property({"raw_body", ""}) + .property({"file", nullptr}) + .property({"data", nullptr}) + .method<&server_request::__construct>("__construct", {}, php::PRIVATE); + ext.add(std::move(class_server_request)); + } + + server_request::server_request() { + } + + server_request::~server_request() { + } + + php::value server_request::__construct(php::parameters& params) { + return nullptr; + } + + void server_request::build_ex(const boost::beast::http::message>& ctr_) { + auto method = boost::beast::http::to_string(ctr_.method()); + set("method", php::string(method.data(), method.size())); + // 目标请求地址 + auto target = ctr_.target(); + std::shared_ptr url_ = php::parse_url(http://github.com/target.data(), target.size()); + if (url_->path) set("path", php::string(url_->path)); + else set("path", php::string("/", 1)); + if (url_->query) { + php::array query(4); + php::callable("parse_str")({php::string(url_->query), query.make_ref()}); + set("query", query); + } + php::array header(4); + for (auto i=ctr_.begin(); i!=ctr_.end(); ++i) { + php::string key {i->name_string().data(), i->name_string().size()}; + php::string val {i->value().data(), i->value().size()}; + + if (key.size() == 6 && strncasecmp(key.c_str(), "cookie", 6) == 0) { + php::array cookie(4); + parser::separator_parser p1('\0','\0','=','\0','\0',';', [&cookie] (std::pair entry) { + if (entry.second.size() > 0) { + entry.second.shrink( php::url_decode_inplace(entry.second.data(), entry.second.size()) ); + cookie.set(entry.first, php::string(std::move(entry.second))); + } + else { + cookie.set(entry.first, php::string(0)); + } + }); + p1.parse(val.c_str(), val.size()); + p1.parse(";", 1); // 结尾分隔符可能不存在(可以认为数据行可能不完整) + set("cookie", cookie); + } + // TODO 多个同名 HEADER 的处理 + php::lowercase_inplace(key.data(), key.size()); + header.set(key, val); + } + set("header", header); + + php::string body = ctr_.body(); + if (body.type_of(php::TYPE::STRING)) { + auto ctype = ctr_.find(boost::beast::http::field::content_type); + if (ctype == ctr_.end() || ctype->value().compare(0, 19, "multipart/form-data") != 0) + set("raw_body", body); // 不在 multipart 时保留原始数据 + + if (ctype != ctr_.end()) { // 存在时按照类型进行解析 + php::array files(4); + set("body", ctype_decode(ctype->value(), body, &files)); + set("file", files); + } + else { // 不存在与 raw_body 相同 + set("body", body); + } + } + } +} diff --git a/src/flame/http/server_request.h b/src/flame/http/server_request.h new file mode 100644 index 0000000..b2f4d68 --- /dev/null +++ b/src/flame/http/server_request.h @@ -0,0 +1,19 @@ +#pragma once +#include "../../vendor.h" +#include "http.h" + +namespace flame::http { + + class handler; + class server_request: public php::class_base { + public: + static void declare(php::extension_entry& ext); + server_request(); + ~server_request(); + php::value __construct(php::parameters& params); + private: + void build_ex(const boost::beast::http::message>& ctr); + + friend class _handler; + }; +} diff --git a/src/flame/http/server_response.cpp b/src/flame/http/server_response.cpp new file mode 100644 index 0000000..8f43910 --- /dev/null +++ b/src/flame/http/server_response.cpp @@ -0,0 +1,233 @@ +#include "../coroutine.h" +#include "../time/time.h" +#include "_handler.h" +#include "server_response.h" + +namespace flame::http { + + void server_response::declare(php::extension_entry& ext) { + php::class_entry class_server_response("flame\\http\\server_response"); + class_server_response + .property({"status", 200}) + .property({"header", nullptr}) + .property({"cookie", nullptr}) + .property({"body", nullptr}) + .method<&server_response::__construct>("__construct", {}, php::PRIVATE) + .method<&server_response::set_cookie>("set_cookie", { + {"name", php::TYPE::STRING}, + {"value", php::TYPE::STRING, false, true}, + {"expire", php::TYPE::INTEGER, false, true}, + {"path", php::TYPE::STRING, false, true}, + {"domain", php::TYPE::STRING, false, true}, + {"secure", php::TYPE::BOOLEAN, false, true}, + {"httponly", php::TYPE::BOOLEAN, false, true}, + }) + .method<&server_response::write_header>("write_header", { + {"status", php::TYPE::INTEGER, false, true} + }) + .method<&server_response::write>("write", { + {"chunk", php::TYPE::UNDEFINED} + }) + .method<&server_response::end>("end", { + {"chunk", php::TYPE::UNDEFINED, false, true} + }) + .method<&server_response::file>("file", { + {"root", php::TYPE::STRING}, + {"path", php::TYPE::STRING}, + }); + + ext.add(std::move(class_server_response)); + } + + server_response::server_response() + : status_(0) { + + } + + server_response::~server_response() { + + } + // 声明为 ZEND_ACC_PRIVATE 禁止创建(不会被调用) + php::value server_response::__construct(php::parameters& params) { + return nullptr; + } + + php::value server_response::set_cookie(php::parameters& params) { + php::array cookie(4); + php::string name = params[0], val; + name.to_string(); + cookie.set("name", name); + if (params.size() > 1) { + val = params[1]; + val.to_string(); + } + else val = php::string(""); + cookie.set("value", val); + if (params.size() > 2) { + // php::object expire = php::datetime(time::now() + * 1000); + cookie.set("expire", params[2].to_integer()); + } + if (params.size() > 3) { + php::string path = params[3]; + path.to_string(); + cookie.set("path", path); + } + if (params.size() > 4) { + php::string domain = params[4]; + domain.to_string(); + cookie.set("domain", domain); + } + if (params.size() > 5) { + bool secure = params[5]; + cookie.set("secure", secure); + } + if (params.size() > 6) { + bool http_only = params[5]; + cookie.set("http_only", http_only); + } + + php::array cookies = get("cookie", true); + if (cookies.type_of(php::TYPE::NULLABLE)) cookies = php::array(4); + cookies.set(cookies.size(), cookie); + return nullptr; + } + // chunked encoding 用法 + php::value server_response::write_header(php::parameters& params) { + if ((status_ & STATUS_HEAD_SENT) || (status_ & STATUS_BODY_SENT)) + throw php::exception(zend_ce_error_exception + , "Failed to write header: already sent" + , -1); + + status_ |= STATUS_HEAD_SENT; + if (params.size() > 0) set("status", params[0].to_integer()); + coroutine_handler ch{coroutine::current}; + handler_->write_head(this, ch); + return nullptr; + } + + php::value server_response::write(php::parameters& params) { + if (status_ & STATUS_BODY_END) + throw php::exception(zend_ce_error_exception + , "Failed to write body: response already done" + , -1); + + coroutine_handler ch{coroutine::current}; + if (!(status_ & STATUS_HEAD_SENT)) { + status_ |= STATUS_HEAD_SENT; + handler_->write_head(this, ch); + } + status_ |= STATUS_BODY_SENT; + php::string chunk = params[0].to_string(); + handler_->write_chunk(this, chunk, ch); + return nullptr; + } + + php::value server_response::end(php::parameters& params) { + if (status_ & STATUS_BODY_END) + throw php::exception(zend_ce_error_exception + , "Failed to write body: response already done" + , -1); + + coroutine_handler ch{coroutine::current}; + if (!(status_ & STATUS_HEAD_SENT)) { + status_ |= STATUS_HEAD_SENT; + handler_->write_head(this, ch); + } + if (params.size() > 0) { + status_ |= STATUS_BODY_SENT; + php::string chunk = params[0].to_string(); + handler_->write_chunk(this, chunk, ch); + } + else { + status_ |= STATUS_BODY_END; + handler_->write_end(this, ch); + handler_.reset(); + } + return nullptr; + } + + php::value server_response::file(php::parameters& params) { + if ((status_ & STATUS_BODY_END) || (status_ & STATUS_BODY_SENT)) + throw php::exception(zend_ce_error_exception + , "Failed to write file: response already done" + , -1); + + coroutine_handler ch{coroutine::current}; + if (!(status_ & STATUS_HEAD_SENT)) { + status_ |= STATUS_HEAD_SENT; + handler_->write_head(this, ch); + } + status_ |= STATUS_BODY_END | STATUS_BODY_SENT; + + std::filesystem::path root, file; + root += params[0].to_string(); + file += params[1].to_string(); + + handler_->write_file(this, (root / file.lexically_normal()).string(), ch); + return nullptr; + } + + // 支持 content-length 形式的用法 + void server_response::build_ex(boost::beast::http::message>& ctr_) { + if (status_ & STATUS_BUILT) return; // 重复使用 + status_ |= STATUS_BUILT; // 在 chunked_writer 中设置 + ctr_.result( get("status").to_integer() ); // 非法 status_code 会抛出异常 + php::array headers = get("header"); + if (headers.type_of(php::TYPE::ARRAY)) { + for (auto i=headers.begin(); i!=headers.end(); ++i) { + php::string key = i->first.to_string(); + php::string val = i->second.to_string(); + ctr_.set(std::string_view(key.c_str(), key.size()), static_cast(val)); + } + } + php::array cookies = get("cookie"); + if (cookies.type_of(php::TYPE::ARRAY)) { + for(auto i=cookies.begin(); i!=cookies.end(); ++i) { + php::array cookie = i->second; + std::string buffer; + buffer.append(cookie.get("name")); + buffer.push_back('='); + php::string value = cookie.get("value"); + if (!value.empty()) { + buffer.push_back('"'); + buffer.append(php::url_encode(value.c_str(), value.size())); + buffer.push_back('"'); + } + if (cookie.exists("expire")) { + std::int64_t expire = cookie.get("expire"); + // 范围 30 天内的数值, 按时间长度计算 + if (expire < 30 * 86400) { + buffer.append("; Max-Age=", 10); + buffer.append(std::to_string(expire)); + buffer.append("; Expire=", 9); + php::object date = php::datetime(expire * 1000 + std::chrono::duration_cast(time::now().time_since_epoch()).count()); + buffer.append(date.call("format", {"l, d-M-Y H:i:s T"})); + } + else { + // 超过上面范围, 按时间点计算 + buffer.append("; Expire=", 9); + php::object date = php::datetime(expire * 1000); + buffer.append(date.call("format", {"l, d-M-Y H:i:s T"})); + } + } + if (cookie.exists("path")) { + buffer.append("; Path=", 7); + buffer.append(cookie.get("path")); + } + if (cookie.exists("domain")) { + buffer.append("; Domain=", 9); + buffer.append(cookie.get("domain")); + } + if (!cookie.get("secure").empty()) buffer.append("; Secure", 8); + if (!cookie.get("http_only").empty()) buffer.append("; HttpOnly", 10); + ctr_.insert("Set-Cookie", buffer); + } + } +CTYPE_AGAIN: + auto ctype = ctr_.find(boost::beast::http::field::content_type); + if (ctype == ctr_.end()) { + ctr_.set(boost::beast::http::field::content_type, "text/plain"); + goto CTYPE_AGAIN; + } + } +} diff --git a/src/flame/http/server_response.h b/src/flame/http/server_response.h new file mode 100644 index 0000000..958c00c --- /dev/null +++ b/src/flame/http/server_response.h @@ -0,0 +1,34 @@ +#pragma once +#include "../../vendor.h" +#include "http.h" +#include "value_body.h" + +namespace flame::http { + + class _handler; + class server_response: public php::class_base { + public: + static void declare(php::extension_entry& ext); + // 声明为 ZEND_ACC_PRIVATE 禁止创建(不会被调用) + php::value __construct(php::parameters& params); + php::value set_cookie(php::parameters& params); + php::value write_header(php::parameters& params); + php::value write(php::parameters& params); + php::value end(php::parameters& params); + php::value file(php::parameters& params); + server_response(); + ~server_response(); + private: + enum { + STATUS_BUILT = 0x01, + STATUS_HEAD_SENT = 0x02, + STATUS_BODY_SENT = 0x04, + STATUS_BODY_END = 0x08, + }; + int status_; + std::shared_ptr<_handler> handler_; // 允许通过 $res 对象保留 handler 持续响应 + void build_ex(boost::beast::http::message> &ctr); + + friend class _handler; + }; +} diff --git a/src/flame/http/value_body.cpp b/src/flame/http/value_body.cpp new file mode 100644 index 0000000..d03cb9d --- /dev/null +++ b/src/flame/http/value_body.cpp @@ -0,0 +1,29 @@ +#include "../coroutine.h" +#include "value_body.h" + +namespace flame::http { + + void value_body_reader::init(boost::optional n, boost::system::error_code& error) { + error.assign(0, error.category()); + } + + void value_body_reader::finish(boost::system::error_code& error) { + v_ = std::move(b_); + error.assign(0, error.category()); + } + + void value_body_writer::init(boost::system::error_code& error) { + error.assign(0, error.category()); + } + + boost::optional> value_body_writer::get(boost::system::error_code& error) { + if (v_.empty()) { + return {{boost::asio::const_buffer(nullptr, 0), false}}; + } + else { + php::string data = v_; + error.assign(0, error.category()); + return {{boost::asio::const_buffer(data.data(), data.size()), false}}; + } + } +} // namespace flame::http diff --git a/src/flame/http/value_body.h b/src/flame/http/value_body.h new file mode 100644 index 0000000..01afcac --- /dev/null +++ b/src/flame/http/value_body.h @@ -0,0 +1,47 @@ +#pragma once +#include "../../vendor.h" +#include "http.h" + +namespace flame::http { + + class value_body_reader; + class value_body_writer; + template + class value_body { + public: + // Body + using value_type = php::value; + static std::uint64_t size(const value_type& v); + // BodyReader (Body) + using reader = value_body_reader; + // BodyWriter (Body) + using writer = value_body_writer; + }; + + class value_body_reader { + public: + template + value_body_reader(boost::beast::http::header& h, php::value& v); + void init(boost::optional n, boost::system::error_code& error); + template + std::size_t put(ConstBufferSequence s, boost::system::error_code& error); + void finish(boost::system::error_code& error); + private: + php::value& v_; + php::buffer b_; + }; + + class value_body_writer { + public: + using const_buffers_type = boost::asio::const_buffer; + template + value_body_writer(const boost::beast::http::header& h, const php::value& v); + void init(boost::system::error_code& error); + boost::optional> get(boost::system::error_code& error); + private: + // const boost::beast::http::header& h_; + const php::value& v_; + }; +} // namnespace flame::http + +#include "value_body.ipp" diff --git a/src/flame/http/value_body.ipp b/src/flame/http/value_body.ipp new file mode 100644 index 0000000..63e9fce --- /dev/null +++ b/src/flame/http/value_body.ipp @@ -0,0 +1,32 @@ +#pragma once + +namespace flame::http { + + template + value_body_reader::value_body_reader(boost::beast::http::header& h, php::value& v) + : v_(v) { + + } + + template + std::size_t value_body_reader::put(ConstBufferSequence s, boost::system::error_code& error) { + std::size_t n = 0; + for (auto i=boost::asio::buffer_sequence_begin(s); i!=boost::asio::buffer_sequence_end(s); ++i) { + boost::asio::const_buffer cbuf(*i); + b_.append((const char *)cbuf.data(), cbuf.size()); + n += cbuf.size(); + } + error.assign(0, error.category()); + return n; + } + + template + value_body_writer::value_body_writer(const boost::beast::http::header& h, const php::value& v) + : v_(v) {} + + template + std::uint64_t value_body::size(const value_body::value_type& v) { + return v.size(); + } + +} // namespace flame::http diff --git a/src/flame/kafka/_consumer.cpp b/src/flame/kafka/_consumer.cpp new file mode 100644 index 0000000..2fb641d --- /dev/null +++ b/src/flame/kafka/_consumer.cpp @@ -0,0 +1,148 @@ +#include "../controller.h" +#include "../time/time.h" +#include "../log/logger.h" +#include "_consumer.h" +#include "../../coroutine_queue.h" +#include "kafka.h" +#include "message.h" + +namespace flame::kafka { + static rd_kafka_topic_partition_list_t *array2topics(const php::array &topics) { + // 目标订阅的 TOPIC + rd_kafka_topic_partition_list_t *t = rd_kafka_topic_partition_list_new(topics.size()); + for (auto i = topics.begin(); i != topics.end(); ++i) + rd_kafka_topic_partition_list_add(t, i->second.to_string().c_str(), RD_KAFKA_PARTITION_UA); + return t; + } + + _consumer::_consumer(php::array &config, php::array &topics) + : close_(false) { + if (!config.exists("bootstrap.servers") && !config.exists("metadata.broker.list")) + throw php::exception(zend_ce_type_error + , "Failed to create Kafka consumer: 'bootstrap.servers' required" + , -1); + + if (!config.exists("group.id")) + throw php::exception(zend_ce_type_error + , "Failed to create Kafka consumer: 'group.id' required" + , -1); + + if (topics.size() == 0) + throw php::exception(zend_ce_type_error + , "Failed to create Kafka consumer: target topics missing" + , -1); + + rd_kafka_conf_t *conf = array2conf(config); + char err[256]; + rd_kafka_conf_set_opaque(conf, this); + rd_kafka_conf_set_error_cb(conf, on_error); + + conn_ = rd_kafka_new(RD_KAFKA_CONSUMER, conf, err, sizeof(err)); + if (!conn_) + throw php::exception(zend_ce_exception + , (boost::format("Failed to create Kafka consumer: %s") % err).str()/*, 0*/); + + auto r = rd_kafka_poll_set_consumer(conn_); + if (r != RD_KAFKA_RESP_ERR_NO_ERROR) + throw php::exception(zend_ce_exception + , (boost::format("Failed to create Kafka Consumer: %s") % rd_kafka_err2str(r)).str() + , r); + + + tops_ = array2topics(topics); + } + + _consumer::~_consumer() { + if (conn_) { + // rd_kafka_consumer_close(conn_); + rd_kafka_topic_partition_list_destroy(tops_); + rd_kafka_destroy(conn_); + } + } + + void _consumer::on_error(rd_kafka_t* conn, int error, const char* reason, void* data) { + _consumer* self = reinterpret_cast<_consumer*>(data); + if (log::logger::LEVEL_OPT <= log::logger::LEVEL_WARNING) + log::logger_->stream() << "[" << time::iso() << "] (WARNING) Kafka Consumer " << rd_kafka_err2str((rd_kafka_resp_err_t)error) << ": " << reason << std::endl; + } + + void _consumer::subscribe(coroutine_handler &ch) { + rd_kafka_resp_err_t err; + boost::asio::post(gcontroller->context_y, [this, &err, &ch] () { + err = rd_kafka_subscribe(conn_, tops_); + ch.resume(); + }); + ch.suspend(); + if (err != RD_KAFKA_RESP_ERR_NO_ERROR) + throw php::exception(zend_ce_exception + , (boost::format("failed to subcribe Kafka topics: %s") % rd_kafka_err2str(err)).str() + , err); + } + + void _consumer::consume(coroutine_queue& q, coroutine_handler& ch) { + rd_kafka_resp_err_t err = RD_KAFKA_RESP_ERR_NO_ERROR; + rd_kafka_message_t* msg = nullptr; +POLL_MESSAGE: + // 采用内部队列,理论上仅占用一个工作线程(应保证占用时间不要过长) + boost::asio::post(gcontroller->context_y, [this, &err, &msg, &ch]() { + msg = rd_kafka_consumer_poll(conn_, 40); + if (!msg) err = RD_KAFKA_RESP_ERR__PARTITION_EOF; + else if (msg->err) { + err = msg->err; + rd_kafka_message_destroy(msg); + } + else err = RD_KAFKA_RESP_ERR_NO_ERROR; + ch.resume(); + }); + ch.suspend(); + if (err == RD_KAFKA_RESP_ERR__PARTITION_EOF) { + if (close_) { + q.close(); // 关闭队列使对应消费协程自行退出 + return; + } // 消费已被关闭 + else goto POLL_MESSAGE; + } + else if (err != RD_KAFKA_RESP_ERR_NO_ERROR) { + throw php::exception(zend_ce_exception + , (boost::format("failed to consume Kafka message: %s") % rd_kafka_err2str(err)).str() + , err); + } + else { + php::object obj(php::class_entry::entry()); + message* ptr = static_cast(php::native(obj)); + ptr->build_ex(msg); // msg 交由 message 对象管理 + q.push(std::move(obj), ch); + goto POLL_MESSAGE; + } + } + + void _consumer::commit(const php::object& obj, coroutine_handler& ch) { + rd_kafka_resp_err_t err; + rd_kafka_message_t *msg = static_cast(php::native(obj))->msg_; + boost::asio::post(gcontroller->context_y, [this, &err, &msg, &ch]() { + err = rd_kafka_commit_message(conn_, msg, 0); + ch.resume(); + }); + ch.suspend(); + if (err != RD_KAFKA_RESP_ERR_NO_ERROR) + throw php::exception(zend_ce_exception + , (boost::format("Failed to commit Kafka message: %s") % err % rd_kafka_err2str(err)).str() + , err); + } + + void _consumer::close(coroutine_handler& ch) { + rd_kafka_resp_err_t err; + boost::asio::post(gcontroller->context_y, [this, &err, &ch] () { + err = rd_kafka_consumer_close(conn_); + ch.resume(); + }); + ch.suspend(); + if (err != RD_KAFKA_RESP_ERR_NO_ERROR) + throw php::exception(zend_ce_exception + , (boost::format("failed to close Kafka consumer: %s") % rd_kafka_err2str(err)).str() + , err); + + close_ = true; + } + +} // namespace flame::kafka diff --git a/src/flame/kafka/_consumer.h b/src/flame/kafka/_consumer.h new file mode 100644 index 0000000..bbf7ec7 --- /dev/null +++ b/src/flame/kafka/_consumer.h @@ -0,0 +1,27 @@ +#pragma once +#include "../../vendor.h" +#include "../coroutine.h" +#include "kafka.h" + + +template +class coroutine_queue; + +namespace flame::kafka { + // 消费者 + // 目前的实现方式无需 shared_from_this 管理 + class _consumer/*: public std::enable_shared_from_this<_consumer> */{ + public: + _consumer(php::array& config, php::array& topics); + ~_consumer(); + void subscribe(coroutine_handler& ch); + void consume(coroutine_queue& q, coroutine_handler& ch); + void commit(const php::object& msg, coroutine_handler& ch); + void close(coroutine_handler& ch); + private: + rd_kafka_t* conn_; + rd_kafka_topic_partition_list_t* tops_; + bool close_; + static void on_error(rd_kafka_t* conn, int error, const char* reason, void* data); + }; +} // namespace flame::kafka diff --git a/src/flame/kafka/_producer.cpp b/src/flame/kafka/_producer.cpp new file mode 100644 index 0000000..ad321dc --- /dev/null +++ b/src/flame/kafka/_producer.cpp @@ -0,0 +1,127 @@ +#include "../controller.h" +#include "../time/time.h" +#include "../log/logger.h" +#include "_producer.h" +#include "kafka.h" + +namespace flame::kafka { + + _producer::_producer(php::array& config, php::array& topics) + : poll_(gcontroller->context_y) + , close_(false) { + if (!config.exists("bootstrap.servers") && !config.exists("metadata.broker.list")) + throw php::exception(zend_ce_type_error + , "Failed to create Kafka producer: 'bootstrap.servers' required" + , -1); + if (topics.size() == 0) + throw php::exception(zend_ce_type_error + , "Failed to create Kafka producer: target topics missing" + , -1); + + rd_kafka_conf_t *conf = array2conf(config); + rd_kafka_conf_set_opaque(conf, this); + rd_kafka_conf_set_error_cb(conf, on_error); + + char err[256]; + conn_ = rd_kafka_new(RD_KAFKA_PRODUCER, conf, err, sizeof(err)); + if (!conn_) + throw php::exception(zend_ce_type_error + , (boost::format("Failed to create Kafka Consumer: %s") % err).str() + , -1); + + for (auto i = topics.begin(); i != topics.end(); ++i) { + php::string topic = i->second.to_string(); + tops_[topic] = rd_kafka_topic_new(conn_, topic.c_str(), nullptr); + } + } + + _producer::~_producer() { + if (!conn_) return; + for(auto i=tops_.begin(); i!=tops_.end(); ++i) { + rd_kafka_topic_destroy(i->second); + } + rd_kafka_destroy(conn_); + } + + void _producer::on_error(rd_kafka_t* conn, int error, const char* reason, void* data) { + _producer* self = reinterpret_cast<_producer*>(data); + if (log::logger::LEVEL_OPT <= log::logger::LEVEL_WARNING) + log::logger_->stream() << "[" << time::iso() << "] (WARNING) Kafka Producer " << rd_kafka_err2str((rd_kafka_resp_err_t)error) << ": " << reason << std::endl; + } + + void _producer::start() { + poll(); + } + + void _producer::poll(int expire) { + poll_.expires_after(std::chrono::milliseconds(expire)); + poll_.async_wait([this, self = shared_from_this()] (const boost::system::error_code& error) { + if (error || close_) return; + auto begin = std::chrono::steady_clock::now(); + int r = rd_kafka_poll(conn_, 40); // 防止对工作线程占用时间过长 + if (close_) return; + auto end = std::chrono::steady_clock::now(); + // 计算实际 poll 占用时间,稳定间隔 + int e = std::chrono::duration_cast(end - begin).count(); + // 无消息时放慢 poll 以减少对工作线程的占用 + if (r > 0) poll(std::max(40 - e, 1)); + else poll(std::max(1600 - e, 1)); + }); + } + + void _producer::close(coroutine_handler& ch) { + if (!conn_) return; + close_ = true; + poll_.cancel(); + boost::asio::post(gcontroller->context_y, [this, &ch] { + rd_kafka_yield(conn_); + rd_kafka_flush(conn_, 10000); + ch.resume(); + }); + ch.suspend(); + } + + void _producer::publish(const php::string &topic, const php::string &key + , const php::string &payload, const php::array &headers, coroutine_handler &ch) { + + rd_kafka_resp_err_t err = RD_KAFKA_RESP_ERR_NO_ERROR; + rd_kafka_headers_t* hdrs = array2hdrs(headers); + boost::asio::post(gcontroller->context_y, [this, &err, &topic, &key, &payload, &hdrs, &ch]() { + if (key.size() > 0) { + err = rd_kafka_producev(conn_, + RD_KAFKA_V_TOPIC(topic.c_str()), + RD_KAFKA_V_KEY(key.c_str(), key.size()), + RD_KAFKA_V_PARTITION(RD_KAFKA_PARTITION_UA), + RD_KAFKA_V_HEADERS(hdrs), + RD_KAFKA_V_MSGFLAGS(RD_KAFKA_MSG_F_COPY), + RD_KAFKA_V_VALUE((void *)payload.c_str(), payload.size()), + RD_KAFKA_V_END); + } + else { // 无 KEY 时默认 partitioner 会随机分配 + err = rd_kafka_producev(conn_, + RD_KAFKA_V_TOPIC(topic.c_str()), + RD_KAFKA_V_PARTITION(RD_KAFKA_PARTITION_UA), + RD_KAFKA_V_HEADERS(hdrs), + RD_KAFKA_V_MSGFLAGS(RD_KAFKA_MSG_F_COPY), + RD_KAFKA_V_VALUE((void *)payload.c_str(), payload.size()), + RD_KAFKA_V_END); + } + ch.resume(); + }); + ch.suspend(); + if (err != RD_KAFKA_RESP_ERR_NO_ERROR) { + if (hdrs) rd_kafka_headers_destroy(hdrs); // 发生错误时, 需要手动销毁 + throw php::exception(zend_ce_exception + , (boost::format("Failed to publish Kafka message: %s") % rd_kafka_err2str(err)).str() + , err); + } + } + + void _producer::flush(coroutine_handler& ch) { + if (!conn_) return; + poll_.cancel(); + rd_kafka_yield(conn_); // 停止当前的 poll + rd_kafka_flush(conn_, 10000); + poll(); // 重新启动 poll 流程 + } +} // namespace flame::kafka diff --git a/src/flame/kafka/_producer.h b/src/flame/kafka/_producer.h new file mode 100644 index 0000000..8269959 --- /dev/null +++ b/src/flame/kafka/_producer.h @@ -0,0 +1,26 @@ +#pragma once +#include "../../vendor.h" +#include "../coroutine.h" +#include "kafka.h" + +namespace flame::kafka { + // 生产者 + // 目前的实现方式 poll 定时调用机制需要 shared_from_this 保证生命周期 + class _producer: public std::enable_shared_from_this<_producer> { + public: + _producer(php::array& config, php::array& topics); + ~_producer(); + void publish(const php::string& topic, const php::string& key, const php::string& payload, const php::array& headers, coroutine_handler& ch); + void flush(coroutine_handler& ch); + void close(coroutine_handler& ch); + void start(); + private: + rd_kafka_t* conn_; + std::map tops_; + boost::asio::steady_timer poll_; + bool close_; + void poll(int expire = 970); + static void on_error(rd_kafka_t* conn, int error, const char* reason, void* data); + friend class producer; + }; +} // namespace flame::kafka diff --git a/src/flame/kafka/consumer.cpp b/src/flame/kafka/consumer.cpp new file mode 100644 index 0000000..026e878 --- /dev/null +++ b/src/flame/kafka/consumer.cpp @@ -0,0 +1,76 @@ +#include "../coroutine.h" +#include "../time/time.h" +#include "consumer.h" +#include "_consumer.h" +#include "kafka.h" +#include "message.h" +#include "../../coroutine_queue.h" +#include "../log/logger.h" + +namespace flame::kafka { + + void consumer::declare(php::extension_entry& ext) { + php::class_entry class_consumer("flame\\kafka\\consumer"); + class_consumer + .method<&consumer::__construct>("__construct", {}, php::PRIVATE) + .method<&consumer::run>("run", { + {"callable", php::TYPE::CALLABLE}, + }) + .method<&consumer::commit>("commit", { + {"message", "flame\\kafka\\message"} + }) + .method<&consumer::close>("close"); + ext.add(std::move(class_consumer)); + } + + php::value consumer::__construct(php::parameters& params) { + return nullptr; + } + + php::value consumer::run(php::parameters& params) { + cb_ = params[0]; + coroutine_handler ch_run {coroutine::current}; + coroutine_queue q; + // 启动若干协程, 然后进行"并行"消费 + int count = cc_; + for (int i = 0; i < count; ++i) { + // 启动协程开始消费 + coroutine::start(php::value([this, &ch_run, &q, &count](php::parameters ¶ms) -> php::value { + coroutine_handler ch {coroutine::current}; + while(true) { + try { + // consume 本身可能出现异常,不应导致进程停止 + std::optional m = q.pop(ch); + if (m) cb_.call({m.value()}); + else break; + } catch(const php::exception& ex) { + // 调用用户异常回调 + gcontroller->event("exception", {ex}); + // 记录错误信息 + php::object obj = ex; + log::logger_->stream() << "[" << time::iso() << "] (ERROR) Uncaught exception in Kafka consumer: " << obj.call("__toString") << std::endl; + } + } + if (--count == 0) ch_run.resume(); + return nullptr; + })); + } + cs_->consume(q, ch_run); + ch_run.suspend(); + return nullptr; + } + + php::value consumer::commit(php::parameters& params) { + php::object obj = params[0]; + coroutine_handler ch {coroutine::current}; + + cs_->commit(obj, ch); + return nullptr; + } + + php::value consumer::close(php::parameters& params) { + coroutine_handler ch {coroutine::current}; + cs_->close(ch); + return nullptr; + } +} // namespace diff --git a/src/flame/kafka/consumer.h b/src/flame/kafka/consumer.h new file mode 100644 index 0000000..255ad80 --- /dev/null +++ b/src/flame/kafka/consumer.h @@ -0,0 +1,24 @@ +#pragma once +#include "../../vendor.h" +#include "../coroutine.h" +#include "kafka.h" + +namespace flame::kafka { + + class _consumer; + class consumer : public php::class_base { + public: + static void declare(php::extension_entry &ext); + php::value __construct(php::parameters ¶ms); // 私有 + php::value run(php::parameters ¶ms); + php::value commit(php::parameters ¶ms); + php::value close(php::parameters ¶ms); + + private: + std::shared_ptr<_consumer> cs_; + int cc_ = 8; + php::callable cb_; + + friend php::value consume(php::parameters ¶ms); + }; +} // namespace flame::kafka diff --git a/src/flame/kafka/kafka.cpp b/src/flame/kafka/kafka.cpp new file mode 100644 index 0000000..24d8454 --- /dev/null +++ b/src/flame/kafka/kafka.cpp @@ -0,0 +1,98 @@ +#include "kafka.h" +#include "message.h" +#include "consumer.h" +#include "_consumer.h" +#include "producer.h" +#include "_producer.h" + +namespace flame::kafka { + void declare(php::extension_entry &ext) { + ext + .function("flame\\kafka\\consume", { + {"options", php::TYPE::ARRAY}, + {"topics", php::TYPE::ARRAY}, + }) + .function("flame\\kafka\\produce", { + {"options", php::TYPE::ARRAY}, + {"topics", php::TYPE::ARRAY}, + });; + message::declare(ext); + consumer::declare(ext); + producer::declare(ext); + } + + php::value consume(php::parameters& params) { + php::array config = params[0]; + php::array topics = params[1]; + + php::object obj(php::class_entry::entry()); + consumer* ptr = static_cast(php::native(obj)); + if (config.exists("concurrent")) { + ptr->cc_ = std::min(std::max(static_cast(config.get("concurrent")), 1), 256); + config.erase("concurrent"); + } + if (!config.exists("log.connection.close")) config.set("log.connection.close", "false"); + + ptr->cs_ = std::make_shared<_consumer>(config, topics); + coroutine_handler ch {coroutine::current}; + // 订阅 + ptr->cs_->subscribe(ch); + return std::move(obj); + } + + php::value produce(php::parameters& params) { + php::array config = params[0]; + php::array topics = params[1]; + + php::object obj(php::class_entry::entry()); + producer* ptr = static_cast(php::native(obj)); + + if (!config.exists("log.connection.close")) config.set("log.connection.close", "false"); + + ptr->pd_ = std::make_shared<_producer>(config, topics); + ptr->pd_->start(); // 不能在构造中启动 poll 流程 + // TODO 优化: 确认首次连接已建立 + return std::move(obj); + } + + rd_kafka_conf_t* array2conf(const php::array &config) { + if (config.empty()) return nullptr; + char err[256]; + rd_kafka_conf_t *conf = rd_kafka_conf_new(); + for (auto i = config.begin(); i != config.end(); ++i) { + php::string key = i->first.to_string(); + php::string val = i->second.to_string(); + + if (RD_KAFKA_CONF_OK != rd_kafka_conf_set(conf, key.data(), val.data(), err, sizeof(err))) + throw php::exception(zend_ce_type_error + , (boost::format("Failed to set Kafka config: %s") % err).str() + , -1); + } + return conf; + } + + rd_kafka_headers_t* array2hdrs(const php::array& data) { + rd_kafka_headers_t *hdrs = nullptr; + if (!data.empty() && data.type_of(php::TYPE::ARRAY) && data.size() > 0) { + hdrs = rd_kafka_headers_new(data.size()); + for (auto i = data.begin(); i != data.end(); ++i) { + php::string key = i->first.to_string(), + val = i->second.to_string(); + rd_kafka_header_add(hdrs, + key.c_str(), key.size(), + val.c_str(), val.size()); + } + } + return hdrs; + } + php::array hdrs2array(rd_kafka_headers_t* hdrs) { + if (hdrs == nullptr) return php::array(0); + php::array header(rd_kafka_header_cnt(hdrs)); + const char *key; + const void *val; + std::size_t len; + for (std::size_t i = 0; rd_kafka_header_get_all(hdrs, i, &key, &val, &len) == RD_KAFKA_RESP_ERR_NO_ERROR; ++i) + header.set(key, php::string((const char *)val, len)); + return header; + } +} diff --git a/src/flame/kafka/kafka.h b/src/flame/kafka/kafka.h new file mode 100644 index 0000000..a9ad7a0 --- /dev/null +++ b/src/flame/kafka/kafka.h @@ -0,0 +1,13 @@ +#pragma once +#include "../../vendor.h" +#include + +namespace flame::kafka { + void declare(php::extension_entry &ext); + php::value consume(php::parameters& params); + php::value produce(php::parameters& params); + + rd_kafka_conf_t* array2conf(const php::array& data); + rd_kafka_headers_t* array2hdrs(const php::array& data); + php::array hdrs2array(rd_kafka_headers_t* hdrs); +} // namespace flame::kafka diff --git a/src/flame/kafka/message.cpp b/src/flame/kafka/message.cpp new file mode 100644 index 0000000..4d71b93 --- /dev/null +++ b/src/flame/kafka/message.cpp @@ -0,0 +1,66 @@ +#include "message.h" +#include "kafka.h" +#include "../time/time.h" + +namespace flame::kafka { + void message::declare(php::extension_entry& ext) { + php::class_entry class_message("flame\\kafka\\message"); + class_message + .implements(&php_json_serializable_ce) + .property({"topic", ""}) + .property({"partition", -1}) + .property({"key", ""}) + .property({"offset", -1}) + .property({"header", nullptr}) + .property({"payload", nullptr}) + .property({"timestamp", 0}) + .method<&message::__construct>("__construct", { + {"payload", php::TYPE::STRING, false, true}, + {"key", php::TYPE::STRING, false, true}, + }) + .method<&message::to_string>("__toString") + .method<&message::to_json>("jsonSerialize"); + ext.add(std::move(class_message)); + } + + message::~message() { + if (msg_) rd_kafka_message_destroy(msg_); + } + + void message::build_ex(rd_kafka_message_t* msg) { + // 用于单挑 message 的提交 + msg_ = msg; + // !!! 是否必须复制出 PHP 的内容? + set("topic", php::string(rd_kafka_topic_name(msg->rkt))); + set("partition", msg->partition); + set("key", php::string((const char*)msg->key, msg->key_len)); + set("offset", msg->offset); + rd_kafka_headers_t* hdrs; + if (rd_kafka_message_headers(msg, &hdrs) == RD_KAFKA_RESP_ERR_NO_ERROR) + set("header", hdrs2array(hdrs)); + // header + set("payload", php::string((const char*)msg->payload, msg->len)); + set("timestamp", rd_kafka_message_timestamp(msg, nullptr)); + } + + php::value message::__construct(php::parameters& params) { + if (params.size() > 0) set("payload", params[0].to_string()); + if (params.size() > 1) set("key", params[1].to_string()); + set("header", php::array(4)); + set("timestamp", + std::chrono::duration_cast(flame::time::now().time_since_epoch()).count()); + return nullptr; + } + + php::value message::to_json(php::parameters& params) { + php::array json(4); + json.set("topic", get("topic")); + json.set("key", get("key")); + json.set("payload",get("payload")); + return json; + } + + php::value message::to_string(php::parameters& params) { + return get("payload"); + } +} // namespace flame::kafka diff --git a/src/flame/kafka/message.h b/src/flame/kafka/message.h new file mode 100644 index 0000000..f2ba398 --- /dev/null +++ b/src/flame/kafka/message.h @@ -0,0 +1,22 @@ +#pragma once +#include "../../vendor.h" +#include "kafka.h" + +namespace flame::kafka { + + class _consumer; + class message: public php::class_base { + public: + static void declare(php::extension_entry& ext); + ~message(); + void build_ex(rd_kafka_message_t* msg); + php::value __construct(php::parameters& params); // 私有 + php::value to_json(php::parameters& params); + php::value to_string(php::parameters& params); + private: + rd_kafka_message_t *msg_ = nullptr; + + friend class _consumer; + // friend class _producer; + }; +} diff --git a/src/flame/kafka/producer.cpp b/src/flame/kafka/producer.cpp new file mode 100644 index 0000000..b141d25 --- /dev/null +++ b/src/flame/kafka/producer.cpp @@ -0,0 +1,63 @@ +#include "../coroutine.h" +#include "kafka.h" +#include "message.h" +#include "_producer.h" +#include "producer.h" + +namespace flame::kafka { + + void producer::declare(php::extension_entry &ext) { + php::class_entry class_producer("flame\\kafka\\producer"); + class_producer + .method<&producer::__construct>("__construct", {}, php::PRIVATE) + .method<&producer::__destruct>("__destruct") + .method<&producer::publish>("publish", { + {"topic", php::TYPE::STRING}, + }) + .method<&producer::flush>("flush"); + ext.add(std::move(class_producer)); + } + + php::value producer::__construct(php::parameters ¶ms) { + return nullptr; + } + + php::value producer::__destruct(php::parameters ¶ms) { + if (pd_) { + coroutine_handler ch {coroutine::current}; + pd_->close(ch); // 内部存在 flush 机制 + } + return nullptr; + } + + php::value producer::publish(php::parameters ¶ms) { + php::string topic = params[0].to_string(), key, payload; + php::array header; + if (params[1].instanceof (php::class_entry::entry())) { + php::object msg = params[1]; + key = msg.get("key").to_string(); + payload = msg.get("payload").to_string(); + header = msg.get("header"); + if (!header.type_of(php::TYPE::ARRAY)) header = php::array(0); + } + else { + payload = params[1].to_string(); + if (params.size() > 2) key = params[2].to_string(); + else key = php::string(0); + if (params.size() > 3) { + if (params[3].type_of(php::TYPE::ARRAY)) header = params[4]; + else header = php::array(0); + } + } + + coroutine_handler ch {coroutine::current}; + pd_->publish(topic, key, payload, header, ch); + return nullptr; + } + + php::value producer::flush(php::parameters ¶ms) { + coroutine_handler ch {coroutine::current}; + pd_->flush(ch); + return nullptr; + } +} // namespace flame::kafka diff --git a/src/flame/kafka/producer.h b/src/flame/kafka/producer.h new file mode 100644 index 0000000..3bdc269 --- /dev/null +++ b/src/flame/kafka/producer.h @@ -0,0 +1,20 @@ +#pragma once +#include "../../vendor.h" +#include "kafka.h" + +namespace flame::kafka { + + class _producer; + class producer : public php::class_base { + public: + static void declare(php::extension_entry &ext); + php::value __construct(php::parameters ¶ms); // 私有 + php::value __destruct(php::parameters ¶ms); + php::value publish(php::parameters ¶ms); + php::value flush(php::parameters ¶ms); + + private: + std::shared_ptr<_producer> pd_; + friend php::value produce(php::parameters ¶ms); + }; +} // namespace flame::kafka diff --git a/src/flame/log/log.cpp b/src/flame/log/log.cpp new file mode 100644 index 0000000..badc556 --- /dev/null +++ b/src/flame/log/log.cpp @@ -0,0 +1,66 @@ +#include "../coroutine.h" +#include "../worker.h" +#include "log.h" +#include "logger.h" + +namespace flame::log { + + static php::value write(php::parameters ¶ms) { + logger_->write_ex(logger::LEVEL_EMPTY, params); + return nullptr; + } + + static php::value trace(php::parameters ¶ms) { + logger_->write_ex(logger::LEVEL_TRACE, params); + return nullptr; + } + + static php::value debug(php::parameters ¶ms) { + logger_->write_ex(logger::LEVEL_DEBUG, params); + return nullptr; + } + + static php::value info(php::parameters ¶ms) { + logger_->write_ex(logger::LEVEL_INFO, params); + return nullptr; + } + + static php::value warning(php::parameters ¶ms) { + logger_->write_ex(logger::LEVEL_WARNING, params); + return nullptr; + } + + static php::value error(php::parameters ¶ms) { + logger_->write_ex(logger::LEVEL_ERROR, params); + return nullptr; + } + + static php::value fatal(php::parameters ¶ms) { + logger_->write_ex(logger::LEVEL_FATAL, params); + return nullptr; + } + + php::value connect(php::parameters& params) { + coroutine_handler ch {coroutine::current}; + php::object obj {php::class_entry::entry()}; + logger* ptr = static_cast(php::native(obj)); + ptr->connect(params[0], ch); + return obj; + } + + void declare(php::extension_entry &ext) { + ext + .function("flame\\log\\connect") + .function("flame\\log\\write") + .function("flame\\log\\trace") + .function("flame\\log\\debug") + .function("flame\\log\\info") + .function("flame\\log\\warn") + .function("flame\\log\\warning") + .function("flame\\log\\error") + .function("flame\\log\\fail") + .function("flame\\log\\fatal"); + + logger::declare(ext); + } +} diff --git a/src/flame/log/log.h b/src/flame/log/log.h new file mode 100644 index 0000000..0b69f90 --- /dev/null +++ b/src/flame/log/log.h @@ -0,0 +1,7 @@ +#pragma once +#include "../../vendor.h" + +namespace flame::log { + void declare(php::extension_entry &ext); + php::value connect(php::parameters& params); +} // namespace flame::log diff --git a/src/flame/log/logger.cpp b/src/flame/log/logger.cpp new file mode 100644 index 0000000..68d45d9 --- /dev/null +++ b/src/flame/log/logger.cpp @@ -0,0 +1,113 @@ +#include "../../worker_logger.h" +#include "log.h" +#include "logger.h" +#include "../worker.h" +#include "../time/time.h" +#include "../coroutine.h" + +namespace flame::log { + + std::array logger::LEVEL_STR = { + "TRACE", + "DEBUG", + "INFO", + "WARNING", + "ERROR", + "FATAL", + }; + int logger::LEVEL_OPT = logger::LEVEL_TRACE; + + logger* logger_ = nullptr; + + void logger::declare(php::extension_entry& ext) { + gcontroller->on_init([] (const php::array& options) { + if (options.exists("level")) logger::LEVEL_OPT = options.get("level").to_integer(); + else logger::LEVEL_OPT = logger::LEVEL_TRACE; + std::string output = ""; + if (options.exists("logger")) output = options.get("logger").to_string(); + logger_ = new logger(); + // 启动的一个更轻量的 C++ 内部协程 + ::coroutine::start(gcontroller->context_x.get_executor(), [output] (::coroutine_handler ch) { + logger_->connect(output, ch); + }); + }); + for(int i=0;i class_logger("flame\\log\\logger"); + class_logger + .method<&logger::__construct>("__construct", php::PRIVATE) // 禁止手动创建 + .method<&logger::write>("write") + .method<&logger::trace>("trace") + .method<&logger::debug>("debug") + .method<&logger::info>("info") + .method<&logger::warning>("warn") + .method<&logger::warning>("warning") + .method<&logger::error>("error") + .method<&logger::fatal>("fail") + .method<&logger::fatal>("fatal"); + ext.add(std::move(class_logger)); + } + + php::value logger::__construct(php::parameters& params) { + return nullptr; + } + + void logger::connect(const std::string& path, ::coroutine_handler& ch) { + lg_ = worker::get()->lm_connect(path, ch); + } + + std::ostream& logger::stream() { + return lg_ ? lg_->stream() : std::clog; // 由于 lg_ 的填充是异步的,须防止其还未初始化就被其他模块访问 + } + + void logger::write_ex(int lv, php::parameters& params) { + if (lv < LEVEL_OPT) return; + std::ostream& os = logger::stream(); + if (lv != LEVEL_EMPTY) { + os << '[' << time::iso() << "] ("; + os << LEVEL_STR[lv]; + os << ") "; + } + int i = 0; + for (; i < params.size() - 1; ++i) + os << params[i].ptr() << ' '; + os << params[i].ptr() << std::endl; // 使用 endl 会进行 flush 使日志快速出现 + } + + php::value logger::write(php::parameters& params) { + write_ex(LEVEL_EMPTY, params); + return nullptr; + } + + php::value logger::trace(php::parameters ¶ms) { + write_ex(LEVEL_TRACE, params); + return nullptr; + } + + php::value logger::debug(php::parameters ¶ms) { + write_ex(LEVEL_DEBUG, params); + return nullptr; + } + + php::value logger::info(php::parameters ¶ms) { + write_ex(LEVEL_INFO, params); + return nullptr; + } + + php::value logger::warning(php::parameters ¶ms) { + write_ex(LEVEL_WARNING, params); + return nullptr; + } + + php::value logger::error(php::parameters ¶ms) { + write_ex(LEVEL_ERROR, params); + return nullptr; + } + + php::value logger::fatal(php::parameters ¶ms) { + write_ex(LEVEL_FATAL, params); + return nullptr; + } +} diff --git a/src/flame/log/logger.h b/src/flame/log/logger.h new file mode 100644 index 0000000..8e55212 --- /dev/null +++ b/src/flame/log/logger.h @@ -0,0 +1,40 @@ +#pragma once +#include "../../vendor.h" + +class coroutine_handler; +class worker_logger; +namespace flame::log { + + class logger: public php::class_base { + public: + enum { + LEVEL_TRACE, + LEVEL_DEBUG, + LEVEL_INFO, + LEVEL_WARNING, + LEVEL_ERROR, + LEVEL_FATAL, + + LEVEL_EMPTY, + }; + static void declare(php::extension_entry& ext); + static std::array LEVEL_STR; + static int LEVEL_OPT; + php::value __construct(php::parameters& params); + // 使用父类型 coroutine_handler 引用能够兼容 C++ / PHP 协程 + void connect(const std::string& path, ::coroutine_handler& ch); + void write_ex(int lv, php::parameters& params); + std::ostream& stream(); + php::value write(php::parameters ¶ms); + php::value trace(php::parameters ¶ms); + php::value debug(php::parameters ¶ms); + php::value info(php::parameters ¶ms); + php::value warning(php::parameters ¶ms); + php::value error(php::parameters ¶ms); + php::value fatal(php::parameters ¶ms); + private: + std::shared_ptr lg_; + }; + // 默认日志记录器 + extern logger* logger_; +} \ No newline at end of file diff --git a/src/flame/master.cpp b/src/flame/master.cpp new file mode 100644 index 0000000..dafdd14 --- /dev/null +++ b/src/flame/master.cpp @@ -0,0 +1,155 @@ +#include "master.h" +#include "controller.h" +#include "../util.h" +#include "../master_logger.h" +#include "../master_process.h" + +namespace flame { + + std::shared_ptr master::mm_; + + void master::declare(php::extension_entry& ext) { + ext + .function("flame\\init") + .function("flame\\go") + .function("flame\\on") + .function("flame\\run"); + } + + php::value master::init(php::parameters& params) { + php::array options = php::array(0); + if (params.size() > 1 && params[1].type_of(php::TYPE::ARRAY)) options = params[1]; + if (options.exists("timeout")) + gcontroller->worker_quit = std::min(std::max(static_cast(options.get("timeout")), 200), 100000); + else + gcontroller->worker_quit = 3000; + + gcontroller->status |= controller::STATUS_INITIALIZED; + // 设置进程标题 + std::string title = params[0]; + php::callable("cli_set_process_title").call({title + " (php-flame/m)"}); + + // 主进程控制对象 + master::mm_.reset(new master()); + if (options.exists("logger")) { + php::string logger = options.get("logger"); + master::mm_->lg_ = master::mm_->lm_connect({logger.data(), logger.size()}); + } + else + master::mm_->lg_ = master::mm_->lm_connect(""); + // 初始化启动 + gcontroller->init(options); + master::mm_->ipc_start(); // IPC 启动 + master::mm_->sw_watch(); // 信号监听启动 + return nullptr; + } + + php::value master::run(php::parameters& params) { + if ((gcontroller->status & controller::STATUS_INITIALIZED) == 0) + throw php::exception(zend_ce_parse_error, "Failed to run flame: exception or missing 'flame\\init()' ?", -1); + + gcontroller->status |= controller::STATUS_RUN; + master::mm_->pm_start(gcontroller->context_x); // 启动工作进程 + + std::thread ts[2]; + ts[0] = std::thread([] () { + gcontroller->context_y.run(); + }); + ts[1] = std::thread([] () { + gcontroller->context_z.run(); + }); + gcontroller->context_x.run(); + master::mm_->sw_close(); + master::mm_->ipc_close(); + master::mm_->lm_close(); + + // if (gcontroller->status & controller::STATUS_EXCEPTION) _exit(-1); + gcontroller->stop(); + ts[0].join(); + ts[1].join(); + + master::mm_.reset(); + return nullptr; + } + + php::value master::dummy(php::parameters& params) { + return nullptr; + } + + master::master() + : master_process_manager(gcontroller->context_y, gcontroller->worker_size, gcontroller->worker_quit) + , signal_watcher(gcontroller->context_y) + , master_logger_manager(gcontroller->context_y) + , master_ipc(gcontroller->context_y) { + + } + + std::ostream& master::output() { + return lg_->stream(); + } + // !!! 此函数在工作线程中工作 + bool master::on_signal(int sig) { + switch(sig) { + break; + case SIGUSR2: + lm_reload(); // 日志重载 + break; + case SIGUSR1: + boost::asio::post(gcontroller->context_y.get_executor(), [this, self = shared_from_this()] () { + gcontroller->status ^= controller::STATUS_CLOSECONN; + gcontroller->status & controller::STATUS_CLOSECONN ? + (output() << "[" << util::system_time() << "] [INFO] append 'Connection: close' header." << std::endl) : + (output() << "[" << util::system_time() << "] [INFO] remove 'Connection: close' header." << std::endl) ; + + pm_kills(SIGUSR1); // 长短连切换 + }); + break; + case SIGINT: + case SIGQUIT: + coroutine::start(gcontroller->context_y.get_executor(), [this, self = shared_from_this()] (coroutine_handler ch) { // 使用轻量级的 C++ 内部协程 + if(gcontroller->status & (controller::STATUS_CLOSING | controller::STATUS_QUITING | controller::STATUS_RSETING)) return; // 关闭进行中 + gcontroller->status |= controller::STATUS_QUITING | controller::STATUS_CLOSECONN; + pm_close(ch, true); // 立即关闭 + }); + return false; // 除强制停止信号外,需要持续监听信号 + break; + case SIGTERM: + coroutine::start(gcontroller->context_y.get_executor(), [this, self = shared_from_this()] (coroutine_handler ch) { // 使用轻量级的 C++ 内部协程 + if(gcontroller->status & (controller::STATUS_CLOSING | controller::STATUS_QUITING | controller::STATUS_RSETING)) return; // 关闭进行中 + gcontroller->status |= controller::STATUS_CLOSING | controller::STATUS_CLOSECONN; + pm_close(ch); // 超时关闭 + }); + break; + default: + if(sig == SIGRTMIN + 1) + coroutine::start(gcontroller->context_y.get_executor(), [this, self = shared_from_this()] (coroutine_handler ch) { // 使用轻量级的 C++ 内部协程 + if(gcontroller->status & (controller::STATUS_CLOSING | controller::STATUS_QUITING | controller::STATUS_RSETING)) return; // 关闭进行中 + gcontroller->status |= controller::STATUS_RSETING | controller::STATUS_CLOSECONN; + pm_reset(ch); + gcontroller->status &= ~(controller::STATUS_RSETING | controller::STATUS_CLOSECONN); + }); + } + return true; + } + // !!! 此函数在工作线程中工作 + bool master::on_message(std::shared_ptr msg, socket_ptr sock) { + switch(msg->command) { + // 工作进程创建了新的日志对象,连接到指定的文件输出 + case ipc::COMMAND_LOGGER_CONNECT: + msg->xdata[0] = lm_connect({&msg->payload[0], msg->length})->index(); + msg->target = msg->source; // 响应给来源的工作进程 + msg->length = 0; + ipc_request(msg); + return true; + // 工作进程写入日志数据 + case ipc::COMMAND_LOGGER_DATA: + // 使用 xdata[0] 标识实际写入的目标日志 + lm_get(msg->xdata[0])->stream() << "~~~~~" << std::string_view {&msg->payload[0], msg->length}; + return true; + case ipc::COMMAND_LOGGER_DESTROY: + lm_destroy(msg->target); + return true; + } + return master_ipc::on_message(msg, sock); + } +} diff --git a/src/flame/master.h b/src/flame/master.h new file mode 100644 index 0000000..33afa68 --- /dev/null +++ b/src/flame/master.h @@ -0,0 +1,44 @@ +#pragma once +#include "../vendor.h" +#include "../master_process_manager.h" +#include "../master_logger_manager.h" +#include "../signal_watcher.h" +#include "../master_ipc.h" + +namespace flame { + class master: public std::enable_shared_from_this, public master_process_manager, public signal_watcher, public master_logger_manager, public master_ipc { + public: + static inline std::shared_ptr get() { + return mm_; + } + static void declare(php::extension_entry& ext); + static php::value init(php::parameters& params); + static php::value run(php::parameters& params); + static php::value dummy(php::parameters& params); + + master(); + std::ostream& output() override; + protected: + std::shared_ptr pm_self() override { + return std::static_pointer_cast(shared_from_this()); + } + virtual std::shared_ptr sw_self() override { + return std::static_pointer_cast(shared_from_this()); + } + virtual std::shared_ptr lm_self() override { + return std::static_pointer_cast(shared_from_this()); + } + virtual std::shared_ptr ipc_self() override { + return std::static_pointer_cast(shared_from_this()); + } + // void on_child_close(master_process* w, bool normal) override; + bool on_signal(int sig) override; + bool on_message(std::shared_ptr msg, socket_ptr sock) override; + + private: + static std::shared_ptr mm_; + master_logger* lg_; + + friend class controller; + }; +} \ No newline at end of file diff --git a/src/flame/mongodb/_connection_base.cpp b/src/flame/mongodb/_connection_base.cpp new file mode 100644 index 0000000..a453af9 --- /dev/null +++ b/src/flame/mongodb/_connection_base.cpp @@ -0,0 +1,98 @@ +#include "../controller.h" +#include "_connection_base.h" +#include "_connection_lock.h" +#include "mongodb.h" +#include "cursor.h" + +namespace flame::mongodb { + + void _connection_base::fake_deleter(bson_t *doc) {} + + php::value _connection_base::exec(std::shared_ptr conn + , php::array& pcmd, int type, coroutine_handler &ch + , std::shared_ptr session) { + + std::shared_ptr cmd = array2bson(pcmd); + // 此处不能使用 bson_new 创建 bson_t 否则会导致内存泄漏 + //(实际对 reply 的使用流程会重新初始化 reply 导致其值被至于 stack 上,导致此处 heap 内存泄漏) + bson_t reply, option = BSON_INITIALIZER; + std::shared_ptr rep(&reply, bson_destroy), opt(&option, bson_destroy); + std::shared_ptr err = std::make_shared(); + std::uint32_t server_id; + int rok = 0; + + boost::asio::post(gcontroller->context_y, [&session, conn, type, &cmd, &rep, &opt, &err, &rok, &ch] () { + if (!session) { + session.reset(mongoc_client_start_session(conn.get(), nullptr, nullptr) + , mongoc_client_session_destroy); + mongoc_client_session_append(session.get(), opt.get(), nullptr); + } + + mongoc_server_description_t* server; + switch(type) { + case COMMAND_READ: + server = mongoc_client_select_server(conn.get(), false, nullptr, nullptr); + break; + case COMMAND_RAW: + case COMMAND_WRITE: + case COMMAND_READ_WRITE: + default: + server = mongoc_client_select_server(conn.get(), true, nullptr, nullptr); + } + uint32_t server_id = mongoc_server_description_id(server); + mongoc_server_description_destroy(server); + bson_append_int32(opt.get(), "serverId", 8, server_id); + switch(type) { + case COMMAND_READ: + rok = mongoc_client_read_command_with_opts(conn.get() + , mongoc_uri_get_database(mongoc_client_get_uri(conn.get())), cmd.get(), nullptr, opt.get(), rep.get(), err.get()); + break; + case COMMAND_WRITE: + rok = mongoc_client_write_command_with_opts(conn.get() + , mongoc_uri_get_database(mongoc_client_get_uri(conn.get())), cmd.get(), opt.get(), rep.get(), err.get()); + break; + case COMMAND_READ_WRITE: + rok = mongoc_client_read_write_command_with_opts(conn.get() + , mongoc_uri_get_database(mongoc_client_get_uri(conn.get())), cmd.get(), nullptr, opt.get(), rep.get(), err.get()); + break; + case COMMAND_RAW: + default: + rok = mongoc_client_command_with_opts(conn.get() + , mongoc_uri_get_database(mongoc_client_get_uri(conn.get())), cmd.get(), nullptr, opt.get(), rep.get(), err.get()); + } + ch.resume(); + }); + ch.suspend(); + if (!rok) throw php::exception(zend_ce_exception + , (boost::format("Failed to execute MongoDB command: %s") % err->message).str() + , err->code); + else if (bson_has_field(rep.get(), "cursor")) { + php::object obj{php::class_entry::entry()}; + auto ptr = static_cast(php::native(obj)); + ptr->cl_.reset(new _connection_lock(conn)); + ptr->ss_ = session; + ptr->cs_.reset( + mongoc_cursor_new_from_command_reply_with_opts(conn.get(), rep.get(), opt.get()), + mongoc_cursor_destroy); + // cursor 实际会窃取 rep 对应的 bson_t 结构; + // 按文档说会被上面函数 "销毁" + // 参考: http://mongoc.org/libmongoc/current/mongoc_cursor_new_from_command_reply_with_opts.html + *std::get_deleter(rep) = fake_deleter; + return std::move(obj); + } + else { + php::array r(4); + + bson_iter_t i; + bson_oid_t oid; + bson_iter_init(&i, rep.get()); + while (bson_iter_next(&i)) { + const char* key = bson_iter_key(&i); + std::uint32_t len = bson_iter_key_len(&i); + if (len > 0 && key[0] == '$') continue; + r.set(php::string(key, len), iter2value(&i)); + } + return std::move(r); + } + } +} // namespace flame::mongodb diff --git a/src/flame/mongodb/_connection_base.h b/src/flame/mongodb/_connection_base.h new file mode 100644 index 0000000..28488da --- /dev/null +++ b/src/flame/mongodb/_connection_base.h @@ -0,0 +1,23 @@ +#pragma once +#include "../../vendor.h" +#include "../coroutine.h" +#include "mongodb.h" + +namespace flame::mongodb { + + class _connection_base { + public: + enum command_type_t { + COMMAND_RAW, + COMMAND_WRITE, + COMMAND_READ, + COMMAND_READ_WRITE, + }; + virtual std::shared_ptr acquire(coroutine_handler& ch) = 0; + php::value exec(std::shared_ptr conn + , php::array& cmd, int type, coroutine_handler& ch + , std::shared_ptr session = nullptr); + + static void fake_deleter(bson_t *doc); + }; +} diff --git a/src/flame/mongodb/_connection_lock.cpp b/src/flame/mongodb/_connection_lock.cpp new file mode 100644 index 0000000..88fada7 --- /dev/null +++ b/src/flame/mongodb/_connection_lock.cpp @@ -0,0 +1,34 @@ +#include "../controller.h" +#include "_connection_lock.h" +#include "mongodb.h" + +namespace flame::mongodb { + _connection_lock::_connection_lock(std::shared_ptr c) + : conn_(c) + , guard_(gcontroller->context_y) { + } + + _connection_lock::~_connection_lock() { + } + + std::shared_ptr _connection_lock::acquire(coroutine_handler &ch) { + return conn_; + } + + php::array _connection_lock::fetch(std::shared_ptr cs, coroutine_handler &ch) { + bool has = false; + auto err = std::make_shared(); + const bson_t *doc; + boost::asio::post(guard_, [this, &cs, &ch, &has, &err, &doc]() { + if (!mongoc_cursor_next(cs.get(), &doc)) has = mongoc_cursor_error(cs.get(), err.get()); + ch.resume(); + }); + ch.suspend(); + // 发生了错误 + if (has) throw php::exception(zend_ce_exception + , (boost::format("Failed to fetch document: %s") % err->message).str() + , err->code); + // 文档 doc 仅为 "引用" 流程 + return bson2array(const_cast(doc)); + } +} // namespace flame::mongodb diff --git a/src/flame/mongodb/_connection_lock.h b/src/flame/mongodb/_connection_lock.h new file mode 100644 index 0000000..a10db40 --- /dev/null +++ b/src/flame/mongodb/_connection_lock.h @@ -0,0 +1,18 @@ +#pragma once +#include "../../vendor.h" +#include "mongodb.h" +#include "_connection_base.h" + +namespace flame::mongodb { + + class _connection_lock : public _connection_base, public std::enable_shared_from_this<_connection_lock> { + public: + _connection_lock(std::shared_ptr c); + ~_connection_lock(); + std::shared_ptr acquire(coroutine_handler &ch) override; + php::array fetch(std::shared_ptr cs, coroutine_handler &ch); + private: + boost::asio::io_context::strand guard_; // 防止对 cursor 的并行访问 + std::shared_ptr conn_; + }; +} // namespace flame::mongodb diff --git a/src/flame/mongodb/_connection_pool.cpp b/src/flame/mongodb/_connection_pool.cpp new file mode 100644 index 0000000..922af34 --- /dev/null +++ b/src/flame/mongodb/_connection_pool.cpp @@ -0,0 +1,52 @@ +#include "../controller.h" +#include "mongodb.h" +#include "_connection_pool.h" + +namespace flame::mongodb { + + _connection_pool::_connection_pool(const std::string& url) + : guard_(gcontroller->context_y) { + std::unique_ptr uri(mongoc_uri_new(url.c_str()), mongoc_uri_destroy); + const bson_t* options = mongoc_uri_get_options(uri.get()); + if (!bson_has_field(options, MONGOC_URI_READPREFERENCE)) { + mongoc_read_prefs_t* pref = mongoc_read_prefs_new(MONGOC_READ_SECONDARY_PREFERRED); // secondaryPreferred + mongoc_uri_set_read_prefs_t(uri.get(), pref); + mongoc_read_prefs_destroy(pref); + } + mongoc_uri_set_option_as_int32(uri.get(), MONGOC_URI_CONNECTTIMEOUTMS, 5000); + mongoc_uri_set_option_as_int32(uri.get(), MONGOC_URI_MAXPOOLSIZE, 6); + + p_ = mongoc_client_pool_new(uri.get()); + } + + _connection_pool::~_connection_pool() { + mongoc_client_pool_destroy(p_); + } + + std::shared_ptr _connection_pool::acquire(coroutine_handler &ch) { + std::shared_ptr conn; + auto self = shared_from_this(); + boost::asio::post(guard_, [this, self, &conn, &ch] () { + await_.push_back([this, self, &conn, &ch] (mongoc_client_t* c) { + // 对应释放(归还)连接过程, 须持有当前对象的引用 self (否则当前对象可能先于连接释放被销毁) + conn.reset(c, [this, self] (mongoc_client_t* c) { + boost::asio::post(guard_, std::bind(&_connection_pool::release, self, c)); + }); + ch.resume(); + }); + mongoc_client_t* c = mongoc_client_pool_try_pop(p_); + if (c) release(c); + }); + ch.suspend(); + return conn; + } + + void _connection_pool::release(mongoc_client_t* c) { + if (await_.empty()) mongoc_client_pool_push(p_, c); + else { + auto cb = await_.front(); + await_.pop_front(); + cb(c); + } + } +} diff --git a/src/flame/mongodb/_connection_pool.h b/src/flame/mongodb/_connection_pool.h new file mode 100644 index 0000000..89c1978 --- /dev/null +++ b/src/flame/mongodb/_connection_pool.h @@ -0,0 +1,19 @@ +#pragma once +#include "../../vendor.h" +#include "mongodb.h" +#include "_connection_base.h" + +namespace flame::mongodb { + + class _connection_pool: public _connection_base, public std::enable_shared_from_this<_connection_pool> { + public: + _connection_pool(const std::string& url); + ~_connection_pool(); + std::shared_ptr acquire(coroutine_handler &ch) override; + void release(mongoc_client_t* c); + private: + mongoc_client_pool_t* p_; + boost::asio::io_context::strand guard_; // 防止下面队列并行访问 + std::list> await_; + }; +} diff --git a/src/flame/mongodb/client.cpp b/src/flame/mongodb/client.cpp new file mode 100644 index 0000000..63532ae --- /dev/null +++ b/src/flame/mongodb/client.cpp @@ -0,0 +1,67 @@ +#include "../coroutine.h" +#include "mongodb.h" +#include "_connection_pool.h" +#include "client.h" +#include "collection.h" + +namespace flame::mongodb { + + void client::declare(php::extension_entry &ext) { + php::class_entry class_client("flame\\mongodb\\client"); + class_client + .constant({"COMMAND_RAW", _connection_base::COMMAND_RAW}) + .constant({"COMMAND_READ", _connection_base::COMMAND_READ}) + .constant({"COMMAND_READ_WRITE", _connection_base::COMMAND_READ_WRITE}) + .constant({"COMMAND_WRITE", _connection_base::COMMAND_WRITE}) + .method<&client::__construct>("__construct", {}, php::PRIVATE) + .method<&client::dump>("dump", { + {"data", php::TYPE::ARRAY}, + }) + .method<&client::execute>("execute", { + {"command", php::TYPE::ARRAY}, + {"write", php::TYPE::INTEGER, false, true}, + }) + .method<&client::__get>("collection", { + {"name", php::TYPE::STRING} + }) + .method<&client::__get>("__get", { + {"name", php::TYPE::STRING} + }) + .method<&client::__isset>("__isset", { + {"name", php::TYPE::STRING} + }); + ext.add(std::move(class_client)); + } + + php::value client::__construct(php::parameters ¶ms) { + return nullptr; + } + + php::value client::dump(php::parameters& params) { + std::clog << bson_as_relaxed_extended_json(array2bson(params[0]).get(), nullptr) << std::endl; + return nullptr; + } + + php::value client::execute(php::parameters ¶ms) { + int write = _connection_base::COMMAND_RAW; + if (params.size() > 1) write = params[1].to_integer(); + coroutine_handler ch {coroutine::current}; + auto conn_ = cp_->acquire(ch); + + php::array cmd = params[0]; + return cp_->exec(conn_, cmd, write, ch); + } + + php::value client::__get(php::parameters ¶ms) { + php::object obj(php::class_entry::entry()); + collection *ptr = static_cast(php::native(obj)); + ptr->cp_ = cp_; + ptr->name_ = params[0]; + obj.set("name", params[0]); + return std::move(obj); + } + + php::value client::__isset(php::parameters ¶ms) { + return true; + } +} // namespace flame::mongodb diff --git a/src/flame/mongodb/client.h b/src/flame/mongodb/client.h new file mode 100644 index 0000000..4dd9693 --- /dev/null +++ b/src/flame/mongodb/client.h @@ -0,0 +1,20 @@ +#pragma once +#include "../../vendor.h" +#include "mongodb.h" + +namespace flame::mongodb { + + class _connection_pool; + class client : public php::class_base { + public: + static void declare(php::extension_entry &ext); + php::value __construct(php::parameters ¶ms); + php::value dump(php::parameters ¶ms); + php::value execute(php::parameters ¶ms); + php::value __get(php::parameters ¶ms); + php::value __isset(php::parameters ¶ms); + private: + std::shared_ptr<_connection_pool> cp_; + friend php::value connect(php::parameters ¶ms); + }; +} // namespace flame::mongodb diff --git a/src/flame/mongodb/collection.cpp b/src/flame/mongodb/collection.cpp new file mode 100644 index 0000000..4d75a92 --- /dev/null +++ b/src/flame/mongodb/collection.cpp @@ -0,0 +1,332 @@ +#include "../coroutine.h" +#include "_connection_pool.h" +#include "collection.h" +#include "cursor.h" + +namespace flame::mongodb { + void collection::declare(php::extension_entry &ext) { + php::class_entry class_collection("flame\\mongodb\\collection"); + class_collection + .property({"name", ""}) + .method<&collection::__construct>("__construct", {}, php::PRIVATE) + .method<&collection::insert>("insert",{ + {"data", php::TYPE::ARRAY}, + {"ordered", php::TYPE::BOOLEAN, false, true} // true + }) + .method<&collection::insert>("insert_many", { + {"data", php::TYPE::ARRAY}, + {"ordered", php::TYPE::BOOLEAN, false, true} // true + }) + .method<&collection::insert>("insert_one", { + {"data", php::TYPE::ARRAY}, + }) + .method<&collection::delete_>("delete", { + {"query", php::TYPE::ARRAY}, + {"limit", php::TYPE::INTEGER, false, true}, // 0 + }) + .method<&collection::delete_>("delete_many", { + {"query", php::TYPE::ARRAY}, + {"limit", php::TYPE::INTEGER, false, true}, // 0 + }) + .method<&collection::delete_>("delete_one", { + {"query", php::TYPE::ARRAY}, + }) + .method<&collection::update>("update", { + {"query", php::TYPE::ARRAY}, + {"update", php::TYPE::ARRAY}, + {"upsert", php::TYPE::BOOLEAN, false, true}, // false + }) + .method<&collection::update>("update_many", { + {"query", php::TYPE::ARRAY}, + {"update", php::TYPE::ARRAY}, + {"upsert", php::TYPE::BOOLEAN, false, true}, // false + }) + .method<&collection::update_one>("update_one", { + {"query", php::TYPE::ARRAY}, + {"update", php::TYPE::ARRAY}, + {"upsert", php::TYPE::BOOLEAN, false, true}, // false + }) + .method<&collection::find>("find", { + {"filter", php::TYPE::ARRAY}, + {"projection", php::TYPE::ARRAY, false, true}, + {"sort", php::TYPE::ARRAY, false, true}, + {"limit", php::TYPE::UNDEFINED, false, true}, + }) + .method<&collection::find_one>("one", { + {"filter", php::TYPE::ARRAY}, + {"sort", php::TYPE::ARRAY, false, true}, + }) + .method<&collection::find_one>("find_one", { // 兼容原 API 名称定义 + {"filter", php::TYPE::ARRAY}, + {"sort", php::TYPE::ARRAY, false, true}, + }) + .method<&collection::get>("get", { + {"filter", php::TYPE::ARRAY}, + {"field", php::TYPE::STRING}, + {"sort", php::TYPE::ARRAY, false, true}, + }) + .method<&collection::count>("count", { + {"query", php::TYPE::ARRAY}, + }) + .method<&collection::aggregate>("aggregate", { + {"pipeline", php::TYPE::ARRAY}, + }) + .method<&collection::find_and_delete>("find_and_delete", { + {"query", php::TYPE::ARRAY}, + {"sort", php::TYPE::ARRAY, false, true}, + {"upsert", php::TYPE::BOOLEAN, false, true}, + {"fields", php::TYPE::ARRAY, false, true}, + }) + .method<&collection::find_and_update>("find_and_update", { + {"query", php::TYPE::ARRAY}, + {"update", php::TYPE::ARRAY}, + {"sort", php::TYPE::ARRAY, false, true}, + {"upsert", php::TYPE::BOOLEAN, false, true}, + {"fields", php::TYPE::ARRAY, false, true}, + {"new", php::TYPE::BOOLEAN, false, true}, + }); + ext.add(std::move(class_collection)); + } + + php::value collection::__construct(php::parameters ¶ms) { // 私有 + return nullptr; + } + + php::value collection::insert(php::parameters ¶ms) { + php::array cmd(8); + cmd.set("insert", name_); + php::array docs = params[0]; + if (!docs.exists(0)) { // 单项插入的情况 + docs = php::array(2); + docs.set(0, params[0]); + } + cmd.set("documents", docs); + if (params.size() > 1) cmd.set("ordered", params[1].to_boolean()); + + coroutine_handler ch {coroutine::current}; + auto conn_ = cp_->acquire(ch); + return cp_->exec(conn_, cmd, _connection_base::COMMAND_WRITE, ch); + } + + php::value collection::delete_(php::parameters ¶ms) { + php::array cmd(8); + cmd.set("delete", name_); + php::array deletes(2), deleteo(2); + deleteo.set("q", params[0]); + if (params.size() > 1 && params[1].type_of(php::TYPE::INTEGER)) + deleteo.set("limit", params[1].to_integer()); + else deleteo.set("limit", 0); + + deletes.set(0, deleteo); + cmd.set("deletes", deletes); + coroutine_handler ch{coroutine::current}; + auto conn_ = cp_->acquire(ch); + return cp_->exec(conn_, cmd, _connection_base::COMMAND_WRITE, ch); + } + + php::value collection::delete_one(php::parameters& params) { + return call("delete", {params[0], 1}); + } + + php::value collection::update(php::parameters ¶ms) { + php::array cmd(8); + cmd.set("update", name_); + php::array updates(2), updateo(4); + updateo.set("q", params[0]); + updateo.set("u", params[1]); + if (params.size() > 2) + updateo.set("upsert", params[2].to_boolean()); + + updateo.set("multi", true); + updates.set(0, updateo); + cmd.set("updates", updates); + coroutine_handler ch{coroutine::current}; + auto conn_ = cp_->acquire(ch); + return cp_->exec(conn_, cmd, _connection_base::COMMAND_WRITE, ch); + } + + php::value collection::update_one(php::parameters ¶ms) { + php::array cmd(8); + cmd.set("update", name_); + php::array updates(2), updateo(4); + updateo.set("q", params[0]); + updateo.set("u", params[1]); + if (params.size() > 2) + updateo.set("upsert", params[2].to_boolean()); + + // updateo.set("multi", false); + updates.set(0, updateo); + cmd.set("updates", updates); + coroutine_handler ch{coroutine::current}; + auto conn_ = cp_->acquire(ch); + return cp_->exec(conn_, cmd, _connection_base::COMMAND_WRITE, ch); + } + + php::value collection::find(php::parameters ¶ms) { + php::array cmd(8); + cmd.set("find", name_); + if(params[0].empty()) cmd.set("filter", php::object(php::CLASS(zend_standard_class_def))); + else cmd.set("filter", params[0]); + + if (params.size() > 1) { + php::array project; + if (params[1].type_of(php::TYPE::ARRAY)) { + php::array fields = params[1]; + if (fields.exists(0)) { // 将纯字段列表形式转换为K/V形式 + project = php::array(8); + for (auto i = fields.begin(); i != fields.end(); ++i) + project.set(php::string(i->second), 1); + } + else project = fields; + } + else if (params[1].type_of(php::TYPE::STRING)) { // 单个字段 + php::array project(2); + project.set(php::string(params[1]), 1); + } + else goto PROJECTION_ALL; + + if (!project.exists("_id")) project.set("_id", 0); + cmd.set("projection", project); + } + PROJECTION_ALL: + if (params.size() > 2 && params[2].type_of(php::TYPE::ARRAY)) + cmd.set("sort", params[2]); + if (params.size() > 3) { + if (params[3].type_of(php::TYPE::INTEGER)) + cmd.set("limit", params[3]); + else if (params[3].type_of(php::TYPE::ARRAY)) { + php::array limits = params[3]; + if (limits.size() > 0) cmd.set("skip", limits[0].to_integer()); + if (limits.size() > 1) cmd.set("limit", limits[1].to_integer()); + } + } + coroutine_handler ch {coroutine::current}; + auto conn_ = cp_->acquire(ch); + return cp_->exec(conn_, cmd, _connection_base::COMMAND_READ, ch); + } + + php::value collection::find_one(php::parameters ¶ms) { + php::array cmd(8); + cmd.set("find", name_); + if(params[0].empty()) cmd.set("filter", php::object(php::CLASS(zend_standard_class_def))); + else cmd.set("filter", params[0]); + + if (params.size() > 1 && params[1].type_of(php::TYPE::ARRAY)) + cmd.set("sort", params[1]); + + cmd.set("limit", 1); + coroutine_handler ch{coroutine::current}; + auto conn_ = cp_->acquire(ch); + php::object cs = cp_->exec(conn_, cmd, _connection_base::COMMAND_READ, ch); + assert(cs.instanceof(php::class_entry::entry())); + return cs.call("fetch_row"); + } + + php::value collection::get(php::parameters ¶ms) { + php::array cmd(8); + cmd.set("find", name_); + php::array project(2); + php::string field = params[1]; + project.set(field, 1); + cmd.set("projection", project); + if (params[0].empty()) cmd.set("filter", php::object(php::CLASS(zend_standard_class_def))); + else cmd.set("filter", params[0]); + if (params.size() > 2 && params[2].type_of(php::TYPE::ARRAY)) + cmd.set("sort", params[2]); + cmd.set("limit", 1); + + coroutine_handler ch{coroutine::current}; + auto conn_ = cp_->acquire(ch); + php::object cs = cp_->exec(conn_, cmd, _connection_base::COMMAND_READ, ch); + assert(cs.instanceof (php::class_entry::entry())); + php::array row = cs.call("fetch_row"); + if (row.empty()) return nullptr; + else return row.get(field); + } + + php::value collection::count(php::parameters ¶ms) { + php::array cmd(8); + cmd.set("count", name_); + if (params[0].empty()) cmd.set("query", php::object(php::CLASS(zend_standard_class_def))); + cmd.set("query", params[0]); + if (params.size() > 1) { + if (params[1].type_of(php::TYPE::INTEGER)) cmd.set("limit", params[1]); + else if (params[1].type_of(php::TYPE::ARRAY)) { + php::array limits = params[1]; + if (limits.size() > 0) cmd.set("skip", limits[0].to_integer()); + if (limits.size() > 1) cmd.set("limit", limits[1].to_integer()); + } + } + if (params.size() > 1 && params[1].type_of(php::TYPE::INTEGER)) cmd.set("limit", params[1]); + coroutine_handler ch{coroutine::current}; + auto conn_ = cp_->acquire(ch); + php::array cs = cp_->exec(conn_, cmd, _connection_base::COMMAND_READ, ch); + return cs.get("n"); + } + + php::value collection::aggregate(php::parameters ¶ms) { + php::array cmd(8); + cmd.set("aggregate", name_); + php::array pipeline = params[0]; + if (!pipeline.exists(0)) throw php::exception(zend_ce_type_error + , "Failed to aggregate: 'pipeline' typeof array required" + , -1); + cmd.set("pipeline", pipeline); + cmd.set("cursor", php::object(php::CLASS(zend_standard_class_def))); + if (params.size() > 1) { + php::array options = params[1]; + for(auto i=options.begin(); i!=options.end(); ++i) + cmd.set(i->first.to_string(), i->second); + } + int execute_type = _connection_base::COMMAND_READ; + if (params.size() > 2) + execute_type = params[2].to_integer(); + + coroutine_handler ch {coroutine::current}; + auto conn_ = cp_->acquire(ch); + return cp_->exec(conn_, cmd, execute_type, ch); + } + + php::value collection::find_and_delete(php::parameters& params) { + php::array cmd(8); + if (params[0].empty()) cmd.set("query", php::object(php::CLASS(zend_standard_class_def))); + else cmd.set("query", params[0]); + cmd.set("remove", true); + if (params.size() > 1) { + php::value f = params[1]; + if (f.type_of(php::TYPE::ARRAY)) cmd.set("sort", f); + } + if (params.size() > 2) cmd.set("upsert", params[2].to_boolean()); + if (params.size() > 3) { + php::value f = params[3]; + if (f.type_of(php::TYPE::ARRAY)) cmd.set("fields", f); + } + coroutine_handler ch {coroutine::current}; + auto conn_ = cp_->acquire(ch); + return cp_->exec(conn_, cmd, _connection_base::COMMAND_READ_WRITE, ch); + } + + php::value collection::find_and_update(php::parameters& params) { + php::array cmd(8); + if (params[0].empty()) cmd.set("query", php::object(php::CLASS(zend_standard_class_def))); + else cmd.set("query", params[0]); + if (params.size() > 1) { + php::value f = params[1]; + if (f.type_of(php::TYPE::ARRAY)) cmd.set("update", f); + } + if (params.size() > 2) { + php::value f = params[2]; + if (f.type_of(php::TYPE::ARRAY)) cmd.set("sort", f); + } + if (params.size() > 3) cmd.set("upsert", params[3].to_boolean()); + if (params.size() > 4) { + php::value f = params[4]; + if (f.type_of(php::TYPE::ARRAY)) cmd.set("fields", f); + } + if (params.size() > 5) cmd.set("new", params[5].to_boolean()); + + coroutine_handler ch {coroutine::current}; + auto conn_ = cp_->acquire(ch); + return cp_->exec(conn_, cmd, _connection_base::COMMAND_READ_WRITE, ch); + } +} // namespace flame::mongodb diff --git a/src/flame/mongodb/collection.h b/src/flame/mongodb/collection.h new file mode 100644 index 0000000..916c291 --- /dev/null +++ b/src/flame/mongodb/collection.h @@ -0,0 +1,29 @@ +#pragma once +#include "../../vendor.h" +#include "mongodb.h" + +namespace flame::mongodb { + + class _connection_pool; + class collection : public php::class_base { + public: + static void declare(php::extension_entry &ext); + php::value __construct(php::parameters ¶ms); + php::value insert(php::parameters ¶ms); + php::value delete_(php::parameters ¶ms); + php::value delete_one(php::parameters& params); + php::value update(php::parameters ¶ms); + php::value update_one(php::parameters& params); + php::value find(php::parameters ¶ms); + php::value find_one(php::parameters ¶ms); + php::value get(php::parameters ¶ms); + php::value count(php::parameters ¶ms); + php::value aggregate(php::parameters ¶ms); + php::value find_and_update(php::parameters& params); + php::value find_and_delete(php::parameters& params); + private: + std::shared_ptr<_connection_pool> cp_; + php::string name_; + friend class client; + }; +} // namespace flame::mongodb diff --git a/src/flame/mongodb/cursor.cpp b/src/flame/mongodb/cursor.cpp new file mode 100644 index 0000000..b57de08 --- /dev/null +++ b/src/flame/mongodb/cursor.cpp @@ -0,0 +1,46 @@ +#include "../coroutine.h" +#include "cursor.h" +#include "_connection_lock.h" + +namespace flame::mongodb { + + void cursor::declare(php::extension_entry &ext) { + php::class_entry class_cursor("flame\\mongodb\\cursor"); + class_cursor + .method<&cursor::__construct>("__construct", {}, php::PRIVATE) + .method<&cursor::__destruct>("__destruct") + .method<&cursor::fetch_row>("fetch_row") + .method<&cursor::fetch_all>("fetch_all"); + ext.add(std::move(class_cursor)); + } + + php::value cursor::fetch_row(php::parameters ¶ms) { + php::array row(nullptr); + if (cs_ && cl_) { + coroutine_handler ch{coroutine::current}; + row = cl_->fetch(cs_, ch); + } + if (row.type_of(php::TYPE::NULLABLE)) { + // 尽早释放连接 (注意顺序) + cs_.reset(); + ss_.reset(); + cl_.reset(); + } + return row; + } + + php::value cursor::fetch_all(php::parameters ¶ms) { + if (cs_ && cl_) { + coroutine_handler ch{coroutine::current}; + php::array data(8); + for(php::array row = cl_->fetch(cs_, ch); !row.type_of(php::TYPE::NULLABLE); row = cl_->fetch(cs_, ch)) + data.set(data.size(), row); + // 尽早释放连接 (注意顺序) + cs_.reset(); + ss_.reset(); + cl_.reset(); + return data; + } + else return nullptr; + } +} // namespace flame::mongodb diff --git a/src/flame/mongodb/cursor.h b/src/flame/mongodb/cursor.h new file mode 100644 index 0000000..d7a995c --- /dev/null +++ b/src/flame/mongodb/cursor.h @@ -0,0 +1,25 @@ +#pragma once +#include "../../vendor.h" +#include "mongodb.h" + +namespace flame::mongodb { + class _connection_lock; + class cursor: public php::class_base { + public: + static void declare(php::extension_entry& ext); + php::value __construct(php::parameters& params) { // 私有 + return nullptr; + } + php::value __destruct(php::parameters& params) { + return nullptr; + } + php::value fetch_row(php::parameters& params); + php::value fetch_all(php::parameters& params); + private: + // 须先销毁 指针 后归还 连接 + std::shared_ptr<_connection_lock> cl_; + std::shared_ptr ss_; + std::shared_ptr cs_; + friend class _connection_base; + }; +} // namespace flame::mongodb diff --git a/src/flame/mongodb/date_time.cpp b/src/flame/mongodb/date_time.cpp new file mode 100644 index 0000000..25e35bc --- /dev/null +++ b/src/flame/mongodb/date_time.cpp @@ -0,0 +1,58 @@ +#include "date_time.h" +#include "../time/time.h" + +namespace flame::mongodb { + + void date_time::declare(php::extension_entry& ext) { + php::class_entry class_date_time("flame\\mongodb\\date_time"); + class_date_time + .implements(&php_json_serializable_ce) + .method<&date_time::__construct>("__construct", { + {"milliseconds", php::TYPE::INTEGER, false, true} + }) + .method<&date_time::to_string>("__toString") + .method<&date_time::to_json>("__toJSON") + .method<&date_time::to_datetime>("__toDateTime") + .method<&date_time::unix>("unix") + .method<&date_time::unix_ms>("unix_ms") + .method<&date_time::to_json>("jsonSerialize") + .method<&date_time::to_json>("__debugInfo") + .method<&date_time::iso>("iso"); + ext.add(std::move(class_date_time)); + } + + php::value date_time::__construct(php::parameters& params) { + if (params.size() > 0) tm_ = params[0].to_integer(); + else // 默认以当前时间建立 + tm_ = std::chrono::duration_cast(time::now().time_since_epoch()).count(); + return nullptr; + } + + php::value date_time::to_string(php::parameters& params) { + return std::to_string(tm_); + } + + php::value date_time::unix(php::parameters& params) { + return std::int64_t(tm_ / 1000); + } + + php::value date_time::unix_ms(php::parameters& params) { + return tm_; + } + + php::value date_time::to_datetime(php::parameters& params) { + return php::datetime(tm_); + } + + php::value date_time::to_json(php::parameters& params) { + // 自定义 JSON 形式 (保持和官方 MongoDB 驱动形式一致) + php::array ret(1), num(1); + num.set("$numberLong", std::to_string(tm_)); + ret.set("$date", num); + return std::move(ret); + } + + php::value date_time::iso(php::parameters& params) { + return time::iso(std::chrono::system_clock::time_point( std::chrono::milliseconds(tm_) )); + } +} diff --git a/src/flame/mongodb/date_time.h b/src/flame/mongodb/date_time.h new file mode 100644 index 0000000..5b75005 --- /dev/null +++ b/src/flame/mongodb/date_time.h @@ -0,0 +1,22 @@ +#pragma once +#include "../../vendor.h" +#include "mongodb.h" + +namespace flame::mongodb { + + class date_time: public php::class_base { + public: + static void declare(php::extension_entry& ext); + php::value __construct(php::parameters& params); + php::value to_string(php::parameters& params); + php::value unix(php::parameters& params); + php::value unix_ms(php::parameters& params); + php::value to_datetime(php::parameters& params); + php::value to_json(php::parameters& params); + php::value iso(php::parameters& params); + private: + std::int64_t tm_; + friend php::value iter2value(bson_iter_t *i); + friend std::shared_ptr array2bson(const php::array &v); + }; +} diff --git a/src/flame/mongodb/mongodb.cpp b/src/flame/mongodb/mongodb.cpp new file mode 100644 index 0000000..462679c --- /dev/null +++ b/src/flame/mongodb/mongodb.cpp @@ -0,0 +1,159 @@ +#include "../coroutine.h" +#include "mongodb.h" +#include "_connection_pool.h" +#include "client.h" +#include "cursor.h" +#include "collection.h" +#include "object_id.h" +#include "date_time.h" + +namespace flame::mongodb { + + void declare(php::extension_entry &ext) { + gcontroller->on_init([] (const php::array& options) { + mongoc_init(); + })->on_stop([]() { + mongoc_cleanup(); + }); + ext + .function("flame\\mongodb\\connect"); + client::declare(ext); + object_id::declare(ext); + date_time::declare(ext); + cursor::declare(ext); + collection::declare(ext); + } + + php::value connect(php::parameters ¶ms) { + php::object obj(php::class_entry::entry()); + client *ptr = static_cast(php::native(obj)); + std::string url = params[0]; + ptr->cp_.reset(new _connection_pool(url)); + + // TODO 优化: 实际确认建立第一个连接? + return std::move(obj); + } + + php::value iter2value(bson_iter_t *i) { + switch (bson_iter_type(i)) { + case BSON_TYPE_DOUBLE: + return bson_iter_double(i); + case BSON_TYPE_UTF8: { + std::uint32_t size = 0; + const char *data = bson_iter_utf8(i, &size); + return php::string(data, size); + } + case BSON_TYPE_DOCUMENT: + case BSON_TYPE_ARRAY: { + bson_iter_t j; + bson_iter_recurse(i, &j); + php::array a(4); + while (bson_iter_next(&j)) a.set(bson_iter_key(&j), iter2value(&j)); + return std::move(a); + } + case BSON_TYPE_BINARY: { + std::uint32_t size = 0; + const unsigned char *data; + bson_iter_binary(i, nullptr, &size, &data); + return php::string((const char *)data, size); + } + case BSON_TYPE_OID: { + php::object o(php::class_entry::entry()); + object_id *o_ = static_cast(php::native(o)); + bson_oid_copy(bson_iter_oid(i), &o_->oid_); + return std::move(o); + } + case BSON_TYPE_BOOL: + return bson_iter_bool(i); + case BSON_TYPE_DATE_TIME: { + php::object o(php::class_entry::entry()); + date_time *o_ = static_cast(php::native(o)); + o_->tm_ = bson_iter_date_time(i); + return o; + } + case BSON_TYPE_INT32: + return bson_iter_int32(i); + case BSON_TYPE_INT64: + return bson_iter_int64(i); + default: + return nullptr; + } + } + + php::array bson2array(std::shared_ptr v) { + return bson2array(v.get()); + } + + php::array bson2array(bson_t* v) { + if (v == nullptr) return nullptr; + + php::array doc(4); + + bson_iter_t i; + bson_oid_t oid; + bson_iter_init(&i, v); + while (bson_iter_next(&i)) doc.set(bson_iter_key(&i), iter2value(&i)); + return std::move(doc); + } + + std::shared_ptr array2bson(const php::array &v) { + assert(v.type_of(php::TYPE::ARRAY)); + bson_t *doc = bson_new(); + for (auto i = v.begin(); i != v.end(); ++i) { + php::string key = i->first; + key.to_string(); + php::value val = i->second; + switch (Z_TYPE_P(static_cast(val))) { + case IS_UNDEF: + break; + case IS_NULL: + bson_append_null(doc, key.c_str(), key.size()); + break; + case IS_TRUE: + bson_append_bool(doc, key.c_str(), key.size(), true); + break; + case IS_FALSE: + bson_append_bool(doc, key.c_str(), key.size(), false); + break; + case IS_LONG: + bson_append_int64(doc, key.c_str(), key.size(), static_cast(val)); + break; + case IS_DOUBLE: + bson_append_double(doc, key.c_str(), key.size(), static_cast(val)); + break; + case IS_STRING: { + php::string str = val; + bson_append_utf8(doc, key.c_str(), key.size(), str.c_str(), str.size()); + break; + } + case IS_ARRAY: { + auto a = array2bson(val); + if (bson_has_field(a.get(), "0") || bson_count_keys(a.get()) == 0) + bson_append_array(doc, key.c_str(), key.size(), a.get()); + else + bson_append_document(doc, key.c_str(), key.size(), a.get()); + break; + } + case IS_OBJECT: { + php::object o = val; + if (o.instanceof (php_date_get_date_ce())) // PHP 内置的 DateTime 类型 + bson_append_date_time(doc, key.c_str(), key.size(), static_cast(o.call("getTimestamp")) * 1000); + else if (o.instanceof (php::class_entry::entry())) { + date_time *o_ = static_cast(php::native(o)); + bson_append_date_time(doc, key.c_str(), key.size(), o_->tm_); + } + else if (o.instanceof (php::class_entry::entry())) { + object_id *o_ = static_cast(php::native(o)); + bson_append_oid(doc, key.c_str(), key.size(), &o_->oid_); + } + else { + bson_t uninitialized; + bson_append_document_begin(doc, key.c_str(), key.size(), &uninitialized); + bson_append_document_end(doc, &uninitialized); + } + } + } + } + return std::shared_ptr(doc, bson_destroy); + } +} // namespace flame::mongodb diff --git a/src/flame/mongodb/mongodb.h b/src/flame/mongodb/mongodb.h new file mode 100644 index 0000000..018631c --- /dev/null +++ b/src/flame/mongodb/mongodb.h @@ -0,0 +1,12 @@ +#pragma once +#include "../../vendor.h" +#include + +namespace flame::mongodb { + void declare(php::extension_entry &ext); + php::value connect(php::parameters ¶ms); + php::value iter2value(bson_iter_t *i); + php::array bson2array(std::shared_ptr v); + php::array bson2array(bson_t* v); + std::shared_ptr array2bson(const php::array &v); +} // namespace flame::mongodb diff --git a/src/flame/mongodb/object_id.cpp b/src/flame/mongodb/object_id.cpp new file mode 100644 index 0000000..2c8478b --- /dev/null +++ b/src/flame/mongodb/object_id.cpp @@ -0,0 +1,68 @@ +#include "object_id.h" + +namespace flame::mongodb { + + void object_id::declare(php::extension_entry &ext) { + php::class_entry class_object_id("flame\\mongodb\\object_id"); + class_object_id + .implements(&php_json_serializable_ce) + .method<&object_id::__construct>("__construct") + .method<&object_id::to_string>("__toString") + .method<&object_id::to_json>("__toJSON") + .method<&object_id::to_datetime>("__toDateTime") + .method<&object_id::unix>("unix") + .method<&object_id::to_json>("jsonSerialize") + .method<&object_id::to_json>("__debugInfo") + .method<&object_id::equal>("equal",{ + {"object_id", "?flame\\mongodb\\object_id"} + }); + ext.add(std::move(class_object_id)); + } + + php::value object_id::__construct(php::parameters ¶ms) { + if (params.size() > 0 && params[0].type_of(php::TYPE::STRING)) { + php::string oid = params[0].to_string(); + bson_oid_init_from_string(&oid_, oid.c_str()); + } + else bson_oid_init(&oid_, nullptr); + return nullptr; + } + + php::value object_id::to_string(php::parameters ¶ms) { + php::string str(24); + bson_oid_to_string(&oid_, str.data()); + return std::move(str); + } + + php::value object_id::unix(php::parameters ¶ms) { + return bson_oid_get_time_t(&oid_); + } + + php::value object_id::to_datetime(php::parameters ¶ms) { + return php::datetime(bson_oid_get_time_t(&oid_) * 1000); + } + + php::value object_id::to_json(php::parameters ¶ms) { + // 定制 JSON 输出形式 (为何官方 MongoDB 驱动保持一致) + php::array oid(1); + php::string str(24); + bson_oid_to_string(&oid_, str.data()); + oid.set("$oid", str); + return std::move(oid); + } + + php::value object_id::equal(php::parameters ¶ms) { + if (params[0].type_of(php::TYPE::STRING)) { + php::string data = params[0]; + bson_oid_t oid; + bson_oid_init_from_string(&oid, data.c_str()); + return bson_oid_equal(&oid_, &oid); + } + else if (params[0].instanceof (php::class_entry::entry())) { + php::object obj = params[0]; + object_id *ptr = static_cast(php::native(obj)); + return bson_oid_equal(&oid_, &ptr->oid_); + } + else return false; + } +} // namespace flame::mongodb diff --git a/src/flame/mongodb/object_id.h b/src/flame/mongodb/object_id.h new file mode 100644 index 0000000..3347d39 --- /dev/null +++ b/src/flame/mongodb/object_id.h @@ -0,0 +1,21 @@ +#pragma once +#include "../../vendor.h" +#include "mongodb.h" + +namespace flame::mongodb { + + class object_id: public php::class_base { + public: + static void declare(php::extension_entry& ext); + php::value __construct(php::parameters& params); + php::value to_string(php::parameters& params); + php::value unix(php::parameters& params); + php::value to_datetime(php::parameters& params); + php::value to_json(php::parameters& params); + php::value equal(php::parameters& params); + private: + bson_oid_t oid_; + friend php::value iter2value(bson_iter_t *i); + friend std::shared_ptr array2bson(const php::array &v); + }; +} diff --git a/src/flame/mutex.cpp b/src/flame/mutex.cpp new file mode 100644 index 0000000..646e044 --- /dev/null +++ b/src/flame/mutex.cpp @@ -0,0 +1,55 @@ +#include "coroutine.h" +#include "mutex.h" +#include "../coroutine_mutex.h" + +namespace flame { + void mutex::declare(php::extension_entry &ext) { + php::class_entry class_mutex("flame\\mutex"); + class_mutex + .method<&mutex::__construct>("__construct", { + {"n", php::TYPE::INTEGER, false, true}, + }) + .method<&mutex::lock>("lock") + .method<&mutex::unlock>("unlock"); + ext.add(std::move(class_mutex)); + } + + php::value mutex::__construct(php::parameters& params) { + mutex_.reset(new coroutine_mutex()); + return nullptr; + } + + php::value mutex::lock(php::parameters& params) { + coroutine_handler ch {coroutine::current}; + mutex_->lock(ch); + return nullptr; + } + + php::value mutex::unlock(php::parameters& params) { + mutex_->unlock(); + return nullptr; + } + + void guard::declare(php::extension_entry &ext) { + php::class_entry class_guard("flame\\guard"); + class_guard + .method<&guard::__construct>("__construct", { + {"mutex", "flame\\mutex"}, + }) + .method<&guard::__destruct>("__destruct"); + ext.add(std::move(class_guard)); + } + + php::value guard::__construct(php::parameters & params) { + php::object obj = params[0]; + mutex_ = static_cast(php::native(obj))->mutex_; + coroutine_handler ch {coroutine::current}; + mutex_->lock(ch); + return nullptr; + } + + php::value guard::__destruct(php::parameters & params) { + mutex_->unlock(); + return nullptr; + } +} diff --git a/src/flame/mutex.h b/src/flame/mutex.h new file mode 100644 index 0000000..2330b47 --- /dev/null +++ b/src/flame/mutex.h @@ -0,0 +1,27 @@ +#pragma once +#include "../vendor.h" + +class coroutine_mutex; +namespace flame { + + class mutex: public php::class_base { + public: + static void declare(php::extension_entry& ext); + php::value __construct(php::parameters& params); + php::value lock(php::parameters& params); + php::value unlock(php::parameters& params); + + private: + std::shared_ptr mutex_; + friend class guard; + }; + + class guard: public php::class_base { + public: + static void declare(php::extension_entry& ext); + php::value __construct(php::parameters& params); + php::value __destruct(php::parameters ¶ms); + private: + std::shared_ptr mutex_; + }; +} diff --git a/src/flame/mysql/_connection_base.cpp b/src/flame/mysql/_connection_base.cpp new file mode 100644 index 0000000..c313f1c --- /dev/null +++ b/src/flame/mysql/_connection_base.cpp @@ -0,0 +1,188 @@ +#include "../controller.h" +#include "_connection_base.h" +#include "_connection_lock.h" +#include "result.h" + +namespace flame::mysql { + + void _connection_base::escape(std::shared_ptr conn, php::buffer &b, const php::value &v, char quote) { + switch (Z_TYPE_P(static_cast(v))) { + case IS_NULL: + b.append("NULL", 4); + break; + case IS_TRUE: + b.append("TRUE", 4); + break; + case IS_FALSE: + b.append("FALSE", 5); + break; + // case IS_LONG: + // case IS_DOUBLE: { + // php::string str = v; + // str.to_string(); + // b.append(str); + // break; + // } + case IS_STRING: { + // 支持字段名 aaa.bbb 进行 ESCAPE 变为 `aaa`.`bbb` + if (quote == '`') { + std::string str = v; // 这里进行拷贝(原字符串可能发生更改) + char *s = str.data(), *c, *e = str.data() + str.size(); + char *t = b.prepare(str.size() * 2 + 2); + std::size_t n = 0; +ESCAPE_REMAINING: + for (c = s; c < e; ++c) { + if (*c == '`') *c = '\\'; + if (*c == '.') { + t[n++] = quote; + n += mysql_real_escape_string(conn.get(), t + n, s, c - s); + t[n++] = quote; + t[n++] = '.'; + s = c + 1; + goto ESCAPE_REMAINING; + } + } + t[n++] = quote; + n += mysql_real_escape_string(conn.get(), t + n, s, c - s); + t[n++] = quote; + b.commit(n); + } else { + php::string str = v; + char *t = b.prepare(str.size() * 2 + 2); + std::size_t n = 0; + t[n++] = quote; + n += mysql_real_escape_string(conn.get(), t + n, str.c_str(), str.size()); + t[n++] = quote; + b.commit(n); + } + break; + } + case IS_ARRAY: { + php::array arr = v; + int index = 0; + b.push_back('('); + for (auto i = arr.begin(); i != arr.end(); ++i) { + if (++index > 1) b.push_back(','); + escape(conn, b, i->second, quote); + } + b.push_back(')'); + break; + } + case IS_OBJECT: { + php::object obj = v; + php::string str; + if (obj.instanceof(php_date_get_date_ce())) // DateTime 类型的 SQL 拼接 + str = obj.call("format", {php::string("Y-m-d H:i:s")}); + else str = obj.to_string(); + escape(conn, b, str, quote); + break; + } + default: { + php::string str = v; + str.to_string(); + escape(conn, b, str, quote); + } + } + } + + php::object _connection_base::query(std::shared_ptr conn, std::string sql, coroutine_handler& ch) { + MYSQL_RES* rst = nullptr; + int err = 0; + boost::asio::post(gcontroller->context_y, [&err, &conn, &ch, &rst, &sql] () { + // 在工作线程执行查询 + err = mysql_real_query(conn.get(), sql.c_str(), sql.size()); + if (err == 0) { + // 防止锁表, 均使用 store 方式 + rst = mysql_store_result(conn.get()); + if (!rst && mysql_field_count(conn.get()) > 0) { + err = -1; + } + } + ch.resume(); + }); + ch.suspend(); + last_query_ = sql; + if (err != 0) { + int err = mysql_errno(conn.get()); + throw php::exception(zend_ce_exception + , (boost::format("Failed to query MySQL server: %s") % mysql_error(conn.get())).str() + , err); + } + if (rst) { // 存在结果集 + php::object obj(php::class_entry::entry()); + result* ptr = static_cast(php::native(obj)); + ptr->cl_.reset(new _connection_lock(conn)); + ptr->rs_.reset(rst, mysql_free_result); + ptr->f_ = mysql_fetch_fields(rst); + ptr->n_ = mysql_num_fields(rst); + obj.set("stored_rows", static_cast(mysql_num_rows(rst))); + return std::move(obj); + } + else { // 更新型操作 + php::array data(2); + data.set(php::string("affected_rows", 13), static_cast(mysql_affected_rows(conn.get()))); + data.set(php::string("insert_id", 9), static_cast(mysql_insert_id(conn.get()))); + return std::move(data); + } + } + + const std::string& _connection_base::last_query() { + return last_query_; + } + + php::array _connection_base::fetch(std::shared_ptr conn, std::shared_ptr rst + , MYSQL_FIELD *f, unsigned int n, coroutine_handler &ch) { + + MYSQL_ROW row; + unsigned long* len; + boost::asio::post(gcontroller->context_y, [&rst, &ch, &row, &len] () { + row = mysql_fetch_row(rst.get()); + if (row) len = mysql_fetch_lengths(rst.get()); + ch.resume(); + }); + ch.suspend(); + if (!row) { + int err = mysql_errno(conn.get()); + if (err != 0) + throw php::exception(zend_ce_error + , (boost::format("Failed to fetch MySQL row: %s") % mysql_error(conn.get())).str() + , err); + else return nullptr; + } + php::array php_row {std::size_t(n)}; + for(int i=0;i(std::strtoll(row[i], nullptr, 10)); + break; + case MYSQL_TYPE_DATETIME: { + php::object obj( php::CLASS{php_date_get_date_ce()} ); + obj.call("__construct", {php::string(row[i], len[i])}); + value = std::move(obj); + break; + } + default: + value = php::string(row[i], len[i]); + } + php_row.set(field, value); + } + return php_row; + } +} // namespace flame::mysql diff --git a/src/flame/mysql/_connection_base.h b/src/flame/mysql/_connection_base.h new file mode 100644 index 0000000..b8a50ff --- /dev/null +++ b/src/flame/mysql/_connection_base.h @@ -0,0 +1,20 @@ +#pragma once +#include "../../vendor.h" +#include "../coroutine.h" +#include "mysql.h" + +namespace flame::mysql { + + class _connection_base { + public: + // 此函数仅允许 escape 主线程调用 + static void escape(std::shared_ptr c, php::buffer &b, const php::value &v, char quote = '\''); // 方便使用 + virtual std::shared_ptr acquire(coroutine_handler& ch) = 0; + php::object query(std::shared_ptr conn, std::string sql, coroutine_handler& ch); + php::array fetch(std::shared_ptr conn, std::shared_ptr rst, MYSQL_FIELD *f, unsigned int n, coroutine_handler &ch); + const std::string& last_query(); + protected: + private: + std::string last_query_; + }; +} // namespace flame::mysql diff --git a/src/flame/mysql/_connection_lock.cpp b/src/flame/mysql/_connection_lock.cpp new file mode 100644 index 0000000..8453607 --- /dev/null +++ b/src/flame/mysql/_connection_lock.cpp @@ -0,0 +1,63 @@ +#include "../coroutine.h" +#include "_connection_base.h" +#include "_connection_lock.h" + +namespace flame::mysql { + + _connection_lock::_connection_lock(std::shared_ptr c) + : conn_(c) { + } + + _connection_lock::~_connection_lock() { + } + + std::shared_ptr _connection_lock::acquire(coroutine_handler &ch) { + return conn_; + } + + void _connection_lock::begin_tx(coroutine_handler &ch) { + int err = 0; + boost::asio::post(gcontroller->context_y, [this, &ch, &err]() { + err = mysql_real_query(conn_.get(), "START TRANSACTION", 17); + ch.resume(); + }); + ch.suspend(); + if (err != 0) { + err = mysql_errno(conn_.get()); + throw php::exception(zend_ce_exception + , (boost::format("Failed to start transaction: %s") % mysql_error(conn_.get())).str() + , err); + } + } + + void _connection_lock::commit(coroutine_handler &ch) { + int err = 0; + boost::asio::post(gcontroller->context_y, [this, &ch, &err]() { + err = mysql_real_query(conn_.get(), "COMMIT", 6); + ch.resume(); + }); + ch.suspend(); + if (err != 0) { + err = mysql_errno(conn_.get()); + throw php::exception(zend_ce_exception + , (boost::format("failed to commit transaction: %s") % mysql_error(conn_.get())).str() + , err); + } + } + + void _connection_lock::rollback(coroutine_handler &ch) { + int err = 0; + boost::asio::post(gcontroller->context_y, [this, &ch, &err]() { + err = mysql_real_query(conn_.get(), "ROLLBACK", 8); + ch.resume(); + }); + ch.suspend(); + if (err != 0) { + err = mysql_errno(conn_.get()); + throw php::exception(zend_ce_error + , (boost::format("failed to rollback transaction: %s") % mysql_error(conn_.get())).str() + , err); + } + } + +} // namespace flame::mysql diff --git a/src/flame/mysql/_connection_lock.h b/src/flame/mysql/_connection_lock.h new file mode 100644 index 0000000..cec27a7 --- /dev/null +++ b/src/flame/mysql/_connection_lock.h @@ -0,0 +1,19 @@ +#pragma once +#include "../../vendor.h" +#include "../coroutine.h" +#include "_connection_base.h" +#include "mysql.h" + +namespace flame::mysql { + class _connection_lock : public _connection_base, public std::enable_shared_from_this<_connection_lock> { + public: + _connection_lock(std::shared_ptr c); + ~_connection_lock(); + std::shared_ptr acquire(coroutine_handler& ch) override; + void begin_tx(coroutine_handler& ch); + void commit(coroutine_handler& ch); + void rollback(coroutine_handler& ch); + private: + std::shared_ptr conn_; + }; +} // namespace flame::mysql diff --git a/src/flame/mysql/_connection_pool.cpp b/src/flame/mysql/_connection_pool.cpp new file mode 100644 index 0000000..d9d94ae --- /dev/null +++ b/src/flame/mysql/_connection_pool.cpp @@ -0,0 +1,160 @@ +#include "../controller.h" +#include "_connection_pool.h" + +namespace flame::mysql { + _connection_pool::_connection_pool(url u) + : url_(std::move(u)), min_(2), max_(6), size_(0), guard_(gcontroller->context_y), tm_(gcontroller->context_y) + , flag_(FLAG_UNKNOWN) { + if(url_.query.count("proxy") > 0 && std::stoi(url_.query["proxy"]) != 0) + flag_ = FLAG_CHARSET_EQUAL | FLAG_REUSE_BY_PROXY; + } + + _connection_pool::~_connection_pool() { + while (!conn_.empty()) { + mysql_close(conn_.front().conn); + conn_.pop_front(); + } + } + + std::shared_ptr _connection_pool::acquire(coroutine_handler& ch) { + std::shared_ptr conn; + int errnum = 0; + std::string errmsg; + // 提交异步任务 + boost::asio::post(guard_, [this, &errnum, &errmsg, &conn, &ch] () { + // 设置对应的回调, 在获取连接后恢复协程 + await_.push_back([&conn, &ch] (std::shared_ptr cc) { + conn = cc; + // RESUME 需要在主线程进行 + ch.resume(); + }); + auto now = std::chrono::steady_clock::now(); + while (!conn_.empty()) { + if (now - conn_.front().ttl < std::chrono::seconds(15) + || mysql_ping(conn_.front().conn) == 0) { // 可用连接 + + MYSQL* c = conn_.front().conn; + conn_.pop_front(); + release(c); + return; + } + else { // 连接已丢失,回收资源 + mysql_close(conn_.front().conn); + conn_.pop_front(); + --size_; + } + } + if (size_ >= max_) return; // 已建立了足够多的连接, 需要等待已分配连接释放 + MYSQL* c = mysql_init(nullptr); + init_options(c); + if (mysql_real_connect(c, url_.host.c_str(), url_.user.c_str(), url_.pass.c_str(), url_.path.c_str() + 1, url_.port, nullptr, 0)) { + if (flag_ & FLAG_REUSE_BY_PROXY) set_names(c); + ++size_; // 当前还存在的连接数量 + release(c); + } + else { + errnum = mysql_errno(c); + errmsg = mysql_error(c); + mysql_close(c); + await_.pop_back(); + ch.resume(); + } + }); + // 暂停, 等待连接获取(异步任务) + ch.suspend(); + if (!conn) { + if (errnum) throw php::exception(zend_ce_exception + , (boost::format("Failed to connect to MySQL server: %s") % errmsg).str() + , errnum); + else throw php::exception(zend_ce_exception + , "Failed to connect to MySQL server", 0); + } + // 恢复, 已经填充连接 + return conn; + } + + void _connection_pool::init_options(MYSQL* c) { + // 这里的 CHARSET 设定会被 reset_connection 重置为系统值 + mysql_options(c, MYSQL_SET_CHARSET_NAME, url_.query["charset"].c_str()); + // 版本 8.0.3 后默认使用 caching_sha2_password 进行认证,低版本不支持 + mysql_options(c, MYSQL_DEFAULT_AUTH, url_.query["auth"].c_str()); + unsigned int timeout = 5; // 连接超时 + mysql_options(c, MYSQL_OPT_CONNECT_TIMEOUT, &timeout); + } + + void _connection_pool::set_names(MYSQL* c) { + // 兼容 proxysql 流程:纯粹的字符集设置,首次 SQL 会卡住: + // 代理:2019, Can't initialize character set (null) (path: compiled_in) + // 后端:不断尝试连接、断开 + std::string sql = (boost::format("SET NAMES '%s'") % url_.query["charset"]).str(); + mysql_real_query(c, sql.c_str(), sql.size()); + } + + void _connection_pool::release(MYSQL *c) { + // 无等待分配的请求, 保存连接(待后续分配) + if (await_.empty()) conn_.push_back({c, std::chrono::steady_clock::now()}); + else { // 立刻分配使用 + // 每次连接复用前,需要清理状态; + // 这里兼容不支持 mysql_reset_connection() 新 API 的情况 + // (不支持自动切换到 mysql_change_user() 兼容老版本或变异版本) + if (!(flag_ & FLAG_REUSE_MASK)) query_version(c); + if (flag_ & FLAG_REUSE_BY_RESET) mysql_reset_connection(c); + else if (flag_ & FLAG_REUSE_BY_CUSER) mysql_change_user(c, url_.user.c_str(), url_.pass.c_str(), url_.path.c_str() + 1); + // 由于 reset 动作会导致字符集被重置为服务端字符集,确认字符集是否匹配 + if (!(flag_ & FLAG_CHARSET_MASK)) query_charset(c); + // else if (flag_ & FLAG_REUSE_BY_PROXY) ; // PROXY 已处理复用流程,此处无需处理 + if (flag_ & FLAG_CHARSET_DIFFER) + mysql_set_character_set(c, url_.query["charset"].c_str()); + std::function c)> cb = await_.front(); + await_.pop_front(); + auto self = this->shared_from_this(); + // 释放回调函数须持有当前对象引用 self (否则连接池可能先于连接归还被销毁) + cb(std::shared_ptr(c, [this, self] (MYSQL *c) { + boost::asio::post(guard_, std::bind(&_connection_pool::release, self, c)); + })); + } + } + + void _connection_pool::query_charset(MYSQL* c) { + // 使用 mysql_get_charactor_set_name() 获取到的字符集与下述查询不同 + if (0 == mysql_real_query(c, "SHOW VARIABLES WHERE `Variable_name`='character_set_client'", 59)) { + std::unique_ptr rst(mysql_store_result(c), mysql_free_result); + if (!rst) return; + MYSQL_ROW row = mysql_fetch_row(rst.get()); + if (!row) return; + unsigned long* len = mysql_fetch_lengths(rst.get()); + std::string charset(row[1], len[1]); + flag_ |= charset == url_.query["charset"] ? FLAG_CHARSET_EQUAL : FLAG_CHARSET_DIFFER; + } + else flag_ |= FLAG_CHARSET_DIFFER; // 这里忽略了 charset 查询的错误 + } + + void _connection_pool::query_version(MYSQL* c) { + // PROXY 存在时不会进行下述动作 + flag_ |= mysql_get_server_version(c) >= 50700 + ? FLAG_REUSE_BY_RESET + : FLAG_REUSE_BY_CUSER; + } + + void _connection_pool::sweep() { + tm_.expires_from_now(std::chrono::seconds(60)); + // 注意, 实际的清理流程需要保证 guard_ 串行流程 + tm_.async_wait(boost::asio::bind_executor(guard_, [this] (const boost::system::error_code &error) { + if (error) return; // 当前对象销毁时会发生对应的 abort 错误 + auto now = std::chrono::steady_clock::now(); + // 超低水位,关闭不活跃或已丢失的连接 + for (auto i = conn_.begin(); i != conn_.end() && size_ > min_;) { + auto duration = now - (*i).ttl; + if (duration > std::chrono::seconds(60) || mysql_ping((*i).conn) != 0) { + mysql_close((*i).conn); + --size_; + i = conn_.erase(i); + } + else ++i; + } + // 再次启动 + sweep(); + })); + } + +} // namespace flame::mysql diff --git a/src/flame/mysql/_connection_pool.h b/src/flame/mysql/_connection_pool.h new file mode 100644 index 0000000..0f4826a --- /dev/null +++ b/src/flame/mysql/_connection_pool.h @@ -0,0 +1,49 @@ +#pragma once +#include "../../vendor.h" +#include "../../url.h" +#include "../coroutine.h" + +#include "_connection_base.h" + +namespace flame::mysql { + + class _connection_pool : public _connection_base, public std::enable_shared_from_this<_connection_pool> { + public: + _connection_pool(url u); + ~_connection_pool(); + std::shared_ptr acquire(coroutine_handler &ch) override; + void sweep(); + private: + url url_; + const std::uint16_t min_; + const std::uint16_t max_; + std::uint16_t size_; + + boost::asio::io_context::strand guard_; // 防止对下面队列操作发生多线程问题; + std::list)>> await_; + struct connection_t { + MYSQL *conn; + std::chrono::time_point ttl; + }; + std::list conn_; + boost::asio::steady_timer tm_; + + int flag_; + enum { + FLAG_UNKNOWN = 0x00, + FLAG_REUSE_MASK = 0x0f, + FLAG_REUSE_BY_RESET = 0x01, + FLAG_REUSE_BY_CUSER = 0x02, + FLAG_REUSE_BY_PROXY = 0x04, + FLAG_CHARSET_MASK = 0xf0, + FLAG_CHARSET_EQUAL = 0x10, + FLAG_CHARSET_DIFFER = 0x20, + }; + + void init_options(MYSQL *c); + void release(MYSQL *c); + void set_names(MYSQL* c); + void query_charset(MYSQL* c); + void query_version(MYSQL* c); + }; +} // namespace flame::mysql diff --git a/src/flame/mysql/client.cpp b/src/flame/mysql/client.cpp new file mode 100644 index 0000000..f878fbc --- /dev/null +++ b/src/flame/mysql/client.cpp @@ -0,0 +1,163 @@ +#include "../coroutine.h" +#include "client.h" +#include "_connection_pool.h" +#include "_connection_lock.h" +#include "mysql.h" +#include "tx.h" + +namespace flame::mysql { + + void client::declare(php::extension_entry &ext) { + php::class_entry class_client("flame\\mysql\\client"); + class_client + .method<&client::__construct>("__construct", {}, php::PRIVATE) + .method<&client::__destruct>("__destruct") + .method<&client::escape>("escape", { + {"data", php::TYPE::UNDEFINED}, + }) + .method<&client::begin_tx>("begin_tx") + .method<&client::query>("query", { + {"sql", php::TYPE::STRING}, + }) + .method<&client::insert>("insert", { + {"table", php::TYPE::STRING}, + {"rows", php::TYPE::ARRAY}, + }) + .method<&client::delete_>("delete", { + {"table", php::TYPE::STRING}, + {"where", php::TYPE::UNDEFINED}, + {"order", php::TYPE::UNDEFINED, false, true}, + {"limit", php::TYPE::UNDEFINED, false, true}, + }) + .method<&client::update>("update", { + {"table", php::TYPE::STRING}, + {"where", php::TYPE::UNDEFINED}, + {"set", php::TYPE::UNDEFINED}, + {"order", php::TYPE::UNDEFINED, false, true}, + {"limit", php::TYPE::UNDEFINED, false, true}, + }) + .method<&client::select>("select", { + {"table", php::TYPE::STRING}, + {"fields", php::TYPE::UNDEFINED}, + {"where", php::TYPE::UNDEFINED}, + {"order", php::TYPE::UNDEFINED, false, true}, + {"limit", php::TYPE::UNDEFINED, false, true}, + }) + .method<&client::one>("one", { + {"table", php::TYPE::STRING}, + {"where", php::TYPE::UNDEFINED}, + {"order", php::TYPE::UNDEFINED, false, true}, + }) + .method<&client::get>("get", { + {"table", php::TYPE::STRING}, + {"field", php::TYPE::STRING}, + {"where", php::TYPE::UNDEFINED}, + {"order", php::TYPE::UNDEFINED, false, true}, + }) + .method<&client::last_query>("last_query") + .method<&client::server_version>("server_version"); + ext.add(std::move(class_client)); + } + + php::value client::__construct(php::parameters& params) { + return nullptr; + } + + php::value client::__destruct(php::parameters ¶ms) { + // std::cout << "~client" << std::endl; + return nullptr; + } + + php::value client::escape(php::parameters ¶ms) { + coroutine_handler ch {coroutine::current}; + std::shared_ptr conn = cp_->acquire(ch); + + php::buffer buffer; + char quote = '\''; + if (params.size() > 1 && params[1].type_of(php::TYPE::STRING)) { + php::string q = params[1]; + if (q.data()[0] == '`') quote = '`'; + } + _connection_base::escape(conn, buffer, params[0], quote); + return std::move(buffer); + } + + php::value client::begin_tx(php::parameters ¶ms) { + coroutine_handler ch{coroutine::current}; + auto conn = cp_->acquire(ch); + auto cl = std::make_shared<_connection_lock>(conn); + cl->begin_tx(ch); + // 构建事务对象 + php::object obj(php::class_entry::entry()); + tx *ptr = static_cast(php::native(obj)); + ptr->cl_ = cl; // 继续持有当前连接 + return std::move(obj); + } + + php::value client::query(php::parameters ¶ms) { + coroutine_handler ch{coroutine::current}; + return cp_->query(cp_->acquire(ch), params[0], ch); + } + + php::value client::insert(php::parameters ¶ms) { + coroutine_handler ch{coroutine::current}; + auto conn = cp_->acquire(ch); + php::buffer buf; + build_insert(conn, buf, params); + return cp_->query(conn, std::string(buf.data(), buf.size()), ch); + } + + php::value client::delete_(php::parameters ¶ms) { + coroutine_handler ch{coroutine::current}; + auto conn = cp_->acquire(ch); + php::buffer buf; + build_delete(conn, buf, params); + return cp_->query(conn, std::string(buf.data(), buf.size()), ch); + } + + php::value client::update(php::parameters ¶ms) { + coroutine_handler ch{coroutine::current}; + auto conn = cp_->acquire(ch); + php::buffer buf; + build_update(conn, buf, params); + return cp_->query(conn, std::string(buf.data(), buf.size()), ch); + } + + php::value client::select(php::parameters ¶ms) { + coroutine_handler ch{coroutine::current}; + auto conn = cp_->acquire(ch); + php::buffer buf; + build_select(conn, buf, params); + return cp_->query(conn, std::string(buf.data(), buf.size()), ch); + } + + php::value client::one(php::parameters ¶ms) { + coroutine_handler ch{coroutine::current}; + auto conn = cp_->acquire(ch); + php::buffer buf; + build_one(conn, buf, params); + php::object rst = cp_->query(conn, std::string(buf.data(), buf.size()), ch); + return rst.call("fetch_row"); + } + + php::value client::get(php::parameters ¶ms) { + coroutine_handler ch{coroutine::current}; + auto conn = cp_->acquire(ch); + php::buffer buf; + build_get(conn, buf, params); + php::object rst = cp_->query(conn, std::string(buf.data(), buf.size()), ch); + php::array row = rst.call("fetch_row"); + if (!row.empty()) return row.get( static_cast(params[1]) ); + else return nullptr; + } + + php::value client::last_query(php::parameters& params) { + return cp_->last_query(); + } + + php::value client::server_version(php::parameters ¶ms) { + coroutine_handler ch{coroutine::current}; + auto conn = cp_->acquire(ch); + return php::string(mysql_get_server_info(conn.get())); + } +} // namespace flame::mysql diff --git a/src/flame/mysql/client.h b/src/flame/mysql/client.h new file mode 100644 index 0000000..ccd6164 --- /dev/null +++ b/src/flame/mysql/client.h @@ -0,0 +1,31 @@ +#pragma once +#include "../../vendor.h" +#include "mysql.h" + +namespace flame::mysql { + + class _connection_pool; + class client : public php::class_base { + public: + static void declare(php::extension_entry &ext); + php::value __construct(php::parameters ¶ms); + php::value __destruct(php::parameters ¶ms); + php::value escape(php::parameters ¶ms); + php::value begin_tx(php::parameters ¶ms); + // php::value where(php::parameters ¶ms); + // php::value order(php::parameters ¶ms); + // php::value limit(php::parameters ¶ms); + php::value query(php::parameters ¶ms); + php::value insert(php::parameters ¶ms); + php::value delete_(php::parameters ¶ms); + php::value update(php::parameters ¶ms); + php::value select(php::parameters ¶ms); + php::value one(php::parameters ¶ms); + php::value get(php::parameters ¶ms); + php::value server_version(php::parameters& params); + php::value last_query(php::parameters& params); + protected: + std::shared_ptr<_connection_pool> cp_; + friend php::value connect(php::parameters ¶ms); + }; +} // namespace flame::mysql diff --git a/src/flame/mysql/mysql.cpp b/src/flame/mysql/mysql.cpp new file mode 100644 index 0000000..4a9c0d3 --- /dev/null +++ b/src/flame/mysql/mysql.cpp @@ -0,0 +1,463 @@ +#include "../coroutine.h" +#include "mysql.h" +#include "_connection_base.h" +#include "_connection_pool.h" +#include "client.h" +#include "result.h" +#include "tx.h" + +namespace flame::mysql { + + void declare(php::extension_entry &ext) { + gcontroller + ->on_init([] (const php::array& options) { + mysql_library_init(0, nullptr, nullptr); + }) + ->on_stop([] () { + mysql_library_end(); + }); + ext + .function("flame\\mysql\\connect", { + {"url", php::TYPE::STRING}, + }); + client::declare(ext); + result::declare(ext); + tx::declare(ext); + } + + php::value connect(php::parameters& params) { + url u(params[0]); + if (u.port < 10) u.port = 3306; + if (!u.query.count("charset")) u.query["charset"] = "utf8mb4"; + // 版本 8.0.3 后默认使用 caching_sha2_password 进行认证,低版本不支持 + if (!u.query.count("auth")) u.query["auth"] = "mysql_native_password"; + + php::object obj(php::class_entry::entry()); + client *ptr = static_cast(php::native(obj)); + ptr->cp_.reset(new _connection_pool(u)); + ptr->cp_->sweep(); // 启动自动清理扫描 + // TODO 优化: 确认第一个连接建立 ? + return std::move(obj); + } + // 相等 + static void where_eq(std::shared_ptr cc, php::buffer &buf, const php::value &cond) { + if (cond.type_of(php::TYPE::NULLABLE)) buf.append(" IS NULL", 8); + else { + php::string str = cond; + str.to_string(); + buf.push_back('='); + _connection_base::escape(cc, buf, str); + } + } + // 不等 {!=} + static void where_ne(std::shared_ptr cc, php::buffer &buf, const php::value &cond) { + if (cond.type_of(php::TYPE::NULLABLE)) buf.append(" IS NOT NULL", 12); + else { + php::string str = cond; + str.to_string(); + buf.append("!=", 2); + _connection_base::escape(cc, buf, str); + } + } + // 大于 {>} + static void where_gt(std::shared_ptr cc, php::buffer &buf, const php::value &cond) { + if (cond.type_of(php::TYPE::NULLABLE)) buf.append(">0", 2); + else { + php::string str = cond; + str.to_string(); + buf.push_back('>'); + _connection_base::escape(cc, buf, str); + } + } + // 小于 {<} + static void where_lt(std::shared_ptr cc, php::buffer &buf, const php::value &cond) { + if (cond.type_of(php::TYPE::NULLABLE)) { + buf.append("<0", 2); + } + else { + php::string str = cond; + str.to_string(); + buf.push_back('<'); + _connection_base::escape(cc, buf, str); + } + } + // 大于等于 {>=} + static void where_gte(std::shared_ptr cc, php::buffer &buf, const php::value &cond) { + if (cond.type_of(php::TYPE::NULLABLE)) buf.append(">=0", 3); + else { + php::string str = cond; + str.to_string(); + buf.append(">=", 2); + _connection_base::escape(cc, buf, str); + } + } + // 小于等于 {<=} + static void where_lte(std::shared_ptr cc, php::buffer &buf, const php::value &cond) { + if (cond.type_of(php::TYPE::NULLABLE)) buf.append("<=0", 3); + else { + php::string str = cond; + str.to_string(); + buf.append("<=", 2); + _connection_base::escape(cc, buf, str); + } + } + // 某个 IN 无对应符号 + static void where_in(std::shared_ptr cc, php::buffer &buf, const php::array &cond) { + assert(cond.type_of(php::TYPE::ARRAY) && "目标格式错误"); + + buf.append(" IN (", 5); + for (auto i = cond.begin(); i != cond.end(); ++i) { + if (static_cast(i->first) > 0) buf.push_back(','); + php::string v = i->second; + v.to_string(); + _connection_base::escape(cc, buf, v); + } + buf.push_back(')'); + } + // WITH IN RANGE + static void where_wir(std::shared_ptr cc, php::buffer &buf, const php::array &cond) { + assert(cond.type_of(php::TYPE::ARRAY) && "目标格式错误"); + + buf.append(" BETWEEN ", 9); + std::int64_t min = std::numeric_limits::max(), max = std::numeric_limits::min(); + for (auto i = cond.begin(); i != cond.end(); ++i) { + std::int64_t x = i->second.to_integer(); + if (x > max) max = x; + if (x < min) min = x; + } + buf.append(std::to_string(min)); + buf.append(" AND ", 5); + buf.append(std::to_string(max)); + } + // OUT OF RANGE + static void where_oor(std::shared_ptr cc, php::buffer &buf, const php::array &cond) { + buf.append(" NOT", 4); + where_wir(cc, buf, cond); + } + // LINK + static void where_lk(std::shared_ptr cc, php::buffer &buf, const php::value &cond) { + buf.append(" LIKE ", 6); + _connection_base::escape(cc, buf, cond); + } + // NOT LIKE + static void where_nlk(std::shared_ptr cc, php::buffer &buf, const php::value &cond) { + buf.append(" NOT LIKE ", 10); + _connection_base::escape(cc, buf, cond); + } + + static void where_ex(std::shared_ptr cc, php::buffer &buf, const php::array &cond + , const php::string &field, const php::string &separator) { + + if (cond.size() > 1) buf.push_back('('); + + int j = -1; + for (auto i = cond.begin(); i != cond.end(); ++i) { + if (++j > 0) buf.append(separator); + + if (i->first.type_of(php::TYPE::INTEGER)) { + if (i->second.type_of(php::TYPE::ARRAY)) where_ex(cc, buf, i->second, i->first, " AND "); + else { + php::string str = i->second; + str.to_string(); + buf.append(str); + } + } + else { + php::string key = i->first; + if (key.c_str()[0] == '{') { // OPERATOR (php::TYPE::STRING) + if ((key.size() == 5 && strncasecmp(key.c_str(), "{NOT}", 5) == 0) + || (key.size() == 3 && strncasecmp(key.c_str(), "{!}", 3) == 0)) { + + assert(i->second.type_of(php::TYPE::ARRAY)); + buf.append(" NOT ", 5); + where_ex(cc, buf, i->second, php::string(nullptr), " AND "); + } + else if (key.size() == 4 && (strncasecmp(key.c_str(), "{OR}", 4) == 0 + || strncasecmp(key.c_str(), "{||}", 4) == 0)) { + + assert(i->second.type_of(php::TYPE::ARRAY)); + where_ex(cc, buf, i->second, php::string(nullptr), " OR "); + } + else if ((key.size() == 5 && strncasecmp(key.c_str(), "{AND}", 5) == 0) + || (key.size() == 4 && strncasecmp(key.c_str(), "{&&}", 4) == 0)) { + assert(i->second.type_of(php::TYPE::ARRAY)); + where_ex(cc, buf, i->second, php::string(nullptr), " AND "); + } + else if (key.size() == 4 && strncasecmp(key.c_str(), "{!=}", 4) == 0) { + _connection_base::escape(cc, buf, field, '`'); + where_ne(cc, buf, i->second); + } + else if (key.size() == 3 && strncasecmp(key.c_str(), "{>}", 3) == 0) { + _connection_base::escape(cc, buf, field, '`'); + where_gt(cc, buf, i->second); + } + else if (key.size() == 3 && strncasecmp(key.c_str(), "{<}", 2) == 0) { + _connection_base::escape(cc, buf, field, '`'); + where_lt(cc, buf, i->second); + } + else if (key.size() == 4 && strncasecmp(key.c_str(), "{>=}", 4) == 0) { + _connection_base::escape(cc, buf, field, '`'); + where_gte(cc, buf, i->second); + } + else if (key.size() == 4 && strncasecmp(key.c_str(), "{<=}", 4) == 0) { + _connection_base::escape(cc, buf, field, '`'); + where_lte(cc, buf, i->second); + } + else if (key.size() == 4 && strncasecmp(key.c_str(), "{<>}", 4) == 0) { + _connection_base::escape(cc, buf, field, '`'); + where_wir(cc, buf, i->second); + } + else if (key.size() == 4 && strncasecmp(key.c_str(), "{><}", 4) == 0) { + _connection_base::escape(cc, buf, field, '`'); + where_oor(cc, buf, i->second); + } + else if (key.size() == 3 && strncasecmp(key.c_str(), "{~}", 3) == 0) { + _connection_base::escape(cc, buf, field, '`'); + where_lk(cc, buf, i->second); + } + else if (key.size() == 4 && strncasecmp(key.c_str(), "{!~}", 4) == 0) { + _connection_base::escape(cc, buf, field, '`'); + where_nlk(cc, buf, i->second); + } + } + else { // php::TYPE::STRING + if (i->second.type_of(php::TYPE::ARRAY)) { + php::array cond = i->second; + if (cond.exists(0)) { + _connection_base::escape(cc, buf, key, '`'); + buf.push_back(' '); + where_in(cc, buf, i->second); + } + else where_ex(cc, buf, cond, key, " AND "); + } + else { + _connection_base::escape(cc, buf, key, '`'); + where_eq(cc, buf, i->second); + } + } + } + } + if (cond.size() > 1) buf.push_back(')'); + } + + void build_where(std::shared_ptr cc, php::buffer &buf, const php::value &data) { + buf.append(" WHERE ", 7); + if (data.type_of(php::TYPE::STRING) || data.type_of(php::TYPE::INTEGER)) { + buf.push_back(' '); + buf.append(data); + } + else if (data.type_of(php::TYPE::ARRAY)) { + where_ex(cc, buf, data, php::string(0), " AND "); + } + else buf.push_back('1'); + } + + void build_order(std::shared_ptr cc, php::buffer &buf, const php::value &data) { + if (data.type_of(php::TYPE::STRING)) { + buf.append(" ORDER BY ", 10); + buf.append(data); + } + else if (data.type_of(php::TYPE::ARRAY)) { + buf.append(" ORDER BY", 9); + php::array order = data; + int j = -1; + for (auto i = order.begin(); i != order.end(); ++i) { + if (++j > 0) buf.push_back(','); + if (i->first.type_of(php::TYPE::INTEGER)) { + if (i->second.type_of(php::TYPE::STRING)) { + buf.push_back(' '); + buf.append(i->second); + } + } + else { + buf.push_back(' '); + _connection_base::escape(cc, buf, i->first, '`'); + if (i->second.type_of(php::TYPE::STRING)) { + buf.push_back(' '); + buf.append(i->second); + } + else if (i->second.type_of(php::TYPE::YES) + || (i->second.type_of(php::TYPE::INTEGER) && static_cast(i->second) >= 0)) + buf.append(" ASC", 4); + else buf.append(" DESC", 5); + } + } + } + else ; // throw php::exception(zend_ce_type_error, "Failed to build 'ORDER BY' clause: unsupported type specified for 'order'", -1); + } + + void build_limit(std::shared_ptr cc, php::buffer &buf, const php::value &data) { + if (data.type_of(php::TYPE::STRING) || data.type_of(php::TYPE::INTEGER)) { + buf.append(" LIMIT ", 7); + buf.append(data); + } + else if (data.type_of(php::TYPE::ARRAY)) { + buf.append(" LIMIT", 6); + php::array limit = data; + int j = -1; + for (auto i = limit.begin(); i != limit.end(); ++i) { + if (++j > 0) buf.push_back(','); + buf.push_back(' '); + buf.append(i->second); + if (j > 0) break; + } + } + else throw php::exception(zend_ce_type_error + , "Failed to build 'LIMIT' clause: unsupported type specified for `limit`" + , -1); + } + + static void insert_row(std::shared_ptr cc, php::buffer &buf, const php::array &row) { + int j = -1; + for (auto i = row.begin(); i != row.end(); ++i) { + if (++j > 0) buf.append(", ", 2); + _connection_base::escape(cc, buf, i->second); + } + } + + void build_insert(std::shared_ptr cc, php::buffer &buf, php::parameters ¶ms) { + buf.append("INSERT INTO ", 12); + // 表名 + _connection_base::escape(cc, buf, params[0], '`'); + // 字段 + buf.push_back('('); + php::array rows = params[1], row; + if (rows.exists(0)) { + row = rows.get(0); + assert(!row.exists(0) && "二级行无字段"); + } + else { + row = rows; + rows = nullptr; + } + assert(row.type_of(php::TYPE::ARRAY) && "行非数组"); + int j = -1; + for (auto i = row.begin(); i != row.end(); ++i) { + if (++j > 0) buf.append(", ", 2); + _connection_base::escape(cc, buf, i->first, '`'); + } + // 数据 + buf.append(") VALUES", 8); + if (rows.type_of(php::TYPE::ARRAY)) { + buf.push_back('('); + int j = -1; + for (auto i = rows.begin(); i != rows.end(); ++i) { + if (++j > 0) buf.append("), (", 4); + insert_row(cc, buf, i->second); + } + buf.push_back(')'); + } + else { + buf.push_back('('); + insert_row(cc, buf, row); + buf.push_back(')'); + } + } + + void build_delete(std::shared_ptr cc, php::buffer &buf, php::parameters ¶ms) { + buf.append("DELETE FROM ", 12); + // 表名 + _connection_base::escape(cc, buf, params[0], '`'); + // 条件 + build_where(cc, buf, params[1]); + // 排序 + if (params.size() > 2) build_order(cc, buf, params[2]); + // 限制 + if (params.size() > 3) build_limit(cc, buf, params[3]); + } + + void build_update(std::shared_ptr cc, php::buffer &buf, php::parameters ¶ms) { + buf.append("UPDATE ", 7); + // 表名 + _connection_base::escape(cc, buf, params[0], '`'); + // 数据 + buf.append(" SET", 4); + php::array data = params[2]; + if (data.type_of(php::TYPE::STRING)) { + buf.push_back(' '); + buf.append(data); + } + else if (data.type_of(php::TYPE::ARRAY)) { + int j = -1; + for (auto i = data.begin(); i != data.end(); ++i) { + if (++j > 0) buf.push_back(','); + buf.push_back(' '); + _connection_base::escape(cc, buf, i->first, '`'); + buf.push_back('='); + _connection_base::escape(cc, buf, i->second); + } + } + else throw php::exception(zend_ce_type_error + , "Failed to build 'UPDATE' clause: unsupported type specified for `update`" + , -1); + // 条件 + build_where(cc, buf, params[1]); + // 排序 + if (params.size() > 3) build_order(cc, buf, params[3]); + // 限制 + if (params.size() > 4) build_limit(cc, buf, params[4]); + } + + void build_select(std::shared_ptr cc, php::buffer &buf, php::parameters ¶ms) { + buf.append("SELECT ", 7); + // 字段 + php::value fields = params[1]; + if (fields.type_of(php::TYPE::STRING)) buf.append(fields); + else if (fields.type_of(php::TYPE::ARRAY)) { + php::array a = fields; + int j = -1; + for (auto i = a.begin(); i != a.end(); ++i) { + if (++j > 0) buf.append(", ", 2); + if (i->first.type_of(php::TYPE::INTEGER)) _connection_base::escape(cc, buf, i->second, '`'); + else { + buf.append(i->first); + buf.push_back('('); + _connection_base::escape(cc, buf, i->second, '`'); + buf.push_back(')'); + } + } + } + else if (fields.type_of(php::TYPE::NULLABLE)) buf.push_back('*'); + else throw php::exception(zend_ce_type_error + , "Failed to build 'SELECT' clause: unsupported type specified for `fields`" + , -1); + // 表名 + buf.append(" FROM ", 6); + _connection_base::escape(cc, buf, params[0], '`'); + // 条件 + if (params.size() > 2) build_where(cc, buf, params[2]); + // 排序 + if (params.size() > 3) build_order(cc, buf, params[3]); + // 限制 + if (params.size() > 4) build_limit(cc, buf, params[4]); + } + + void build_one(std::shared_ptr cc, php::buffer &buf, php::parameters ¶ms) { + buf.append("SELECT * FROM ", 14); + // 表名 + _connection_base::escape(cc, buf, params[0], '`'); + // 条件 + if (params.size() > 1) build_where(cc, buf, params[1]); + // 排序 + if (params.size() > 2) build_order(cc, buf, params[2]); + buf.append(" LIMIT 1", 8); + } + + void build_get(std::shared_ptr cc, php::buffer &buf, php::parameters ¶ms) { + buf.append("SELECT ", 7); + // 字段 + if (params[1].type_of(php::TYPE::STRING)) _connection_base::escape(cc, buf, params[1], '`'); + else throw php::exception(zend_ce_type_error + , "Failed to build 'SELECT' clause: unsupported type specified for `field`" + , -1); + // 表名 + buf.append(" FROM ", 6); + _connection_base::escape(cc, buf, params[0], '`'); + // 条件 + if (params.size() > 2) build_where(cc, buf, params[2]); + // 排序 + if (params.size() > 3) build_order(cc, buf, params[3]); + buf.append(" LIMIT 1", 8); + } + +} diff --git a/src/flame/mysql/mysql.h b/src/flame/mysql/mysql.h new file mode 100644 index 0000000..781a718 --- /dev/null +++ b/src/flame/mysql/mysql.h @@ -0,0 +1,19 @@ +#pragma once +#include "../../vendor.h" +#include + +namespace flame::mysql { + + void declare(php::extension_entry &ext); + php::value connect(php::parameters& params); + + void build_where(std::shared_ptr cm, php::buffer &buf, const php::value &data); + void build_order(std::shared_ptr cm, php::buffer &buf, const php::value &data); + void build_limit(std::shared_ptr cm, php::buffer &buf, const php::value &data); + void build_insert(std::shared_ptr cm, php::buffer &buf, php::parameters ¶ms); + void build_delete(std::shared_ptr cm, php::buffer &buf, php::parameters ¶ms); + void build_update(std::shared_ptr cm, php::buffer &buf, php::parameters ¶ms); + void build_select(std::shared_ptr cm, php::buffer &buf, php::parameters ¶ms); + void build_one(std::shared_ptr cm, php::buffer &buf, php::parameters ¶ms); + void build_get(std::shared_ptr cm, php::buffer &buf, php::parameters ¶ms); +} diff --git a/src/flame/mysql/result.cpp b/src/flame/mysql/result.cpp new file mode 100644 index 0000000..2deb12b --- /dev/null +++ b/src/flame/mysql/result.cpp @@ -0,0 +1,49 @@ +#include "../coroutine.h" +#include "result.h" +#include "_connection_lock.h" + +namespace flame::mysql { + + void result::declare(php::extension_entry &ext) { + php::class_entry class_result("flame\\mysql\\result"); + class_result + .property({"stored_rows", 0}) + .method<&result::fetch_row>("fetch_row") + .method<&result::fetch_all>("fetch_all"); + + ext.add(std::move(class_result)); + } + + php::value result::fetch_row(php::parameters ¶ms) { + php::array row(nullptr); + if (cl_ && rs_) { + coroutine_handler ch{coroutine::current}; + row = cl_->fetch(cl_->acquire(ch), rs_, f_, n_, ch); + } + if (row.type_of(php::TYPE::NULLABLE)) { + // 尽早的释放连接 + rs_.reset(); + cl_.reset(); + } + return row; + } + + php::value result::fetch_all(php::parameters ¶ms) { + if (cl_ && rs_) { + coroutine_handler ch{coroutine::current}; + auto conn = cl_->acquire(ch); + php::array data {4}, row; + + for(row = cl_->fetch(conn, rs_, f_, n_, ch); !row.type_of(php::TYPE::NULLABLE); row = cl_->fetch(conn, rs_, f_, n_, ch)) { + data.set(data.size(), row); + } + // 尽早的释放连接 + rs_.reset(); + cl_.reset(); + return data; + } + else { + return nullptr; + } + } +} // namespace flame::mysql diff --git a/src/flame/mysql/result.h b/src/flame/mysql/result.h new file mode 100644 index 0000000..ae18a0b --- /dev/null +++ b/src/flame/mysql/result.h @@ -0,0 +1,21 @@ +#pragma once +#include "../../vendor.h" +#include "mysql.h" + +namespace flame::mysql { + + class _connection_lock; + class result: public php::class_base { + public: + static void declare(php::extension_entry &ext); + php::value fetch_row(php::parameters ¶ms); + php::value fetch_all(php::parameters ¶ms); + + private: + std::shared_ptr<_connection_lock> cl_; + std::shared_ptr rs_; + MYSQL_FIELD *f_; + unsigned int n_; // Number Of Fields + friend class _connection_base; + }; +} // namespace flame::mysql diff --git a/src/flame/mysql/tx.cpp b/src/flame/mysql/tx.cpp new file mode 100644 index 0000000..7f96b65 --- /dev/null +++ b/src/flame/mysql/tx.cpp @@ -0,0 +1,152 @@ +#include "../coroutine.h" +#include "tx.h" +#include "_connection_lock.h" +#include "mysql.h" + +namespace flame::mysql { + void tx::declare(php::extension_entry &ext) { + php::class_entry class_tx("flame\\mysql\\tx"); + class_tx + .method<&tx::__construct>("__construct", {}, php::PRIVATE) + .method<&tx::__destruct>("__destruct") + .method<&tx::escape>("escape", { + {"data", php::TYPE::UNDEFINED}, + }) + .method<&tx::commit>("commit") + .method<&tx::rollback>("rollback") + .method<&tx::query>("query", { + {"sql", php::TYPE::STRING}, + }) + .method<&tx::insert>("insert", { + {"table", php::TYPE::STRING}, + {"rows", php::TYPE::ARRAY}, + }) + .method<&tx::delete_>("delete", { + {"table", php::TYPE::STRING}, + {"where", php::TYPE::UNDEFINED}, + {"order", php::TYPE::UNDEFINED, false, true}, + {"limit", php::TYPE::UNDEFINED, false, true}, + }) + .method<&tx::update>("update", { + {"table", php::TYPE::STRING}, + {"where", php::TYPE::UNDEFINED}, + {"set", php::TYPE::UNDEFINED}, + {"order", php::TYPE::UNDEFINED, false, true}, + {"limit", php::TYPE::UNDEFINED, false, true}, + }) + .method<&tx::select>("select", { + {"table", php::TYPE::STRING}, + {"fields", php::TYPE::UNDEFINED}, + {"where", php::TYPE::UNDEFINED}, + {"order", php::TYPE::UNDEFINED, false, true}, + {"limit", php::TYPE::UNDEFINED, false, true}, + }) + .method<&tx::one>("one", { + {"table", php::TYPE::STRING}, + {"where", php::TYPE::UNDEFINED}, + {"order", php::TYPE::UNDEFINED, false, true}, + }) + .method<&tx::get>("get", { + {"table", php::TYPE::STRING}, + {"field", php::TYPE::STRING}, + {"where", php::TYPE::UNDEFINED}, + {"order", php::TYPE::UNDEFINED, false, true}, + }); + ext.add(std::move(class_tx)); + } + + php::value tx::__construct(php::parameters ¶ms) { + return nullptr; + } + + php::value tx::__destruct(php::parameters ¶ms) { + if (!done_) rollback(params); + return nullptr; + } + + php::value tx::escape(php::parameters ¶ms) { + coroutine_handler ch{coroutine::current}; + std::shared_ptr conn = cl_->acquire(ch); + + php::buffer buffer; + char quote = '\''; + if (params.size() > 1 && params[1].type_of(php::TYPE::STRING)) { + php::string q = params[1]; + if (q.data()[0] == '`') quote = '`'; + } + _connection_base::escape(conn, buffer, params[0], quote); + return std::move(buffer); + } + + php::value tx::commit(php::parameters ¶ms) { + done_ = true; + coroutine_handler ch{coroutine::current}; + cl_->commit(ch); + return nullptr; + } + + php::value tx::rollback(php::parameters ¶ms) { + done_ = true; + coroutine_handler ch{coroutine::current}; + cl_->rollback(ch); + return nullptr; + } + + php::value tx::query(php::parameters ¶ms) { + coroutine_handler ch{coroutine::current}; + std::shared_ptr conn = cl_->acquire(ch); + return cl_->query(conn, params[0], ch); + } + + php::value tx::insert(php::parameters ¶ms) { + coroutine_handler ch{coroutine::current}; + auto conn = cl_->acquire(ch); + php::buffer buf; + build_insert(conn, buf, params); + return cl_->query(conn, std::string(buf.data(), buf.size()), ch); + } + + php::value tx::delete_(php::parameters ¶ms) { + coroutine_handler ch{coroutine::current}; + auto conn = cl_->acquire(ch); + php::buffer buf; + build_delete(conn, buf, params); + return cl_->query(conn, std::string(buf.data(), buf.size()), ch); + } + + php::value tx::update(php::parameters ¶ms) { + coroutine_handler ch{coroutine::current}; + auto conn = cl_->acquire(ch); + php::buffer buf; + build_update(conn, buf, params); + return cl_->query(conn, std::string(buf.data(), buf.size()), ch); + } + + php::value tx::select(php::parameters ¶ms) { + coroutine_handler ch{coroutine::current}; + auto conn = cl_->acquire(ch); + php::buffer buf; + build_select(conn, buf, params); + return cl_->query(conn, std::string(buf.data(), buf.size()), ch); + } + + php::value tx::one(php::parameters ¶ms) { + coroutine_handler ch{coroutine::current}; + auto conn = cl_->acquire(ch); + php::buffer buf; + build_one(conn, buf, params); + php::object rst = cl_->query(conn, std::string(buf.data(), buf.size()), ch); + return rst.call("fetch_row"); + } + + php::value tx::get(php::parameters ¶ms) { + coroutine_handler ch{coroutine::current}; + auto conn = cl_->acquire(ch); + php::buffer buf; + build_get(conn, buf, params); + php::object rst = cl_->query(conn, std::string(buf.data(), buf.size()), ch); + php::array row = rst.call("fetch_row"); + if (!row.empty()) return row.get( static_cast(params[1]) ); + else return nullptr; + } +} // namespace flame::mysql diff --git a/src/flame/mysql/tx.h b/src/flame/mysql/tx.h new file mode 100644 index 0000000..bbaec9a --- /dev/null +++ b/src/flame/mysql/tx.h @@ -0,0 +1,31 @@ +#pragma once +#include "../../vendor.h" +#include "mysql.h" + +namespace flame::mysql { + + class _connection_lock; + class tx : public php::class_base { + public: + static void declare(php::extension_entry &ext); + php::value __construct(php::parameters ¶ms); // 私有 + php::value __destruct(php::parameters& params); + php::value commit(php::parameters ¶ms); + php::value rollback(php::parameters ¶ms); + + php::value escape(php::parameters ¶ms); + php::value query(php::parameters ¶ms); + + php::value insert(php::parameters ¶ms); + php::value delete_(php::parameters ¶ms); + php::value update(php::parameters ¶ms); + php::value select(php::parameters ¶ms); + php::value one(php::parameters ¶ms); + php::value get(php::parameters ¶ms); + + protected: + std::shared_ptr<_connection_lock> cl_; + bool done_ = false; + friend class client; + }; +} // namespace flame::mysql diff --git a/src/flame/os/os.cpp b/src/flame/os/os.cpp new file mode 100644 index 0000000..18fb1f1 --- /dev/null +++ b/src/flame/os/os.cpp @@ -0,0 +1,116 @@ +#include "../coroutine.h" +#include "../controller.h" +#include "os.h" +#include "process.h" +#include +// 获取本地网卡地址 +#include +#include +#include +#include +#include +#include + +namespace flame::os { + void declare(php::extension_entry &ext) { + ext + .function("flame\\os\\interfaces") + .function("flame\\os\\spawn", { + {"executable", php::TYPE::STRING}, + {"arguments", php::TYPE::ARRAY, false, true}, + {"options", php::TYPE::ARRAY, false, true}, + }) + .function("flame\\os\\exec", { + {"executable", php::TYPE::STRING}, + {"arguments", php::TYPE::ARRAY, false, true}, + {"options", php::TYPE::ARRAY, false, true}, + }); + + process::declare(ext); + } + + static void interface(php::array& data, const struct ifaddrs *addr) { + php::string name(addr->ifa_name); + php::array info(2); + info.set("family", addr->ifa_addr->sa_family == AF_INET ? "IPv4" : "IPv6"); + char address[NI_MAXHOST]; + if (getnameinfo(addr->ifa_addr, + addr->ifa_addr->sa_family == AF_INET ? sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6), + address, NI_MAXHOST, nullptr, 0, NI_NUMERICHOST) != 0) + throw php::exception(zend_ce_exception, gai_strerror(errno), errno); + + info.set("address", address); + if (data.exists(name)) { + php::array iface = data.get(name); + iface.set(iface.size(), info); + } + else { + php::array iface(2); + iface.set(iface.size(), info); + data.set(name, iface); + } + } + + php::value interfaces(php::parameters ¶ms) { + struct ifaddrs *addr; + if (getifaddrs(&addr) != 0) throw php::exception(zend_ce_exception, std::strerror(errno), errno); + else if (!addr) return php::array(0); + + php::array data(4); + // 用 shared_ptr 作自动释放保证 + std::shared_ptr autofree(addr, freeifaddrs); + do { + if (addr->ifa_addr->sa_family != AF_INET && addr->ifa_addr->sa_family != AF_INET6) continue; + interface(data, addr); + } while ((addr = addr->ifa_next) != nullptr); + return std::move(data); + } + + php::value spawn(php::parameters ¶ms) { + php::object proc(php::class_entry::entry()); + process *proc_ = static_cast(php::native(proc)); + proc_->exit_ = false; + + auto env = boost::this_process::environment(); + std::string exec = params[0].to_string(); + if (exec[0] != '.' && exec[0] != '/') + exec = boost::process::search_path(exec).native(); + + std::vector argv; + std::string cwdv = boost::filesystem::current_path().native(); + if (params.size() > 1) { + php::array args = params[1]; + for (auto i = args.begin(); i != args.end(); ++i) + argv.push_back(i->second.to_string()); + } + if (params.size() > 2) { + php::array opts = params[2]; + php::array envs = opts.get("env"); + if (envs.type_of(php::TYPE::ARRAY)) { + for (auto i = envs.begin(); i != envs.end(); ++i) + env[i->first.to_string()] = i->second.to_string(); + } + php::string cwds = opts.get("cwd"); + if (cwds.type_of(php::TYPE::STRING)) cwdv = cwds.to_string(); + } + + boost::process::child c(exec, boost::process::args = argv, env, gcontroller->context_x, + boost::process::start_dir = cwdv, + boost::process::std_out > proc_->out_, + boost::process::std_err > proc_->err_, + boost::process::on_exit = [proc, proc_] (int exit_code, const std::error_code &) { + proc_->exit_ = true; + if (proc_->ch_) proc_->ch_.resume(); + }); + proc.set("pid", c.id()); + proc_->c_ = std::move(c); + return proc; + } + + php::value exec(php::parameters ¶ms) { + php::object proc = spawn(params); + process *proc_ = static_cast(php::native(proc)); + proc.call("wait"); + return proc_->out_.get(); + } +} // namespace flame::os diff --git a/src/flame/os/os.h b/src/flame/os/os.h new file mode 100644 index 0000000..f75b35c --- /dev/null +++ b/src/flame/os/os.h @@ -0,0 +1,11 @@ +#pragma once +#include "../../vendor.h" + +namespace flame::os { + + void declare(php::extension_entry &ext); + php::value interfaces(php::parameters ¶ms); + php::value spawn(php::parameters ¶ms); + php::value exec(php::parameters ¶ms); + +} // namespace flame::os diff --git a/src/flame/os/process.cpp b/src/flame/os/process.cpp new file mode 100644 index 0000000..17ae03d --- /dev/null +++ b/src/flame/os/process.cpp @@ -0,0 +1,71 @@ +#include "../coroutine.h" +#include "process.h" + +namespace flame::os { + + void process::declare(php::extension_entry &ext) { + ext + .constant({"flame\\os\\SIGTERM", SIGTERM}) + .constant({"flame\\os\\SIGKILL", SIGKILL}) + .constant({"flame\\os\\SIGINT", SIGINT}) + .constant({"flame\\os\\SIGUSR1", SIGUSR1}) + .constant({"flame\\os\\SIGUSR2", SIGUSR2}); + + php::class_entry class_process("flame\\os\\process"); + class_process + .property({"pid", 0}) + .method<&process::__construct>("__construct", {}, php::PRIVATE) + .method<&process::__destruct>("__destruct") + .method<&process::kill>("kill", { + {"signal", php::TYPE::INTEGER, false, true} + }) + .method<&process::detach>("detach") + .method<&process::wait>("wait") + .method<&process::stdout>("stdout") + .method<&process::stderr>("stderr"); + ext.add(std::move(class_process)); + } + + php::value process::__construct(php::parameters& params) { + return nullptr; + } + + php::value process::__destruct(php::parameters& params) { + if (!detach_) { + if (!c_.wait_for(std::chrono::milliseconds(10000))) c_.terminate(); + if (c_.joinable()) c_.join(); + } + return nullptr; + } + + php::value process::kill(php::parameters ¶ms) { + if (params.size() > 0) ::kill(get("pid"), params[0].to_integer()); + else ::kill(get("pid"), SIGTERM); + return nullptr; + } + + php::value process::detach(php::parameters& params) { + detach_ = true; + c_.detach(); + return nullptr; + } + + php::value process::wait(php::parameters ¶ms) { + if (!exit_) { + ch_.reset(coroutine::current); + ch_.suspend(); + c_.join(); + } + return nullptr; + } + + php::value process::stdout(php::parameters ¶ms) { + wait(params); + return out_.get(); + } + + php::value process::stderr(php::parameters ¶ms) { + wait(params); + return out_.get(); + } +} // namespace flame::os diff --git a/src/flame/os/process.h b/src/flame/os/process.h new file mode 100644 index 0000000..e58e32e --- /dev/null +++ b/src/flame/os/process.h @@ -0,0 +1,28 @@ +#pragma once +#include "../../vendor.h" +#include "../coroutine.h" +#include +#include + +namespace flame::os { + class process: public php::class_base { + public: + static void declare(php::extension_entry& ext); + php::value __construct(php::parameters& params); + php::value __destruct(php::parameters& params); + php::value kill(php::parameters& params); + php::value wait(php::parameters& params); + php::value detach(php::parameters& params); + php::value stdout(php::parameters& params); + php::value stderr(php::parameters& params); + private: + boost::process::child c_; + coroutine_handler ch_; + std::future out_; + std::future err_; + bool exit_ = false; + bool detach_ = false; + friend class php::value spawn(php::parameters& params); + friend class php::value exec(php::parameters& params); + }; +} diff --git a/src/flame/queue.cpp b/src/flame/queue.cpp new file mode 100644 index 0000000..38c95c8 --- /dev/null +++ b/src/flame/queue.cpp @@ -0,0 +1,67 @@ +#include "coroutine.h" +#include "queue.h" + +namespace flame { + void queue::declare(php::extension_entry &ext) { + php::class_entry class_queue("flame\\queue"); + class_queue + .method<&queue::__construct>("__construct", { + {"n", php::TYPE::INTEGER, false, true}, + }) + .method<&queue::push>("push", { + {"value", php::TYPE::UNDEFINED} + }) + .method<&queue::pop>("pop") + .method<&queue::close>("close") + .method<&queue::is_closed>("is_closed"); + ext + .add(std::move(class_queue)) + .function("flame\\select"); + } + + php::value queue::__construct(php::parameters& params) { + if (params.size() > 0) + q_.reset(new coroutine_queue(static_cast(params[0]))); + else + q_.reset(new coroutine_queue(1)); + return nullptr; + } + + php::value queue::push(php::parameters ¶ms) { + coroutine_handler ch{coroutine::current}; + q_->push(params[0], ch); + return nullptr; + } + + php::value queue::pop(php::parameters ¶ms) { + coroutine_handler ch{coroutine::current}; + auto x = q_->pop(ch); + if (x) return x.value(); + else return nullptr; + } + + php::value queue::close(php::parameters ¶ms) { + q_->close(); + return nullptr; + } + + php::value queue::is_closed(php::parameters& params) { + return q_->is_closed(); + } + + php::value queue::select(php::parameters& params) { + std::vector< std::shared_ptr> > qs; + std::map< std::shared_ptr>, php::object > mm; + for (auto i = 0; i < params.size(); ++i) { + if (!params[i].instanceof(php::class_entry::entry())) + throw php::exception(zend_ce_type_error, "Failed to select: instanceof flame\\queue required", -1); + php::object obj = params[i]; + queue* ptr = static_cast(php::native(obj)); + qs.push_back( ptr->q_ ); + mm.insert({ptr->q_, obj}); + } + coroutine_handler ch{coroutine::current}; + std::shared_ptr> q = select_queue(qs, ch); + return mm[q]; + } +} diff --git a/src/flame/queue.h b/src/flame/queue.h new file mode 100644 index 0000000..90c6201 --- /dev/null +++ b/src/flame/queue.h @@ -0,0 +1,19 @@ +#pragma once +#include "../vendor.h" +#include "../coroutine_queue.h" + +namespace flame { + + class queue: public php::class_base { + public: + static void declare(php::extension_entry &ext); + static php::value select(php::parameters& params); + php::value __construct(php::parameters& params); + php::value push(php::parameters ¶ms); + php::value pop(php::parameters ¶ms); + php::value close(php::parameters ¶ms); + php::value is_closed(php::parameters ¶ms); + private: + std::shared_ptr> q_; + }; +} diff --git a/src/flame/rabbitmq/_client.cpp b/src/flame/rabbitmq/_client.cpp new file mode 100644 index 0000000..de11754 --- /dev/null +++ b/src/flame/rabbitmq/_client.cpp @@ -0,0 +1,191 @@ +#include "../coroutine.h" +#include "_client.h" +#include "message.h" + +namespace flame::rabbitmq { + + static bool str2bool(const char *str){ + return strncasecmp(str, "1", 1) == 0 || strncasecmp(str, "yes", 3) == 0 || + strncasecmp(str, "yes", 3) == 0 || strncasecmp(str, "true", 4); + } + +#define CHECK_AND_SET_FLAG(xflag, yflag) \ + for (auto i = u.query.find(#xflag); i != u.query.end();) { \ + const char *str = i->second.c_str(); \ + if (str2bool(str)) fl_ |= yflag; \ + } + + _client::_client(url u, coroutine_handler& ch) + : chl_(gcontroller->context_x) + , con_(&chl_, AMQP::Address(u.str(true, false).c_str(), u.str().size())) + , chn_(&con_) + , tm_(gcontroller->context_x) + , producer_cb_(false) { + chn_.onReady([this, &ch]() { + // ch_.onError([] (const char* message) {}); + chn_.onError([this] (const char* message) { + error_.assign(message); + if (!consumer_tg_.empty()) consumer_close(); + }); + ch.resume(); + }); + chn_.onError([this, &ch](const char *message) { + error_ = message; + chn_.onReady(nullptr); + ch.resume(); + }); + ch.suspend(); + + if (!error_.empty()) { + std::string err = std::move(error_); + throw php::exception(zend_ce_exception + , (boost::format("Failed to connect RabbitMQ server: %s") % err).str() + , -1); + } + + auto i = u.query.find("prefetch"); + if (i == u.query.end()) pf_ = 8; + else pf_ = std::min(std::max(std::atoi(i->second.c_str()), 1), 256); + chn_.setQos(pf_); + + CHECK_AND_SET_FLAG(nolocal, AMQP::nolocal); + CHECK_AND_SET_FLAG(noack, AMQP::noack); + CHECK_AND_SET_FLAG(exclusive, AMQP::noack); + CHECK_AND_SET_FLAG(mandatory, AMQP::mandatory); + CHECK_AND_SET_FLAG(immediate, AMQP::immediate); + + // 启动心跳 + heartbeat(); + } + + void _client::heartbeat() { + tm_.expires_after(std::chrono::seconds(45)); + tm_.async_wait([this] (const boost::system::error_code& error) { + if (error) return; + con_.heartbeat(); + heartbeat(); + }); + } + + bool _client::has_error() { + return !error_.empty(); + } + + const std::string& _client::error() { + return error_; + } + + void _client::consume(const std::string &queue, coroutine_queue& q, coroutine_handler &ch) { + php::object obj = nullptr; + const char* err = nullptr; + chn_.consume(queue) + .onReceived([this, &q, &obj, &ch](const AMQP::Message &m, std::uint64_t tag, bool redelivered) { + obj = php::object(php::class_entry::entry()); + message* ptr = static_cast(php::native(obj)); + ptr->build_ex(m, tag); + ch.resume(); // ----> 2 + }) + .onSuccess([this, &ch](const std::string &t) { + consumer_tg_ = t; + ch.resume(); // ----> 2 + }) + .onError([&err, &ch](const char *message) { + err = message; + ch.resume(); // ----> 2 + }); + ch.suspend(); + if (err) throw php::exception(zend_ce_exception + , (boost::format("Failed to consume RabbitMQ queue: %s") % err).str() + , -1); + consumer_ch_ = ch; + ch.suspend(); + // 非关闭恢复执行, 消息对象一定存在 + while(!consumer_tg_.empty()) { + q.push(std::move(obj), ch); + ch.suspend(); // 2 <---- + } + if (err) throw php::exception(zend_ce_exception + , (boost::format("Failed to consume RabbitMQ queue: %s") % err).str() + , -1); + q.close(); + consumer_ch_.reset(); + } + + void _client::confirm(std::uint64_t tag, coroutine_handler &ch) { + chn_.ack(tag, 0); + } + + void _client::reject(std::uint64_t tag, int flags, coroutine_handler &ch) { + chn_.reject(tag, flags); + } + + void _client::consumer_close() { + const char* err = nullptr; + chn_.cancel(consumer_tg_); + consumer_tg_.clear(); + // 标记结束后,消费流程队将自行关闭 + if (consumer_ch_) consumer_ch_.resume(); + } + + void _client::consumer_close(coroutine_handler& ch) { + const char* err = nullptr; + chn_.cancel(consumer_tg_) + .onSuccess([&ch] () { + ch.resume(); + }) + .onError([&err, &ch] (const char* message) { + err = message; + ch.resume(); + }); + ch.suspend(); + consumer_tg_.clear(); + if (consumer_ch_) consumer_ch_.resume(); // ----> 2 标记结束后,消费流程队将自行关闭 + if (err) throw php::exception(zend_ce_exception + , (boost::format("Failed to close RabbitMQ consumer: %s") % err).str() + , -1); + } + + void _client::publish(const std::string &ex, const std::string &rk, const AMQP::Envelope &env, coroutine_handler& ch) { + /*auto& defer = */chn_.publish(ex, rk, env, fl_); + // producer_cb(defer, ch); + if (!error_.empty()) { + std::string err = std::move(error_); + throw php::exception(zend_ce_exception + , (boost::format("Failed to publish to RabbitMQ: %s") % err).str() + , -1); + } + } + + // void _client::producer_cb(AMQP::DeferredPublisher& defer, coroutine_handler& ch) { + // producer_ch_ = ch; + // if (!producer_cb_) { + // producer_cb_ = true; + // const char* err = nullptr; + // defer.onSuccess([this]() { + // if (producer_ch_) producer_ch_.resume(); + // }).onError([this](const char *message) { + // error_ = message; + // if (producer_ch_) producer_ch_.resume(); + // }); + // } + // ch.suspend(); + // producer_ch_.reset(); + // if (!error_.empty()) { + // std::string err = std::move(error_); + // throw php::exception(zend_ce_error + // , (boost::format("Failed to publish RabbitMQ message: %s") % err).str() + // , -1); + // } + // } + + void _client::publish(const std::string &ex, const std::string &rk, const char *msg, size_t len, coroutine_handler& ch) { + /*auto& defer = */chn_.publish(ex, rk, msg, len, fl_); + // producer_cb(defer, ch); + if (!error_.empty()) { + std::string err = std::move(error_); + throw php::exception(zend_ce_exception + , (boost::format("Failed to publish RabbitMQ message: %s") % err).str() + , -1); + } + } +} diff --git a/src/flame/rabbitmq/_client.h b/src/flame/rabbitmq/_client.h new file mode 100644 index 0000000..4843d00 --- /dev/null +++ b/src/flame/rabbitmq/_client.h @@ -0,0 +1,40 @@ +#pragma once +#include "../../vendor.h" +#include "../../url.h" +#include "../../coroutine_queue.h" +#include "../coroutine.h" +#include "rabbitmq.h" + +namespace flame::rabbitmq { + + class _client { + public: + _client(url u, coroutine_handler& ch); + + void consume(const std::string& queue, coroutine_queue& q, coroutine_handler& ch); + void confirm(std::uint64_t tag, coroutine_handler& ch); + void reject(std::uint64_t tag, int flags, coroutine_handler& ch); + void consumer_close(coroutine_handler& ch); + void consumer_close(); + void publish(const std::string& ex, const std::string& rk, const AMQP::Envelope& env, coroutine_handler& ch); + void publish(const std::string& ex, const std::string& rk, const char* msg, size_t len, coroutine_handler& ch); + // void producer_cb(AMQP::DeferredPublisher& defer, coroutine_handler& ch); + void heartbeat(); + bool has_error(); + const std::string& error(); + private: + AMQP::LibBoostAsioHandler chl_; + AMQP::TcpConnection con_; + AMQP::TcpChannel chn_; + int pf_; + int fl_; + boost::asio::steady_timer tm_; + std::string consumer_tg_; + coroutine_handler consumer_ch_; + bool producer_cb_; + coroutine_handler producer_ch_; + std::string error_; + + friend class consumer; + }; +} diff --git a/src/flame/rabbitmq/consumer.cpp b/src/flame/rabbitmq/consumer.cpp new file mode 100644 index 0000000..3564755 --- /dev/null +++ b/src/flame/rabbitmq/consumer.cpp @@ -0,0 +1,96 @@ +#include "../coroutine.h" +#include "consumer.h" +#include "_client.h" +#include "message.h" +#include "../time/time.h" +#include "../log/logger.h" + +namespace flame::rabbitmq { + + void consumer::declare(php::extension_entry &ext) { + php::class_entry class_consumer("flame\\rabbitmq\\consumer"); + class_consumer + .method<&consumer::__construct>("__construct", {}, php::PRIVATE) + .method<&consumer::__destruct>("__destruct") + .method<&consumer::run>("run", { + {"callable", php::TYPE::CALLABLE}, + }) + .method<&consumer::confirm>("confirm", { + {"message", "flame\\rabbitmq\\message"} + }) + .method<&consumer::reject>("reject", { + {"message", "flame\\rabbitmq\\message"}, + {"requeue", php::TYPE::BOOLEAN, false, true}, + }) + .method<&consumer::close>("close"); + ext.add(std::move(class_consumer)); + } + + php::value consumer::__construct(php::parameters ¶ms) { + return nullptr; + } + + php::value consumer::__destruct(php::parameters& params) { + return nullptr; + } + + php::value consumer::run(php::parameters ¶ms) { + cb_ = params[0]; + + coroutine_handler ch {coroutine::current}; + // 下述 q 需要在堆分配, 否则当 cc_->consume() 结束后可能与被提前销毁 (协程销毁稍晚) + coroutine_queue q(cc_->pf_); + // 启动若干协程, 然后进行"并行>"消费 + int count = (int)std::ceil(cc_->pf_ / 2.0); + for (int i = 0; i < count; ++i) { + // 启动协程开始消费 + coroutine::start(php::value([this, &q, &count, &ch, i] (php::parameters ¶ms) -> php::value { + coroutine_handler cch {coroutine::current}; + while(auto x = q.pop(cch)) { + try { + cb_.call( {x.value()} ); + } catch(const php::exception& ex) { + // 调用用户异常回调 + gcontroller->event("exception", {ex}); + // 记录错误信息 + php::object obj = ex; + log::logger_->stream() << "[" << time::iso() << "] (ERROR) Uncaught Exception in RabbitMQ consumer: " << obj.call("__toString") << std::endl; + } + } + if (--count == 0) ch.resume(); // ----> 1 + return nullptr; + })); + } + cc_->consume(qn_, q, ch); + ch.suspend(); // 1 <---- + cb_ = nullptr; + if (cc_->has_error()) throw php::exception(zend_ce_exception + , (boost::format("Failed to consume RabbitMQ queue: %s") % cc_->error()).str() + , -1); + return nullptr; + } + + php::value consumer::confirm(php::parameters ¶ms) { + php::object obj = params[0]; + message *ptr = static_cast(php::native(obj)); + coroutine_handler ch {coroutine::current}; + cc_->confirm(ptr->tag_, ch); + return nullptr; + } + + php::value consumer::reject(php::parameters ¶ms) { + php::object obj = params[0]; + message *ptr = static_cast(php::native(obj)); + int flags = 0; + if (params.size() > 1 && params[1].to_boolean()) flags |= AMQP::requeue; + coroutine_handler ch {coroutine::current}; + cc_->reject(ptr->tag_, flags, ch); + return nullptr; + } + + php::value consumer::close(php::parameters ¶ms) { + coroutine_handler ch {coroutine::current}; + cc_->consumer_close(ch); + return nullptr; + } +} // namespace flame::rabbitmq diff --git a/src/flame/rabbitmq/consumer.h b/src/flame/rabbitmq/consumer.h new file mode 100644 index 0000000..6e0ff24 --- /dev/null +++ b/src/flame/rabbitmq/consumer.h @@ -0,0 +1,26 @@ +#pragma once +#include "../../vendor.h" +#include "rabbitmq.h" + +namespace flame::rabbitmq { + + class _client; + class consumer : public php::class_base { + public: + static void declare(php::extension_entry &ext); + php::value __construct(php::parameters ¶ms); // 私有 + php::value __destruct(php::parameters& params); + php::value run(php::parameters ¶ms); + php::value confirm(php::parameters ¶ms); + php::value reject(php::parameters ¶ms); + php::value close(php::parameters ¶ms); + private: + // 实际的客户端对象可能超过当前对象的生存期 + std::shared_ptr<_client> cc_; + php::callable cb_; + std::string qn_; + int concurrent_; + + friend php::value consume(php::parameters ¶ms); + }; +} // namespace flame::rabbitmq diff --git a/src/flame/rabbitmq/message.cpp b/src/flame/rabbitmq/message.cpp new file mode 100644 index 0000000..8ebe499 --- /dev/null +++ b/src/flame/rabbitmq/message.cpp @@ -0,0 +1,106 @@ +#include "../coroutine.h" +#include "rabbitmq.h" +#include "message.h" + +namespace flame::rabbitmq { + + void message::declare(php::extension_entry &ext) { + php::class_entry class_message("flame\\rabbitmq\\message"); + class_message + .implements(&php_json_serializable_ce) + .property({"routing_key", ""}) + .property({"body", ""}) + .property({"expiration", ""}) + .property({"reply_to", ""}) + .property({"correlation_id", ""}) + .property({"priority", 0}) + .property({"delivery_mode", 0}) + .property({"header", nullptr}) + .property({"content_encoding", ""}) + .property({"content_type", ""}) + .property({"cluster_id", ""}) + .property({"app_id", ""}) + .property({"user_id", ""}) + .property({"type_name", ""}) + .property({"timestamp", 0}) + .property({"message_id", ""}) + .method<&message::__construct>("__construct", { + {"body", php::TYPE::STRING, false, true}, + }) + .method<&message::to_string>("__toString") + .method<&message::to_json>("jsonSerialize"); + + ext.add(std::move(class_message)); + } + + php::value message::__construct(php::parameters ¶ms) { + if (params.size() > 0) set("body", params[0]); + if (params.size() > 1) set("routing_key", params[1]); + return nullptr; + } + + php::value message::to_json(php::parameters ¶ms) { + php::array data(16); + data.set("routing_key", get("routing_key")); + data.set("body", get("body")); + data.set("timestamp", get("timestamp")); + return std::move(data); + } + + php::value message::to_string(php::parameters ¶ms) { + return get("body"); + } + + void message::build_ex(const AMQP::Message &msg, std::uint64_t tag) { + tag_ = tag; + set("routing_key", msg.routingkey()); + set("body", php::string(msg.body(), msg.bodySize())); + if (msg.hasExpiration()) set("expiration", msg.expiration()); + if (msg.hasReplyTo()) set("reply_to", msg.replyTo()); + if (msg.hasCorrelationID()) set("correlation_id", msg.correlationID()); + if (msg.hasPriority()) set("priority", msg.priority()); + if (msg.hasDeliveryMode()) set("delivery_mode", msg.deliveryMode()); + if (msg.hasHeaders()) set("header", table2array(msg.headers())); + if (msg.hasContentEncoding()) set("content_encoding", msg.contentEncoding()); + if (msg.hasContentType()) set("content_type", msg.contentType()); + if (msg.hasClusterID()) set("cluster_id", msg.clusterID()); + if (msg.hasAppID()) set("app_id", msg.appID()); + if (msg.hasUserID()) set("user_id", msg.userID()); + if (msg.hasTypeName()) set("type_name", msg.typeName()); + if (msg.hasTimestamp()) set("timestamp", msg.timestamp()); + if (msg.hasMessageID()) set("message_id", msg.messageID()); + } + + void message::build_ex(AMQP::Envelope &env) { + php::string v; + + v = get("expiration"); + if (!v.empty()) env.setExpiration(v.to_string()); + v = get("reply_to"); + if (!v.empty()) env.setReplyTo(v.to_string()); + v = get("correlation_id"); + if (!v.empty()) env.setCorrelationID(v.to_string()); + v = get("priority"); + if (!v.empty()) env.setPriority(v.to_integer()); + v = get("delivery_mode"); + if (!v.empty()) env.setDeliveryMode(v.to_integer()); + v = get("header"); + if (!v.empty()) env.setHeaders(array2table(v)); + v = get("content_encoding"); + if (!v.empty()) env.setContentEncoding(v.to_string()); + v = get("content_type"); + if (!v.empty()) env.setContentType(v.to_string()); + v = get("cluster_id"); + if (!v.empty()) env.setClusterID(v.to_string()); + v = get("app_id"); + if (!v.empty()) env.setAppID(v.to_string()); + v = get("user_id"); + if (!v.empty()) env.setUserID(v.to_string()); + v = get("type_name"); + if (!v.empty()) env.setTypeName(v.to_string()); + v = get("timestamp"); + if (!v.empty()) env.setTimestamp(v.to_integer()); + v = get("message_id"); + if (!v.empty()) env.setMessageID(v.to_string()); + } +} // namespace flame::rabbitmq diff --git a/src/flame/rabbitmq/message.h b/src/flame/rabbitmq/message.h new file mode 100644 index 0000000..9bc9c1e --- /dev/null +++ b/src/flame/rabbitmq/message.h @@ -0,0 +1,18 @@ +#pragma once +#include "../../vendor.h" +#include "rabbitmq.h" + +namespace flame::rabbitmq { + + class message : public php::class_base { + public: + static void declare(php::extension_entry &ext); + php::value __construct(php::parameters ¶ms); // 私有 + php::value to_json(php::parameters ¶ms); + php::value to_string(php::parameters ¶ms); + + void build_ex(const AMQP::Message &msg, std::uint64_t tag); + void build_ex(AMQP::Envelope &env); + std::uint64_t tag_; + }; +} // namespace flame::rabbitmq diff --git a/src/flame/rabbitmq/producer.cpp b/src/flame/rabbitmq/producer.cpp new file mode 100644 index 0000000..440b24f --- /dev/null +++ b/src/flame/rabbitmq/producer.cpp @@ -0,0 +1,49 @@ +#include "../coroutine.h" +#include "producer.h" +#include "_client.h" +#include "message.h" + +namespace flame::rabbitmq { + + void producer::declare(php::extension_entry &ext) { + php::class_entry class_producer("flame\\rabbitmq\\producer"); + class_producer + .method<&producer::__construct>("__construct", {}, php::PRIVATE) + .method<&producer::publish>("publish", { + {"exchange", php::TYPE::STRING}, + {"message", php::TYPE::UNDEFINED}, + }); + ext.add(std::move(class_producer)); + } + + php::value producer::__construct(php::parameters ¶ms) { + return nullptr; + } + + php::value producer::publish(php::parameters ¶ms) { + std::string exch = params[0]; + std::string routing_key; + if (params.size() > 2) routing_key = params[2].to_string(); + + coroutine_handler ch{coroutine::current}; + if (params[1].instanceof(php::class_entry::entry())) { + php::object msg = params[1]; + message *msg_ = static_cast(php::native(msg)); + php::string body = msg.get("body"); + body.to_string(); + AMQP::Envelope env(body.c_str(), body.size()); + msg_->build_ex(env); + // 未指定时使用 message 默认的 routing_key + if (routing_key.empty()) routing_key = msg.get("routing_key").to_string(); + + coroutine_handler ch {coroutine::current}; + cc_->publish(exch, routing_key, env, ch); + } + else { + php::string msg = params[1]; + msg.to_string(); + cc_->publish(exch, routing_key, msg.c_str(), msg.size(), ch); + } + return nullptr; + } +} // namespace flame::rabbitmq diff --git a/src/flame/rabbitmq/producer.h b/src/flame/rabbitmq/producer.h new file mode 100644 index 0000000..53fee1f --- /dev/null +++ b/src/flame/rabbitmq/producer.h @@ -0,0 +1,19 @@ +#pragma once +#include "../../vendor.h" +#include "rabbitmq.h" + +namespace flame::rabbitmq { + class _client; + class producer: public php::class_base { + public: + static void declare(php::extension_entry& ext); + php::value __construct(php::parameters& params); // 私有 + php::value publish(php::parameters& params); + private: + // 实际的客户端对象可能超过当前对象的生存期 + std::shared_ptr<_client> cc_; + + friend php::value produce(php::parameters& params); + friend class _client; + }; +} // namespace flame::rabbitmq diff --git a/src/flame/rabbitmq/rabbitmq.cpp b/src/flame/rabbitmq/rabbitmq.cpp new file mode 100644 index 0000000..5d240f0 --- /dev/null +++ b/src/flame/rabbitmq/rabbitmq.cpp @@ -0,0 +1,83 @@ +#include "../../url.h" +#include "../coroutine.h" +#include "_client.h" +#include "rabbitmq.h" +#include "consumer.h" +#include "producer.h" +#include "message.h" + +namespace flame::rabbitmq { + void declare(php::extension_entry &ext) { + ext + .function("flame\\rabbitmq\\consume", { + {"address", php::TYPE::STRING}, + {"queue", php::TYPE::STRING}, + }) + .function("flame\\rabbitmq\\produce", { + {"address", php::TYPE::STRING}, + }); + + consumer::declare(ext); + producer::declare(ext); + message::declare(ext); + } + + php::value consume(php::parameters ¶ms) { + coroutine_handler ch {coroutine::current}; + url u(params[0]); + // u.query.clear(); + std::shared_ptr<_client> cc = std::make_shared<_client>(u, ch); + + // cc->connect(ch); + + php::object obj(php::class_entry::entry()); + consumer* ptr = static_cast(php::native(obj)); + ptr->cc_ = cc; + ptr->qn_ = static_cast(params[1]); + return std::move(obj); + } + + php::value produce(php::parameters ¶ms) { + coroutine_handler ch{coroutine::current}; + url u(params[0]); + // u.query.clear(); + std::shared_ptr<_client> cc = std::make_shared<_client>(u, ch); + + // cc->connect(ch); + + php::object obj(php::class_entry::entry()); + producer *ptr = static_cast(php::native(obj)); + ptr->cc_ = cc; + return std::move(obj); + } + // 仅支持一维数组 + php::array table2array(const AMQP::Table& table) { + php::array data(table.size()); + for (auto key : table.keys()) { + const AMQP::Field &field = table.get(key); + if (field.isBoolean()) data.set(key, static_cast(field) ? true : false); + else if (field.isInteger()) data.set(key, static_cast(field)); + else if (field.isDecimal()) data.set(key, static_cast(field)); + else if (field.isString()) data.set(key, (const std::string &)field); + else if (field.typeID() == 'd') data.set(key, static_cast(field)); + else if (field.typeID() == 'f') data.set(key, static_cast(field)); + // TODO 记录警告信息? + } + return std::move(data); + } + // 仅支持一维数组 + AMQP::Table array2table(const php::array& table) { + AMQP::Table data; + if (!table.type_of(php::TYPE::ARRAY) || table.empty()) return std::move(data); + for (auto i = table.begin(); i != table.end(); ++i) { + if (i->second.type_of(php::TYPE::BOOLEAN)) data.set(i->first.to_string(), i->second.to_boolean()); + else if (i->second.type_of(php::TYPE::INTEGER)) data.set(i->first.to_string(), i->second.to_integer()); + else if (i->second.type_of(php::TYPE::STRING)) data.set(i->first.to_string(), i->second.to_string()); + else if (i->second.type_of(php::TYPE::FLOAT)) data.set(i->first.to_string(), AMQP::Double{i->second.to_float()}); + else throw php::exception(zend_ce_type_error + , (boost::format("Failed to convert array: unsupported type '%s'") % i->second.type_of().name()).str() + , -1); + } + return std::move(data); + } +} // namespace flame::rabbitmq diff --git a/src/flame/rabbitmq/rabbitmq.h b/src/flame/rabbitmq/rabbitmq.h new file mode 100644 index 0000000..09bb7dc --- /dev/null +++ b/src/flame/rabbitmq/rabbitmq.h @@ -0,0 +1,14 @@ +#pragma once +#include "../../vendor.h" +#include +#include + +namespace flame::rabbitmq { + + void declare(php::extension_entry &ext); + php::value consume(php::parameters ¶ms); + php::value produce(php::parameters ¶ms); + + php::array table2array(const AMQP::Table &table); + AMQP::Table array2table(const php::array &table); +} // namespace flame::rabbitmq diff --git a/src/flame/redis/_connection_base.cpp b/src/flame/redis/_connection_base.cpp new file mode 100644 index 0000000..de052cd --- /dev/null +++ b/src/flame/redis/_connection_base.cpp @@ -0,0 +1,81 @@ +#include "_connection_base.h" + +namespace flame::redis { + + static php::value simple2value(redisReply* rp) { + if (!rp) return nullptr; + switch (rp->type) { + case REDIS_REPLY_STATUS: + case REDIS_REPLY_ERROR: + case REDIS_REPLY_STRING: + return php::string(rp->str, rp->len); + case REDIS_REPLY_INTEGER: + return static_cast(rp->integer); + case REDIS_REPLY_ARRAY: { + php::array data(rp->elements); + for(int i=0;ielements;++i) { + data.set(data.size(), simple2value(rp->element[i])); + } + return data; + } + case REDIS_REPLY_NIL: + default: + return nullptr; + } + } + + php::value _connection_base::reply2value(redisReply* rp, php::array &argv, reply_type rt) { + switch(rt) { + case reply_type::ASSOC_ARRAY_1: { + php::array data(rp->elements/2 + 1); + for (int i = 0; i < rp->elements; i += 2) { + php::string key = simple2value(rp->element[i]).to_string(); + data.set(key, simple2value(rp->element[i+1])); + } + return data; + } + case reply_type::ASSOC_ARRAY_2: { + assert(rp->elements == 2 && rp->element[0]->type == REDIS_REPLY_STRING && rp->element[1]->type == REDIS_REPLY_ARRAY); + php::array wrap(2); + php::array data(rp->element[1]->elements/2 + 1); + wrap.set(0, simple2value(rp->element[0])); + for (int i = 0; i < rp->element[1]->elements; i += 2) { + php::string key = simple2value(rp->element[1]->element[i]).to_string(); + data.set(key, simple2value(rp->element[1]->element[i+1])); + } + wrap.set(1, data); + return wrap; + } + case reply_type::COMBINE_1: { + php::array data(rp->elements); + for (int i = 0; i < rp->elements; ++i) { + php::string key = argv[i].to_string(); + data.set(key, simple2value(rp->element[i])); + } + return data; + } + case reply_type::COMBINE_2: { + php::array data(rp->elements); + for (int i = 0; i < rp->elements; ++i) { + php::string key = argv[i+1].to_string(); + data.set(key, simple2value(rp->element[i])); + } + return data; + } + case reply_type::SIMPLE: + default: + return simple2value(rp); + } + } + + std::string _connection_base::format(php::string& name, php::array& argv) { + std::ostringstream ss; + ss << "*" << 1 + argv.size() << "\r\n$" << name.size() << "\r\n" << name << "\r\n"; + for (auto i = argv.begin(); i != argv.end(); ++i) { + php::string v = i->second.to_string(); + if (v.size() > 0) ss << "$" << v.size() << "\r\n" << v << "\r\n"; + else ss << "$0\r\n\r\n"; + } + return ss.str(); + } +} \ No newline at end of file diff --git a/src/flame/redis/_connection_base.h b/src/flame/redis/_connection_base.h new file mode 100644 index 0000000..fe2f4eb --- /dev/null +++ b/src/flame/redis/_connection_base.h @@ -0,0 +1,24 @@ +#pragma once +#include "../../vendor.h" +#include "../coroutine.h" +#include "redis.h" + +namespace flame::redis { + + enum class reply_type { + SIMPLE = 0x00, // 默认返回处理 + ASSOC_ARRAY_1 = 0x01, // 返回数据多项按 KEY VAL 生成关联数组 + ASSOC_ARRAY_2 = 0x02, // 第一层普通数组, 第二层关联数组 (HSCAN/ZSCAN) + COMBINE_1 = 0x04, // 与参数结合生成关联数组 + COMBINE_2 = 0x08, // 同上, 但偏移错位 1 个参数 + EXEC = 0x100, // 事务特殊处理方式(需要结合上面几种方式) + PIPE = 0x200, // 与事务形式相同 + }; + + class _connection_base { + public: + virtual std::shared_ptr acquire(coroutine_handler &ch) = 0; + php::value reply2value(redisReply *rp, php::array &argv, reply_type rt); + std::string format(php::string& name, php::array& argv); + }; +} // namespace flame::mysql diff --git a/src/flame/redis/_connection_lock.cpp b/src/flame/redis/_connection_lock.cpp new file mode 100644 index 0000000..6fd257c --- /dev/null +++ b/src/flame/redis/_connection_lock.cpp @@ -0,0 +1,52 @@ +#include "../coroutine.h" +#include "_connection_base.h" +#include "_connection_lock.h" + +namespace flame::redis { + + _connection_lock::_connection_lock(std::shared_ptr c) + : conn_(c) { + } + + _connection_lock::~_connection_lock() { + } + + std::shared_ptr _connection_lock::acquire(coroutine_handler &ch) { + return conn_; + } + + void _connection_lock::push(php::string &name, php::array &argv, reply_type type) { + // 暂未提交, 仅记录 + // TODO 优化: 是否可以在提交前在进行 format 操作 (减少内存占用的持续时间) ? + cmds_.push_back({name, argv, type, format(name, argv)}); + } + + void _connection_lock::push(php::string &name, php::parameters& params, reply_type type) { + php::array argv {params}; + push(name, argv, type); + } + + php::array _connection_lock::exec(coroutine_handler &ch) { + boost::asio::post(gcontroller->context_x, [this, &ch] () { + // 在工作线程中, 提交所有待执行命令 + for(auto i=cmds_.begin(); i!=cmds_.end(); ++i) + redisAppendFormattedCommand(conn_.get(), i->strs.c_str(), i->strs.size()); + // 读取对应的返回值 + for(auto i=cmds_.begin(); i!=cmds_.end(); ++i) + redisGetReply(conn_.get(), (void**)&i->reply); + // 回到主线程 + ch.resume(); + }); + ch.suspend(); + // TODO 若执行过程当中存在异常,如何上报?(似乎 Redis 原则上不会部分失败) + // 整合个命令返回值 + php::array data(cmds_.size()); + for(auto i=cmds_.begin(); i!=cmds_.end(); ++i) { + data.set(data.size(), reply2value(i->reply, i->argv, i->type)); + freeReplyObject(i->reply); + } + // 命令执行完毕 (所有返回值处理完成) + cmds_.clear(); + return std::move(data); + } +} // namespace flame::redis diff --git a/src/flame/redis/_connection_lock.h b/src/flame/redis/_connection_lock.h new file mode 100644 index 0000000..d4cb379 --- /dev/null +++ b/src/flame/redis/_connection_lock.h @@ -0,0 +1,28 @@ +#pragma once +#include "../../vendor.h" +#include "../coroutine.h" +#include "redis.h" +#include "_connection_base.h" + +namespace flame::redis { + + class _connection_lock : public _connection_base, public std::enable_shared_from_this<_connection_lock> { + public: + _connection_lock(std::shared_ptr c); + ~_connection_lock(); + std::shared_ptr acquire(coroutine_handler &ch) override; + void push(php::string &name, php::array &argv, reply_type rt); + void push(php::string &name, php::parameters &argv, reply_type rt); + php::array exec(coroutine_handler& ch); + struct command_t { + php::string name; + php::array argv; + reply_type type; + std::string strs; + redisReply* reply; + }; + private: + std::shared_ptr conn_; + std::list cmds_; + }; +} // namespace flame::redis diff --git a/src/flame/redis/_connection_pool.cpp b/src/flame/redis/_connection_pool.cpp new file mode 100644 index 0000000..3f051d6 --- /dev/null +++ b/src/flame/redis/_connection_pool.cpp @@ -0,0 +1,177 @@ +#include "../controller.h" +#include "redis.h" +#include "_connection_pool.h" + +namespace flame::redis { + + _connection_pool::_connection_pool(url u) + : url_(std::move(u)), min_(1), max_(6) + , size_(0) + , guard_(gcontroller->context_y) + , tm_(gcontroller->context_y) { + if (url_.port < 10) url_.port = 6379; + } + + _connection_pool::~_connection_pool() { + while (!conn_.empty()) { + redisFree(conn_.front().conn); + conn_.pop_front(); + } + } + + std::shared_ptr _connection_pool::acquire(coroutine_handler &ch) { + std::shared_ptr conn; + std::string err; + // 提交异步任务 + boost::asio::post(guard_, [this, &conn, &err, &ch]() { + // 设置对应的回调, 在获取连接后恢复协程 + await_.push_back([&conn, &ch](std::shared_ptr c) { + conn = c; + // RESUME 需要在主线程进行 + ch.resume(); + }); + auto now = std::chrono::steady_clock::now(); + while (!conn_.empty()) { + if (now - conn_.front().ttl < std::chrono::seconds(15) || ping(conn_.front().conn)) { + // 可用连接 + redisContext *c = conn_.front().conn; + conn_.pop_front(); + release(c); + return; + } + else { // 连接已丢失,回收资源 + redisFree(conn_.front().conn); + conn_.pop_front(); + --size_; + } + } + if (size_ >= max_) return; // 已建立了足够多的连接, 需要等待已分配连接释放 + + redisContext *c = create(err); + if (c == nullptr) { // 创建新连接失败 + await_.pop_back(); + ch.resume(); + } + else { + ++size_; // 当前还存在的连接数量 + release(c); + } + }); + // 暂停, 等待连接获取(异步任务) + ch.suspend(); + if (!conn) throw php::exception(zend_ce_exception + , (boost::format("Failed to connect to Redis server: %s") % err).str() + , -1); + // 恢复, 已经填充连接 + return conn; + } + + php::value _connection_pool::exec(std::shared_ptr rc, php::string &name, php::array &argv, reply_type rt, coroutine_handler& ch) { + std::string ss = format(name, argv); + redisReply *rp = nullptr; + boost::asio::post(gcontroller->context_y, [&rc, &ss, &ch, &rp]() { + redisAppendFormattedCommand(rc.get(), ss.c_str(), ss.size()); + redisGetReply(rc.get(), (void **)&rp); + ch.resume(); + }); + ch.suspend(); + if (rp) { + php::value rv = reply2value(rp, argv, rt); + freeReplyObject(rp); + return std::move(rv); + } + else if (rc->err) throw php::exception(zend_ce_exception + , (boost::format("Failed to exec Redis command: %s") % rc->errstr).str() + , rc->err); + else return nullptr; + } + + php::value _connection_pool::exec(std::shared_ptr rc, php::string &name, php::parameters &argv, reply_type rt, coroutine_handler& ch) { + php::array data {argv}; + return exec(rc, name, data, rt, ch); + } + + redisContext *_connection_pool::create(std::string& err) { + struct timeval tv {5, 0}; + std::unique_ptr c { + redisConnectWithTimeout(url_.host.c_str(), url_.port, tv), redisFree }; + if (!c) { + err.assign("connection failed"); + return nullptr; + } + else if (c->err) { + err.assign(c->errstr); + return nullptr; + } + if (!url_.pass.empty()) { // 认证 + std::unique_ptr rp { + (redisReply*) redisCommand(c.get(), "AUTH %s", url_.pass.c_str()), + (void (*)(redisReply*)) freeReplyObject }; + + if (!rp) { + err.assign(c->errstr); + return nullptr; + } + if (rp->type == REDIS_REPLY_ERROR) { + err.assign(rp->str, rp->len); + return nullptr; + } + } + if (url_.path.length() > 1) { // 指定数据库 + std::unique_ptr rp { + (redisReply *) redisCommand(c.get(), "SELECT %d", std::atoi(url_.path.c_str() + 1)), + (void (*)(redisReply*)) freeReplyObject }; + if (!rp) { + err.assign(c->errstr); + return nullptr; + } + if (rp->type == REDIS_REPLY_ERROR) { + err.assign(rp->str, rp->len); + return nullptr; + } + } + return c.release(); + } + + void _connection_pool::release(redisContext *c) { + if (c->err) --size_; // 出现上下文异常的连接直接抛弃 + else if (await_.empty()) + conn_.push_back({c, std::chrono::steady_clock::now()}); // 无等待分配的请求 + else { // 立刻分配使用 + std::function c)> cb = await_.front(); + await_.pop_front(); + auto self = this->shared_from_this(); + // 释放回调函数须持有当前对象引用 self (否则连接池可能先于连接归还被销毁) + cb(std::shared_ptr(c, [this, self](redisContext *c) { + boost::asio::post(guard_, std::bind(&_connection_pool::release, self, c)); + })); + } + } + + bool _connection_pool::ping(redisContext* c) { + std::unique_ptr rp((redisReply *)redisCommand(c, "PING"), (void (*)(redisReply*))freeReplyObject); + if (!rp || rp->type == REDIS_REPLY_ERROR) return false; + else return true; + } + + void _connection_pool::sweep() { + tm_.expires_from_now(std::chrono::seconds(60)); + // 注意, 实际的清理流程需要保证 guard_ 串行流程 + tm_.async_wait(boost::asio::bind_executor(guard_, [this](const boost::system::error_code &error) { + if (error) return; // 当前对象销毁时会发生对应的 abort 错误 + auto now = std::chrono::steady_clock::now(); + for (auto i = conn_.begin(); i != conn_.end() && size_ > min_;) { + // 超低水位,关闭不活跃连接 + auto duration = now - (*i).ttl; + if (duration > std::chrono::seconds(60) || !ping((*i).conn)) { + redisFree((*i).conn); + --size_; + i = conn_.erase(i); + } + else ++i; + } + sweep(); // 再次启动 + })); + } + +} // namespace flame::redis diff --git a/src/flame/redis/_connection_pool.h b/src/flame/redis/_connection_pool.h new file mode 100644 index 0000000..70e906c --- /dev/null +++ b/src/flame/redis/_connection_pool.h @@ -0,0 +1,37 @@ +#pragma once +#include "../../vendor.h" +#include "../../url.h" +#include "../coroutine.h" +#include "redis.h" +#include "_connection_base.h" + + +namespace flame::redis { + class _connection_pool : public _connection_base, public std::enable_shared_from_this<_connection_pool> { + public: + _connection_pool(url u); + ~_connection_pool(); + std::shared_ptr acquire(coroutine_handler &ch) override; + php::value exec(std::shared_ptr rc, php::string &name, php::array &argv, reply_type rt, coroutine_handler &ch); + php::value exec(std::shared_ptr rc, php::string &name, php::parameters &argv, reply_type rt, coroutine_handler &ch); + void sweep(); + private: + url url_; + const std::uint16_t min_; + const std::uint16_t max_; + std::uint16_t size_; + + boost::asio::io_context::strand guard_; // 防止对下面队列操作发生多线程问题; + std::list)>> await_; + struct connection_t { + redisContext *conn; + std::chrono::time_point ttl; + }; + std::list conn_; + boost::asio::steady_timer tm_; + + bool ping(redisContext* c); + redisContext *create(std::string& err); + void release(redisContext *c); + }; +} // namespace flame::mysql diff --git a/src/flame/redis/client.cpp b/src/flame/redis/client.cpp new file mode 100644 index 0000000..7560f23 --- /dev/null +++ b/src/flame/redis/client.cpp @@ -0,0 +1,181 @@ +#include "../coroutine.h" +#include "client.h" +#include "_connection_pool.h" +#include "tx.h" +#include "_connection_lock.h" + +namespace flame::redis { + void client::declare(php::extension_entry& ext) { + php::class_entry class_client("flame\\redis\\client"); + class_client + .method<&client::__construct>("__construct", {}, php::PRIVATE) + .method<&client::__call>("__call", { + {"cmd", php::TYPE::STRING}, + {"arg", php::TYPE::ARRAY}, + }) + .method<&client::__isset>("__isset", { + {"cmd", php::TYPE::STRING}, + }) + .method<&client::mget>("mget", { + {"key", php::TYPE::STRING}, + }) + .method<&client::hmget>("hmget", { + {"hash", php::TYPE::STRING}, + }) + .method<&client::hgetall>("hgetall", { + {"hash", php::TYPE::STRING}, + }) + .method<&client::hscan>("hscan", { + {"hash", php::TYPE::STRING}, + {"cursor", php::TYPE::INTEGER}, + }) + .method<&client::zscan>("zscan", { + {"zset", php::TYPE::STRING}, + {"cursor", php::TYPE::INTEGER}, + }) + .method<&client::zrange>("zrange", { + {"zset", php::TYPE::STRING}, + {"start", php::TYPE::INTEGER}, + {"stop", php::TYPE::INTEGER}, + }) + .method<&client::zrevrange>("zrevrange", { + {"zset", php::TYPE::STRING}, + {"start", php::TYPE::INTEGER}, + {"stop", php::TYPE::INTEGER}, + }) + .method<&client::zrangebyscore>("zrangebyscore", { + {"zset", php::TYPE::STRING}, + {"min", php::TYPE::STRING}, + {"max", php::TYPE::STRING}, + }) + .method<&client::zrevrangebyscore>("zrevrangebyscore", { + {"zset", php::TYPE::STRING}, + {"min", php::TYPE::STRING}, + {"max", php::TYPE::STRING}, + }) + .method<&client::multi>("multi") + // 以下暂未实现 + .method<&client::unimplement>("subscribe") + .method<&client::unimplement>("psubscribe") + .method<&client::unimplement>("unsubscribe") + .method<&client::unimplement>("punsubscribe"); + + ext.add(std::move(class_client)); + } + + php::value client::__construct(php::parameters& params) { + return nullptr; + } + + php::value client::__call(php::parameters& params) { + coroutine_handler ch {coroutine::current}; + auto rc = cp_->acquire(ch); + php::string name = params[0]; + php::array argv = params[1]; + return cp_->exec(rc, name, argv, reply_type::SIMPLE, ch); + } + + php::value client::__isset(php::parameters& params) { + return true; + } + + php::value client::mget(php::parameters& params) { + coroutine_handler ch{coroutine::current}; + auto rc = cp_->acquire(ch); + php::string name("MGET", 4); + return cp_->exec(rc, name, params, reply_type::COMBINE_1, ch); + } + + php::value client::hmget(php::parameters& params) { + coroutine_handler ch{coroutine::current}; + auto rc = cp_->acquire(ch); + php::string name("HMGET", 5); + return cp_->exec(rc, name, params, reply_type::COMBINE_2, ch); + } + + php::value client::hgetall(php::parameters& params) { + coroutine_handler ch{coroutine::current}; + auto rc = cp_->acquire(ch); + php::string name("HGETALL", 7); + return cp_->exec(rc, name, params, reply_type::ASSOC_ARRAY_1, ch); + } + + php::value client::hscan(php::parameters& params) { + coroutine_handler ch{coroutine::current}; + auto rc = cp_->acquire(ch); + php::string name("HSCAN", 5); + return cp_->exec(rc, name, params, reply_type::ASSOC_ARRAY_2, ch); + } + + php::value client::zscan(php::parameters& params) { + coroutine_handler ch{coroutine::current}; + auto rc = cp_->acquire(ch); + php::string name("ZSCAN", 5); + return cp_->exec(rc, name, params, reply_type::ASSOC_ARRAY_2, ch); + } + + php::value client::zrange(php::parameters& params) { + coroutine_handler ch{coroutine::current}; + auto rc = cp_->acquire(ch); + php::string name("ZRANGE", 6); + php::string last = params[params.size()-1]; + if (last.type_of(php::TYPE::STRING) && last.size() == 10 && strncasecmp("WITHSCORES", last.c_str(), 10) == 0) + return cp_->exec(rc, name, params, reply_type::ASSOC_ARRAY_1, ch); + else + return cp_->exec(rc, name, params, reply_type::SIMPLE, ch); + } + + php::value client::zrevrange(php::parameters& params) { + coroutine_handler ch{coroutine::current}; + auto rc = cp_->acquire(ch); + php::string name("ZREVRANGE", 9); + php::string last = params[params.size() - 1]; + if (last.type_of(php::TYPE::STRING) &&last.size() == 10 && strncasecmp("WITHSCORES", last.c_str(), 10) == 0) + return cp_->exec(rc, name, params, reply_type::ASSOC_ARRAY_1, ch); + else + return cp_->exec(rc, name, params, reply_type::SIMPLE, ch); + } + + php::value client::zrangebyscore(php::parameters& params) { + coroutine_handler ch{coroutine::current}; + auto rc = cp_->acquire(ch); + php::string name("ZRANGEBYSCORE", 13); + + for(int i=3; iexec(rc, name, params, reply_type::ASSOC_ARRAY_1, ch); + } + } + return cp_->exec(rc, name, params, reply_type::SIMPLE, ch); + } + + php::value client::zrevrangebyscore(php::parameters& params) { + coroutine_handler ch{coroutine::current}; + auto rc = cp_->acquire(ch); + php::string name("ZREVRANGEBYSCORE", 16); + + for(int i=3; iexec(rc, name, params, reply_type::ASSOC_ARRAY_1, ch); + } + } + return cp_->exec(rc, name, params, reply_type::SIMPLE, ch); + } + + php::value client::multi(php::parameters& params) { + coroutine_handler ch{coroutine::current}; + auto conn_ = cp_->acquire(ch); + php::object obj(php::class_entry::entry()); + tx *ptr = static_cast(php::native(obj)); + ptr->cl_.reset(new _connection_lock(conn_)); + return std::move(obj); + } + + php::value client::unimplement(php::parameters& params) { + throw php::exception(zend_ce_error_exception, "This redis command is NOT yet implemented"); + } +} // namespace flame::redis diff --git a/src/flame/redis/client.h b/src/flame/redis/client.h new file mode 100644 index 0000000..dbbdcec --- /dev/null +++ b/src/flame/redis/client.h @@ -0,0 +1,34 @@ +#pragma once +#include "../../vendor.h" +#include "redis.h" + +namespace flame::redis { + class _connection_pool; + class client : public php::class_base { + public: + static void declare(php::extension_entry &ext); + php::value __construct(php::parameters ¶ms); // 私有 + php::value __call(php::parameters ¶ms); + php::value __isset(php::parameters& params); + // 处理特殊情况的命令 + php::value mget(php::parameters ¶ms); + php::value hmget(php::parameters ¶ms); + php::value hgetall(php::parameters ¶ms); + php::value hscan(php::parameters ¶ms); + php::value sscan(php::parameters ¶ms); + php::value zscan(php::parameters ¶ms); + php::value zrange(php::parameters ¶ms); + php::value zrevrange(php::parameters ¶ms); + php::value zrangebyscore(php::parameters ¶ms); + php::value zrevrangebyscore(php::parameters ¶ms); + php::value unsubscribe(php::parameters ¶ms); + php::value punsubscribe(php::parameters ¶ms); + // 批量 + php::value multi(php::parameters ¶ms); + // 用于标记不实现的功能 + php::value unimplement(php::parameters ¶ms); + private: + std::shared_ptr<_connection_pool> cp_; + friend php::value connect(php::parameters ¶ms); + }; +} // namespace flame::redis diff --git a/src/flame/redis/redis.cpp b/src/flame/redis/redis.cpp new file mode 100644 index 0000000..82a40d6 --- /dev/null +++ b/src/flame/redis/redis.cpp @@ -0,0 +1,25 @@ +#include "redis.h" +#include "_connection_pool.h" +#include "client.h" +#include "tx.h" + +namespace flame::redis { + + void declare(php::extension_entry& ext) { + ext.function("flame\\redis\\connect", { + {"uri", php::TYPE::STRING} + }); + client::declare(ext); + tx::declare(ext); + } + + php::value connect(php::parameters ¶ms) { + url u(params[0]); + php::object obj {php::class_entry::entry()}; + client *ptr = static_cast(php::native(obj)); + ptr->cp_.reset(new _connection_pool(u)); + ptr->cp_->sweep(); // 启动自动清理扫描 + // TODO 优化: 确认第一个连接建立 ? + return std::move(obj); + } +} diff --git a/src/flame/redis/redis.h b/src/flame/redis/redis.h new file mode 100644 index 0000000..988604d --- /dev/null +++ b/src/flame/redis/redis.h @@ -0,0 +1,8 @@ +#pragma once +#include "../../vendor.h" +#include + +namespace flame::redis { + void declare(php::extension_entry &ext); + php::value connect(php::parameters ¶ms); +} // namespace flame::redis diff --git a/src/flame/redis/tx.cpp b/src/flame/redis/tx.cpp new file mode 100644 index 0000000..6b331b1 --- /dev/null +++ b/src/flame/redis/tx.cpp @@ -0,0 +1,163 @@ +#include "../coroutine.h" +#include "tx.h" +#include "_connection_lock.h" + +namespace flame::redis { + void tx::declare(php::extension_entry& ext) { + php::class_entry class_tx("flame\\redis\\tx"); + class_tx + .method<&tx::__construct>("__construct", {}, php::PRIVATE) + .method<&tx::exec>("exec") + .method<&tx::__call>("__call", { + {"cmd", php::TYPE::STRING}, + {"arg", php::TYPE::ARRAY}, + }) + .method<&tx::mget>("mget", { + {"key", php::TYPE::STRING}, + }) + .method<&tx::hmget>("hmget", { + {"hash", php::TYPE::STRING}, + }) + .method<&tx::hgetall>("hgetall", { + {"hash", php::TYPE::STRING}, + }) + .method<&tx::hscan>("hscan", { + {"hash", php::TYPE::STRING}, + {"cursor", php::TYPE::INTEGER}, + }) + .method<&tx::zscan>("zscan", { + {"zset", php::TYPE::STRING}, + {"cursor", php::TYPE::INTEGER}, + }) + .method<&tx::zrange>("zrange", { + {"zset", php::TYPE::STRING}, + {"start", php::TYPE::INTEGER}, + {"stop", php::TYPE::INTEGER}, + }) + .method<&tx::zrevrange>("zrevrange", { + {"zset", php::TYPE::STRING}, + {"start", php::TYPE::INTEGER}, + {"stop", php::TYPE::INTEGER}, + }) + .method<&tx::zrangebyscore>("zrangebyscore", { + {"zset", php::TYPE::STRING}, + {"min", php::TYPE::STRING}, + {"max", php::TYPE::STRING}, + }) + .method<&tx::zrevrangebyscore>("zrevrangebyscore", { + {"zset", php::TYPE::STRING}, + {"min", php::TYPE::STRING}, + {"max", php::TYPE::STRING}, + }) + .method<&tx::unimplement>("multi") + // 以下暂未实现 + .method<&tx::unimplement>("subscribe") + .method<&tx::unimplement>("psubscribe") + .method<&tx::unimplement>("unsubscribe") + .method<&tx::unimplement>("punsubscribe"); + + ext.add(std::move(class_tx)); + } + + php::value tx::__construct(php::parameters& params) { + return nullptr; + } + + php::value tx::exec(php::parameters& params) { + coroutine_handler ch {coroutine::current}; + return cl_->exec(ch); + } + + php::value tx::__call(php::parameters& params) { + php::string name = params[0]; + php::array argv = params[1]; + cl_->push(name, argv, reply_type::SIMPLE); + return this; + } + + php::value tx::mget(php::parameters& params) { + php::string name("MGET", 4); + cl_->push(name, params, reply_type::COMBINE_1); + return this; + } + + php::value tx::hmget(php::parameters& params) { + php::string name("HMGET", 5); + cl_->push(name, params, reply_type::COMBINE_2); + return this; + } + + php::value tx::hgetall(php::parameters& params) { + php::string name("HGETALL", 7); + cl_->push(name, params, reply_type::ASSOC_ARRAY_1); + return this; + } + + php::value tx::hscan(php::parameters& params) { + php::string name("HSCAN", 5); + cl_->push(name, params, reply_type::ASSOC_ARRAY_2); + return this; + } + + php::value tx::zscan(php::parameters& params) { + php::string name("ZSCAN", 5); + cl_->push(name, params, reply_type::ASSOC_ARRAY_2); + return this; + } + + php::value tx::zrange(php::parameters& params) { + php::string name("ZRANGE", 6); + php::string last = params[params.size()-1]; + if (last.type_of(php::TYPE::STRING) && last.size() == 10 && strncasecmp("WITHSCORES", last.c_str(), 10) == 0) + cl_->push(name, params, reply_type::ASSOC_ARRAY_1); + else + cl_->push(name, params, reply_type::SIMPLE); + return this; + } + + php::value tx::zrevrange(php::parameters& params) { + php::string name("ZREVRANGE", 9); + php::string last = params[params.size() - 1]; + if (last.type_of(php::TYPE::STRING) &&last.size() == 10 && strncasecmp("WITHSCORES", last.c_str(), 10) == 0) + cl_->push(name, params, reply_type::ASSOC_ARRAY_1); + else + cl_->push(name, params, reply_type::SIMPLE); + return this; + } + + php::value tx::zrangebyscore(php::parameters& params) { + php::string name("ZRANGEBYSCORE", 13); + + for(int i=3; ipush(name, params, reply_type::ASSOC_ARRAY_1); + return this; + } + } + } + cl_->push(name, params, reply_type::SIMPLE); + return this; + } + + php::value tx::zrevrangebyscore(php::parameters& params) { + php::string name("ZREVRANGEBYSCORE", 16); + + for(int i=3; ipush(name, params, reply_type::ASSOC_ARRAY_1); + return this; + } + } + } + cl_->push(name, params, reply_type::SIMPLE); + return this; + } + + php::value tx::unimplement(php::parameters& params) { + throw php::exception(zend_ce_error_exception, "This redis command is NOT yet implemented"); + } +} // namespace flame::redis diff --git a/src/flame/redis/tx.h b/src/flame/redis/tx.h new file mode 100644 index 0000000..475aa69 --- /dev/null +++ b/src/flame/redis/tx.h @@ -0,0 +1,35 @@ +#pragma once +#include "../../vendor.h" +#include "redis.h" + +namespace flame::redis { + + class _connection_lock; + class tx : public php::class_base { + public: + static void declare(php::extension_entry &ext); + php::value __construct(php::parameters ¶ms); // 私有 + php::value exec(php::parameters& params); + php::value __call(php::parameters ¶ms); + // 处理特殊情况的命令 + php::value mget(php::parameters ¶ms); + php::value hmget(php::parameters ¶ms); + php::value hgetall(php::parameters ¶ms); + php::value hscan(php::parameters ¶ms); + php::value sscan(php::parameters ¶ms); + php::value zscan(php::parameters ¶ms); + php::value zrange(php::parameters ¶ms); + php::value zrevrange(php::parameters ¶ms); + php::value zrangebyscore(php::parameters ¶ms); + php::value zrevrangebyscore(php::parameters ¶ms); + php::value unsubscribe(php::parameters ¶ms); + php::value punsubscribe(php::parameters ¶ms); + // 批量 + php::value multi(php::parameters ¶ms); + // 用于标记不实现的功能 + php::value unimplement(php::parameters ¶ms); + private: + std::shared_ptr<_connection_lock> cl_; + friend class client; + }; +} // namespace flame::redis diff --git a/src/flame/tcp/server.cpp b/src/flame/tcp/server.cpp new file mode 100644 index 0000000..7253510 --- /dev/null +++ b/src/flame/tcp/server.cpp @@ -0,0 +1,108 @@ +#include "../coroutine.h" +#include "../udp/udp.h" +#include "../time/time.h" +#include "server.h" +#include "tcp.h" +#include "socket.h" +#include "../log/logger.h" + +namespace flame::tcp { + + void server::declare(php::extension_entry &ext) { + php::class_entry class_server("flame\\tcp\\server"); + class_server + .method<&server::__construct>("__construct", { + {"bind", php::TYPE::STRING}, + }) + .method<&server::run>("run", { + {"cb", php::TYPE::CALLABLE}, + }) + .method<&server::close>("close"); + ext.add(std::move(class_server)); + } + + server::server() + : acceptor_(gcontroller->context_x) + , socket_(gcontroller->context_x) { + + } + + typedef boost::asio::detail::socket_option::boolean reuse_port; + + php::value server::__construct(php::parameters ¶ms) { + closed_ = false; + std::string str_addr = params[0]; + auto pair = udp::addr2pair(str_addr); + if (pair.first.empty() || pair.second.empty()) + throw php::exception(zend_ce_error_exception + , "Failed to bind TCP socket: address malformed" + , -1); + boost::asio::ip::address addr = boost::asio::ip::make_address(pair.first); + addr_.address(addr); + addr_.port(std::atoi(pair.second.c_str())); + + set("address", str_addr); + acceptor_.open(addr_.protocol()); + boost::asio::socket_base::reuse_address opt1(true); + acceptor_.set_option(opt1); + reuse_port opt2(true); + acceptor_.set_option(opt2); + + boost::system::error_code err; + acceptor_.bind(addr_, err); + if (err) throw php::exception(zend_ce_exception + , (boost::format("Failed to bind TCP socket: %s") % err.message()).str() + , err.value()); + + acceptor_.listen(boost::asio::socket_base::max_listen_connections, err); + if (err) throw php::exception(zend_ce_exception + , (boost::format("Failed to listen TCP socket: %s") % err.message()).str() + , err.value()); + return nullptr; + } + + php::value server::run(php::parameters ¶ms) { + cb_ = params[0]; + coroutine_handler ch {coroutine::current}; + boost::system::error_code err; + while(!closed_) { + acceptor_.async_accept(socket_, ch[err]); + if (err == boost::asio::error::operation_aborted) break; + else if (err) throw php::exception(zend_ce_error_exception + , (boost::format("Failed to accept TCP socket: %s") % err.message()).str() + , err.value()); + else{ + php::object obj(php::class_entry::entry()); + socket* ptr = static_cast(php::native(obj)); + ptr->socket_ = std::move(socket_); + obj.set("local_address", + (boost::format("%s:%d") % ptr->socket_.local_endpoint().address().to_string() % ptr->socket_.local_endpoint().port()).str()); + obj.set("remote_address", + (boost::format("%s:%d") % ptr->socket_.remote_endpoint().address().to_string() % ptr->socket_.remote_endpoint().port()).str()); + + coroutine::start(php::callable([obj, cb = cb_] (php::parameters& params) -> php::value { + try { + cb.call({obj}); + } catch(const php::exception& ex) { + // 调用用户异常回调 + gcontroller->event("exception", {ex}); + // 记录错误信息 + php::object obj = ex; + log::logger_->stream() << "[" << time::iso() << "] (ERROR) Uncaught Exception in TCP handler: "<< obj.call("__toString") << std::endl; + } + return nullptr; + })); + } + } + cb_ = nullptr; + return nullptr; + } + + php::value server::close(php::parameters ¶ms) { + if (!closed_) { + closed_ = true; + acceptor_.cancel(); + } + return nullptr; + } +} diff --git a/src/flame/tcp/server.h b/src/flame/tcp/server.h new file mode 100644 index 0000000..940a81a --- /dev/null +++ b/src/flame/tcp/server.h @@ -0,0 +1,19 @@ +#pragma once +#include "../../vendor.h" + +namespace flame::tcp { + class server : public php::class_base { + public: + static void declare(php::extension_entry &ext); + server(); + php::value __construct(php::parameters ¶ms); + php::value run(php::parameters ¶ms); + php::value close(php::parameters ¶ms); + private: + boost::asio::ip::tcp::acceptor acceptor_; + boost::asio::ip::tcp::socket socket_; + boost::asio::ip::tcp::endpoint addr_; + php::callable cb_; + bool closed_; + }; +} // namespace flame::tcp diff --git a/src/flame/tcp/socket.cpp b/src/flame/tcp/socket.cpp new file mode 100644 index 0000000..0db883d --- /dev/null +++ b/src/flame/tcp/socket.cpp @@ -0,0 +1,88 @@ +#include "../coroutine.h" +#include "socket.h" + +namespace flame::tcp { + + void socket::declare(php::extension_entry& ext) { + php::class_entry class_socket("flame\\tcp\\socket"); + class_socket + .property({"local_address", ""}) + .property({"remote_address", ""}) + .method<&socket::read>("read", { + {"completion", php::TYPE::UNDEFINED, false, true} + }) + .method<&socket::write>("write", { + {"data", php::TYPE::STRING} + }) + .method<&socket::close>("close"); + ext.add(std::move(class_socket)); + } + + socket::socket() + : socket_(gcontroller->context_x) { + + } + + php::value socket::read(php::parameters& params) { + coroutine_handler ch{coroutine::current}; + // 使用下面锁保证不会同时进行读取 + coroutine_guard guard(rmutex_, ch); + + boost::system::error_code err; + std::size_t len; + if (params.size() == 0) {// 1. 随意读取一段数据 + len = socket_.async_read_some(boost::asio::buffer(buffer_.prepare(8192), 8192), ch[err]); + buffer_.commit(len); + } + else if (params[0].type_of(php::TYPE::STRING)) { // 2. 读取到指定的结束符 + std::string delim = params[0]; + len = boost::asio::async_read_until(socket_, buffer_, delim, ch[err]); + } + else if (params[0].type_of(php::TYPE::INTEGER)) { // 3. 读取指定长度 + std::size_t want = params[0].to_integer(); + if (buffer_.size() >= want) { + len = want; + goto RETURN_DATA; + } + // 读取指定长度 (剩余的缓存数据也算在长度中) + len = boost::asio::async_read(socket_, buffer_, boost::asio::transfer_exactly(want - buffer_.size()), ch[err]); + len = want; + } + else throw php::exception(zend_ce_type_error + , "Failed to read TCP socket: unknown completion type" + , -1); // 未知读取方式 +RETURN_DATA: + // 数据返回 + if (err == boost::asio::error::operation_aborted || err == boost::asio::error::eof) return nullptr; + else if (err) throw php::exception(zend_ce_exception + , (boost::format("Failed to read TCP socket: %s") % err.message()).str() + , err.value()); + else { + php::string data(len); + boost::asio::buffer_copy(boost::asio::buffer(data.data(), len), buffer_.data(), len); + buffer_.consume(len); + return std::move(data); + } + } + + php::value socket::write(php::parameters& params) { + coroutine_handler ch{coroutine::current}; + // 使用下面锁保证不会同时写入 + coroutine_guard guard(wmutex_, ch); + + boost::system::error_code err; + std::string data = params[0]; + std::size_t len = boost::asio::async_write(socket_, boost::asio::buffer(data), ch); + + if (!err || err == boost::asio::error::operation_aborted) return nullptr; + else throw php::exception(zend_ce_exception + , (boost::format("Failed to write TCP socket: %s") % err.message()).str() + , err.value()); + } + + php::value socket::close(php::parameters& params) { + socket_.shutdown(boost::asio::socket_base::shutdown_both); + return nullptr; + } +} + \ No newline at end of file diff --git a/src/flame/tcp/socket.h b/src/flame/tcp/socket.h new file mode 100644 index 0000000..4ecd4f3 --- /dev/null +++ b/src/flame/tcp/socket.h @@ -0,0 +1,25 @@ +#pragma once +#include "../../vendor.h" +#include "../../coroutine_mutex.h" +#include "../coroutine.h" + +namespace flame::tcp { + + class socket : public php::class_base { + public: + static void declare(php::extension_entry &ext); + socket(); + php::value read(php::parameters ¶m); + php::value write(php::parameters ¶ms); + php::value close(php::parameters ¶ms); + + private: + boost::asio::ip::tcp::socket socket_; + boost::asio::streambuf buffer_; + coroutine_mutex rmutex_; + coroutine_mutex wmutex_; + + friend class server; + friend php::value connect(php::parameters ¶ms); + }; +} // namespace flame::tcp diff --git a/src/flame/tcp/tcp.cpp b/src/flame/tcp/tcp.cpp new file mode 100644 index 0000000..ed297c2 --- /dev/null +++ b/src/flame/tcp/tcp.cpp @@ -0,0 +1,66 @@ +#include "../coroutine.h" +#include "../udp/udp.h" +#include "tcp.h" +#include "socket.h" +#include "server.h" + +namespace flame::tcp { + static std::unique_ptr resolver_; + void declare(php::extension_entry& ext) { + gcontroller->on_init([] (const php::array& options) { + resolver_.reset(new boost::asio::ip::tcp::resolver(gcontroller->context_x)); + })->on_stop([] () { + resolver_.reset(); + }); + ext + .function("flame\\tcp\\connect", { + {"address", php::TYPE::STRING}, + }); + socket::declare(ext); + server::declare(ext); + } + + php::value connect(php::parameters& params) { + php::object obj(php::class_entry::entry()); + socket *ptr = static_cast(php::native(obj)); + + std::string str = params[0]; + auto pair = udp::addr2pair(str); + if (pair.first.empty() || pair.second.empty()) throw php::exception(zend_ce_type_error + , "Failed to connect TCP socket: illegal address format" + , -1); + + coroutine_handler ch{coroutine::current}; + boost::system::error_code err; + boost::asio::ip::tcp::resolver::results_type eps; + // DNS 地址解析 + resolver_->async_resolve(pair.first, pair.second, [&err, &eps, &ch] (const boost::system::error_code& error, boost::asio::ip::tcp::resolver::results_type results) { + if (error) err = error; + else eps = results; + ch.resume(); + }); + ch.suspend(); + if (err) throw php::exception(zend_ce_exception + , (boost::format("Failed to resolve address: %s") % err.message()).str() + , err.value()); + + // 连接 + boost::asio::async_connect(ptr->socket_, eps, [&err, &obj, &ptr, &ch](const boost::system::error_code &error, const boost::asio::ip::tcp::endpoint &edp) { + if (error) err = error; + else { + obj.set("local_address", (boost::format("%s:%d") + % ptr->socket_.local_endpoint().address().to_string() + % ptr->socket_.local_endpoint().port() ).str()); + obj.set("remote_address", (boost::format("%s:%d") + % ptr->socket_.remote_endpoint().address().to_string() + % ptr->socket_.remote_endpoint().port() ).str()); + } + ch.resume(); + }); + ch.suspend(); + if (err) throw php::exception(zend_ce_exception + , (boost::format("Failed to connect TCP socket: %s") % err.message()).str() + , err.value()); + return std::move(obj); + } +} // namespace flame::tcp diff --git a/src/flame/tcp/tcp.h b/src/flame/tcp/tcp.h new file mode 100644 index 0000000..9b21a4b --- /dev/null +++ b/src/flame/tcp/tcp.h @@ -0,0 +1,7 @@ +#pragma once +#include "../../vendor.h" + +namespace flame::tcp { + void declare(php::extension_entry &ext); + php::value connect(php::parameters ¶ms); +} // namespace flame::tcp diff --git a/src/flame/time/time.cpp b/src/flame/time/time.cpp new file mode 100644 index 0000000..7913e94 --- /dev/null +++ b/src/flame/time/time.cpp @@ -0,0 +1,64 @@ +#include "time.h" +#include "../coroutine.h" + +namespace flame::time { + + static std::chrono::time_point time_system; + static std::chrono::time_point time_steady; + + static php::value sleep(php::parameters& params) { + coroutine_handler ch {coroutine::current}; + boost::asio::steady_timer tm(gcontroller->context_x); + int ms = static_cast(params[0]); + if (ms < 0) ms = 1; + tm.expires_from_now(std::chrono::milliseconds(ms)); + tm.async_wait(ch); + return nullptr; + } + + std::chrono::time_point now() { + std::chrono::milliseconds diff = std::chrono::duration_cast( + std::chrono::steady_clock::now() - time_steady); + return time_system + diff; + } + + php::string iso(const std::chrono::time_point& now) { + php::string data(19); + std::time_t t = std::chrono::system_clock::to_time_t(now); + struct tm *m = std::localtime(&t); + sprintf(data.data(), "%04d-%02d-%02d %02d:%02d:%02d", + 1900 + m->tm_year, + 1 + m->tm_mon, + m->tm_mday, + m->tm_hour, + m->tm_min, + m->tm_sec); + return std::move(data); + } + + php::string iso() { + return iso(now()); + } + + static php::value now(php::parameters ¶ms) { + return std::chrono::duration_cast(now().time_since_epoch()).count(); + } + + static php::value iso(php::parameters ¶ms) { + return iso(); + } + + void declare(php::extension_entry& ext) { + time_steady = std::chrono::steady_clock::now(); + time_system = std::chrono::system_clock::now(); + + ext + .function("flame\\time\\sleep", { + {"duration", php::TYPE::INTEGER}, + }) + // 毫秒时间戳 + .function("flame\\time\\now") + // 标准时间 YYYY-mm-dd HH:ii:ss + .function("flame\\time\\iso"); + } +} diff --git a/src/flame/time/time.h b/src/flame/time/time.h new file mode 100644 index 0000000..f3e666f --- /dev/null +++ b/src/flame/time/time.h @@ -0,0 +1,8 @@ +#include "../../vendor.h" + +namespace flame::time { + void declare(php::extension_entry &ext); + std::chrono::time_point now(); + php::string iso(); + php::string iso(const std::chrono::time_point& now); +} diff --git a/src/flame/toml/_decode.cpp b/src/flame/toml/_decode.cpp new file mode 100755 index 0000000..744999f --- /dev/null +++ b/src/flame/toml/_decode.cpp @@ -0,0 +1,7 @@ +#include "_decode.h" + +namespace flame::toml { + void decode_inplace(php::string& str) { + php_stripcslashes(str); + } +} \ No newline at end of file diff --git a/src/flame/toml/_decode.h b/src/flame/toml/_decode.h new file mode 100755 index 0000000..7af516c --- /dev/null +++ b/src/flame/toml/_decode.h @@ -0,0 +1,6 @@ +#pragma once +#include "../../vendor.h" + +namespace flame::toml { + void decode_inplace(php::string& str); +} \ No newline at end of file diff --git a/src/flame/toml/_executor.cpp b/src/flame/toml/_executor.cpp new file mode 100755 index 0000000..4ef63e0 --- /dev/null +++ b/src/flame/toml/_executor.cpp @@ -0,0 +1,70 @@ +#include "_executor.h" +#include "_decode.h" + +namespace flame::toml { + +void _executor::operator ()(const toml_parser::parser& p, std::string_view chunk) { + buffer_.append(chunk.data(), chunk.size()); +} + +void _executor::operator ()(const toml_parser::parser& p) { + php::string raw = std::move(buffer_); + // std::cout << "(" << (int)p.value_type() << ") " << p.field() << " => [" << raw << "]\n"; + php::value val = _executor::restore(raw, p.value_type()); + + // switch(p.container_type()) { + // case toml_parser::CONTAINER_TYPE_ARRAY: + // break; + // case toml_parser::CONTAINER_TYPE_ARRAY_TABLE: + // break; + // case toml_parser::CONTAINER_TYPE_TABLE: + // default: + // ; + // } + set(root_, p.field(), p.value_array_index(), val); +} + +php::value _executor::restore(php::string& raw, std::uint8_t value_type) { + php::value val; + // 根据类型还原数据 + switch(value_type) { + case toml_parser::VALUE_TYPE_BOOLEAN: + val = raw.to_boolean(); + break; + case toml_parser::VALUE_TYPE_INTEGER: + val = raw.to_integer(); + break; + case toml_parser::VALUE_TYPE_HEX_INTEGER: + val = std::strtol(raw.data(), nullptr, 16); + break; + case toml_parser::VALUE_TYPE_OCT_INTEGER: + val = std::strtol(raw.data(), nullptr, 8); + break; + case toml_parser::VALUE_TYPE_BIN_INTEGER: + val = std::strtol(raw.data(), nullptr, 2); + break; + case toml_parser::VALUE_TYPE_INFINITY: + val = raw.data()[0] == '-' ? - std::numeric_limits::infinity() : std::numeric_limits::infinity(); + break; + case toml_parser::VALUE_TYPE_NAN: + val = NAN; + break; + case toml_parser::VALUE_TYPE_FLOAT: + val = raw.to_float(); + break; + case toml_parser::VALUE_TYPE_DATE: + val = php::datetime(raw.c_str()); + break; + case toml_parser::VALUE_TYPE_BASIC_STRING: + decode_inplace(raw); + // fallthrough + case toml_parser::VALUE_TYPE_UNKNOWN: // 未知类型的数据按原始字符串处理 + case toml_parser::VALUE_TYPE_RAW_STRING: + default: + val = raw; + } + + return val; +} + +} diff --git a/src/flame/toml/_executor.h b/src/flame/toml/_executor.h new file mode 100755 index 0000000..ce8a2cb --- /dev/null +++ b/src/flame/toml/_executor.h @@ -0,0 +1,21 @@ +#pragma once + +#include "../../vendor.h" +#include "toml.h" + +namespace flame::toml { + + class _executor { + public: + _executor(php::array& root) noexcept + : root_(root) {} + void operator ()(const toml_parser::parser& p, std::string_view chunk); + void operator ()(const toml_parser::parser& p); + + private: + php::array& root_; + php::buffer buffer_; + + static php::value restore(php::string& raw, std::uint8_t value_type); + }; +} diff --git a/src/flame/toml/toml.cpp b/src/flame/toml/toml.cpp new file mode 100755 index 0000000..5b27819 --- /dev/null +++ b/src/flame/toml/toml.cpp @@ -0,0 +1,120 @@ +#include "toml.h" +#include "_executor.h" +#include + +namespace flame::toml { + + void set(php::array& root, std::string_view prefix, std::size_t index, const php::value& v) { + std::size_t pe = -1, ps = -1; + zval x; + ZVAL_ARR(&x, static_cast(root)); + php::array ctr (&x, true); // 纯引用:容器数组 + std::string_view field = prefix.substr( prefix.find_last_of('.') + 1 ); + prefix = prefix.substr(0, prefix.size() - field.size() - 1); + do { + ps = pe = pe + 1; + pe = prefix.find_first_of('.', ps); // prefix 存在,但不含有 . 也需要进行下面动作 + auto sv = prefix.substr(ps, pe - ps); + if (sv.size() == 0 || sv == "$") { + // root 元素不做处理 + } + else if (sv == "#") { + php::value y = ctr.get(index); + if(y.type_of(php::TYPE::UNDEFINED)) { + php::value z = php::array(4); + ctr.set(index, z, /* seperate = */false); // 更新原数组 + ZVAL_ARR(&x, static_cast(z)); + } + else if (y.type_of(php::TYPE::ARRAY)) ZVAL_ARR(&x, static_cast(y)); + else throw php::exception(zend_ce_type_error, "Failed to set: container (index) is not an array", -1); + } + else { + php::string key {sv.data(), sv.size()}; + php::value y = ctr.get(key); + if(y.type_of(php::TYPE::UNDEFINED)) { + php::value z = php::array(4); + ctr.set(key, z, /* seperate = */false); // 更新原数组 + ZVAL_ARR(&x, static_cast(z)); + } + else if(y.type_of(php::TYPE::ARRAY)) ZVAL_ARR(&x, static_cast(y)); + else throw php::exception(zend_ce_type_error, "Failed to set: container (field) is not an array", -1); + } + } while(pe != prefix.npos); + + if(field.empty()) ctr.set(ctr.size(), v); + else ctr.set({field.data(), field.size()}, v); + } + + php::value get(php::array root, std::string_view prefix, std::size_t index) { + std::size_t pe = -1, ps = -1; + zval x; + ZVAL_ARR(&x, static_cast(root)); + php::array ctr (&x, true); // 指向容器数组 + std::string_view field = prefix.substr( prefix.find_last_of('.') + 1 ); + prefix = prefix.substr(0, prefix.size() - field.size() - 1); + do { + ps = pe = pe + 1; + pe = prefix.find_first_of('.', ps); // prefix 存在,但不含有 . 也需要进行下面动作 + auto sv = prefix.substr(ps, pe - ps); + if (sv.size() == 0 || sv == "$") { + // root 元素不做处理 + // 无 prefix 不做处理 + } + else if (sv == "#") { + php::value y = ctr.get(index); + if (y.type_of(php::TYPE::UNDEFINED)) return php::value(); + else if (y.type_of(php::TYPE::ARRAY)) ZVAL_ARR(&x, static_cast(y)); + else throw php::exception(zend_ce_type_error, "Failed to get: container (index) is not an array", -1); + } + else { + php::value y = ctr.get({sv.data(), sv.size()}); + if (y.type_of(php::TYPE::UNDEFINED)) return php::value(); + else if (y.type_of(php::TYPE::ARRAY)) ZVAL_ARR(&x, static_cast(y)); + else throw php::exception(zend_ce_type_error, "Failed to get: container (field) is not an array", -1); + } + } while(pe != prefix.npos); + + if(field.empty()) return ctr; + else return ctr.get({field.data(), field.size()}); + } + + static php::value parse_string(php::parameters& params) { + php::string s = params[0]; + php::array r(8); + _executor e(r); + llparse::toml::parser p({std::ref(e), std::ref(e)}); + p.parse({s.data(), s.size()}); + return r; + } + + static php::value parse_file(php::parameters& params) { + std::string f = params[0]; + std::ifstream s {f.c_str()}; + if (!s.is_open()) + throw php::exception(zend_ce_error_exception, (boost::format("Failed to parse toml: unable to open file '%s'") % f).str(), -1); + + php::array r(8); + _executor e(r); + llparse::toml::parser p({std::ref(e), std::ref(e)}); + char buffer[2048]; + std::size_t length = 0; + while(!s.eof()) { + s.read(buffer, sizeof(buffer)); + length = s.gcount(); + if (length > 0) p.parse({buffer, length}); + } + return r; + } + + + + void declare(php::extension_entry &ext) { + ext + .function("flame\\toml\\parse_string", { + {"toml", php::TYPE::STRING}, + }) + .function("flame\\toml\\parse_file", { + {"file", php::TYPE::STRING}, + }); + } +} \ No newline at end of file diff --git a/src/flame/toml/toml.h b/src/flame/toml/toml.h new file mode 100755 index 0000000..c0dab8b --- /dev/null +++ b/src/flame/toml/toml.h @@ -0,0 +1,10 @@ +#include "../../vendor.h" +#include +namespace toml_parser = llparse::toml; + +namespace flame::toml { + void declare(php::extension_entry &ext); + + void set(php::array& r, std::string_view prefix, std::size_t index, const php::value& v); + php::value get(php::array r, std::string_view prefix, std::size_t index); +} diff --git a/src/flame/udp/server.cpp b/src/flame/udp/server.cpp new file mode 100644 index 0000000..945ff3a --- /dev/null +++ b/src/flame/udp/server.cpp @@ -0,0 +1,165 @@ +#include "../../coroutine_queue.h" +#include "../coroutine.h" +#include "../time/time.h" +#include "udp.h" +#include "server.h" +#include "../log/logger.h" + +namespace flame::udp { + + void server::declare(php::extension_entry& ext) { + php::class_entry class_server("flame\\udp\\server"); + class_server + .method<&server::__construct>("__construct", { + {"address", php::TYPE::STRING, false, false}, + {"options", php::TYPE::ARRAY, false, true}, + }) + .method<&server::run>("run", { + {"length", php::TYPE::CALLABLE, false, false}, + }) + .method<&server::send_to>("send_to", { + {"data", php::TYPE::STRING, false, false}, + {"address", php::TYPE::STRING, false, false}, + }) + .method<&server::close>("close"); + ext.add(std::move(class_server)); + } + server::server() + : socket_(gcontroller->context_x) + , closed_(false) + , concurrent_(8) + , max_(64 * 1024) { + + } + + typedef boost::asio::detail::socket_option::boolean reuse_port; + + php::value server::__construct(php::parameters& params) { + auto pair = addr2pair(params[0]); + boost::system::error_code err; + boost::asio::ip::address address = boost::asio::ip::make_address(std::string_view(pair.first.data(), pair.first.size()), err); + if (err) throw php::exception(zend_ce_exception + , (boost::format("Failed to bind: %s") % err.message()).str() + , err.value()); + std::uint16_t port = std::stoi(pair.second); + addr_.address(address); + addr_.port(port); + + socket_.open(addr_.protocol()); + boost::asio::socket_base::reuse_address opt1(true); + socket_.set_option(opt1); + reuse_port opt2(true); + socket_.set_option(opt2); + socket_.bind(addr_, err); + if (err) throw php::exception(zend_ce_exception + , (boost::format("Failed to bind: %s") % err.message()).str() + , err.value()); + + if (params.size() > 1) { + php::array options = params[1]; + if (options.exists("concurrent")) + concurrent_ = std::max(std::min(static_cast(options.get("concurrent")), 256), 1); + if (options.exists("max")) + max_ = std::min(std::max(static_cast(options.get("max")) , 1), 64 * 1024); + } + return nullptr; + } + + php::value server::run(php::parameters& params) { + php::callable cb_ = params[0]; + + coroutine_handler ch {coroutine::current}; + coroutine_queue> q(128); + // 启动若干协程, 然后进行"并行>"消费 + int count = concurrent_; + for (int i = 0; i < concurrent_; ++i) { + // 启动协程开始消费 + coroutine::start(php::value([&q, &count, &ch, &cb_, i] (php::parameters ¶ms) -> php::value { + coroutine_handler cch {coroutine::current}; + while(auto x = q.pop(cch)) { + try { + cb_.call({x->first, x->second}); + } catch(const php::exception& ex) { + // 调用用户异常回调 + gcontroller->event("exception", {ex}); + // 记录错误信息 + php::object obj = ex; + log::logger_->stream() << "[" << time::iso() << "] (ERROR) Uncaught Exception in UDP handler: " << obj.call("__toString") << std::endl; + } + } + if (--count == 0) ch.resume(); + return nullptr; + })); + } + // 生产 + std::size_t len = 0; + boost::system::error_code err; + boost::asio::ip::udp::endpoint ep; + php::buffer buffer; + while(!closed_) { + socket_.async_receive_from(boost::asio::buffer(buffer.prepare(max_), max_), ep + , [&len, &err, &ch] (const boost::system::error_code& error, std::size_t nread) { + + if (error) err = error; + else len = nread; + ch.resume(); + }); + ch.suspend(); + // 存在可能被关闭, 直接返回 + if (err == boost::asio::error::operation_aborted) goto CLOSING; + else if (err) throw php::exception(zend_ce_exception + , (boost::format("Failed to read TCP socket: %s") % err.message()).str() + , err.value()); + buffer.commit(len); + q.push(std::make_pair(php::string(std::move(buffer)) + , (boost::format("%s:%d") % ep.address().to_string(err) % ep.port()).str()), ch); + } +CLOSING: + q.close(); + ch.suspend(); + return nullptr; + } + + php::value server::send_to(php::parameters& params) { + coroutine_handler ch{coroutine::current}; + + boost::system::error_code err; + php::string data = params[0]; + auto pair = addr2pair(params[1]); + if (pair.first.empty() || pair.second.empty()) throw php::exception(zend_ce_type_error + , "Failed to send udp packet: illegal address format" + , -1); + + boost::asio::ip::udp::resolver::results_type eps; + resolver_->async_resolve(pair.first, pair.second + , [&ch, &eps, &err] (const boost::system::error_code& error, boost::asio::ip::udp::resolver::results_type results) { + + if (error) err = error; + else eps = results; + ch.resume(); + }); + ch.suspend(); + if (err == boost::asio::error::operation_aborted) return nullptr; + else if (err) throw php::exception(zend_ce_exception + , (boost::format("Failed to resolve address: %s") % err.message()).str() + , err.value()); + + // 发送 + int sent = 0; + for(auto i=eps.begin(); i!=eps.end(); ++i) { + socket_.async_send_to(boost::asio::buffer(data.c_str(), data.size()), *i, ch[err]); + if (!err) return nullptr; + } + + throw php::exception(zend_ce_exception + , (boost::format("Failed to send UDP packet: %s") % err.message()).str() + , err.value()); + } + + php::value server::close(php::parameters& params) { + closed_ = true; + boost::system::error_code err; + socket_.close(err); + return true; + } +} diff --git a/src/flame/udp/server.h b/src/flame/udp/server.h new file mode 100644 index 0000000..d3b0d4a --- /dev/null +++ b/src/flame/udp/server.h @@ -0,0 +1,22 @@ +#pragma once +#include "../../vendor.h" +#include "../../coroutine_mutex.h" +#include "../coroutine.h" + +namespace flame::udp { + class server: public php::class_base { + public: + static void declare(php::extension_entry &ext); + server(); + php::value __construct(php::parameters& params); + php::value run(php::parameters& params); + php::value send_to(php::parameters& params); + php::value close(php::parameters& params); + private: + boost::asio::ip::udp::socket socket_; + boost::asio::ip::udp::endpoint addr_; + int concurrent_; + int max_; + bool closed_; + }; +} diff --git a/src/flame/udp/socket.cpp b/src/flame/udp/socket.cpp new file mode 100644 index 0000000..25351a1 --- /dev/null +++ b/src/flame/udp/socket.cpp @@ -0,0 +1,177 @@ +#include "../coroutine.h" +#include "udp.h" +#include "socket.h" + +namespace flame::udp { + + void socket::declare(php::extension_entry& ext) { + php::class_entry class_socket("flame\\udp\\socket"); + class_socket + .property({"local_address", ""}) + .property({"remote_address", ""}) + .method<&socket::__construct>("__construct", { + {"address", php::TYPE::STRING, false, true}, + }) + .method<&socket::recv_from>("recv_from", { + {"address", php::TYPE::STRING, true, true}, + }) + .method<&socket::recv>("recv") + .method<&socket::send_to>("send_to", { + {"data", php::TYPE::STRING, false, false}, + {"address", php::TYPE::STRING, false, false}, + }) + .method<&socket::send>("send") + .method<&socket::close>("close"); + ext.add(std::move(class_socket)); + } + + socket::socket() + : socket_(gcontroller->context_x) + , connected_(false) + , max_(64 * 1024) { + + } + + typedef boost::asio::detail::socket_option::boolean reuse_port; + + php::value socket::__construct(php::parameters& params) { + boost::system::error_code err; + php::array option; + if (params.size() > 0 && params[0].type_of(php::TYPE::STRING)) { + auto pair = addr2pair(params[0]); + boost::asio::ip::address address = boost::asio::ip::make_address(std::string_view(pair.first.data(), pair.first.size()), err); + if (err) throw php::exception(zend_ce_exception + , (boost::format("Failed to bind: %s") % err.message()).str() + , err.value()); + std::uint16_t port = std::stoi(pair.second); + auto ep = boost::asio::ip::udp::endpoint(address, port); + socket_.open(ep.protocol()); + boost::asio::socket_base::reuse_address opt1(true); + socket_.set_option(opt1); + reuse_port opt2(true); + socket_.set_option(opt2); + socket_.bind(ep, err); + if (err) throw php::exception(zend_ce_exception + , (boost::format("failed to bind: %s") % err.message()).str() + , err.value()); + if (params.size() > 1 && params[1].type_of(php::TYPE::ARRAY)) { + option = params[0]; + goto PARSE_OPTION; + } + } + else { + socket_.open(boost::asio::ip::udp::v6(), err); + if (err) socket_.open(boost::asio::ip::udp::v4(), err); + if (err) throw php::exception(zend_ce_exception + , (boost::format("Failed to bind: %s") % err.message()).str() + , err.value()); + if (params.size() > 0 && params[0].type_of(php::TYPE::ARRAY)) { + option = params[0]; + goto PARSE_OPTION; + } + } + return nullptr; +PARSE_OPTION: + if (option.exists("max")) + max_ = std::min(std::max(static_cast(option.get("max")), 64), 64 * 1024); + return nullptr; + } + + php::value socket::recv(php::parameters& params) { + if (!connected_) throw php::exception(zend_ce_error_exception + , "Failed to send: socket not connected" + , -1); + coroutine_handler ch{coroutine::current}; + // 使用下面锁保证不会同时进行读取 + coroutine_guard guard(rmutex_, ch); + boost::system::error_code err; + std::size_t len = 0; + len = socket_.async_receive(boost::asio::buffer(buffer_.prepare(max_), max_), ch[err]); + buffer_.commit(len); + // 数据返回 + if (err == boost::asio::error::operation_aborted || err == boost::asio::error::eof) return nullptr; + else if (err) throw php::exception(zend_ce_exception + , (boost::format("Failed to recv UDP packet: %s") % err.message()).str() + , err.value()); + else return php::string(std::move(buffer_)); + } + + php::value socket::recv_from(php::parameters& params) { + coroutine_handler ch{coroutine::current}; + // 使用下面锁保证不会同时进行读取 + coroutine_guard guard(rmutex_, ch); + + boost::system::error_code err; + std::size_t len = 0; + boost::asio::ip::udp::endpoint ep; + len = socket_.async_receive_from(boost::asio::buffer(buffer_.prepare(max_), max_), ep, ch[err]); + buffer_.commit(len); + // 数据返回 + if (err == boost::asio::error::operation_aborted || err == boost::asio::error::eof) return nullptr; + else if (err) throw php::exception(zend_ce_exception + , (boost::format("failed to read UDP socket: %s") % err.message()).str() + , err.value()); + if (params.size() > 0) params[0] = (boost::format("%s:%d") % ep.address().to_string(err) % ep.port()).str(); + return php::string(std::move(buffer_)); + } + + php::value socket::send(php::parameters& params) { + if (!connected_) throw php::exception(zend_ce_error_exception + , "Failed to send: connected socket required" + , -1); + coroutine_handler ch{coroutine::current}; + + boost::system::error_code err; + std::string data = params[0]; + socket_.async_send(boost::asio::buffer(data), ch[err]); + + if (!err || err == boost::asio::error::operation_aborted + || err == boost::asio::error::connection_refused) return nullptr; + else throw php::exception(zend_ce_exception + , (boost::format("Failed to write UDP socket: %s") % err.message()).str() + , err.value()); + return nullptr; + } + + php::value socket::send_to(php::parameters& params) { + coroutine_handler ch{coroutine::current}; + + boost::system::error_code err; + php::string data = params[0]; + auto pair = addr2pair(params[1]); + if (pair.first.empty() || pair.second.empty()) throw php::exception(zend_ce_type_error + , "Failed to send udp packet: illegal address format" + , -1); + + boost::asio::ip::udp::resolver::results_type eps; + resolver_->async_resolve(pair.first, pair.second, + [&ch, &eps, &err] (const boost::system::error_code& error, boost::asio::ip::udp::resolver::results_type results) { + + if (error) err = error; + else eps = results; + ch.resume(); + }); + ch.suspend(); + if (err == boost::asio::error::operation_aborted) return nullptr; + else if (err) throw php::exception(zend_ce_exception + , (boost::format("Failed to resolve address: %s") % err.message()).str() + , err.value()); + + // 发送 + int sent = 0; + for(auto i=eps.begin(); i!=eps.end(); ++i) { + socket_.async_send_to(boost::asio::buffer(data.c_str(), data.size()), *i, ch[err]); + if (!err) return nullptr; + } + + throw php::exception(zend_ce_exception + , (boost::format("Failed to send UDP packet: %s") % err.message()).str() + , err.value()); + } + + php::value socket::close(php::parameters& params) { + connected_ = false; + socket_.shutdown(boost::asio::socket_base::shutdown_both); + return nullptr; + } +} diff --git a/src/flame/udp/socket.h b/src/flame/udp/socket.h new file mode 100644 index 0000000..8e95ae5 --- /dev/null +++ b/src/flame/udp/socket.h @@ -0,0 +1,29 @@ +#pragma once +#include "../../vendor.h" +#include "../../coroutine_mutex.h" +#include "../coroutine.h" + +namespace flame::udp { + + class socket : public php::class_base { + public: + static void declare(php::extension_entry &ext); + socket(); + php::value __construct(php::parameters ¶m); + php::value recv_from(php::parameters ¶m); + php::value recv(php::parameters& params); + php::value send_to(php::parameters ¶ms); + php::value send(php::parameters& params); + php::value close(php::parameters ¶ms); + + private: + boost::asio::ip::udp::socket socket_; + php::buffer buffer_; + coroutine_mutex rmutex_; + bool connected_; + int max_; + + friend class server; + friend php::value connect(php::parameters ¶ms); + }; +} // namespace flame::udp diff --git a/src/flame/udp/udp.cpp b/src/flame/udp/udp.cpp new file mode 100644 index 0000000..63f7e62 --- /dev/null +++ b/src/flame/udp/udp.cpp @@ -0,0 +1,75 @@ +#include "../coroutine.h" +#include "udp.h" +#include "socket.h" +#include "server.h" + +namespace flame::udp { + + std::unique_ptr resolver_; + + void declare(php::extension_entry& ext) { + gcontroller->on_init([] (const php::array& options) { + resolver_.reset(new boost::asio::ip::udp::resolver(gcontroller->context_x)); + })->on_stop([] () { + resolver_.reset(); + }); + ext + .function("flame\\udp\\connect", { + {"address", php::TYPE::STRING}, + }); + socket::declare(ext); + server::declare(ext); + } + + php::value connect(php::parameters& params) { + php::object obj(php::class_entry::entry()); + socket *ptr = static_cast(php::native(obj)); + + std::string str = params[0]; + auto pair = addr2pair(str); + if (pair.first.empty() || pair.second.empty()) throw php::exception(zend_ce_type_error + , "Failed to connect UDP socket: illegal address format" + , -1); + + coroutine_handler ch{coroutine::current}; + boost::system::error_code err; + boost::asio::ip::udp::resolver::results_type eps; + resolver_->async_resolve(pair.first, pair.second + , [&err, &obj, &ptr, &eps, &ch] (const boost::system::error_code& error, boost::asio::ip::udp::resolver::results_type results) { // DNS 地址解析 + + if (error) err = error; + else eps = results; + ch.resume(); + }); + ch.suspend(); + if (err) throw php::exception(zend_ce_exception + , (boost::format("Failed to resolve address: %s") % err.message()).str() + , err.value()); + + boost::asio::async_connect(ptr->socket_, eps + , [&err, &obj, &ptr, &ch](const boost::system::error_code &error, const boost::asio::ip::udp::endpoint &edp) { // 连接 + + if (error) err = error; + else { + obj.set("local_address", (boost::format("%s:%d") % ptr->socket_.local_endpoint().address().to_string() % ptr->socket_.local_endpoint().port()).str()); + obj.set("remote_address", (boost::format("%s:%d") % ptr->socket_.remote_endpoint().address().to_string() % ptr->socket_.remote_endpoint().port()).str()); + } + ch.resume(); + }); + ch.suspend(); + if (err) throw php::exception(zend_ce_exception + , (boost::format("Failed to connect UDP socket: %s") % err.message()).str() + , err.value()); + ptr->connected_ = true; + return std::move(obj); + } + + std::pair addr2pair(const std::string &addr) { + char *s = const_cast(addr.data()), *p, *e = s + addr.size(); + for (p = e - 1; p > s; --p) if (*p == ':') break; // 分离 地址与端口 + if (*p != ':') return std::make_pair(std::string(addr), ""); + else return std::make_pair( + std::string(s, p - s), std::string(p+1, e-p-1)); + } + +} // namespace flame::tcp diff --git a/src/flame/udp/udp.h b/src/flame/udp/udp.h new file mode 100644 index 0000000..b61a209 --- /dev/null +++ b/src/flame/udp/udp.h @@ -0,0 +1,10 @@ +#pragma once +#include "../../vendor.h" + +namespace flame::udp { + extern std::unique_ptr resolver_; + void declare(php::extension_entry &ext); + php::value bind_and_listen(php::parameters& params); + php::value connect(php::parameters ¶ms); + std::pair addr2pair(const std::string& addr); +} // namespace flame::tcp diff --git a/src/flame/version.cpp b/src/flame/version.cpp new file mode 100644 index 0000000..c1af73a --- /dev/null +++ b/src/flame/version.cpp @@ -0,0 +1,43 @@ +#include "version.h" +#include +#include +#include +#include +#include +#include +// #include +#include +#include + +namespace flame { + +#define VERSION_MACRO(major, minor, patch) VERSION_JOIN(major, minor, patch) +#define VERSION_JOIN(major, minor, patch) #major"."#minor"."#patch + +static std::string openssl_version_str() { + std::string version = (boost::format("%d.%d.%d") + % ((OPENSSL_VERSION_NUMBER & 0xff0000000L) >> 28) + % ((OPENSSL_VERSION_NUMBER & 0x00ff00000L) >> 20) + % ((OPENSSL_VERSION_NUMBER & 0x0000ff000L) >> 12) ).str(); + char status = (OPENSSL_VERSION_NUMBER & 0x000000ff0L) >> 4; + if (status > 0) { + version.push_back('a' + status - 1); + } + return version; +} + + void version::declare(php::extension_entry& ext) { + ext + .desc({"vendor/openssl", openssl_version_str()}) + .desc({"vendor/boost", BOOST_LIB_VERSION}) + .desc({"vendor/phpext", PHPEXT_LIB_VERSION}) + .desc({"vendor/hiredis", VERSION_MACRO(HIREDIS_MAJOR, HIREDIS_MINOR, HIREDIS_PATCH)}) + // .desc({"vendor/mysqlc", mysql_get_client_info()}) + .desc({"vendor/mariac", MARIADB_PACKAGE_VERSION}) + .desc({"vendor/amqpcpp", "4.1.5"}) + .desc({"vendor/rdkafka", rd_kafka_version_str()}) + .desc({"vendor/mongoc", MONGOC_VERSION_S}) + .desc({"vendor/nghttp2", NGHTTP2_VERSION}) + .desc({"vendor/curl", LIBCURL_VERSION}); + } +} \ No newline at end of file diff --git a/src/flame/version.h b/src/flame/version.h new file mode 100644 index 0000000..471fd7f --- /dev/null +++ b/src/flame/version.h @@ -0,0 +1,9 @@ +#pragma once +#include "../vendor.h" + +namespace flame { + class version { + public: + static void declare(php::extension_entry& ext); + }; +} \ No newline at end of file diff --git a/src/flame/worker.cpp b/src/flame/worker.cpp new file mode 100644 index 0000000..018c1ae --- /dev/null +++ b/src/flame/worker.cpp @@ -0,0 +1,333 @@ +#include "../util.h" +#include "../worker_logger.h" +#include "controller.h" +#include "worker.h" +#include "coroutine.h" + +#include "queue.h" +#include "mutex.h" +#include "log/log.h" +#include "log/logger.h" +#include "os/os.h" +#include "time/time.h" +#include "mysql/mysql.h" +#include "redis/redis.h" +#include "mongodb/mongodb.h" +#include "kafka/kafka.h" +#include "rabbitmq/rabbitmq.h" +#include "tcp/tcp.h" +#include "udp/udp.h" +#include "http/http.h" +#include "hash/hash.h" +#include "encoding/encoding.h" +#include "compress/compress.h" +#include "toml/toml.h" + +namespace flame { + std::shared_ptr worker::ww_; + + void worker::declare(php::extension_entry& ext) { + ext + .on_request_shutdown([] (php::extension_entry& ext) -> bool { + if (gcontroller->status & controller::STATUS_INITIALIZED) { + if (php::error::exists()) + log::logger_->stream() << "[" << util::system_time() << "] (WARNING) process exited prematurely: uncaught exception / error\n" << std::endl; + if (!(gcontroller->status & controller::STATUS_RUN)) + log::logger_->stream() << "[" << util::system_time() << "] (WARNING) process exited prematurely: missing 'flame\\run();'" << std::endl; + + gcontroller->status & controller::STATUS_EXCEPTION ? _exit(-1) : _exit(0); + } + return true; + }) + .function("flame\\init", { + {"process_name", php::TYPE::STRING}, + {"options", php::TYPE::ARRAY, false, true}, + }) + + .function("flame\\go", { + {"coroutine", php::TYPE::CALLABLE}, + }) + .function("flame\\on", { + {"event", php::TYPE::STRING}, + {"callback", php::TYPE::CALLABLE}, + }) + .function("flame\\off", { + {"event", php::TYPE::STRING}, + }) + .function("flame\\run") + .function("flame\\quit") + .function("flame\\co_id") + .function("flame\\co_ct") + .function("flame\\co_count") + .function("flame\\set", { + {"target", php::TYPE::ARRAY, true}, + {"fields", php::TYPE::STRING}, + {"values", php::TYPE::UNDEFINED}, + }) + .function("flame\\get", { + {"target", php::TYPE::ARRAY}, + {"fields", php::TYPE::STRING}, + }) + .function("flame\\send", { + {"target", php::TYPE::INTEGER}, + {"data", php::TYPE::UNDEFINED}, + }); + // 顶级命名空间 + flame::mutex::declare(ext); + flame::queue::declare(ext); + // 子命名空间模块注册 + flame::log::declare(ext); + flame::os::declare(ext); + flame::time::declare(ext); + flame::mysql::declare(ext); + flame::redis::declare(ext); + flame::mongodb::declare(ext); + flame::kafka::declare(ext); + flame::rabbitmq::declare(ext); + flame::tcp::declare(ext); + flame::udp::declare(ext); + flame::http::declare(ext); + flame::hash::declare(ext); + flame::encoding::declare(ext); + flame::compress::declare(ext); + flame::toml::declare(ext); + } + + php::value worker::init(php::parameters& params) { + php::array options = php::array(0); + if(params.size() > 1 && params[1].type_of(php::TYPE::ARRAY)) options = params[1]; + if (options.exists("timeout")) + gcontroller->worker_quit = std::min(std::max(static_cast(options.get("timeout")), 200), 100000); + else + gcontroller->worker_quit = 3000; + + gcontroller->status |= controller::STATUS_INITIALIZED; + // 设置进程标题 + std::string title = params[0]; + if (gcontroller->worker_size > 0) { + std::string index = gcontroller->env["FLAME_CUR_WORKER"].to_string(); + php::callable("cli_set_process_title").call({title + " (php-flame/" + index + ")"}); + } + else + php::callable("cli_set_process_title").call({title + " (php-flame/w)"}); + + worker::ww_.reset(new worker(gcontroller->worker_idx)); + + // 信号监听启动 + worker::ww_->sw_watch(); + if(gcontroller->worker_size > 0) worker::ww_->ipc_start(); + + gcontroller->init(options); // 首个 logger 的初始化过程在 logger 注册 on_init 回调中进行(异步的) + return nullptr; + } + + php::value worker::go(php::parameters& params) { + if ((gcontroller->status & controller::STATUS_INITIALIZED) == 0) + throw php::exception(zend_ce_parse_error, "Failed to run flame: exception or missing 'flame\\init()' ?", -1); + + coroutine::start(php::callable([fn = php::callable(params[0])] (php::parameters& params) -> php::value { + try { // 函数调用产生了堆栈过程,可以捕获异常 + fn.call(); + } catch (const php::exception &ex) { + gcontroller->event("exception", {ex}); // 调用用户异常回调 + php::object obj = ex; // 记录错误信息 + log::logger_->stream() << "[" << time::iso() << "] (FATAL) " << obj.call("__toString") << std::endl; + + boost::asio::post(gcontroller->context_x, [] () { + gcontroller->status |= controller::STATUS_EXCEPTION; + gcontroller->context_x.stop(); + gcontroller->context_y.stop(); + gcontroller->context_z.stop(); + }); + } + return nullptr; + })); + // 如下形式无法在内部无法捕获到 PHP 错误(顶层栈) + // flame::coroutine::start(params[0]); + return nullptr; + } + + php::value worker::on(php::parameters ¶ms) { + + if ((gcontroller->status & controller::STATUS_INITIALIZED) == 0) + throw php::exception(zend_ce_parse_error, "Failed to run flame: exception or missing 'flame\\init()' ?", -1); + + std::string event = params[0].to_string(); + if (!params[1].type_of(php::TYPE::CALLABLE)) throw php::exception(zend_ce_type_error, "Failed to set callback: callable required", -1); + // message 事件需要启动协程辅助,故需要对应的停止机制 + if(event == "message" && gcontroller->cnt_event(event) == 0) { + worker::ww_->msg_start(); + } + gcontroller->add_event(event, params[1]); + return nullptr; + } + + php::value worker::off(php::parameters& params) { + if ((gcontroller->status & controller::STATUS_INITIALIZED) == 0) + throw php::exception(zend_ce_parse_error, "Failed to run flame: exception or missing 'flame\\init()' ?", -1); + + std::string event = params[0].to_string(); + gcontroller->del_event(event); + // message 事件启动了额外的协程,需要停止 + if(event == "message"/*&& gcontroller->cnt_event(event) == 0 */) { + worker::ww_->msg_close(); + } + return nullptr; + } + + php::value worker::run(php::parameters& params) { + if ((gcontroller->status & controller::STATUS_INITIALIZED) == 0) + throw php::exception(zend_ce_parse_error, "Failed to run flame: exception or missing 'flame\\init()' ?", -1); + + gcontroller->status |= controller::STATUS_RUN; + + auto ywork = boost::asio::make_work_guard(gcontroller->context_y); + auto zwork = boost::asio::make_work_guard(gcontroller->context_z); + std::thread ts[4]; + for (int i=0; i<3; ++i) { + ts[i] = std::thread([] { + gcontroller->context_y.run(); + }); + } + ts[3] = std::thread([] { + gcontroller->context_z.run(); + }); + gcontroller->context_x.run(); + ywork.reset(); + zwork.reset(); + + worker::ww_->sw_close(); + worker::ww_->ipc_close(); + worker::ww_->lm_close(); + + for (int i=0; i<4; ++i) { + ts[i].join(); + } + gcontroller->stop(); + worker::ww_.reset(); + return nullptr; + } + + php::value worker::quit(php::parameters& params) { + if ((gcontroller->status & controller::STATUS_RUN) == 0) + throw php::exception(zend_ce_parse_error, "Failed to run flame: exception or missing 'flame\\init()' ?", -1); + + gcontroller->context_x.stop(); + gcontroller->context_y.stop(); + gcontroller->context_z.stop(); + return nullptr; + } + + php::value worker::co_id(php::parameters& params) { + return reinterpret_cast(coroutine::current.get()); + } + + php::value worker::co_ct(php::parameters& params) { + return coroutine::count; + } + + php::value worker::get(php::parameters& params) { + php::array target = params[0]; + php::string fields = params[1]; + return flame::toml::get(target, { fields.data(), fields.size() }, 0); + } + + php::value worker::set(php::parameters& params) { + php::array target = params.get(0, true); + php::string fields = params[1]; + php::value values = params[2]; + flame::toml::set(target, {fields.data(), fields.size()}, 0, values); + return nullptr; + } + + php::value worker::send(php::parameters& params) { + worker::ww_->ipc_notify(static_cast(params[0]), params[1]); + return nullptr; + } + + worker::worker(std::uint8_t idx) + : signal_watcher(gcontroller->context_z) + , worker_ipc(gcontroller->context_z, idx) + , worker_logger_manager(this) { + + } + + std::ostream& worker::output() { + return log::logger_ ? log::logger_->stream() : std::clog; + } + + // !!! 此函数在工作线程中运行 + bool worker::on_signal(int sig) { + switch(sig) { + case SIGINT: + if(gcontroller->worker_size > 0) break; // 多进程模式忽略 + [[fallthrough]]; // 单进程模式通 SIGQUIT 处理 + case SIGQUIT: + boost::asio::post(gcontroller->context_x, [] () { + if(gcontroller->worker_size > 0) { + gcontroller->status |= controller::STATUS_EXCEPTION; + } + gcontroller->context_x.stop(); + gcontroller->context_y.stop(); + gcontroller->context_z.stop(); + }); + return false; // 多次停止信号立即结束 + break; + case SIGTERM: + coroutine::start(php::callable([] (php::parameters& params) -> php::value { // 通知用户退出(超时后主进程将杀死子进程) + gcontroller->event("quit"); + return nullptr; + })); + case SIGUSR1: + boost::asio::post(gcontroller->context_x, [] () { + gcontroller->status ^= controller::STATUS_CLOSECONN; + }); + break; + case SIGUSR2: // 日志重载, 与子进程无关 + break; + } + return true; + } + // !!! 此函数在工作线程中工作 + bool worker::on_message(std::shared_ptr msg) { + switch(msg->command) { + case ipc::COMMAND_MESSAGE_STRING: + boost::asio::post(gcontroller->context_x, [this, self = worker::ww_, msg] () { + msgq_.push_back(php::string(&msg->payload[0], msg->length)); + if(msgq_.size() == 1 && msgc_) msgc_.resume(); + }); + break; + case ipc::COMMAND_MESSAGE_JSON: // !!! json_decode('"abc"') === null !!! + boost::asio::post(gcontroller->context_x, [this, self = worker::ww_, msg] () { + msgq_.push_back(php::json_decode(&msg->payload[0], msg->length)); + if(msgq_.size() == 1 && msgc_) msgc_.resume(); + }); + break; + } + return true; + } + + void worker::msg_start() { + coroutine::start(php::callable([this, self = worker::ww_] (php::parameters& params) -> php::value { + coroutine_handler ch {coroutine::current}; + while(true) { + while(msgq_.empty()) { + msgc_.reset(ch); + ch.suspend(); + msgc_.reset(); + } + php::value v = msgq_.front(); + msgq_.pop_front(); + + if(v.type_of(php::TYPE::UNDEFINED)) break; + gcontroller->event("message", {v}); + } + return nullptr; + })); + } + + void worker::msg_close() { + msgq_.push_back(php::value()); + if(msgq_.size() == 1 && msgc_) msgc_.resume(); + } +} \ No newline at end of file diff --git a/src/flame/worker.h b/src/flame/worker.h new file mode 100644 index 0000000..2ff56f1 --- /dev/null +++ b/src/flame/worker.h @@ -0,0 +1,53 @@ +#pragma once +#include "../vendor.h" +#include "../worker_logger_manager.h" +#include "../signal_watcher.h" +#include "../worker_ipc.h" +#include "../coroutine_queue.h" + +namespace flame { + class worker: public std::enable_shared_from_this, public signal_watcher, public worker_ipc, public worker_logger_manager { + public: + static inline std::shared_ptr get() { + return worker::ww_; + } + static void declare(php::extension_entry& ext); + // 主流程相关 + static php::value init(php::parameters& params); + static php::value go(php::parameters& params); + static php::value on(php::parameters ¶ms); + static php::value off(php::parameters& params); + static php::value run(php::parameters& params); + static php::value quit(php::parameters& params); + // 协程相关 + static php::value co_id(php::parameters& params); + static php::value co_ct(php::parameters& params); + // 级联数组设置、读取 + static php::value get(php::parameters& params); + static php::value set(php::parameters& params); + // 传递消息到其他工作进程 + static php::value send(php::parameters& params); + + worker(std::uint8_t idx); + std::ostream& output() override; + protected: + virtual std::shared_ptr sw_self() override { + return std::static_pointer_cast(shared_from_this()); + } + virtual std::shared_ptr ipc_self() override { + return std::static_pointer_cast(shared_from_this()); + } + virtual std::shared_ptr lm_self() override { + return std::static_pointer_cast(shared_from_this()); + } + bool on_signal(int sig) override; + bool on_message(std::shared_ptr msg) override; + private: + static std::shared_ptr ww_; + std::list msgq_; // notify queue + coroutine_handler msgc_; // notify coroutine + + void msg_start(); + void msg_close(); + }; +} \ No newline at end of file diff --git a/src/ipc.cpp b/src/ipc.cpp new file mode 100644 index 0000000..da656a5 --- /dev/null +++ b/src/ipc.cpp @@ -0,0 +1,34 @@ +#include "ipc.h" + +static boost::pool<> pool_(ipc::MESSAGE_INIT_CAPACITY); + +static void free_message(ipc::message_t* msg) { + pool_.free(msg); +} + +std::shared_ptr ipc::create_message(std::uint16_t length) { + if(length > ipc::MESSAGE_INIT_CAPACITY - sizeof(ipc::message_t)) { + ipc::message_t* msg = (ipc::message_t*)pool_.malloc(); + msg->length = ipc::MESSAGE_INIT_CAPACITY - sizeof(ipc::message_t); + return std::shared_ptr(msg, free_message); + } + else { + length = (sizeof(ipc::message_t) + length + 4096 - 1) & ~(4096-1); + ipc::message_t* msg = (ipc::message_t*)malloc(length); + msg->length = length - sizeof(ipc::message_t); + return std::shared_ptr(msg, free); + } +} + +void ipc::relloc_message(std::shared_ptr& msg, std::uint16_t copy, std::uint16_t length) { + length = (sizeof(ipc::message_t) + length + 4096 - 1) & ~(4096-1); + + ipc::message_t* m = (ipc::message_t*)malloc(length); + std::memcpy(m, msg.get(), sizeof(ipc::message_t) + copy); + msg.reset(m, free); +} + +std::uint32_t ipc::create_uid() { + static std::uint32_t start = 0; + return ++start; +} diff --git a/src/ipc.h b/src/ipc.h new file mode 100644 index 0000000..c971ff3 --- /dev/null +++ b/src/ipc.h @@ -0,0 +1,44 @@ +#pragma once +#include "vendor.h" +#include "coroutine.h" + +class ipc { +public: + // 传输协议:消息格式 + struct message_t { + std::uint8_t command; + std::uint8_t source; + std::uint8_t target; + std::uint8_t xdata[3]; + std::uint16_t length; + std::uint32_t unique_id; + char payload[0]; + }; + // 传输协议:命令 + enum command_t { + MESSAGE_INIT_CAPACITY = 2048, + + COMMAND_REGISTER = 0x01, + COMMAND_LOGGER_CONNECT = 0x02, + COMMAND_LOGGER_DESTROY = 0x03, + COMMAND_LOGGER_DATA = 0x04, + + COMMAND_TRANSFER_TO_CHILD = 0x10, + COMMAND_MESSAGE_STRING = 0x10, + COMMAND_MESSAGE_JSON = 0x11, + }; + struct callback_t { + coroutine_handler& cch; + std::shared_ptr& res; + }; + // 消息容器构建 + static std::shared_ptr create_message(std::uint16_t length = ipc::MESSAGE_INIT_CAPACITY - sizeof(ipc::message_t)); + // 消息容器长度调整 + static void relloc_message(std::shared_ptr& msg, std::uint16_t copy, std::uint16_t length); + // 生成消息ID + static std::uint32_t create_uid(); + // 请求并等待响应 + virtual std::shared_ptr ipc_request(std::shared_ptr data, coroutine_handler& ch) = 0; + // 请求(无响应) + virtual void ipc_request(std::shared_ptr data) = 0; +}; diff --git a/src/master_ipc.cpp b/src/master_ipc.cpp new file mode 100644 index 0000000..54cb38e --- /dev/null +++ b/src/master_ipc.cpp @@ -0,0 +1,122 @@ +#include "master_ipc.h" +#include "master_logger.h" +#include "util.h" +#include "coroutine.h" + +master_ipc::master_ipc(boost::asio::io_context& io) +: io_(io) +, server_(io, boost::asio::local::stream_protocol()) { + svrsck_ = (boost::format("/tmp/flame_ipc_%d.sock") % ::getpid()).str(); +} + +master_ipc::~master_ipc() { + ipc_close(); + std::filesystem::remove(svrsck_); +} + +void master_ipc::ipc_start() { + ::coroutine::start(io_.get_executor(), std::bind(&master_ipc::ipc_run, ipc_self(), std::placeholders::_1)); +} +// !!! 工作线程中的协程 +void master_ipc::ipc_run(coroutine_handler ch) { + std::error_code ec; + std::filesystem::remove(svrsck_, ec); + boost::asio::local::stream_protocol::endpoint addr(svrsck_); + server_.bind(addr); + server_.listen(); + boost::system::error_code error; + while(true) { + auto sock = std::make_shared(io_); + server_.async_accept(*sock, ch[error]); + if (error) break; + coroutine::start(io_.get_executor(), std::bind(&master_ipc::ipc_read, ipc_self(), sock, std::placeholders::_1)); + } + if (error && error != boost::asio::error::operation_aborted) + output() << "[" << util::system_time() << "] (ERROR) Failed to accept ipc connection: (" << error.value() << ") " << error.message() << "\n"; + + server_.close(error); +} + +void master_ipc::ipc_close() { + boost::system::error_code error; + server_.close(error); + for(auto i=socket_.begin();i!=socket_.end();++i) { + i->second->close(error); + } +} + +void master_ipc::ipc_read(socket_ptr sock, coroutine_handler ch) { + boost::system::error_code error; + while(true) { + auto msg = ipc::create_message(); + boost::asio::async_read(*sock, boost::asio::buffer(msg.get(), sizeof(ipc::message_t)), ch[error]); + if (error) break; + if (msg->length > ipc::MESSAGE_INIT_CAPACITY - sizeof(ipc::message_t)) { + ipc::relloc_message(msg, 0, msg->length); + } + boost::asio::async_read(*sock, boost::asio::buffer(&msg->payload[0], msg->length), ch[error]); + if (error) break; + if (!on_message(msg, sock)) break; + } + if (error && error != boost::asio::error::operation_aborted && error != boost::asio::error::eof) + output() << "[" << util::system_time() << "] (ERROR) Failed to read ipc connection: (" << error.value() << ") " << error.message() << "\n"; + // 发生次数较少,效率不太重要了 + for(auto i=socket_.begin();i!=socket_.end();++i) { + if(i->second == sock) { + socket_.erase(i); + break; + } + } + sock->close(error); +} + +bool master_ipc::on_message(std::shared_ptr msg, socket_ptr sock) { + if(msg->command >= ipc::COMMAND_TRANSFER_TO_CHILD) send(msg); + else if(msg->command == ipc::COMMAND_REGISTER) socket_[msg->source] = sock; + return true; +} + +std::shared_ptr master_ipc::ipc_request(std::shared_ptr req, coroutine_handler& ch) { + std::shared_ptr res; + callback_.emplace(req->unique_id, ipc::callback_t {ch, res}); + req->source = 0; + send(req); + ch.suspend(); + return res; +} + +void master_ipc::ipc_request(std::shared_ptr req) { + req->source = 0; + send(req); +} + +void master_ipc::send(std::shared_ptr msg) { + boost::asio::post(io_, [this, self = ipc_self(), msg] () { + if(sendq_.empty()) { + sendq_.push_back(msg); + send_next(); + }else + sendq_.push_back(msg); + }); +} + +void master_ipc::send_next() { + if (sendq_.empty()) return; + std::shared_ptr msg = sendq_.front(); + auto i = socket_.find(msg->target); + if(i == socket_.end()) { + // std::cout << "message_dropped: " << (int) msg->command << " => " << (int) msg->target << "\n"; + sendq_.pop_front(); + boost::asio::post(io_, std::bind(&master_ipc::send_next, ipc_self())); + } + else { + boost::asio::async_write(*i->second, boost::asio::buffer(msg.get(), sizeof(ipc::message_t) + msg->length), [this] (const boost::system::error_code& error, std::size_t size) { + if(error) { + output() << "[" << util::system_time() << "] [FATAL] Failed to write ipc message: (" << error.value() << ") " << error.message() << "\n"; + return; + } + sendq_.pop_front(); + send_next(); + }); + } +} \ No newline at end of file diff --git a/src/master_ipc.h b/src/master_ipc.h new file mode 100644 index 0000000..2194185 --- /dev/null +++ b/src/master_ipc.h @@ -0,0 +1,37 @@ +#pragma once +#include "vendor.h" +#include "coroutine.h" +#include "ipc.h" + +class master_ipc { +public: + using socket_ptr = std::shared_ptr; + master_ipc(boost::asio::io_context& io); + virtual ~master_ipc(); + // 输出 + virtual std::ostream& output() = 0; + void ipc_start(); + // 监听连接及连接数据接收 + void ipc_run(coroutine_handler ch); + void ipc_read(socket_ptr sock, coroutine_handler ch); + // void ipc_read(socket_ptr sock, coroutine_handler& ch); + void ipc_close(); + // 请求并等待响应 + std::shared_ptr ipc_request(std::shared_ptr req, coroutine_handler& ch); + // 请求(无响应) + void ipc_request(std::shared_ptr data); +protected: + virtual bool on_message(std::shared_ptr msg, socket_ptr sock); + virtual std::shared_ptr ipc_self() = 0; +private: + boost::asio::io_context& io_; + std::filesystem::path svrsck_; + boost::asio::local::stream_protocol::acceptor server_; + std::map socket_; + std::map callback_; + std::list> sendq_; + // 连接数据接收 + + void send(std::shared_ptr msg); + void send_next(); +}; diff --git a/src/master_logger.cpp b/src/master_logger.cpp new file mode 100644 index 0000000..528174c --- /dev/null +++ b/src/master_logger.cpp @@ -0,0 +1,26 @@ +#include "master_logger.h" +#include "master_logger_buffer.h" +#include "util.h" + +void master_logger::reload(boost::asio::io_context& io) { + oss_.reset(&std::clog, boost::null_deleter()); + + if(path_.string() != "") { + auto fb = new master_logger_buffer(io, path_); + // fb->open(path_, std::ios_base::app); + if(fb->is_open()) { + ssb_.reset(fb); + oss_.reset(new std::ostream(fb)); + fb->persync(); // 启动周期性文件缓冲刷新服务 + } + else { // 文件打开失败时不会抛出异常,需要额外的状态检查 + std::cerr << "[" << util::system_time() << "] (WARNING) Failed to access / create logger file, fallback to 'clog'" << std::endl; + } + } +} + +void master_logger::close() { + oss_.reset(&std::clog, boost::null_deleter()); + // ssb_->close(); + ssb_.reset(); +} \ No newline at end of file diff --git a/src/master_logger.h b/src/master_logger.h new file mode 100644 index 0000000..382ec36 --- /dev/null +++ b/src/master_logger.h @@ -0,0 +1,23 @@ +#pragma once +#include "vendor.h" +#include + +class master_logger { +private: + std::uint8_t idx_; + unsigned int ref_; + std::shared_ptr oss_; + std::unique_ptr ssb_; + std::filesystem::path path_; +public: + master_logger(std::filesystem::path path, int index): idx_(index), ref_(1), path_(path) {} + std::uint8_t index() { + return idx_; + } + void reload(boost::asio::io_context& io); + std::ostream& stream() { + return *oss_; + } + void close(); + friend class master_logger_manager; +}; diff --git a/src/master_logger_buffer.cpp b/src/master_logger_buffer.cpp new file mode 100644 index 0000000..c570455 --- /dev/null +++ b/src/master_logger_buffer.cpp @@ -0,0 +1,45 @@ +#include "master_logger_buffer.h" +#include "master_logger_manager.h" + +master_logger_buffer::master_logger_buffer(boost::asio::io_context& io, std::filesystem::path path) +: tms_(io) { + open(path, std::ios_base::app); +} + +int master_logger_buffer::overflow(int ch) { + char c = ch; + ch = std::filebuf::overflow(ch); + + if(c == '\n' && ++lns_ > 64) { // 积攒的行数超过 64 行刷新 + lns_ = 0; + pubsync(); + persync(); + } + return ch; +} + +long master_logger_buffer::xsputn(const char* s, long c) { + c = std::filebuf::xsputn(s, c); + + if(s[c-1] == '\n' && ++lns_ > 64) { // 积攒的行数超过 64 行刷新 + lns_ = 0; + pubsync(); + persync(); + } + return c; +} + + +void master_logger_buffer::persync() { + tms_.cancel(); + tms_.expires_after(std::chrono::milliseconds(2400)); + tms_.async_wait([this] (const boost::system::error_code& error) { + if(error) return; + pubsync(); + persync(); // 每 2400 毫秒对文件进行一次刷新 + }); +} + +// void master_logger_buffer::close() { +// tms_.cancel(); +// } \ No newline at end of file diff --git a/src/master_logger_buffer.h b/src/master_logger_buffer.h new file mode 100644 index 0000000..a0e93cf --- /dev/null +++ b/src/master_logger_buffer.h @@ -0,0 +1,20 @@ +#pragma once +#include +#include +#include "ipc.h" + +class master_logger_manager; +// 实现行数或周期为同步、刷写标志的缓冲区 +class master_logger_buffer: public std::filebuf { +public: + master_logger_buffer(boost::asio::io_context& io, std::filesystem::path path); + void persync(); + // void close(); +protected: + int overflow(int ch = EOF) override; + long xsputn(const char* s, long c) override; +private: + master_logger_manager* mgr_; + boost::asio::steady_timer tms_; + unsigned int lns_ = 0; +}; diff --git a/src/master_logger_manager.cpp b/src/master_logger_manager.cpp new file mode 100644 index 0000000..a7df097 --- /dev/null +++ b/src/master_logger_manager.cpp @@ -0,0 +1,41 @@ +#include "master_logger_manager.h" + +master_logger_manager::master_logger_manager(boost::asio::io_context& io) +: io_(io) { + +} + +master_logger_manager::~master_logger_manager() { + // std::cout << "~master_logger_manager\n"; +} + +master_logger* master_logger_manager::lm_connect(std::string_view filepath) { + std::filesystem::path path = filepath; + + for(auto i=logger_.begin();i!=logger_.end();++i) { + if(static_cast(i->second.get())->path_ == path) { + ++i->second->ref_; + return i->second.get(); + } + } + auto p = logger_.insert({index_, std::make_unique(path, index_)}); + ++index_; + p.first->second->reload(io_); + return p.first->second.get(); +} + +void master_logger_manager::lm_destroy(std::uint8_t idx) { + auto i = logger_.find(idx); + if(i == logger_.end()) return; + assert(i->second->ref_ > 0 && "引用计数异常"); + if(--i->second->ref_ > 0) return; + logger_.erase(i); +} + +void master_logger_manager::lm_reload() { + for(auto i=logger_.begin();i!=logger_.end();++i) i->second->reload(io_); +} + +void master_logger_manager::lm_close() { + for(auto i=logger_.begin();i!=logger_.end();++i) i->second->close(); +} \ No newline at end of file diff --git a/src/master_logger_manager.h b/src/master_logger_manager.h new file mode 100644 index 0000000..d1e26d3 --- /dev/null +++ b/src/master_logger_manager.h @@ -0,0 +1,33 @@ +#pragma once +#include "vendor.h" +#include "master_logger.h" + +class master_logger_manager { +public: + master_logger_manager(boost::asio::io_context& io); + virtual ~master_logger_manager(); + // 日志引用 + master_logger* lm_connect(std::string_view filepath); + master_logger* lm_get(std::uint8_t idx); + std::ostream& lm_get(std::uint8_t idx, bool output); + // 引用清理 + void lm_destroy(std::uint8_t idx); + // 日志重载 + void lm_reload(); + void lm_close(); + virtual std::shared_ptr lm_self() = 0; +private: + boost::asio::io_context& io_; + std::uint8_t index_ = 0; + std::map> logger_; +}; + +inline master_logger* master_logger_manager::lm_get(std::uint8_t idx) { + auto i = logger_.find(idx); + return i == logger_.end() ? nullptr : i->second.get(); +} + +inline std::ostream& master_logger_manager::lm_get(std::uint8_t idx, bool output) { + auto i = logger_.find(idx); + return i == logger_.end() ? std::clog : i->second.get()->stream(); +} diff --git a/src/master_process.cpp b/src/master_process.cpp new file mode 100755 index 0000000..9f42768 --- /dev/null +++ b/src/master_process.cpp @@ -0,0 +1,67 @@ +#include "master_process.h" +#include "master_process_manager.h" +#include "master_logger.h" +#include "util.h" + +extern "C" { + PHPAPI extern char *php_ini_opened_path; +} + +static std::string php_cmd() { + std::ostringstream ss; + ss << php::constant("PHP_BINARY"); + ss << " -c " << php_ini_opened_path; + php::array argv = php::server("argv"); + for (auto i = argv.begin(); i != argv.end(); ++i) ss << " " << i->second; + return ss.str(); +} + +master_process::master_process(boost::asio::io_context& io, master_process_manager* mgr, std::uint8_t idx) +: mgr_(mgr), idx_(idx) +, sout_(io), eout_(io) { + // 准备命令行及环境变量(完全重新启动一个新的 PHP, 通过环境变量标识其为工作进程) + boost::process::environment env = boost::this_process::environment(); + env["FLAME_CUR_WORKER"] = std::to_string(idx + 1); + // 构造进程 + proc_ = boost::process::child(io, php_cmd(), env, + boost::process::std_out > sout_, boost::process::std_err > eout_, + // boost::process::std_out > boost::process::null, boost::process::std_err > boost::process::null, + // boost::process::std_out > stdout, boost::process::std_err > stdout, + // 结束回调 + boost::process::on_exit = [this, &io] (int exit_code, const std::error_code &error) { + if (error.value() == static_cast(std::errc::no_child_process)) return; + boost::asio::post(io, [exit_code, this] () { + mgr_->on_child_close(this, exit_code == 0); + }); + }); + redirect_output(sout_, sbuf_); + redirect_output(eout_, ebuf_); + + boost::asio::post(io, [this] () { + mgr_->on_child_start(this); + }); +} + +void master_process::redirect_output(boost::process::async_pipe& pipe, std::string& data) { + boost::asio::async_read_until(pipe, boost::asio::dynamic_buffer(data), '\n', [this, &pipe, &data] (const boost::system::error_code &error, std::size_t nread) { + if (error == boost::asio::error::operation_aborted || error == boost::asio::error::eof) + ; // std::cout << "redirect_output stopped" << std::endl; // 忽略 + else if (error) { + mgr_->output() << "[" << util::system_time() << "] (ERROR) Failed to read from worker process: (" << error.value() << ") " << error.message() << "\n"; + } + else { + mgr_->output() << "-----" << std::string_view(data.data(), nread); + data.erase(0, nread); + redirect_output(pipe, data); + } + }); +} + +void master_process::close(bool force) { + if(force) proc_.terminate(); + else ::kill(proc_.id(), SIGQUIT); +} + +void master_process::signal(int sig) { + ::kill(proc_.id(), sig); +} \ No newline at end of file diff --git a/src/master_process.h b/src/master_process.h new file mode 100755 index 0000000..41c5e77 --- /dev/null +++ b/src/master_process.h @@ -0,0 +1,24 @@ +#pragma once +#include "vendor.h" +#include +#include + +class master_process_manager; +class master_process { +public: + master_process(boost::asio::io_context& io, master_process_manager* m, std::uint8_t idx); + void close(bool force = false); + void signal(int sig); +private: + master_process_manager* mgr_; + std::uint8_t idx_; + + boost::process::child proc_; + boost::process::async_pipe sout_; + boost::process::async_pipe eout_; + std::string sbuf_; + std::string ebuf_; + void redirect_output(boost::process::async_pipe& pipe, std::string& buffer); + + friend class master_process_manager; +}; \ No newline at end of file diff --git a/src/master_process_manager.cpp b/src/master_process_manager.cpp new file mode 100644 index 0000000..633d31f --- /dev/null +++ b/src/master_process_manager.cpp @@ -0,0 +1,108 @@ +#include "master_process_manager.h" +#include "master_process.h" +#include "coroutine.h" +#include "util.h" + +master_process_manager::master_process_manager(boost::asio::io_context& io, unsigned int count, unsigned int close) +: io_(io) +, cmax_(count) +, crun_(0) +, tquit_(close) +, child_(count) +, timer_(io) +, status_(0) { + +} + +master_process_manager::~master_process_manager() { + // std::cout << "~master_process_manager\n"; +} + +void master_process_manager::pm_start(boost::asio::io_context& io) { + // 这个 work_guard 为了使得主线程在子进程未全部退出前保持运行 + // 实际主线程并没有工作量 + work_.reset(new boost::asio::io_context::work(io)); + for(int i=0;i> start(cmax_); + for(int i=0;i (3) + return; + } + status_ |= STATUS_CLOSING; + for(int i=0;iclose(now); + if (now) { + status_ |= STATUS_ACLOSED; + timer_.cancel(); // 可能有异常重启中的进程存在 + goto SHUTDOWN; + } + { + int stopped = cmax_; + bool timeout = false; + timer_.expires_after(std::chrono::milliseconds(tquit_)); + timer_.async_wait([&ch, &timeout, &stopped, this, self = pm_self()] (const boost::system::error_code& error) mutable { + if(status_ & STATUS_ACLOSED) return; // (2) 结束后栈数据丢失(ch/timeout/stopped) + timeout = true; // (3) 还未完全关闭时超时或提前强制关闭 + ch.resume(); + }); + WAIT_FOR_CLOSE: + ch_close_.reset(ch); + ch_close_.suspend(); // 等待回调 on_child_event + ch_close_.reset(); + if(timeout) return pm_close(ch, true); + else if(--stopped > 0) goto WAIT_FOR_CLOSE; + } +SHUTDOWN: + status_ |= STATUS_ACLOSED; // -------> (2) + timer_.cancel(); + work_.reset(); +} + +void master_process_manager::pm_kills(int sig) { + for(int i=0;isignal(sig); +} + +void master_process_manager::on_child_start(master_process* w) { + ++crun_; + if (ch_start_) ch_start_.resume(); // 陆续起停流程 +} + +void master_process_manager::on_child_close(master_process* w, bool normal) { + --crun_; + if (!(status_ & STATUS_CLOSING) && !normal) { + unsigned int rand = std::rand() % 2000 + 1000; + output() << "[" << util::system_time() << "] (WARNING) unexpected worker process stopping, restart in " << int(rand/1000) << "s ...\n"; + timer_.expires_after(std::chrono::milliseconds(rand)); + timer_.async_wait([this, self = pm_self(), idx = w->idx_] (const boost::system::error_code& error) { + if(error) return; + child_[idx].reset(new master_process(io_, this, idx)); + }); + } + if (ch_close_) ch_close_.resume(); // 陆续起停流程 + if (crun_ == 0 && normal) work_.reset(); // 正常退出了最后一个进程 + // 这里不太严格:三个进程 1、2 异常退出,3正常退出时也结束了程序 +} diff --git a/src/master_process_manager.h b/src/master_process_manager.h new file mode 100644 index 0000000..9bac11a --- /dev/null +++ b/src/master_process_manager.h @@ -0,0 +1,51 @@ +#pragma once +#include "vendor.h" +#include "coroutine.h" + +class master_process; +class master_process_manager { +public: + master_process_manager(boost::asio::io_context& io, unsigned int count, unsigned int timeout_ms); + virtual ~master_process_manager(); + // 输出 + virtual std::ostream& output() = 0; +protected: + virtual std::shared_ptr pm_self() = 0; + // 启动子进程 + void pm_start(boost::asio::io_context& io); // 此 io 非彼 io + // 重启子进程(陆续) + void pm_reset(coroutine_handler& ch); + // 停止子进程 + void pm_close(coroutine_handler& ch, bool now = false); + // 发送信号 + void pm_kills(int sig); + // 进程数量 + std::uint8_t pm_count() { + return cmax_; + } +protected: + // 进程启动回调 + virtual void on_child_start(master_process* w); + // 进程停止回调 + virtual void on_child_close(master_process* w, bool normal); +private: + std::unique_ptr work_; + boost::asio::io_context& io_; + unsigned int cmax_; + unsigned int crun_; + unsigned int tquit_; + + std::vector> child_; + boost::asio::steady_timer timer_; + coroutine_handler ch_close_; + coroutine_handler ch_start_; + int status_; + enum { + STATUS_CLOSING = 0x01, + STATUS_RSETING = 0x02, + STATUS_QUITING = 0x04, + STATUS_ACLOSED = 0x08, + }; + + friend class master_process; +}; diff --git a/src/net/init.cpp b/src/net/init.cpp deleted file mode 100644 index 58617f8..0000000 --- a/src/net/init.cpp +++ /dev/null @@ -1,29 +0,0 @@ -#include "../vendor.h" -#include "udp_socket.h" -#include "tcp_socket.h" -#include "tcp_server.h" - -namespace net { - void init(php::extension_entry& extension) { - udp_socket::init(extension); - tcp_socket::init(extension); - tcp_server::init(extension); - } - address addr_from_str(const std::string& addr, bool use_ipv6) { - auto addr_ = address::from_string(addr); - if(use_ipv6 && addr_.is_v4()) { - try{ - addr_ = boost::asio::ip::address_v6::v4_mapped(addr_.to_v4()); - }catch(std::bad_cast& ex) { - throw php::exception("illegal address: using IPv6 but IPv4 addr given", 0); - } - }else if(!use_ipv6 && addr_.is_v6()) { - try{ - addr_ = addr_.to_v6().to_v4(); - }catch(std::bad_cast& ex) { - throw php::exception("illegal address: using IPv4 but IPv6 addr given", 0); - } - } - return addr_; - } -} diff --git a/src/net/init.h b/src/net/init.h deleted file mode 100644 index b5adeaa..0000000 --- a/src/net/init.h +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -namespace net { - void init(php::extension_entry& extension); - address addr_from_str(const std::string& addr, bool use_ipv6); -} diff --git a/src/net/tcp_server.cpp b/src/net/tcp_server.cpp deleted file mode 100644 index a19ef89..0000000 --- a/src/net/tcp_server.cpp +++ /dev/null @@ -1,96 +0,0 @@ -#include "../vendor.h" -#include "tcp_server.h" -#include "../core.h" -#include "init.h" -#include "tcp_socket.h" - -namespace net { - - void tcp_server::init(php::extension_entry& extension) { - php::class_entry ce_tcp_server("flame\\net\\tcp_server"); - ce_tcp_server.add<&tcp_server::__construct>("__construct"); - ce_tcp_server.add<&tcp_server::__destruct>("__destruct"); - ce_tcp_server.add<&tcp_server::listen>("listen", { - php::of_string("addr"), - php::of_integer("port"), - }); - ce_tcp_server.add<&tcp_server::accept>("accept"); - ce_tcp_server.add(php::property_entry("local_addr", nullptr)); - ce_tcp_server.add(php::property_entry("local_port", nullptr)); - ce_tcp_server.add<&tcp_server::close>("close"); - extension.add(std::move(ce_tcp_server)); - } - tcp_server::tcp_server() - : acceptor_(core::io()) - , is_ipv6_(true) // 默认按 IPv6 - , listened_(false) { - - } - php::value tcp_server::__construct(php::parameters& params) { - boost::system::error_code err; - acceptor_.open(tcp::v6(), err); // 优先使用 v6 协议(能够兼容 v4) - if(err) { - is_ipv6_ = false; - acceptor_.open(tcp::v4(), err); - } - if(err) { - throw php::exception("failed to create: " + err.message(), err.value()); - } - return nullptr; - } - php::value tcp_server::listen(php::parameters& params) { - boost::system::error_code err; - std::string addr = params[0].is_null() ? "" : params[0]; - int port = params[1]; -#ifdef SO_REUSEPORT - // 服务端需要启用下面选项,以支持更高性能的多进程形式 - int opt = 1; - if(0 != setsockopt(acceptor_.native_handle(), SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof (opt))) { - throw php::exception("failed to bind (SO_REUSEPORT)", errno); - } -#endif - acceptor_.bind(tcp::endpoint(addr_from_str(addr, is_ipv6_), port), err); - if(err) { - throw php::exception("failed to bind: " + err.message(), err.value()); - } - acceptor_.listen(); - set_prop_local_addr(); - listened_ = true; - return nullptr; - } - void tcp_server::set_prop_local_addr() { - auto ep = acceptor_.local_endpoint(); - prop("local_addr") = ep.address().to_string(); - prop("local_port") = ep.port(); - } - php::value tcp_server::__destruct(php::parameters& params) { - boost::system::error_code err; - acceptor_.close(err); // 存在重复关闭的可能,排除错误 - return nullptr; - } - php::value tcp_server::accept(php::parameters& params) { - if(!listened_) throw php::exception("accept failed: not listened"); - return php::value([this] (php::parameters& params) -> php::value { - php::callable done = params[0]; - - php::object cli = php::object::create(); - tcp_socket* cli_= cli.native(); - acceptor_.async_accept(cli_->socket_, cli_->remote_, - [this, done, cli, cli_] (const boost::system::error_code& err) mutable { - if(err) { - done(core::error_to_exception(err)); - }else{ - cli_->connected_ = true; - done(nullptr, cli); - } - }); - return nullptr; - }); - } - php::value tcp_server::close(php::parameters& params) { - boost::system::error_code err; - acceptor_.close(err); // 存在重复关闭的可能,排除错误 - listened_ = false; - return nullptr; - } -} diff --git a/src/net/tcp_server.h b/src/net/tcp_server.h deleted file mode 100644 index dae19e6..0000000 --- a/src/net/tcp_server.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -namespace net { - class tcp_server: public php::class_base { - public: - static void init(php::extension_entry& extension); - tcp_server(); - php::value __construct(php::parameters& params); - php::value __destruct(php::parameters& params); - php::value listen(php::parameters& params); - php::value accept(php::parameters& params); - php::value close(php::parameters& params); - private: - tcp::acceptor acceptor_; - bool is_ipv6_; - bool listened_; - void set_prop_local_addr(); - }; -} diff --git a/src/net/tcp_socket.cpp b/src/net/tcp_socket.cpp deleted file mode 100644 index 5b7c1f2..0000000 --- a/src/net/tcp_socket.cpp +++ /dev/null @@ -1,143 +0,0 @@ -#include "../vendor.h" -#include "tcp_socket.h" -#include "../core.h" -#include "init.h" - -namespace net { - - void tcp_socket::init(php::extension_entry& extension) { - php::class_entry ce_tcp_socket("flame\\net\\tcp_socket"); - ce_tcp_socket.add<&tcp_socket::__construct>("__construct"); - ce_tcp_socket.add<&tcp_socket::__destruct>("__destruct"); - ce_tcp_socket.add<&tcp_socket::connect>("connect", { - php::of_string("addr"), - php::of_integer("port"), - }); - ce_tcp_socket.add<&tcp_socket::remote_addr>("remote_addr"); - ce_tcp_socket.add<&tcp_socket::remote_port>("remote_port"); - ce_tcp_socket.add<&tcp_socket::read>("read"); - ce_tcp_socket.add<&tcp_socket::write>("write", { - php::of_string("data"), - }); - ce_tcp_socket.add(php::property_entry("local_addr", nullptr)); - ce_tcp_socket.add(php::property_entry("local_port", nullptr)); - ce_tcp_socket.add<&tcp_socket::close>("close"); - extension.add(std::move(ce_tcp_socket)); - } - tcp_socket::tcp_socket(bool connected) - : socket_(core::io()) - , buffer_(8*1024*1024) // TODO buffer_ 最大尺寸? - , connected_(connected) - , is_ipv6_(true) { // 默认按 IPv6 - - } - php::value tcp_socket::__construct(php::parameters& params) { - boost::system::error_code err; - socket_.open(tcp::v6(), err); // 优先使用 v6 协议(一般来说能够兼容 v4) - if(err) { - is_ipv6_ = false; - socket_.open(tcp::v4(), err); - } - if(err) { - throw php::exception("failed to create: " + err.message(), err.value()); - } - return nullptr; - } - php::value tcp_socket::connect(php::parameters& params) { - boost::system::error_code err; - std::string addr = params[0]; - int port = params[1]; - remote_ = tcp::endpoint(addr_from_str(addr, is_ipv6_), port); - return php::value([this] (php::parameters& params) -> php::value { - php::callable done = params[0]; - socket_.async_connect(remote_, [this, done] (const boost::system::error_code& err) mutable { - if(err) { - done(core::error_to_exception(err)); - }else{ - connected_ = true; - set_prop_local_addr(); - done(nullptr); - } - }); - }); - } - void tcp_socket::set_prop_local_addr() { - auto ep = socket_.local_endpoint(); - prop("local_addr") = ep.address().to_string(); - prop("local_port") = ep.port(); - } - php::value tcp_socket::__destruct(php::parameters& params) { - boost::system::error_code err; - socket_.close(err); // 存在重复关闭的可能,排除错误 - return nullptr; - } - php::value tcp_socket::remote_addr(php::parameters& params) { - return remote_.address().to_string(); - } - php::value tcp_socket::remote_port(php::parameters& params) { - return remote_.port(); - } - php::value tcp_socket::read(php::parameters& params) { - if(!connected_) throw php::exception("read failed: not connected"); - // 考虑提供两种方式的数据读取机制: - if(params[0].is_long()) { // 1. 读取指定长度 - std::size_t length = static_cast(params[0]); - return php::value([this, length] (php::parameters& params) -> php::value { - auto buf = buffer_.prepare(length); - php::callable done = params[0]; - boost::asio::async_read(socket_, buf, [this, done] (const boost::system::error_code& err, std::size_t n) mutable { - buffer_.commit(n); - if(err) { - done(core::error_to_exception(err)); - }else{ - php::string buf(n); - buffer_.sgetn(buf.data(), n); - done(nullptr, buf); - } - }); - }); - }else if(params[0].is_string()) { // 2. 读取到指定分隔符 - std::string delim = params[0]; - return php::value([this, delim] (php::parameters& params) -> php::value { - php::callable done = params[0]; - boost::asio::async_read_until(socket_, buffer_, delim, [this, done] (const boost::system::error_code& err, std::size_t n) mutable { - if(err) { - done(core::error_to_exception(err)); - }else{ - php::string buf(n); - buffer_.sgetn(buf.data(), n); - buffer_.consume(n); - done(nullptr, buf); - } - }); - }); - }else{ - throw php::exception("failed to read: unknown read method"); - } - } - php::value tcp_socket::write(php::parameters& params) { - if(!connected_) throw php::exception("write failed: not connected"); - zend_string* data = params[0]; - return php::value([this, data] (php::parameters& params) -> php::value { - php::callable done = params[0]; - // 需要进行类型转换,否则 asio::buffer 会将 zend_string -> val 长度解为 1 - boost::asio::async_write( - socket_, - boost::asio::buffer(reinterpret_cast(data->val), data->len), - [this, done] (const boost::system::error_code& err, std::size_t n) mutable { - if(err) { - done(core::error_to_exception(err)); - }else{ - done(nullptr, static_cast(n)); - } - }); - return nullptr; - }); - } - php::value tcp_socket::close(php::parameters& params) { - boost::system::error_code err; - socket_.close(err); // 存在重复关闭的可能,排除错误 - connected_ = false; - return nullptr; - } -} diff --git a/src/net/tcp_socket.h b/src/net/tcp_socket.h deleted file mode 100644 index e2ba5c8..0000000 --- a/src/net/tcp_socket.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -namespace net { - class tcp_server; - namespace http { class request; } - class tcp_socket: public php::class_base { - public: - static void init(php::extension_entry& extension); - tcp_socket(bool connected = false); - php::value __construct(php::parameters& params); - php::value __destruct(php::parameters& params); - php::value connect(php::parameters& params); - php::value remote_addr(php::parameters& params); - php::value remote_port(php::parameters& params); - php::value close(php::parameters& params); - php::value read(php::parameters& params); - php::value write(php::parameters& params); - private: - tcp::socket socket_; - boost::asio::streambuf buffer_; - tcp::endpoint remote_; - bool connected_; - bool is_ipv6_; - void set_prop_local_addr(); - friend class tcp_server; - friend class http::request; - }; -} diff --git a/src/net/udp_socket.cpp b/src/net/udp_socket.cpp deleted file mode 100644 index 3b86451..0000000 --- a/src/net/udp_socket.cpp +++ /dev/null @@ -1,188 +0,0 @@ -#include "../vendor.h" -#include "udp_socket.h" -#include "../core.h" -#include "init.h" - -namespace net { - - void udp_socket::init(php::extension_entry& extension) { - php::class_entry ce_udp_socket("flame\\net\\udp_socket"); - php::class_entry ce_udp_server("flame\\net\\udp_server"); - ce_udp_socket.add<&udp_socket::__construct>("__construct"); - ce_udp_server.add<&udp_socket::__construct>("__construct"); - ce_udp_socket.add<&udp_socket::__destruct>("__destruct"); - ce_udp_server.add<&udp_socket::__destruct>("__destruct"); - // ce_udp_socket 不提供 bind 方法 - ce_udp_server.add<&udp_socket::bind>("bind", { - php::of_string("addr"), - php::of_integer("port"), - }); - ce_udp_socket.add<&udp_socket::connect>("connect", { - php::of_string("addr"), - php::of_integer("port"), - }); - // ce_udp_server 不提供 connect 方法 - ce_udp_socket.add<&udp_socket::remote_addr>("remote_addr"); - ce_udp_server.add<&udp_socket::remote_addr>("remote_addr"); - ce_udp_socket.add<&udp_socket::remote_port>("remote_port"); - ce_udp_server.add<&udp_socket::remote_port>("remote_port"); - ce_udp_socket.add<&udp_socket::read>("read"); - ce_udp_server.add<&udp_socket::read>("read"); - ce_udp_socket.add<&udp_socket::write>("write", { - php::of_string("data"), - }); - // ce_udp_server 不提供 write 方法(仅提供 write_to) - ce_udp_socket.add<&udp_socket::write_to>("write_to", { - php::of_string("data"), - php::of_string("addr"), - php::of_integer("port"), - }); - ce_udp_server.add<&udp_socket::write_to>("write_to", { - php::of_string("data"), - php::of_string("addr"), - php::of_integer("port"), - }); - ce_udp_socket.add(php::property_entry("local_addr", nullptr)); - ce_udp_server.add(php::property_entry("local_addr", nullptr)); - ce_udp_socket.add(php::property_entry("local_port", nullptr)); - ce_udp_server.add(php::property_entry("local_port", nullptr)); - ce_udp_socket.add<&udp_socket::close>("close"); - ce_udp_server.add<&udp_socket::close>("close"); - extension.add(std::move(ce_udp_socket)); - extension.add(std::move(ce_udp_server)); - } - - udp_socket::udp_socket() - : socket_(core::io()) - , connected_(false) - , is_ipv6_(true) { // 默认按 IPv6 - - } - php::value udp_socket::__construct(php::parameters& params) { - boost::system::error_code err; - socket_.open(udp::v6(), err); // 优先使用 v6 协议(一般来说能够兼容 v4) - if(err) { - is_ipv6_ = false; - socket_.open(udp::v4(), err); - } - if(err) { - throw php::exception("failed to create: " + err.message(), err.value()); - } - return nullptr; - } - - php::value udp_socket::bind(php::parameters& params) { - boost::system::error_code err; - std::string addr = params[0].is_null() ? "" : params[0]; - int port = params[1]; -#ifdef SO_REUSEPORT - // 服务端需要启用下面选项,以支持更高性能的多进程形式 - int opt = 1; - if(0 != setsockopt(socket_.native_handle(), SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof (opt))) { - throw php::exception("failed to bind (SO_REUSEPORT)", errno); - } -#endif - socket_.bind(udp::endpoint(addr_from_str(addr, is_ipv6_), port), err); - if(err) { - throw php::exception("failed to bind: " + err.message(), err.value()); - } - set_prop_local_addr(); - connected_ = true; - return nullptr; - } - php::value udp_socket::connect(php::parameters& params) { - boost::system::error_code err; - std::string addr = params[0]; - int port = params[1]; - remote_ = udp::endpoint(addr_from_str(addr, is_ipv6_), port); - return php::value([this] (php::parameters& params) -> php::value { - php::callable done = params[0]; - socket_.async_connect(remote_, [this, done] (const boost::system::error_code& err) mutable { - if(err) { - done(core::error_to_exception(err)); - }else{ - connected_ = true; - set_prop_local_addr(); - done(nullptr); - } - }); - }); - } - void udp_socket::set_prop_local_addr() { - auto ep = socket_.local_endpoint(); - prop("local_addr") = ep.address().to_string(); - prop("local_port") = ep.port(); - } - php::value udp_socket::__destruct(php::parameters& params) { - boost::system::error_code err; - socket_.close(err); // 存在重复关闭的可能,排除错误 - return nullptr; - } - php::value udp_socket::remote_addr(php::parameters& params) { - return remote_.address().to_string(); - } - php::value udp_socket::remote_port(php::parameters& params) { - return remote_.port(); - } - php::value udp_socket::read(php::parameters& params) { - if(connected_) throw php::exception("connected socket should use 'write' instead of 'write_to'"); - return php::value([this] (php::parameters& params) -> php::value { - php::callable done = params[0]; - socket_.async_receive_from( - boost::asio::buffer(buffer_), remote_, - [this, done] (const boost::system::error_code& err, std::size_t n) mutable { - if(err) { - done(core::error_to_exception(err)); - }else{ - done(nullptr, php::value(buffer_, n)); - } - }); - return nullptr; - }); - } - php::value udp_socket::write_to(php::parameters& params) { - if(connected_) throw php::exception("connected socket should use 'write' instead of 'write_to'"); - zend_string* data = params[0]; - zend_string* addr = params[1]; - int port = params[2]; - remote_.address(address::from_string(std::string(addr->val, addr->len))); - remote_.port(port); - return php::value([this, data] (php::parameters& params) -> php::value { - php::callable done = params[0]; - // 需要进行类型转换,否则 asio::buffer 会将 zend_string -> val 长度解为 1 - socket_.async_send_to( - boost::asio::buffer(reinterpret_cast(data->val), data->len), - remote_, [this, done] (const boost::system::error_code& err, std::size_t n) mutable { - if(err) { - done(core::error_to_exception(err)); - }else{ - done(nullptr, static_cast(n)); - } - }); - return nullptr; - }); - } - php::value udp_socket::write(php::parameters& params) { - zend_string* data = params[0]; - return php::value([this, data] (php::parameters& params) -> php::value { - php::callable done = params[0]; - // asio::buffer 会将 zend_string -> val 长度解为 1 - socket_.async_send( - boost::asio::buffer(reinterpret_cast(data->val), data->len), - [this, done] (const boost::system::error_code& err, std::size_t n) mutable { - if(err) { - done(core::error_to_exception(err)); - }else{ - done(nullptr, static_cast(n)); - } - }); - return nullptr; - }); - } - php::value udp_socket::close(php::parameters& params) { - boost::system::error_code err; - socket_.close(err); // 存在重复关闭的可能,排除错误 - connected_ = false; - return nullptr; - } -} diff --git a/src/net/udp_socket.h b/src/net/udp_socket.h deleted file mode 100644 index 8693e6d..0000000 --- a/src/net/udp_socket.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -namespace net { - class udp_socket: public php::class_base { - public: - static void init(php::extension_entry& extension); - udp_socket(); - php::value __construct(php::parameters& params); - php::value __destruct(php::parameters& params); - php::value bind(php::parameters& params); - php::value connect(php::parameters& params); - php::value remote_addr(php::parameters& params); - php::value remote_port(php::parameters& params); - php::value close(php::parameters& params); - php::value read(php::parameters& params); - php::value write_to(php::parameters& params); - php::value write(php::parameters& params); - private: - udp::socket socket_; - char buffer_[64*1024]; - // 最近一次接收的来源 - udp::endpoint remote_; - bool connected_; - bool is_ipv6_; - void set_prop_local_addr(); - }; -} diff --git a/src/net_mill/addr.cpp b/src/net_mill/addr.cpp deleted file mode 100644 index 7bbe2b3..0000000 --- a/src/net_mill/addr.cpp +++ /dev/null @@ -1,29 +0,0 @@ -#include "../vendor.h" -#include "addr.h" - -namespace net { - - php::value addr_t::__toString(php::parameters& params) { - php::value r = php::value::string(MILL_IPADDR_MAXSTRLEN + 6); - zend_string* buffer = r; - mill_ipaddrstr(addr_, buffer->val); - buffer->len = std::strlen(buffer->val); -#define PHP_SPRINTF sprintf -#undef sprintf - buffer->len += std::sprintf(buffer->val + buffer->len, ":%d", port_); -#define sprintf PHP_SPRINTF - return std::move(r); - } - - php::value addr_t::port(php::parameters& params) { - return port_; - } - - php::value addr_t::host(php::parameters& params) { - php::value r = php::value::string(MILL_IPADDR_MAXSTRLEN); - zend_string* buffer = r; - mill_ipaddrstr(addr_, buffer->val); - buffer->len = std::strlen(buffer->val); - return std::move(r); - } -} diff --git a/src/net_mill/addr.h b/src/net_mill/addr.h deleted file mode 100644 index b7f5203..0000000 --- a/src/net_mill/addr.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -namespace net { - class udp_socket; - class tcp_socket; - class addr_t: public php::class_base { - public: - php::value __toString(php::parameters& params); - php::value port(php::parameters& params); - php::value host(php::parameters& params); - private: - mill_ipaddr addr_; - unsigned short port_; - friend class net::udp_socket; - friend class net::tcp_socket; - }; -} diff --git a/src/net_mill/http/init.cpp b/src/net_mill/http/init.cpp deleted file mode 100644 index f909045..0000000 --- a/src/net_mill/http/init.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include "../../vendor.h" -#include "init.h" -#include "request.h" - -namespace net { namespace http { - void init(php::extension_entry& extension) { - request::init(extension); - /*php::class_entry mill_http_request("mill\\http\\request");*/ - //mill_http_request.add("parse"); - //mill_http_request.add<&request::__construct>("__construct"); - //mill_http_request.add(php::property_entry("version", "HTTP/1.1")); - //mill_http_request.add(php::property_entry("method", "GET")); - //mill_http_request.add(php::property_entry("uri", "/")); - //mill_http_request.add(php::property_entry("body", nullptr)); - //mill_http_request.add(php::property_entry("header", nullptr)); - //mill_http_request.add(php::property_entry("query", nullptr)); - //mill_http_request.add(php::property_entry("cookie", nullptr)); - /*extension.add(std::move(mill_http_request));*/ - } -} } diff --git a/src/net_mill/http/init.h b/src/net_mill/http/init.h deleted file mode 100644 index 1a99bde..0000000 --- a/src/net_mill/http/init.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -namespace net { namespace http { - void init(php::extension_entry& extension); -} } diff --git a/src/net_mill/http/request.cpp b/src/net_mill/http/request.cpp deleted file mode 100644 index 85e534b..0000000 --- a/src/net_mill/http/request.cpp +++ /dev/null @@ -1,229 +0,0 @@ -#include "../../vendor.h" -#include "../../core.h" -#include "../../net/tcp_socket.h" -#include "request.h" - -namespace net { namespace http { - - void request::init(php::extension_entry& extension) { - php::class_entry http_request("flame\\net\\http\\request"); - http_request.add<&request::__construct>("__construct"); - http_request.add(php::property_entry("method", nullptr)); - http_request.add(php::property_entry("uri", nullptr)); - http_request.add(php::property_entry("version", nullptr)); - http_request.add(php::property_entry("header", nullptr)); - http_request.add(php::property_entry("query", nullptr)); - http_request.add(php::property_entry("cookie", nullptr)); - http_request.add(php::property_entry("body", nullptr)); - http_request.add<&request::parse>("parse"); - extension.add(std::move(http_request)); - return ; - } - - static void parse_first(char* buffer, std::size_t n, php::object& req) { - for(int p=0,x=0,i=1;iquery !=nullptr) { - req.prop("query") = php::parse_str('&', url->query, std::strlen(url->query)); - }else{ - req.prop("query") = php::array((size_t)0); - } - p=i+1; - ++x; - } - break; - case 2: // VERSION - if(std::isspace(buffer[i])) { - req.prop("version") = php::value(php::strtoupper(buffer+p, i-p), i-p); - // TODO 确认协议形式是否正确? - return; // 解析结束 - } - break; - } - } - // TODO 解析失败 - } - - static void parse_header(char* buffer, std::size_t n, php::array& hdr, php::object& req) { - //php::array item(nullptr); - //int len; - int key_beg, key_end, key_len; - int val_beg, val_end, val_len; - char* key; - char* val; - for(int x = 0,i = 0; i < n; ++i) { - - switch(x) { - case 0: - if(!std::isspace(buffer[i]) && buffer[i] != ':') { - key_beg = i; - ++x; - } - break; - case 1: - if(!std::isspace(buffer[i]) && buffer[i] != ':') { - continue; - }else if(buffer[i] == ':') { - key_end = i; - key_len = key_end - key_beg; - php::strtolower(buffer + key_beg, key_len); - - key = buffer + key_beg; - //item.at(key, len); - //item.reset( hdr.item(buffer+p, len) ); - //item = php::value("", 0); - ++x; - } - break; - case 2: - if(!std::isspace(buffer[i])) { - val_beg = i; - ++x; - } - break; - case 3: - val_end = i; - if(!std::isspace(buffer[i])) { - continue; - }else if(buffer[i] == '\r') { - hdr.at(key, key_len) = php::value(buffer + val_beg, val_end - val_beg); - if(std::memcmp(key, "cookie", key_len) == 0) { - req.prop("cookie") = php::parse_str(';', buffer + val_beg, val_end - val_beg); - } - i+=1; - x=0; - } - } - } - return; // 解析完成 - // TODO 解析失败? - } - - static std::size_t parse_content_length(php::array& hdr) { - php::array h = hdr; - php::value len = h.at("content-length", 14); - if(len.is_null()) { - return 0; - } - // TODO 限制 BODY 的最大大小? - return /*std::atoi(static_cast(len.to_string()))*/len.to_long(); - } - - void request::parse_request_line(php::object& req, size_t n) { - php::string request_line(n); - buffer_.sgetn(request_line.data(), n); - //buffer_.consume(n); - parse_first(request_line.data(), n, req); - //char* p = strstr(request_line.data(), "\r\n") + 2; - //header.rev(request_line.data() - p); - //size_t len = request_line.data() + request_line.length() - p; - //if(len) std::memcpy(header.rev(len), p, len); - size_t len = buffer_.size(); - if(len) { - buffer_.sgetn(header.put(len), len); - //buffer_.consume(len); - } - } - - void request::parse_request_header(php::object& req, size_t n) { - // header string - buffer_.sgetn(header.put(n), n); - //buffer_.consume(n); - - // parse header - parse_header(((zend_string*)header)->val, header.size(), hdr, req); - req.prop("header") = hdr; - } - - void request::parse_request_body(php::object& req, size_t n) { - php::string body(n); - buffer_.commit(n); - buffer_.sgetn(body.data(), n); - - php::value type_ = hdr.at("content-type", 12); - // 下述两种 content-type 自动解析: - // 1. application/x-www-form-urlencoded 33 - // 2. application/json 16 - zend_string* type = type_; - php::strtolower(type->val, type->len); - if(type->len >=33 && std::memcmp(type->val, "application/x-www-form-urlencoded", 33) == 0) { - //zend_string* str = body.c_str(); - req.prop("body") = php::parse_str('&', body.data(), body.length()); - }else if(type->len >=16 && std::memcmp(type->val, "application/json", 16) == 0) { - //zend_string* str = body.c_str(); - req.prop("body") = php::json_decode(body.data(), body.length()); - }else{ - // 其他 content-type 返回原始 body 内容 - req.prop("body") = body; - } - } - - php::value request::parse(php::parameters& params) { - php::value tcp_= params[0]; - if(!tcp_.is_object() || !tcp_.is_instance_of()) { - throw php::exception("type error: object of mill\\net\\tcp_socket expected"); - } - - php::object req = php::object::create(); /*= php::value::object()*/ - request* r = req.native(); - r->tcp = tcp_.native(); - - return php::value([r, req](php::parameters& params) mutable -> php::value { - php::callable done = params[0]; - // parse request line - boost::asio::async_read_until(r->tcp->socket_, r->buffer_, "\r\n", - [r, req, done](const boost::system::error_code err, std::size_t n) mutable { - if(err) { - done(core::error_to_exception(err)); - } else { - r->parse_request_line(req, n); - // parse header - boost::asio::async_read_until(r->tcp->socket_, r->buffer_, "\r\n\r\n", - [r, req, done](const boost::system::error_code err, std::size_t n) mutable { - if(err) { - done(core::error_to_exception(err)); - } else { - r->parse_request_header(req, n); - std::size_t content_length = parse_content_length(r->hdr); - if(content_length > 0) { - auto body_buff = r->buffer_.prepare(content_length); - // parse body - //boost::asio::async_read_until(tcp, body_buff, "\r\n", - //[r, req](const boost::system::error_code err, std::size_t n) mutable { - boost::asio::async_read(r->tcp->socket_, body_buff, - [r, req, done] (const boost::system::error_code err, std::size_t n) mutable { - if(err) { - done(core::error_to_exception(err)); - } else { - r->parse_request_body(req, n); - done(nullptr, req); - } - }); - } else { - done(nullptr, req); - } - } - }); - - } - }); - }); - - } - php::value request::__construct(php::parameters& params) { - return nullptr; - } -}} diff --git a/src/net_mill/http/request.h b/src/net_mill/http/request.h deleted file mode 100644 index 789fcf5..0000000 --- a/src/net_mill/http/request.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -namespace net { - class tcp_socket; - namespace http { - class request: public php::class_base { - public: - //request() = default ; - static void init(php::extension_entry& extension); - static php::value parse(php::parameters& params); - php::value __construct(php::parameters& params); - void parse_request_line(php::object& req, size_t n); - void parse_request_header(php::object& req, size_t n); - void parse_request_body(php::object& req, size_t n); - - private: - php::buffer header; - php::array hdr; - boost::asio::streambuf buffer_; - net::tcp_socket* tcp; - php::value done; - }; -} } diff --git a/src/net_mill/init.cpp b/src/net_mill/init.cpp deleted file mode 100644 index cff9a32..0000000 --- a/src/net_mill/init.cpp +++ /dev/null @@ -1,81 +0,0 @@ -#include "../vendor.h" -#include "init.h" -#include "addr.h" -#include "udp_socket.h" -#include "tcp_socket.h" -#include "tcp_server.h" -#include "http/init.h" - -namespace net { - void init(php::extension_entry& extension) { - // mill_address - // --------------------------------------------------------------------- - php::class_entry mill_addr("mill\\addr"); - mill_addr.add<&addr_t::host>("host"); - mill_addr.add<&addr_t::port>("port"); - mill_addr.add<&addr_t::__toString>("__toString"); - extension.add(std::move(mill_addr)); - // mill_udp_socket - // --------------------------------------------------------------------- - php::class_entry mill_udp_socket("mill\\net\\udp_socket"); - mill_udp_socket.add<&udp_socket::__construct>("__construct", { - php::of_string("addr"), - php::of_integer("port"), - }); - mill_udp_socket.add<&udp_socket::__destruct>("__destruct"); - mill_udp_socket.add<&udp_socket::remote_addr>("remote_addr"); - mill_udp_socket.add<&udp_socket::close>("close"); - mill_udp_socket.add<&udp_socket::recv>("recv"); - mill_udp_socket.add<&udp_socket::send>("send", { - php::of_string("data"), - php::of_string("host"), - php::of_integer("port"), - }); - mill_udp_socket.add(php::property_entry("local_port", nullptr)); - mill_udp_socket.add(php::property_entry("closed", true)); - extension.add(std::move(mill_udp_socket)); - // mill_tcp_socket - // --------------------------------------------------------------------- - php::class_entry mill_tcp_socket("mill\\net\\tcp_socket"); - mill_tcp_socket.add<&tcp_socket::__construct>("__construct", { - php::of_string("addr"), - php::of_integer("port"), - php::of_integer("deadline"), - }); - mill_tcp_socket.add<&tcp_socket::__destruct>("__destruct"); - mill_tcp_socket.add<&tcp_socket::remote_addr>("remote_addr"); - mill_tcp_socket.add<&tcp_socket::close>("close"); - mill_tcp_socket.add<&tcp_socket::recv>("recv", { - php::of_mixed("stop"), // integer or string - php::of_integer("deadline"), - }); - mill_tcp_socket.add<&tcp_socket::send>("send", { - php::of_string("data"), - php::of_integer("deadline"), - }); - mill_tcp_socket.add<&tcp_socket::send_buffer>("send_buffer", { - php::of_string("data"), - php::of_integer("deadline"), - }); - mill_tcp_socket.add<&tcp_socket::flush>("flush_buffer", { - php::of_integer("deadline"), - }); - mill_tcp_socket.add(php::property_entry("closed", true)); - extension.add(std::move(mill_tcp_socket)); - // mill_tcp_socket - // --------------------------------------------------------------------- - php::class_entry mill_tcp_server("mill\\net\\tcp_server"); - mill_tcp_server.add<&tcp_server::__construct>("__construct", { - php::of_string("addr"), - php::of_integer("port"), - }); - mill_tcp_server.add<&tcp_server::__destruct>("__destruct"); - mill_tcp_server.add<&tcp_server::close>("close"); - mill_tcp_server.add<&tcp_server::accept>("accept"); - mill_tcp_server.add(php::property_entry("local_port", nullptr)); - mill_tcp_server.add(php::property_entry("closed", true)); - extension.add(std::move(mill_tcp_server)); - - http::init(extension); - } -} diff --git a/src/net_mill/init.h b/src/net_mill/init.h deleted file mode 100644 index 5820423..0000000 --- a/src/net_mill/init.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -namespace net { - void init(php::extension_entry& extension); -} diff --git a/src/net_mill/tcp_server.cpp b/src/net_mill/tcp_server.cpp deleted file mode 100644 index e735956..0000000 --- a/src/net_mill/tcp_server.cpp +++ /dev/null @@ -1,52 +0,0 @@ -#include "../vendor.h" -#include "tcp_socket.h" -#include "tcp_server.h" - -namespace net { - php::value tcp_server::__construct(php::parameters& params) { - local_addr_ = mill_iplocal(params[0], params[1], 0); - if(errno != 0) { - throw php::exception("failed to create tcp server (iplocal)", errno); - } - server_ = mill_tcplisten(local_addr_, 2048); - if(errno != 0) { - throw php::exception("failed to create tcp server (tcplisten)", errno); - } - prop("local_port") = mill_tcpport(server_); - closed_ = false; - prop("closed", closed_); - return nullptr; - } - php::value tcp_server::__destruct(php::parameters& params) { - if(!closed_) { - closed_ = true; - mill_tcpclose(server_); - } - return nullptr; - } - php::value tcp_server::accept(php::parameters& params) { - std::int64_t dead = -1; - if(params.length()>0) { - dead = mill_now() + static_cast(params[0]); - } - php::value sock_= php::value::object(); - tcp_socket* sock = sock_.native(); - sock->socket_ = mill_tcpaccept(server_, dead); - if(errno != 0) { - throw php::exception("failed to accept tcp socket", errno); - } - sock->remote_addr_ = mill_tcpaddr(sock->socket_); - sock->closed_ = false; - sock->prop("closed") = sock->closed_; - std::printf("accepted: %08x %08x\n", sock, sock->socket_); - return std::move(sock_); - } - php::value tcp_server::close(php::parameters& params) { - if(!closed_) { - closed_ = true; - prop("closed", closed_); - mill_tcpclose(server_); - } - return nullptr; - } -} diff --git a/src/net_mill/tcp_server.h b/src/net_mill/tcp_server.h deleted file mode 100644 index 72a5917..0000000 --- a/src/net_mill/tcp_server.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -namespace net { - class tcp_server: public php::class_base { - public: - tcp_server():closed_(true) {} - php::value __construct(php::parameters& params); - php::value __destruct(php::parameters& params); - php::value accept(php::parameters& params); - php::value close(php::parameters& params); - private: - mill_tcpsock server_; - bool closed_; - mill_ipaddr local_addr_; - }; -} diff --git a/src/net_mill/tcp_socket.cpp b/src/net_mill/tcp_socket.cpp deleted file mode 100644 index d832d26..0000000 --- a/src/net_mill/tcp_socket.cpp +++ /dev/null @@ -1,124 +0,0 @@ -#include "../vendor.h" -#include "addr.h" -#include "tcp_socket.h" - -namespace net { - php::value tcp_socket::__construct(php::parameters& params) { - std::int64_t dead = -1; - if(params.length() > 2) { - dead = mill_now() + static_cast(params[3]); - } - remote_addr_ = mill_ipremote(params[0], params[1], 0, dead); - if(errno != 0) { - throw php::exception("failed to connect (dns)", errno); - } - std::printf("tcp_socket::__construct\n"); - socket_ = mill_tcpconnect(remote_addr_, dead); - if(errno != 0) { - throw php::exception("failed to connect (tcp)", errno); - } - closed_ = false; - prop("closed", closed_); - return nullptr; - } - php::value tcp_socket::__destruct(php::parameters& params) { - if(!closed_) { - closed_ = true; - std::printf("before close: %08x %08x\n", this, socket_); - mill_tcpclose(socket_); - std::printf("after close: %08x %08x\n", this, socket_); - } - return nullptr; - } - php::value tcp_socket::remote_addr(php::parameters& params) { - php::value addr = php::value::object(); - addr.native()->addr_ = remote_addr_; - addr.native()->port_ = mill_ipport(remote_addr_); - return std::move(addr); - } - php::value tcp_socket::recv(php::parameters& params) { - php::value stop = params[0]; - std::int64_t dead = -1; - if(params.length() > 1) { - dead = mill_now() + static_cast(params[1]); - } - if(stop.is_long()) { // 接收一定量 - return recv_length(stop, dead); - }else if(stop.is_string()) { // 接收到指定任意停止符号 - return recv_delims(stop, dead); - } - throw php::exception("unsupported recv condition: long for specified length or string for specified delim expected"); - } - php::value tcp_socket::recv_length(int length, std::int64_t dead) { - php::value r = php::value::string(length); - zend_string* b = r; - b->len = mill_tcprecv(socket_, b->val, length, dead); - if(errno != 0) { - throw php::exception("failed to recv length", errno); - } - return std::move(r); - } - php::value tcp_socket::recv_delims(zend_string* delims, std::int64_t dead) { - php::buffer b(1024); // 初始容量 -AGAIN: - char* x = b.rev(512); - std::size_t n = mill_tcprecvuntil(socket_, x, 512, delims->val, delims->len, dead); - b.put(n); - if(errno == ENOBUFS) { // 未遇到结束符 - goto AGAIN; - }else if(errno != 0) { - throw php::exception("failed to recv delim", errno); - } - return std::move(b); - } - php::value tcp_socket::send_buffer(php::parameters& params) { - zend_string* b = params[0]; - std::int64_t d = -1; - if(params.length() > 1) { - d = mill_now() + static_cast(params[1]); - } - std::size_t n = mill_tcpsend(socket_, b->val, b->len, d); - if(errno != 0) { - throw php::exception("failed to send_buffer", errno); - } - return php::value((std::int64_t)n); - } - php::value tcp_socket::flush(php::parameters& params) { - std::int64_t d = -1; - if(params.length() > 1) { - d = mill_now() + static_cast(params[1]); - } - mill_tcpflush(socket_, d); - if(errno != 0) { - throw php::exception("failed to flush", 0); - } - return nullptr; - } - php::value tcp_socket::send(php::parameters& params) { - std::printf("socket::send: %08x %08x\n", this, socket_); - zend_string* b = params[0]; - std::int64_t d = -1; - if(params.length() > 1) { - d = mill_now() + static_cast(params[1]); - } - std::size_t n = mill_tcpsend(socket_, b->val, b->len, d); - if(errno != 0) { - throw php::exception("failed to send (send)", errno); - } - mill_tcpflush(socket_, d); - if(errno != 0) { - throw php::exception("failed to send (flush)", errno); - } - return php::value((std::int64_t)n); - } - php::value tcp_socket::close(php::parameters& params) { - if(!closed_) { - closed_ = true; - prop("closed", closed_); - std::printf("before close: %08x %08x\n", this, socket_); - mill_tcpclose(socket_); - std::printf("after close: %08x %08x\n", this, socket_); - } - return nullptr; - } -} diff --git a/src/net_mill/tcp_socket.h b/src/net_mill/tcp_socket.h deleted file mode 100644 index e9345a2..0000000 --- a/src/net_mill/tcp_socket.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -namespace net { - namespace http { - class request; - } - class tcp_server; - class tcp_socket: public php::class_base { - public: - tcp_socket():socket_(nullptr),closed_(true) {} - // 通过指定连接地址构造一个 tcp_socket,支持指定超时时间 - php::value __construct(php::parameters& params); - php::value __destruct(php::parameters& params); - php::value remote_addr(php::parameters& params); - // recv 支持两种形式: - // 1. 接收指定大小 - // 2. 接收指定任意停止符 - // 两种形式都支持指定超时 - php::value recv(php::parameters& params); - php::value send_buffer(php::parameters& params); - php::value flush(php::parameters& params); - php::value send(php::parameters& params); - php::value close(php::parameters& params); - private: - php::value recv_length(int length, std::int64_t dead); - php::value recv_delims(zend_string* delims, std::int64_t dead); - mill_tcpsock socket_; - bool closed_; - mill_ipaddr remote_addr_; - friend class tcp_server; - friend class http::request; - }; -} diff --git a/src/os/signal_watcher.cpp b/src/os/signal_watcher.cpp deleted file mode 100644 index 8bff649..0000000 --- a/src/os/signal_watcher.cpp +++ /dev/null @@ -1,26 +0,0 @@ -#include "../vendor.h" -#include "signal_watcher.h" - -signal_watcher::signal_watcher(std::initializer_list signals) { - sigemptyset(&mask_); - for(auto i=signals.begin();i!=signals.end();++i) { - sigaddset(&mask_, *i); - } - sigprocmask(SIG_BLOCK, &mask_, nullptr); - fd_ = signalfd(-1, &mask_, SFD_NONBLOCK); - if(fd_ == -1) { - perror("failed to set signal watcher"); - exit(errno); - } -} - -int signal_watcher::next() { - int events = mill_fdwait(fd_, MILL_FDW_IN, -1); - if(events & MILL_FDW_ERR) { - // TODO 错误处理? - } - if(events & MILL_FDW_IN) { - read(fd_, buffer_, sizeof(buffer_)); - return *reinterpret_cast(buffer_); - } -} diff --git a/src/os/signal_watcher.h b/src/os/signal_watcher.h deleted file mode 100644 index 4477ac8..0000000 --- a/src/os/signal_watcher.h +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once - -class signal_watcher { -public: - signal_watcher(std::initializer_list signals); - int next(); -private: - sigset_t mask_; - int fd_; - char buffer_[sizeof(struct signalfd_siginfo)]; -}; \ No newline at end of file diff --git a/src/signal_watcher.h b/src/signal_watcher.h new file mode 100644 index 0000000..93ea894 --- /dev/null +++ b/src/signal_watcher.h @@ -0,0 +1,34 @@ +#pragma once +#include "vendor.h" + +class logger; +class signal_watcher { +public: + explicit signal_watcher(boost::asio::io_context& io) + : ss_(new boost::asio::signal_set(io)) { + ss_->add(SIGINT); + ss_->add(SIGTERM); + ss_->add(SIGUSR1); + ss_->add(SIGUSR2); + ss_->add(SIGQUIT); + } + virtual ~signal_watcher() { + ss_.reset(); + // std::cout << "~signal_watcher\n"; + } + void sw_watch() { + ss_->async_wait([this, self = sw_self()] (const boost::system::error_code& error, int sig) { + if (error) return; + if (!on_signal(sig)) return; + sw_watch(); + }); + } + void sw_close() { + ss_.reset(); + } + virtual std::shared_ptr sw_self() = 0; +protected: + virtual bool on_signal(int sig) = 0; +private: + std::unique_ptr ss_; +}; \ No newline at end of file diff --git a/src/url.cpp b/src/url.cpp new file mode 100644 index 0000000..82dd653 --- /dev/null +++ b/src/url.cpp @@ -0,0 +1,62 @@ +#include "vendor.h" +#include "url.h" +#include + +typedef parser::separator_parser parser_t; + +url::url(http://github.com/const%20php::string%20&str,%20bool%20parse_query) +: raw_(str) +, port(0) { // 原始类型 port 初始值可能随机 + auto u = curl_url(); + curl_url_set(u, CURLUPART_URL, str.c_str(), CURLU_NON_SUPPORT_SCHEME); + char * tmp; + if(curl_url_get(u, CURLUPART_SCHEME, &tmp, 0) == CURLUE_OK) { // SCHEME + schema.assign(tmp); + curl_free(tmp); + } + if(curl_url_get(u, CURLUPART_USER, &tmp, 0) == CURLUE_OK) { // USER + user.assign(tmp); + curl_free(tmp); + } + if(curl_url_get(u, CURLUPART_PASSWORD, &tmp, 0) == CURLUE_OK) { // PASS + pass.assign(tmp); + curl_free(tmp); + } + if(curl_url_get(u, CURLUPART_HOST, &tmp, 0) == CURLUE_OK) { // USER + host.assign(tmp); + curl_free(tmp); + } + if(curl_url_get(u, CURLUPART_PORT, &tmp, 0) == CURLUE_OK) { // PORT + port = std::atoi(tmp); + curl_free(tmp); + } + if(curl_url_get(u, CURLUPART_PATH, &tmp, 0) == CURLUE_OK) { // PATH + path.assign(tmp); + curl_free(tmp); + } + if(parse_query && curl_url_get(u, CURLUPART_QUERY, &tmp, 0) == CURLUE_OK) { // QUERY + parser_t p('\0', '\0', '=', '\0', '\0', '&', [this](parser_t::entry_type et) { + php::url_decode_inplace(et.second.data(), et.second.size()); + query[et.first] = et.second; + }); + p.parse(tmp, strlen(tmp)); + p.end(); + curl_free(tmp); + } + curl_url_cleanup(u); +} + +std::string url::str(bool with_query, bool update) { + if (update) { + std::ostringstream ss; + ss << schema << "://" << user << ":" << pass << "@" << host << ":" << port << path; + if (with_query) { + ss << "?"; + for (auto i=query.begin();i!=query.end();++i) { + ss << i->first << "=" << php::url_encode(i->second.c_str(), i->second.size()) << "&"; + } + } + raw_ = ss.str(); + } + return raw_; +} diff --git a/src/url.h b/src/url.h new file mode 100644 index 0000000..cc030e9 --- /dev/null +++ b/src/url.h @@ -0,0 +1,18 @@ +#pragma once +#include "vendor.h" + +class url { +public: + url(http://github.com/const%20php::string&%20str,%20bool%20parse_query=true); + std::string schema; + std::string host; + std::uint16_t port; + std::string path; + std::map query; + std::string user; + std::string pass; + + std::string str(bool update = false, bool with_query = true); +private: + std::string raw_; +}; \ No newline at end of file diff --git a/src/util.cpp b/src/util.cpp new file mode 100644 index 0000000..36c9358 --- /dev/null +++ b/src/util.cpp @@ -0,0 +1,23 @@ +#include "util.h" +#include "coroutine.h" + +const char* util::system_time() { + static char buffer[24] = {0}; + std::time_t t = std::time(nullptr); + struct tm *m = std::localtime(&t); + sprintf(buffer, "%04d-%02d-%02d %02d:%02d:%02d", + 1900 + m->tm_year, + 1 + m->tm_mon, + m->tm_mday, + m->tm_hour, + m->tm_min, + m->tm_sec); + return buffer; +} + + +void util::co_sleep(boost::asio::io_context& io, std::chrono::milliseconds ms, coroutine_handler& ch) { + boost::asio::steady_timer tm(io); + tm.expires_after(ms); + tm.async_wait(ch); +} \ No newline at end of file diff --git a/src/util.h b/src/util.h new file mode 100644 index 0000000..3b775a4 --- /dev/null +++ b/src/util.h @@ -0,0 +1,9 @@ +#pragma once +#include "vendor.h" + +class coroutine_handler; +class util { +public: + static const char* system_time(); + static void co_sleep(boost::asio::io_context& io, std::chrono::milliseconds ms, coroutine_handler& ch); +}; diff --git a/src/vendor.h b/src/vendor.h index 6aa2bd7..c24d050 100644 --- a/src/vendor.h +++ b/src/vendor.h @@ -1,21 +1,37 @@ #pragma once -#include +#define EXTENSION_NAME "flame" +#define EXTENSION_VERSION "0.15.0" + + +#include #include -#include -#include +#include +#include +#include +#include +#include +#include #include -#include -#include -// #include - -#include -#include -#include +#include +#include +#include +#include +#include +#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include -#include using boost::asio::ip::tcp; using boost::asio::ip::udp; -using boost::asio::ip::address; +#include +#include +#include diff --git a/src/worker_ipc.cpp b/src/worker_ipc.cpp new file mode 100644 index 0000000..28e2cfb --- /dev/null +++ b/src/worker_ipc.cpp @@ -0,0 +1,142 @@ +#include "worker_ipc.h" +#include "util.h" + +worker_ipc::worker_ipc(boost::asio::io_context& io, std::uint8_t idx) +: io_(io) +, socket_(new boost::asio::local::stream_protocol::socket(io)) +, idx_(idx) { + svrsck_ = (boost::format("/tmp/flame_ipc_%d.sock") % ::getppid()).str(); +} + +worker_ipc::~worker_ipc() { + // std::cout << "~worker_ipc\n"; + ipc_close(); +} + +std::shared_ptr worker_ipc::ipc_request(std::shared_ptr req, coroutine_handler& ch) { + std::shared_ptr res; + callback_.emplace(req->unique_id, ipc::callback_t {ch, res}); + req->source = idx_; + send(req); + ch.suspend(); + return res; +} + +void worker_ipc::ipc_request(std::shared_ptr req) { + req->source = idx_; + send(req); +} + +static void free_json_message(ipc::message_t* msg) { + zend_string* ptr = reinterpret_cast((char*)(msg) - offsetof(zend_string, val)); + smart_str str { ptr, 0 }; + smart_str_free(&str); +} + +void worker_ipc::ipc_notify(std::uint8_t target, php::value data) { + if(data.type_of(php::TYPE::STRING)) { + php::string str = data; + auto msg = ipc::create_message(str.size()); + msg->command = ipc::COMMAND_MESSAGE_STRING; + msg->source = idx_; + msg->target = target; + std::memcpy(&msg->payload[0], str.data(), str.size()); + msg->length = str.size(); + send(msg); + } + else { + smart_str str {nullptr, 0}; // !!!! 避免 JSON 文本的 COPY 复制 + smart_str_alloc(&str, 1, false); + ipc::message_t* msg = (ipc::message_t*)str.s->val; + msg->command = ipc::COMMAND_MESSAGE_JSON; + msg->source = idx_; + msg->target = target; + str.s->len = sizeof(ipc::message_t); + php::json_encode_to(&str, data); + msg->length = str.s->len - sizeof(ipc::message_t); + send(std::shared_ptr(msg, free_json_message)); + } +} + +void worker_ipc::ipc_start() { + // !!!! 注意本地日志的判定机制与时序、时间有关 + ++status_; // 0 + coroutine::start(io_.get_executor(), + std::bind(&worker_ipc::ipc_run, ipc_self(), std::placeholders::_1)); // 由于线程池影响,特殊的启动方式 +} +// !!!! 工作线程 +void worker_ipc::ipc_run(coroutine_handler ch) { + boost::system::error_code error; + std::shared_ptr msg; + std::uint64_t tmp; + + // 连接主进程 IPC 通道 + boost::asio::local::stream_protocol::endpoint addr(svrsck_); + socket_->async_connect(addr, ch[error]); + if (error) goto IPC_FAILED; + ++status_; // 1 + // 发送注册消息 + msg = ipc::create_message(); + msg->command = ipc::COMMAND_REGISTER; + msg->source = idx_; + msg->target = 0; + msg->length = 0; + boost::asio::async_write(*socket_, boost::asio::buffer(msg.get(), sizeof(ipc::message_t) + msg->length), ch[error]); + if (error) goto IPC_FAILED; + ++status_; // 2 + // 已经缓冲在队列中的数据开始发送 + send_next(); + while(true) { + msg = ipc::create_message(); + boost::asio::async_read(*socket_, boost::asio::buffer(msg.get(), sizeof(ipc::message_t)), ch[error]); + if(error) break; + if(msg->length > ipc::MESSAGE_INIT_CAPACITY - sizeof(ipc::message_t)) { + ipc::relloc_message(msg, 0, msg->length); + } + if(msg->length > 0) { + boost::asio::async_read(*socket_, boost::asio::buffer(&msg->payload[0], msg->length), ch[error]); + if(error) break; + } + if(!on_message(msg)) break; + auto pcb = callback_.extract(msg->unique_id); + if(!pcb.empty()) { + pcb.mapped().res = msg; + pcb.mapped().cch.resume(); + } + } +IPC_FAILED: + if (error && error != boost::asio::error::operation_aborted && error != boost::asio::error::eof) + output() << "[" << util::system_time() << "] (ERROR) Failed during IPC with master: (" << error.value() << ") " << error.message() << "\n"; + socket_->close(error); +} + +void worker_ipc::ipc_close() { + socket_->close(); +} + +bool worker_ipc::ipc_enabled() { + return status_ > -1; +} +// 由于实际调用者来自主线程,需要线性转接到(多个)工作线程中 +void worker_ipc::send(std::shared_ptr msg) { + assert(status_ > -1); + boost::asio::post(io_, [this, self = ipc_self(), msg] () { + sendq_.push_back(msg); + if(status_ > 1 && sendq_.size() == 1) send_next(); + // C++11 后, std::list::size() 是常数及时间复杂度 + }); +} + +void worker_ipc::send_next() { + if (sendq_.empty()) return; + std::shared_ptr msg = sendq_.front(); + + boost::asio::async_write(*socket_, boost::asio::buffer(msg.get(), sizeof(ipc::message_t) + msg->length), [this] (const boost::system::error_code& error, std::size_t size) { + if(error) { + output() << "[" << util::system_time() << "] [FATAL] Failed during IPC with master: (" << error.value() << ") " << error.message() << "\n"; + return; + } + sendq_.pop_front(); + send_next(); + }); +} diff --git a/src/worker_ipc.h b/src/worker_ipc.h new file mode 100644 index 0000000..cdc8671 --- /dev/null +++ b/src/worker_ipc.h @@ -0,0 +1,39 @@ +#pragma once +#include "vendor.h" +#include "ipc.h" + +class worker_ipc { +public: + worker_ipc(boost::asio::io_context& io, std::uint8_t idx); + virtual ~worker_ipc(); + using socket_ptr = std::shared_ptr; + + virtual std::ostream& output() = 0; + // 请求并等待响应 + std::shared_ptr ipc_request(std::shared_ptr req, coroutine_handler& ch); + // 请求(无响应) + void ipc_request(std::shared_ptr data); + // 简化 notify 请求 + void ipc_notify(std::uint8_t target, php::value data); + void ipc_start(); + // 读取及消息监听 + void ipc_run(coroutine_handler ch); + // 关闭通道 + void ipc_close(); + // IPC 检测 + bool ipc_enabled(); +protected: + virtual std::shared_ptr ipc_self() = 0; + virtual bool on_message(std::shared_ptr msg) = 0; +private: + boost::asio::io_context& io_; + std::filesystem::path svrsck_; + std::uint8_t idx_; + socket_ptr socket_; + std::map callback_; + std::list> sendq_; + int status_ = -1; + + void send(std::shared_ptr msg); + void send_next(); +}; diff --git a/src/worker_logger.cpp b/src/worker_logger.cpp new file mode 100644 index 0000000..9794049 --- /dev/null +++ b/src/worker_logger.cpp @@ -0,0 +1,26 @@ +#include "worker_logger.h" +#include "worker_logger_manager.h" +#include "worker_logger_buffer.h" + +worker_logger::worker_logger(worker_logger_manager* mgr, const std::filesystem::path& path, std::uint8_t idx) +: idx_(0) +, path_(path) +, wlb_(new worker_logger_buffer(mgr, idx)) +, oss_(new std::ostream(wlb_.get())) { + +} + +worker_logger::worker_logger(worker_logger_manager* mgr, const std::filesystem::path& path, std::uint8_t idx, bool local) +: idx_(0) +, path_(path) { + if(path.string() != "") { + auto fb = new std::filebuf(); + fb->open(path, std::ios_base::app); + oss_.reset(new std::ostream(fb)); + wlb_.reset(fb); + } +} + +std::ostream& worker_logger::stream() { + return oss_ ? (*oss_) : std::clog; +} diff --git a/src/worker_logger.h b/src/worker_logger.h new file mode 100644 index 0000000..c9a0811 --- /dev/null +++ b/src/worker_logger.h @@ -0,0 +1,20 @@ +#pragma once +#include "vendor.h" +#include + +class coroutine_handler; +class worker_logger_manager; +class worker_logger_buffer; +class worker_logger { +public: + worker_logger(worker_logger_manager* mgr, const std::filesystem::path& file, std::uint8_t index); + worker_logger(worker_logger_manager* mgr, const std::filesystem::path& file, std::uint8_t index, bool local); + std::ostream& stream(); +private: + std::uint8_t idx_; + std::filesystem::path path_; + std::unique_ptr wlb_; + std::unique_ptr oss_; + friend class worker_logger_manager; +}; + diff --git a/src/worker_logger_buffer.cpp b/src/worker_logger_buffer.cpp new file mode 100644 index 0000000..7961d07 --- /dev/null +++ b/src/worker_logger_buffer.cpp @@ -0,0 +1,43 @@ +#include "worker_logger_buffer.h" +#include "worker_logger_manager.h" +#include "worker_ipc.h" + +worker_logger_buffer::worker_logger_buffer(worker_logger_manager* mgr, std::uint8_t idx) +: mgr_(mgr) +, msg_(ipc::create_message()) +, idx_(idx) { + cap_ = msg_->length; + msg_->xdata[0] = idx_; + msg_->length = 0; +} + +int worker_logger_buffer::overflow(int ch) { + char c = ch; + msg_->payload[msg_->length] = c; + ++msg_->length; + if(c == '\n') transfer_msg(); + return ch; +} + +long worker_logger_buffer::xsputn(const char* s, long c) { + if(cap_ - msg_->length < c) { + assert(cap_ < 2048 * 1024); + ipc::relloc_message(msg_, msg_->length, msg_->length + c); + } + std::memcpy(msg_->payload + msg_->length, s, c); + msg_->length += c; + + if(s[c-1] == '\n') transfer_msg(); + return c; +} + +void worker_logger_buffer::transfer_msg() { + msg_->command = ipc::COMMAND_LOGGER_DATA; + msg_->target = 0; + mgr_->ipc_->ipc_request(msg_); + + msg_ = ipc::create_message(); + cap_ = msg_->length; + msg_->xdata[0] = idx_; // 实际写入的日志文件 + msg_->length = 0; // 以 length 累计当前需要发送的数据 +} \ No newline at end of file diff --git a/src/worker_logger_buffer.h b/src/worker_logger_buffer.h new file mode 100644 index 0000000..362da50 --- /dev/null +++ b/src/worker_logger_buffer.h @@ -0,0 +1,20 @@ +#pragma once +#include +#include +#include "ipc.h" + +class worker_logger_manager; +class worker_logger_buffer: public std::streambuf { +public: + worker_logger_buffer(worker_logger_manager* mgr, std::uint8_t idx); +protected: + int overflow(int ch = EOF) override; + long xsputn(const char* s, long c) override; +private: + worker_logger_manager* mgr_; + std::shared_ptr msg_; + std::uint16_t cap_; + std::uint8_t idx_; + + void transfer_msg(); +}; diff --git a/src/worker_logger_manager.cpp b/src/worker_logger_manager.cpp new file mode 100644 index 0000000..0cb3dcf --- /dev/null +++ b/src/worker_logger_manager.cpp @@ -0,0 +1,61 @@ +#include "worker_logger_manager.h" +#include "worker_logger.h" +#include "worker_ipc.h" + +worker_logger_manager::~worker_logger_manager() { + // std::cout << "~worker_logger_manager\n"; +} + +std::shared_ptr worker_logger_manager::lm_connect(const std::string& file, coroutine_handler& ch) { + std::filesystem::path path = file; + path = path.lexically_normal(); + + for (auto i=logger_.begin();i!=logger_.end();) { + if(i->second.expired()) i = logger_.erase(i); + else { + std::shared_ptr lg = i->second.lock(); + if(lg->path_ == path) return lg; + ++i; + } + } + + std::shared_ptr wl; + std::uint8_t index = 0; + if (ipc_->ipc_enabled()) { + auto msg = ipc::create_message(); + msg->command = ipc::COMMAND_LOGGER_CONNECT; + msg->target = 0; // 发送给主进程的消息 + msg->unique_id = ipc::create_uid(); + msg->length = file.size(); + std::memcpy(msg->payload, file.data(), msg->length); + msg = ipc_->ipc_request(msg, ch); + index = msg->xdata[0]; + // 使用该日志文件标号创建 LOGGER 对象,以支持实际日志发送 + wl = std::make_shared(this, file, index); + } + else { + index = lindex_; + wl = std::make_shared(this, file, index, true); + ++lindex_; + } + + logger_.emplace(index, wl); + return wl; +} + +void worker_logger_manager::lm_destroy(std::uint8_t idx) { + auto i = logger_.find(idx); + if(i == logger_.end()) return; + logger_.erase(i); + + // IPC: 通知父进程日志器不再使用 + auto msg = ipc::create_message(); + msg->command = ipc::COMMAND_LOGGER_DESTROY; + msg->target = idx; + msg->length = 0; + ipc_->ipc_request(msg); +} + +void worker_logger_manager::lm_close() { + +} \ No newline at end of file diff --git a/src/worker_logger_manager.h b/src/worker_logger_manager.h new file mode 100644 index 0000000..8f1d564 --- /dev/null +++ b/src/worker_logger_manager.h @@ -0,0 +1,22 @@ +#pragma once +#include "ipc.h" + +class worker_ipc; +class worker_logger; +class worker_logger_manager { +public: + worker_logger_manager(worker_ipc* c): ipc_(c) {} + virtual ~worker_logger_manager(); + // 连接到指定的日志文件 + std::shared_ptr lm_connect(const std::string& filepath, coroutine_handler& ch); + void lm_destroy(std::uint8_t idx); + void lm_close(); +protected: + virtual std::shared_ptr lm_self() = 0; +private: + std::map> logger_; + std::uint8_t lindex_ = 0; + worker_ipc* ipc_; // 目前的继承实现方式,此字段与 this 相等 + friend class worker_logger; + friend class worker_logger_buffer; +}; diff --git a/test/compress_1.php b/test/compress_1.php new file mode 100644 index 0000000..b2e5487 --- /dev/null +++ b/test/compress_1.php @@ -0,0 +1,15 @@ +1234567890123,"b"=>1234567890123,"x"=>"中文","y"=>"中文"], JSON_UNESCAPED_UNICODE)); + echo "(", strlen($x), ") ", bin2hex($x), "\n"; + $y = flame\compress\snappy_uncompress($x); + echo "(", strlen($y), ") ", print_r(json_decode($y,true)), "\n"; + + $x = "3f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d842256000000000000000000000400000000000000003f507b2261223a313233343536373839303132332c22624212006078223a22e4b8ade69687222c2279223a22e4b8ade69687227d22560000b0a0af832256000090a1af832256000010a0af8322560000e0239d84225600000000000000000000040000000000000000"; + $y = flame\compress\snappy_compress($x); + echo strlen($x), " < ", strlen($y), "\n"; +}); + +flame\run(); diff --git a/test/core.php b/test/core.php deleted file mode 100644 index b6f1532..0000000 --- a/test/core.php +++ /dev/null @@ -1,35 +0,0 @@ - ", $i,"\n"; - } -} -function test2() { - for($i=0;$i<10;++$i) { - yield flame\sleep(700); - echo "[2] ", time(), " -> ", $i,"\n"; - } -} - -function test3() { - for($i=0;$i<10;++$i) { - yield $i; - sleep(1); // PHP 原生提供的大部分函数均是阻塞形式,无法行程 “调度”,是真实的阻塞 - echo "[3] ", time(), " -> ", $i,"\n"; - } -} diff --git a/test/coroutine_1.php b/test/coroutine_1.php new file mode 100644 index 0000000..19ad46b --- /dev/null +++ b/test/coroutine_1.php @@ -0,0 +1,20 @@ +1234567890123,"x"=>"中文"]); + echo "(",strlen($x),") ", bin2hex($x), "\n"; + $y = flame\encoding\bson_decode($x); + var_dump( $y ); +}); + +flame\run(); diff --git a/test/exception_1.php b/test/exception_1.php new file mode 100644 index 0000000..ca46bfd --- /dev/null +++ b/test/exception_1.php @@ -0,0 +1,24 @@ +getMessage() , ", you can do something with it.\n"; + flame\time\sleep(1000); + echo "quit\n"; +}); + +flame\run(); diff --git a/test/hash_1.php b/test/hash_1.php new file mode 100644 index 0000000..8993905 --- /dev/null +++ b/test/hash_1.php @@ -0,0 +1,14 @@ +method = "POST"; + $req->header["test1"] = "string"; + $req->header["test2"] = 123456; + // $req->http_version(flame\http\client_request::HTTP_VERSION_2_PRI); + var_dump(flame\http\exec($req)); + echo "done.\n"; + }); +} +flame\run(); diff --git a/test/http_2.php b/test/http_2.php new file mode 100644 index 0000000..9d3ca42 --- /dev/null +++ b/test/http_2.php @@ -0,0 +1,69 @@ + $date->unix()]; + } +} + +flame\go(function() { + $server = new flame\http\server(":::56101"); + $q = new flame\queue(); + + $server->before(function($req, $res, $m) { + $req->data["start"] = flame\time\now(); + if(!$m) { + $res->status = intval(substr($req->path, 1)); + $res->header["Access-Control-Allow-Origin"] = "*"; + $res->header["Content-Type"] = "application/json"; + $res->body = ["a" => "bbbb"]; + return false; + } + })->get("/", function($req, $res) { + $req->header["test1"] = "string"; + $req->header["test2"] = 123456; + $res->set_cookie("a","b", 3600); + $res->body = json_encode($req); + })->post("/", function($req, $res) { + $res->status = 200; + $res->header["Content-Type"] = "application/json"; + $res->body = $req->body; + // 文件上传 + var_dump($req->file); + // return $res->body = json_encode($req); + })->get("/poll", function($req, $res) use($q) { + $res->header["Content-Type"] = "text/event-stream"; + $res->header["Cache-Control"] = "no-cache"; + $res->write_header(200); + while($data = $q->pop()) { + $res->write($data); + } + })->get("/push", function($req, $res) use($q) { + $q->push("event: time\ndata: ".flame\time\now()."\n\n"); + $res->body = "done"; + })->get("/file", function($req, $res) { + $res->header["Content-Type"] = "text/html"; + $res->file(__DIR__, "/coroutine_1.php"); + })->get("/exception", function($req, $res) { + throw new exception("Some Exception"); + $res->body = "done"; + })->get("/json", function($req, $res) { + $res->header["content-type"] = "application/json"; + $res->body = ["data" => new JsonObject()]; + })->after(function($req, $res, $m) { + $end = flame\time\now(); + // echo "elapsed: ", ($end - $req->data["start"]), "ms\n";5 + }); + $server->run(); + echo "done2\n"; +}); + +// flame\on("quit", function() use($server) { +// echo "quit\n"; +// $server->close(); +// }); + +flame\run(); diff --git a/test/kafka_1.php b/test/kafka_1.php new file mode 100644 index 0000000..d199ab6 --- /dev/null +++ b/test/kafka_1.php @@ -0,0 +1,21 @@ + "host1:port1,host2:port2", + ], ["test"]); + + for($i=0;$i<10000;++$i) { + echo $i, "\n"; + $message = new flame\kafka\message("this is the payload", "this is the key"); + $message->header["key"] = "value"; + $producer->publish("test", $message); + flame\time\sleep(50); + $producer->publish("test", "this is the payload", "this is the key"); + flame\time\sleep(50); + } + $producer->flush(); +}); + +flame\run(); diff --git a/test/kafka_2.php b/test/kafka_2.php new file mode 100644 index 0000000..e21164b --- /dev/null +++ b/test/kafka_2.php @@ -0,0 +1,23 @@ + "host1:port1,host2:port2", + "group.id" => "flame-test-consumer", + "auto.offset.reset" => "smallest", + ], ["test"]); + flame\go(function() use($consumer) { + // 60 秒后关闭消费者 + flame\time\sleep(60000); + $consumer->close(); + }); + $consumer->run(function($msg) { + var_dump($msg); + flame\time\sleep(50); + }); + echo "done.\n"; + unset($consumer); +}); + +flame\run(); diff --git a/test/logger_1.php b/test/logger_1.php new file mode 100644 index 0000000..5f8013d --- /dev/null +++ b/test/logger_1.php @@ -0,0 +1,12 @@ + __DIR__."/logger_1.log" +]); + +flame\go(function() { + flame\log\warning("This is a warning message"); + flame\time\sleep(1000); + flame\log\warn("This is another warning message"); +}); + +flame\run(); diff --git a/test/logger_2.php b/test/logger_2.php new file mode 100644 index 0000000..5dd0716 --- /dev/null +++ b/test/logger_2.php @@ -0,0 +1,22 @@ + __DIR__."/logger_2.log", // 目标日志文件 +]); + +flame\go(function() { + for($i=0;$i<10000;++$i) { + flame\time\sleep(rand(4000, 5000)); + flame\log\trace("WORKING:", intval(getenv("FLAME_CUR_WORKER"))); + flame\time\sleep(rand(4000, 5000)); + // 标准、错误输出在多进程模式重定向到默认日志文件 + echo "[", flame\time\iso(), "] (TRACE) WORKING: ", intval(getenv("FLAME_CUR_WORKER")), "\n"; + } + flame\log\trace("DONE:", intval(getenv("FLAME_CUR_WORKER"))); +}); + +flame\on("quit", function() { + flame\log\trace("QUITING:", intval(getenv("FLAME_CUR_WORKER"))); + // flame\quit(); +}); + +flame\run(); diff --git a/test/logger_3.php b/test/logger_3.php new file mode 100644 index 0000000..b41b4f3 --- /dev/null +++ b/test/logger_3.php @@ -0,0 +1,14 @@ + __DIR__."/logger_3.log" +]); + +flame\go(function() { + $logger = flame\log\connect(__DIR__."/logger_3.log.extra"); + for($i=0;$i<10000;++$i) { + $logger->write("无前缀的日志数据:", flame\time\now()); + flame\time\sleep(rand(4000, 5000)); + } +}); + +flame\run(); diff --git a/test/mongodb_1.php b/test/mongodb_1.php new file mode 100644 index 0000000..c015991 --- /dev/null +++ b/test/mongodb_1.php @@ -0,0 +1,27 @@ +execute([ + "find" => "photo", + "filter" => ["uid" => 25865119921602568], + "limit" => 10, + ]); + echo "execute\n"; + // var_dump( $cs->fetch_row() ); + $cs->fetch_all(); + flame\time\sleep(100); + // unset($cs); + echo "sleep\n"; + $cs = $cli->photo->find(["uid" => 25865119921602568]); + // var_dump( $cs->fetch_all() ); + echo "done: ". $i. "\n"; + }); + } +}); + + +flame\run(); diff --git a/test/mutex_1.php b/test/mutex_1.php new file mode 100644 index 0000000..b3e3d0b --- /dev/null +++ b/test/mutex_1.php @@ -0,0 +1,32 @@ +lock(); + echo "(3) lock acquired\n"; + flame\time\sleep(1000); + echo "(3) protected echo\n"; + $mutex->unlock(); + echo "(3) lock released\n"; + }); + echo "(1) awaiting lock\n"; + $mutex->lock(); + echo "(1) lock acquired\n"; + flame\time\sleep(2000); + echo "(1) protected echo\n"; + $mutex->unlock(); + echo "(1) lock released\n"; +}); + +flame\run(); diff --git a/test/mysql_1.php b/test/mysql_1.php new file mode 100644 index 0000000..f09f052 --- /dev/null +++ b/test/mysql_1.php @@ -0,0 +1,28 @@ +escape('a.b', '`') ); + $rs = $cli->query("SELECT * FROM `test_0` LIMIT 3"); + $row = $rs->fetch_row(); + var_dump($row); + $row = $rs->fetch_row(); + var_dump($row); + $data = $rs->fetch_all(); + var_dump($data); + $tx = $cli->begin_tx(); + try{ + $tx->insert("test_0", ["key"=>123, "val"=>"456"]); + $tx->update("test_0", ["key"=>123], ["val"=>"567"]); + $tx->commit(); + }catch(Exception $ex) { + $tx->rollback(); + return; + } + }); +} + +flame\run(); diff --git a/test/net_http_server.php b/test/net_http_server.php deleted file mode 100644 index 70a0bcf..0000000 --- a/test/net_http_server.php +++ /dev/null @@ -1,28 +0,0 @@ -listen("127.0.0.1", 6676); - while(true) { - $socket = yield $server->accept(); - // 启动“协程”,不阻塞 accept 过程 - flame\go(function() use($socket) { - while(true) { - echo "request ......\n"; - //throw new Exception("aaaaa"); - try { - $req = yield flame\net\http\request::parse($socket); - } catch (exception $ex) { - var_dump($ex); - //throw $ex; - break; - } - echo "process complete ......\n"; - var_dump($req); - //var_dump($req->head["cookie"]); - } - }); - } -}); - diff --git a/test/net_tcp_server.php b/test/net_tcp_server.php deleted file mode 100644 index 880e9d8..0000000 --- a/test/net_tcp_server.php +++ /dev/null @@ -1,26 +0,0 @@ -listen("127.0.0.1", 6676); - while(true) { - $socket = yield $server->accept(); - // 启动“协程”,不阻塞 accept 过程 - flame\go(function() use($socket) { - while(true) { - /*$packet = yield $socket->read("]");*/ - //echo $socket->remote_addr(), ":", $socket->remote_port(), " -> ", $packet, "\n"; - //yield $socket->write($packet); - //echo "-> ", $packet, "\n"; - //$packet = yield $socket->read("\n"); - //echo $socket->remote_addr(), ":", $socket->remote_port(), " -> ", $packet; - //yield $socket->write($packet); - /*echo "-> ", $packet;*/ - $req = yeild $request($socket); - } - }); - } - // $server->close(); - // close 可选,对象会自动销毁 -}); diff --git a/test/net_tcp_socket.php b/test/net_tcp_socket.php deleted file mode 100644 index 8c82af7..0000000 --- a/test/net_tcp_socket.php +++ /dev/null @@ -1,26 +0,0 @@ -connect("127.0.0.1", 6676); - echo "local_addr: ", $socket->local_addr, ":", $socket->local_port, "\n"; - while(true) { - $packet = "method-length[".rand()."]"; - echo "<- ", $packet, "\n"; - $n = yield $socket->write($packet); - // 读取方式 1. 读取指定长度的数据 - $packet = yield $socket->read($n); - echo "-> ", $packet, "\n"; - yield flame\sleep(1000); - $packet = "method-delim[".rand()."]\n"; - echo "<- ", $packet; - yield $socket->write($packet); - // 读取方式 2. 读取指定结束符标识的数据 - $packet = yield $socket->read("\n"); - echo "-> ", $packet; - yield flame\sleep(3000); - } - // close 过程可选,$socket 对象析构时会自动关闭 - // $socket->close(); -}); diff --git a/test/net_udp_client.php b/test/net_udp_client.php deleted file mode 100644 index 41817f3..0000000 --- a/test/net_udp_client.php +++ /dev/null @@ -1,21 +0,0 @@ -connect("127.0.0.1", 6676); - echo "local_addr: ", $socket->local_addr, ":", $socket->local_port, "\n"; - while(true) { - try{ - // connect 方式 write 可能发生异常 connection refused - yield $socket->write("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); - // 上面 connect + write 可以考虑替换为: - // yield $socket->write_to("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "127.0.0.1", 6676); - }catch(Exception $e) { - var_dump($e); - } - yield flame\sleep(10); - } - // close 操作可选,析构时会自动清理 - // $socket->close(); -}); diff --git a/test/net_udp_server.php b/test/net_udp_server.php deleted file mode 100644 index 818c2b3..0000000 --- a/test/net_udp_server.php +++ /dev/null @@ -1,22 +0,0 @@ -bind("0.0.0.0", 6676); - }catch(Exception $ex) { - echo $ex, "\n"; - exit; - } - while(true) { - $packet = yield $server->read(); - // remote_addr() / remote_port() 与 local_* 不同,前者为方法,后者为属性 - echo "from: ", $server->remote_addr(), ":", $server->remote_port(), " => ", $packet, "\n"; - } - // close 动作可选,对象销毁时会自动 close 底层的 socket - // $socket->close(); -}); diff --git a/test/os_1.php b/test/os_1.php new file mode 100644 index 0000000..e861013 --- /dev/null +++ b/test/os_1.php @@ -0,0 +1,29 @@ + "/data/htdocs", + ]); + $proc->wait(); + var_dump( $proc->stdout() ); + + $proc = flame\os\spawn("ping", ["-c", 5, "www.baidu.com"], [ + "cwd" => "/data/htdocs", + ]); + flame\time\sleep(1000); + $proc->wait(); + + + for($i = 0; $i<10; ++$i) { + flame\go(function() { + for($j=0;$j<100;++$j) { + flame\os\exec("ping", ["-c", 1, "www.baidu.com"]); + } + }); + } + flame\time\sleep(60000); + echo "done.\n"; +}); +flame\run(); diff --git a/test/queue_1.php b/test/queue_1.php new file mode 100644 index 0000000..c7c5931 --- /dev/null +++ b/test/queue_1.php @@ -0,0 +1,59 @@ +push($i); + flame\time\sleep(rand(50, 100)); + } + $q1->close(); + }); + flame\go(function() use($q2) { + for($i=1;$i<10;++$i) { + $q2->push($i + 10); + flame\time\sleep(rand(50, 150)); + } + $q2->close(); + }); + flame\go(function() use($q3) { + for($i=1;$i<10;++$i) { + $q3->push($i + 100); + flame\time\sleep(rand(150, 250)); + } + $q3->close(); + }); + flame\go(function() use($q1) { + while($x = $q1->pop()) { + echo "(1) q1: ", $x, "\n"; + flame\time\sleep(rand(100, 200)); + } + echo "(1) q1: consume done\n"; + }); + flame\go(function() use($q1) { + while($x = $q1->pop()) { + echo "(2) q1: ", $x, "\n"; + flame\time\sleep(rand(100, 200)); + } + echo "(2) q1: consume done\n"; + }); + flame\go(function() use($q2, $q3) { + while($q = flame\select($q2, $q3)) { + // 必须使用 全等 符号比较对象 + if($q === $q2) { + echo "q2: ", $q->pop(), "\n"; + }else if($q === $q3) { + echo "q3: ", $q->pop(), "\n"; + }else{ + // + } + } + echo "q2: consume done\n"; + echo "q3: consume done\n"; + }); +}); + +flame\run(); diff --git a/test/rabbitmq_1.php b/test/rabbitmq_1.php new file mode 100644 index 0000000..f706ec1 --- /dev/null +++ b/test/rabbitmq_1.php @@ -0,0 +1,34 @@ +close(); + }); + $consumer->run(function($msg) use($consumer) { + var_dump($msg); + $consumer->confirm($msg); + }); + echo "consumer done\n"; +}); + +flame\go(function() { + $producer = flame\rabbitmq\produce("amqp://username:password@host:port/vhost"); + for($i=0;$i<20;++$i) { + flame\time\sleep(20); + echo $i * 2, "\n"; + $producer->publish("", "this is the data", "test_q1"); + flame\time\sleep(20); + $message = new flame\rabbitmq\message("abcdefg", "test_q1"); + $message->header["a"] = 1234567890123; + $message->header["b"] = "Asd"; + $message->header["c"] = 123.456; + echo $i * 2 +1, "\n"; + $producer->publish("", $message); + } +}); + +flame\run(); diff --git a/test/redis_1.php b/test/redis_1.php new file mode 100644 index 0000000..c2cff4d --- /dev/null +++ b/test/redis_1.php @@ -0,0 +1,25 @@ +set("test1", "this is a text") ); + var_dump( $cli->get("test1") ); + var_dump( $cli->get("test1") ); + var_dump( $cli->set("test2", 123456) ); + var_dump( $cli->get("test2") ); + var_dump( $cli->incr("test2") ); + + var_dump( $cli->zrange("rank:relay_20181122", 0, 30, "WITHSCORES") ); + var_dump( $cli->hgetall("rank:relay_201811") ); + + $rv = $cli->multi() + ->get("test1") + ->get("test2") + ->del("test1") + ->del("test2") + ->exec(); + var_dump( $rv ); +}); + +flame\run(); diff --git a/test/sleep_1.php b/test/sleep_1.php new file mode 100644 index 0000000..620c891 --- /dev/null +++ b/test/sleep_1.php @@ -0,0 +1,24 @@ +close(); + }); + $server->run(function($socket) { + echo "connection accepted\n"; + while($line = $socket->read("\n")) { + var_dump($line); + $socket->write($line); + } + echo "disconnected\n"; + }); + echo "server closed\n"; +}); +flame\go(function() { + flame\time\sleep(1000); + $socket = flame\tcp\connect("127.0.0.1:8687"); + + for($i=0;$i<10;++$i) { + $socket->write("aaaaaa\nbbbbbb\n"); + $socket->read("\n"); + flame\time\sleep(100); + $socket->read("\n"); + flame\time\sleep(100); + } +}); + +flame\run(); diff --git a/test/tcp_2.php b/test/tcp_2.php new file mode 100644 index 0000000..03fdff2 --- /dev/null +++ b/test/tcp_2.php @@ -0,0 +1,25 @@ +run(function($socket) { + $length = 0; + while( ($line = $socket->read("\n")) !== null) { + echo $line; + if(substr($line, 0, 14) == "Content-Length") { + $length = intval(substr($line, 16)); + }else if($line == "\r\n") { + if($length > 0) { + $body = $socket->read($length); + echo $body, "\n"; + } + flame\time\sleep(200); + $socket->write("HTTP/1.1 200 OK\r\nConnection: Keep-Alive\r\nContent-Type: text/plain\r\nContent-Length: 2\r\n\r\nok"); + } + } + }); + echo "done.\n"; +}); + +flame\run(); diff --git a/test/time_1.php b/test/time_1.php new file mode 100644 index 0000000..675a04f --- /dev/null +++ b/test/time_1.php @@ -0,0 +1,9 @@ + [ + "b" => "111111", + ] +]; +$y = flame\get($x, "a.b"); +var_dump($y); +flame\set($x, "a.c", "222222"); +flame\set($x, "a.x.d", "333333"); +flame\set($x, "e.f", "444444"); +flame\set($x, "g.h.i", "555555"); +var_dump($x); + + +$r = flame\toml\parse_file(__DIR__."/toml_1.txt"); +var_dump($r); +$data = file_get_contents(__DIR__."/toml_1.txt"); +$r = flame\toml\parse_string($data); +var_dump($r); diff --git a/test/toml_1.txt b/test/toml_1.txt new file mode 100644 index 0000000..9bff83a --- /dev/null +++ b/test/toml_1.txt @@ -0,0 +1,36 @@ + +s2 = "234" +s3 = '345' +s4 = """4 \ +5 \ +6""" # commment 1 +s5 = '''5 +6 +7''' + +f1 = 1.2 #comment 2 +f2 = 1e+10 +f3 = 2.22e-5 + +#comment 3 + +[a.b] +c = [ x, {y = 1, z = 2} ] +# comment 4 +d = 3 +e = {f = 4, g = 5} + +[[h.i]] +#comment 5 +x = 1 +y = 2019-01-01 01:01:01 +z = 01:01:01 + +[[h.i]] +x = 2 +y = 2019-02-22 02:02:02 +z = 02:02:02 + +[c] #comment 6 +a = true +b.c = false \ No newline at end of file diff --git a/test/udp_1.php b/test/udp_1.php new file mode 100644 index 0000000..e32a0db --- /dev/null +++ b/test/udp_1.php @@ -0,0 +1,34 @@ +send_to("hello", "127.0.0.1:6666"); // 服务器 IPv4 兼容, 也可以收到 + $socket->send_to("world", "::1:6666"); + $socket->send_to("!", "localhost:6666"); + flame\time\sleep(1000); + $socket = flame\udp\connect("127.0.0.1:6666"); + $socket->send("hello"); + $socket->send("world!"); + echo "received: ". $socket->recv()."\n"; + flame\time\sleep(10000); + $server->close(); + }); + $server->run(function($data, $from) use($server) { + echo "(".$from.") [".$data."]\n"; + $server->send_to($data, $from); // echo back + }); +}); + +flame\go(function() { + $socket = flame\udp\connect("127.0.0.1:6666"); + for($i=0;$i<10;++$i) { + $socket->send("hello"); + flame\time\sleep(100); + } +}); + +flame\run(); diff --git a/test/warning_1.php b/test/warning_1.php new file mode 100644 index 0000000..ab7b2c4 --- /dev/null +++ b/test/warning_1.php @@ -0,0 +1,9 @@ +