├── .editorconfig ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README-EN.md ├── README.md ├── bin ├── SMProxy └── bootstrap.php ├── box.json ├── composer.json ├── conf ├── database.json.example └── server.json.example ├── docs ├── .nojekyll ├── BENCHMARK.md ├── CNAME ├── ISSUE_TEMPLATE.md ├── README.md ├── _coverpage.md ├── _navbar.md ├── _sidebar.md ├── en │ ├── BENCHMARK.md │ ├── README.md │ ├── _coverpage.md │ ├── _navbar.md │ └── _sidebar.md ├── favicon.ico ├── index.html ├── robots.txt ├── ror.xml ├── sitemap.html ├── sitemap.xml ├── sitemap.xml.gz └── urllist.txt ├── phpcs.xml ├── src ├── Base.php ├── BaseServer.php ├── Command │ ├── Command.php │ ├── HelpMessage.php │ ├── ServerCommand.php │ └── Table.php ├── Context.php ├── Handler │ ├── Backend │ │ └── BackendAuthenticator.php │ └── Frontend │ │ ├── FrontendAuthenticator.php │ │ ├── FrontendConnection.php │ │ ├── FrontendQueryHandler.php │ │ └── ServerQueryHandler.php ├── Helper │ ├── PhpHelper.php │ ├── ProcessHelper.php │ └── functions.php ├── Log │ └── Log.php ├── MysqlClient.php ├── MysqlPacket │ ├── AuthPacket.php │ ├── BinaryPacket.php │ ├── ErrorPacket.php │ ├── HandshakePacket.php │ ├── MySQLMessage.php │ ├── MySQLPacket.php │ ├── MySqlPacketDecoder.php │ ├── OkPacket.php │ ├── SMProxyPacket.php │ └── Util │ │ ├── BufferUtil.php │ │ ├── ByteUtil.php │ │ ├── Capabilities.php │ │ ├── CharsetUtil.php │ │ ├── ErrorCode.php │ │ ├── RandomUtil.php │ │ ├── SecurityUtil.php │ │ └── Versions.php ├── MysqlPool │ ├── MySQLException.php │ └── MySQLPool.php ├── MysqlProxy.php ├── Parser │ ├── ServerParse.php │ └── Util │ │ ├── CharTypes.php │ │ └── ParseUtil.php ├── Route │ └── RouteService.php ├── SMProxyException.php └── SMProxyServer.php └── tests ├── conf ├── database.json └── server.json ├── run └── test.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE Configuration 2 | /.idea 3 | /.vscode 4 | 5 | # Configuration 6 | /conf/*.json 7 | 8 | # Other 9 | /vendor 10 | /logs 11 | 12 | # Composer lock file 13 | /composer.lock -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | dist: trusty 4 | 5 | matrix: 6 | include: 7 | - php: 7.1 8 | - php: 7.2 9 | - php: 7.3 10 | - php: 7.2 11 | env: LINT=true 12 | - php: 7.2 13 | env: PHAR=true 14 | 15 | services: 16 | - mysql 17 | 18 | install: 19 | - printf "\n" | pecl install -f swoole 20 | 21 | before_script: 22 | - phpenv config-rm xdebug.ini 23 | - echo "USE mysql;\nUPDATE user SET password=PASSWORD('123456') WHERE user='root';\nFLUSH PRIVILEGES;\n" | mysql -u root 24 | - composer install --no-dev 25 | - php -v 26 | - php -m 27 | 28 | script: 29 | - | 30 | if [[ $LINT != "" ]]; then 31 | composer install --dev 32 | vendor/bin/phpcs 33 | elif [[ $PHAR != "" ]]; then 34 | composer install --dev 35 | vendor/bin/box compile 36 | ./SMProxy.phar start -c tests/conf 37 | php tests/test.php 38 | else 39 | tests/run 40 | php tests/test.php 41 | fi -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.2.13-cli-alpine 2 | 3 | ENV SMProxy_VERSION 1.3.1 4 | 5 | RUN apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS linux-headers \ 6 | && pecl install swoole \ 7 | && docker-php-ext-enable swoole \ 8 | \ 9 | # xBring in gettext so we can get `envsubst`, then throw 10 | # the rest away. To do this, we need to install `gettext` 11 | # then move `envsubst` out of the way so `gettext` can 12 | # be deleted completely, then move `envsubst` back. 13 | && apk add --no-cache --virtual .gettext gettext \ 14 | && mv /usr/bin/envsubst /tmp/ \ 15 | \ 16 | && runDeps="$( \ 17 | scanelf --needed --nobanner --format '%n#p' /usr/local/bin/php /usr/local/lib/php/extensions/*/*.so /tmp/envsubst \ 18 | | tr ',' '\n' \ 19 | | sort -u \ 20 | | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ 21 | )" \ 22 | && apk add --no-cache --virtual .php-rundeps $runDeps \ 23 | && apk del .phpize-deps \ 24 | && apk del .gettext \ 25 | && mv /tmp/envsubst /usr/local/bin/ \ 26 | \ 27 | && cd /usr/local \ 28 | && wget https://github.com/louislivi/smproxy/releases/download/v$SMProxy_VERSION/smproxy.tar.gz \ 29 | && tar -zxvf smproxy.tar.gz \ 30 | && ls -lna 31 | 32 | VOLUME /usr/local/smproxy/conf 33 | VOLUME /usr/local/smproxy/logs 34 | 35 | EXPOSE 3366 36 | 37 | CMD ["/usr/local/smproxy/SMProxy", "start", "--console"] 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 SMProxy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-EN.md: -------------------------------------------------------------------------------- 1 | English | [中文](./README.md) 2 | ``` 3 | /$$$$$$ /$$ /$$ /$$$$$$$ 4 | /$$__ $$| $$$ /$$$| $$__ $$ 5 | | $$ \__/| $$$$ /$$$$| $$ \ $$ /$$$$$$ /$$$$$$ /$$ /$$ /$$ /$$ 6 | | $$$$$$ | $$ $$/$$ $$| $$$$$$$//$$__ $$ /$$__ $$| $$ /$$/| $$ | $$ 7 | \____ $$| $$ $$$| $$| $$____/| $$ \__/| $$ \ $$ \ $$$$/ | $$ | $$ 8 | /$$ \ $$| $$\ $ | $$| $$ | $$ | $$ | $$ >$$ $$ | $$ | $$ 9 | | $$$$$$/| $$ \/ | $$| $$ | $$ | $$$$$$/ /$$/\ $$| $$$$$$$ 10 | \______/ |__/ |__/|__/ |__/ \______/ |__/ \__/ \____ $$ 11 | /$$ | $$ 12 | | $$$$$$/ 13 | \______/ 14 | ``` 15 | # [SMProxy](https://smproxy.louislivi.com/#/en/) 16 | 17 | [![release](https://img.shields.io/github/release/louislivi/SMProxy.svg?style=popout-square)](https://github.com/louislivi/SMProxy/releases) 18 | [![forks](https://img.shields.io/github/forks/louislivi/SMProxy.svg?style=popout-square)](https://github.com/louislivi/SMProxy/network/members) 19 | [![stars](https://img.shields.io/github/stars/louislivi/SMProxy.svg?style=popout-square)](https://github.com/louislivi/SMProxy/stargazers) 20 | [![Build Status](https://img.shields.io/travis/com/louislivi/SMProxy.svg?style=popout-square)](https://travis-ci.com/louislivi/SMProxy) 21 | [![Gitter](https://img.shields.io/gitter/room/louislivi/SMproxy.svg?style=popout-square)](https://gitter.im/louislivi/SMproxy) 22 | [![license](https://img.shields.io/github/license/louislivi/SMProxy.svg?style=popout-square)](https://github.com/louislivi/SMProxy/blob/master/LICENSE) 23 | [![SMProxy](https://img.shields.io/badge/SMProxy-%F0%9F%92%97-pink.svg?style=popout-square)](https://github.com/louislivi/SMProxy) 24 | [![Backers on Open Collective](https://opencollective.com/SMProxy/backers/badge.svg?style=popout-square)](#backers) 25 | [![Sponsors on Open Collective](https://opencollective.com/SMProxy/sponsors/badge.svg?style=popout-square)](#sponsors) 26 | 27 | ## Swoole MySQL Proxy 28 | 29 | A MySQL database connection pool based on MySQL protocol and Swoole. 30 | 31 | ## Principle 32 | 33 | Store the database connection as an object in memory. When users need to access the database, a connection will be established for the first time. After that, instead of establishing a new connection, free connections will be retrieved from the connection pool when users require. Also, users don't need to close connection but put it back into the connection pool for other requests to use. 34 | 35 | All these things, connecting, disconnecting are managed by the connection pool itself. At the same time, you can also configure the parameters of the connection pool, like: 36 | 37 | - The initial number of connections 38 | - Min / Max number of connections 39 | - Number of max requests per connection 40 | - Max idle time of connections 41 | 42 | ...etc. 43 | 44 | It's also possible to monitor the number of database connections, usage, etc. through its own management system. 45 | 46 | If the maximum number of connections is exceeded, the coroutine will be suspended and wait until a connection is released. 47 | 48 | ## Features 49 | 50 | - Read/Write Splitting 51 | - Connection Pool 52 | - SQL92 Standard 53 | - Coroutine Scheduling 54 | - Multiple database connections, multiple databases, multiple users... 55 | - Build with MySQL native protocol, cross-language, cross-platform. 56 | - Compatible with MySQL Transaction 57 | - Compatible with HandshakeV10 58 | - Compatible with MySQL 5.5 - 8.0 59 | - Compatible with Various Frameworks 60 | 61 | ## Why This 62 | 63 | For early design reasons, PHP does not have a native connection pool. So the number of database connections will be easily increasing and reaching the maximum when we got lots of requests. 64 | Using one of many database middlewares like Mycat will cause some limitations, e.g. batch inserts. And it's also too heavy in most cases. 65 | So we created SMProxy using 100% PHP + Swoole, which only supports connection pool and read/write separation, but much more lightweight. 66 | Not like Mycat, we're trying to build SMProxy with Swoole Coroutine to schedule HandshakeV10 packet forwarding, so we don't have to parse all SQL packets. 67 | That really makes SMProxy more stable and reliable. 68 | 69 | ## Contributing & Discussing 70 | 71 | - Documentation: 72 | - Community: [![Gitter](https://img.shields.io/gitter/room/louislivi/SMproxy.svg?style=popout-square)](https://gitter.im/louislivi/SMproxy) 73 | - Issues and Pull requests are always welcome. 74 | 75 | ## Contributors 76 | 77 | This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. 78 | 79 | 80 | ## Backers 81 | 82 | Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/SMProxy#backer)] 83 | 84 | 85 | 86 | ## Sponsors 87 | 88 | Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/SMProxy#sponsor)] 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 中文 | [English](./README-EN.md) 2 | ``` 3 | /$$$$$$ /$$ /$$ /$$$$$$$ 4 | /$$__ $$| $$$ /$$$| $$__ $$ 5 | | $$ \__/| $$$$ /$$$$| $$ \ $$ /$$$$$$ /$$$$$$ /$$ /$$ /$$ /$$ 6 | | $$$$$$ | $$ $$/$$ $$| $$$$$$$//$$__ $$ /$$__ $$| $$ /$$/| $$ | $$ 7 | \____ $$| $$ $$$| $$| $$____/| $$ \__/| $$ \ $$ \ $$$$/ | $$ | $$ 8 | /$$ \ $$| $$\ $ | $$| $$ | $$ | $$ | $$ >$$ $$ | $$ | $$ 9 | | $$$$$$/| $$ \/ | $$| $$ | $$ | $$$$$$/ /$$/\ $$| $$$$$$$ 10 | \______/ |__/ |__/|__/ |__/ \______/ |__/ \__/ \____ $$ 11 | /$$ | $$ 12 | | $$$$$$/ 13 | \______/ 14 | ``` 15 | # [SMProxy](https://smproxy.louislivi.com) 16 | 17 | [![release](https://img.shields.io/github/release/louislivi/SMProxy.svg?style=popout-square)](https://github.com/louislivi/SMProxy/releases) 18 | [![forks](https://img.shields.io/github/forks/louislivi/SMProxy.svg?style=popout-square)](https://github.com/louislivi/SMProxy/network/members) 19 | [![stars](https://img.shields.io/github/stars/louislivi/SMProxy.svg?style=popout-square)](https://github.com/louislivi/SMProxy/stargazers) 20 | [![Build Status](https://img.shields.io/travis/com/louislivi/SMProxy.svg?style=popout-square)](https://travis-ci.com/louislivi/SMProxy) 21 | [![Gitter](https://img.shields.io/gitter/room/louislivi/SMproxy.svg?style=popout-square)](https://gitter.im/louislivi/SMproxy) 22 | [![license](https://img.shields.io/github/license/louislivi/SMProxy.svg?style=popout-square)](https://github.com/louislivi/SMProxy/blob/master/LICENSE) 23 | [![SMProxy](https://img.shields.io/badge/SMProxy-%F0%9F%92%97-pink.svg?style=popout-square)](https://github.com/louislivi/SMProxy) 24 | [![Backers on Open Collective](https://opencollective.com/SMProxy/backers/badge.svg?style=popout-square)](#backers) 25 | [![Sponsors on Open Collective](https://opencollective.com/SMProxy/sponsors/badge.svg?style=popout-square)](#sponsors) 26 | 27 | ## Swoole MySQL Proxy 28 | 29 | 一个基于 MySQL 协议,Swoole 开发的MySQL数据库连接池。 30 | 31 | ## 原理 32 | 33 | 将数据库连接作为对象存储在内存中,当用户需要访问数据库时,首次会建立连接,后面并非建立一个新的连接,而是从连接池中取出一个已建立的空闲连接对象。 34 | 使用完毕后,用户也并非将连接关闭,而是将连接放回连接池中,以供下一个请求访问使用。而连接的建立、断开都由连接池自身来管理。 35 | 36 | 同时,还可以通过设置连接池的参数来控制连接池中的初始连接数、连接的上下限数以及每个连接的最大使用次数、最大空闲时间等等。 37 | 也可以通过其自身的管理机制来监视数据库连接的数量、使用情况等。超出最大连接数会采用协程挂起,等到有连接关闭再恢复协程继续操作。 38 | 39 | ## 特性 40 | 41 | - 支持读写分离 42 | - 支持数据库连接池,能够有效解决 PHP 带来的数据库连接瓶颈 43 | - 支持 SQL92 标准 44 | - 采用协程调度 45 | - 支持多个数据库连接,多个数据库,多个用户,灵活搭配 46 | - 遵守 MySQL 原生协议,跨语言,跨平台的通用中间件代理 47 | - 支持 MySQL 事务 48 | - 支持 HandshakeV10 协议版本 49 | - 完美兼容 MySQL5.5 - 8.0 50 | - 兼容各大框架,无缝提升性能 51 | 52 | ## 设计初衷 53 | 54 | PHP 没有连接池,所以高并发时数据库会出现连接打满的情况,Mycat 等数据库中间件会出现部分 SQL 无法使用,例如不支持批量添加等,而且过于臃肿。 55 | 所以就自己编写了这个仅支持连接池和读写分离的轻量级中间件,使用 Swoole 协程调度 HandshakeV10 协议转发使程序更加稳定,不用像 Mycat 一样解析所有 SQL 包体,增加复杂度。 56 | 57 | ## 开发与讨论 58 | - 文档: 59 | - QQ群:722124111 60 | - 欢迎各类 Issue 和 Pull Request。 61 | 62 | ## 贡献者列表 63 | 64 | 因为有你们,SMProxy 才能走到现在。 65 | 66 | 67 | 68 | ## Backers 69 | 70 | Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/SMProxy#backer)] 71 | 72 | 73 | 74 | ## Sponsors 75 | 76 | Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/SMProxy#sponsor)] 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /bin/SMProxy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 5 | * Date: 2018/10/26 6 | * Time: 下午6:32 7 | */ 8 | 9 | require_once __DIR__.'/bootstrap.php'; 10 | 11 | (new \SMProxy\Command\Command())->run($argv); 12 | -------------------------------------------------------------------------------- /bin/bootstrap.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 2018/11/23 5 | * Time: 下午2:33. 6 | */ 7 | 8 | use function SMProxy\Helper\absorb_version_from_git; 9 | use function SMProxy\Helper\smproxy_error; 10 | 11 | require_once __DIR__ . '/../vendor/autoload.php'; 12 | 13 | // Define constants 14 | define('IN_PHAR', boolval(Phar::running(false))); 15 | define('ROOT', IN_PHAR ? dirname(Phar::running(false)) : realpath(__DIR__ . '/..')); 16 | define('DB_DELIMITER', 'SΜ'); 17 | define('SMPROXY_VERSION', IN_PHAR ? '@phar-version@' : absorb_version_from_git()); 18 | 19 | // Set global error handler 20 | set_error_handler('SMProxy\Helper\_error_handler', E_ALL | E_STRICT); 21 | 22 | // Check requirements - PHP 23 | if (version_compare(PHP_VERSION, '7.0', '<')) { 24 | smproxy_error('ERROR: SMProxy requires [PHP >= 7.0].'); 25 | } 26 | 27 | // Check requirements - Swoole 28 | if (extension_loaded('swoole') && defined('SWOOLE_VERSION')) { 29 | if (version_compare(SWOOLE_VERSION, '2.1.3', '<')) { 30 | smproxy_error('ERROR: SMProxy requires [Swoole >= 2.1.3].'); 31 | } 32 | } else { 33 | smproxy_error('ERROR: Swoole was not installed.'); 34 | } 35 | 36 | if (extension_loaded('xdebug')) { 37 | smproxy_error('ERROR: XDebug has been enabled, which conflicts with SMProxy.'); 38 | } 39 | -------------------------------------------------------------------------------- /box.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "bin/SMProxy", 3 | "output": "SMProxy.phar", 4 | "alias": "deploy", 5 | "directories": [ 6 | "bin", 7 | "src" 8 | ], 9 | "finder": [ 10 | { 11 | "notName": "/LICENSE|.*\\.md|.*\\.dist|composer\\.json|composer\\.lock/", 12 | "exclude": [ 13 | "doc", 14 | "docs", 15 | "test", 16 | "test_old", 17 | "tests", 18 | "Tests", 19 | "vendor-bin" 20 | ], 21 | "in": "vendor" 22 | }, 23 | { 24 | "name": "composer.json", 25 | "in": "." 26 | } 27 | ], 28 | "compression": "NONE", 29 | "compactors": [ 30 | "KevinGH\\Box\\Compactor\\Json", 31 | "KevinGH\\Box\\Compactor\\Php" 32 | ], 33 | "git": "phar-version" 34 | } 35 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "louislivi/smproxy", 3 | "description": "A MySQL database connection pool based on MySQL protocol and Swoole.", 4 | "type": "project", 5 | "keywords": ["smproxy", "mysql", "pool", "connection", "database", "swoole"], 6 | "homepage": "https://github.com/louislivi/smproxy", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "louislivi", 11 | "email": "574747417@qq.com" 12 | } 13 | ], 14 | "require": { 15 | "php": "^7.0", 16 | "psr/log": "~1.0", 17 | "ext-swoole": "^2.1.3||^4.0", 18 | "ext-json": "*", 19 | "ext-openssl": "*", 20 | "ext-mbstring": "*" 21 | }, 22 | "require-dev": { 23 | "friendsofphp/php-cs-fixer": "2.15.0", 24 | "squizlabs/php_codesniffer": "3.4.2", 25 | "humbug/box": "^2.7.5||^3.3", 26 | "eaglewu/swoole-ide-helper": "dev-master" 27 | }, 28 | "conflict": { 29 | "ext-xdebug": "*" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "SMProxy\\": "src/" 34 | }, 35 | "files": [ 36 | "src/Helper/functions.php" 37 | ] 38 | }, 39 | "scripts": { 40 | "post-autoload-dump": [ 41 | "@php -r \"file_exists('conf/database.json') || copy('conf/database.json.example', 'conf/database.json');\"", 42 | "@php -r \"file_exists('conf/server.json') || copy('conf/server.json.example', 'conf/server.json');\"" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /conf/database.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "account": { 4 | "root": { 5 | "user": "root", 6 | "password": "123456" 7 | } 8 | }, 9 | "serverInfo": { 10 | "server1": { 11 | "write": { 12 | "host": ["127.0.0.1"], 13 | "port": 3306, 14 | "timeout": 2, 15 | "account": "root" 16 | }, 17 | "read": { 18 | "host": ["127.0.0.1"], 19 | "port": 3306, 20 | "timeout": 2, 21 | "account": "root", 22 | "startConns": "swoole_cpu_num()*10", 23 | "maxSpareConns": "swoole_cpu_num()*10", 24 | "maxSpareExp": 3600, 25 | "maxConns": "swoole_cpu_num()*20" 26 | } 27 | } 28 | }, 29 | "databases": { 30 | "dbname": { 31 | "serverInfo": "server1", 32 | "startConns": "swoole_cpu_num()*2", 33 | "maxSpareConns": "swoole_cpu_num()*2", 34 | "maxSpareExp": 3600, 35 | "maxConns": "swoole_cpu_num()*2", 36 | "charset": "utf8mb4" 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /conf/server.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "user": "root", 4 | "password": "123456", 5 | "charset": "utf8mb4", 6 | "host": "0.0.0.0", 7 | "port": "3366", 8 | "mode": "SWOOLE_PROCESS", 9 | "sock_type": "SWOOLE_SOCK_TCP", 10 | "logs": { 11 | "open":true, 12 | "config": { 13 | "system": { 14 | "log_path": "ROOT/logs", 15 | "log_file": "system.log", 16 | "format": "Y/m/d" 17 | }, 18 | "mysql": { 19 | "log_path": "ROOT/logs", 20 | "log_file": "mysql.log", 21 | "format": "Y/m/d" 22 | } 23 | } 24 | }, 25 | "swoole": { 26 | "worker_num": "swoole_cpu_num()", 27 | "max_coro_num": 6000, 28 | "open_tcp_nodelay": true, 29 | "daemonize": true, 30 | "heartbeat_check_interval": 60, 31 | "heartbeat_idle_time": 600, 32 | "reload_async": true, 33 | "log_file": "ROOT/logs/swoole.log", 34 | "pid_file": "ROOT/logs/pid/server.pid" 35 | }, 36 | "swoole_client_setting": { 37 | "package_max_length": 16777215 38 | }, 39 | "swoole_client_sock_setting": { 40 | "sock_type": "SWOOLE_SOCK_TCP" 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/louislivi/SMProxy/743baea06dcf215a742c5772393d204a31179e0d/docs/.nojekyll -------------------------------------------------------------------------------- /docs/BENCHMARK.md: -------------------------------------------------------------------------------- 1 | # SMProxy 连接测试 2 | 3 | 测试SMProxy与测试MySQL完全一致,MySQL怎么连接,SMProxy就怎么连接。 4 | 5 | 推荐先采用命令行测试: 6 | (SMProxy<1.2.5 请勿使用MYSQL8.0客户端链接测试) 7 | 8 | ``` 9 | mysql -uroot -p123456 -P3366 -h127.0.0.1 10 | ``` 11 | 12 | 也可采用工具连接。 13 | 14 | ## 没用框架的 PHP 7.2.6 15 | 16 | ![PHP7.2.6](https://file.gesmen.com.cn/smproxy/1542782011408.jpg) 17 | 18 | 没用:0.15148401260376, 用了:0.040808916091919 19 | 20 | 未使用连接池: 21 | 22 | ![ab](https://file.gesmen.com.cn/smproxy/1542782075073.jpg) 23 | 24 | 使用连接池: 25 | 26 | ![ab](https://file.gesmen.com.cn/smproxy/1542782043730.jpg) 27 | 28 | ## ThinkPHP 5.0 29 | 30 | ![ThinkPHP5](https://file.gesmen.com.cn/smproxy/8604B3D4-0AB0-4772-83E0-EEDA6B86F065.png) 31 | 32 | 未使用连接池: 33 | 34 | ![ab](https://file.gesmen.com.cn/smproxy/1542685140126.jpg) 35 | 36 | 使用连接池: 37 | 38 | ![ab](https://file.gesmen.com.cn/smproxy/1542685109798.jpg) 39 | 40 | ## Laravel 5.7 41 | 42 | ![Laravel5.7](https://file.gesmen.com.cn/smproxy/3FE76B55-9422-40DB-B8CE-7024F36BB5A9.png) 43 | 44 | 未使用连接池: 45 | 46 | ![ab](https://file.gesmen.com.cn/smproxy/1542686575874.jpg) 47 | 48 | 使用连接池: 49 | 50 | ![ab](https://file.gesmen.com.cn/smproxy/1542686580551.jpg) 51 | 52 | ## MySQL 连接数 53 | 54 | 未使用连接池: 55 | 56 | ![MySQL](https://file.gesmen.com.cn/smproxy/1542625044913.jpg) 57 | 58 | 使用连接池: 59 | 60 | ![MySQL](https://file.gesmen.com.cn/smproxy/1542625037536.jpg) 61 | 62 | 请以实际压测为准,根数据量,网络环境,数据库配置有关。 63 | 测试中因超出最大连接数会采用协程挂起 等到有连接关闭再恢复协程继续操作, 64 | 所有并发量与配置文件maxConns设置的不合适,会导致比原链接慢,主要是为了控制连接数。 65 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | smproxy.github.louislivi.com -------------------------------------------------------------------------------- /docs/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Describe Your Environment (描述你的环境) 2 | 3 | - System: 4 | - PHP version: 5 | - Mysql version: 6 | - Swoole version: 7 | - SMProxy version: 8 | 9 | ### How to Reproduce the Problem? (如何重现问题) 10 | 11 | 1. 12 | 2. 13 | 3. 14 | 15 | ### Expected Behavior (预期行为) 16 | 17 | ... 18 | 19 | ### Actual Behavior (实际行为) 20 | 21 | ... 22 | 23 | ### More Information (更多信息) 24 | 25 | ... -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # SMProxy 2 | 3 | [![release](https://img.shields.io/github/release/louislivi/SMProxy.svg?style=popout-square)](https://github.com/louislivi/SMProxy/releases) 4 | [![forks](https://img.shields.io/github/forks/louislivi/SMProxy.svg?style=popout-square)](https://github.com/louislivi/SMProxy/network/members) 5 | [![stars](https://img.shields.io/github/stars/louislivi/SMProxy.svg?style=popout-square)](https://github.com/louislivi/SMProxy/stargazers) 6 | [![Build Status](https://img.shields.io/travis/com/louislivi/SMProxy.svg?style=popout-square)](https://travis-ci.com/louislivi/SMProxy) 7 | [![Gitter](https://img.shields.io/gitter/room/louislivi/SMproxy.svg?style=popout-square)](https://gitter.im/louislivi/SMproxy) 8 | [![license](https://img.shields.io/github/license/louislivi/SMProxy.svg?style=popout-square)](https://github.com/louislivi/SMProxy/blob/master/LICENSE) 9 | [![SMProxy](https://img.shields.io/badge/SMProxy-%F0%9F%92%97-pink.svg?style=popout-square)](https://github.com/louislivi/SMProxy) 10 | 11 | > `Github` 项目地址:[https://github.com/louislivi/smproxy](https://github.com/louislivi/smproxy) (支持请点Star) 12 | 13 | ## Swoole MySQL Proxy 14 | 15 | 一个基于 MySQL 协议,Swoole 开发的MySQL数据库连接池。 16 | 17 | ## 原理 18 | 19 | 将数据库连接作为对象存储在内存中,当用户需要访问数据库时,首次会建立连接,后面并非建立一个新的连接,而是从连接池中取出一个已建立的空闲连接对象。 20 | 使用完毕后,用户也并非将连接关闭,而是将连接放回连接池中,以供下一个请求访问使用。而连接的建立、断开都由连接池自身来管理。 21 | 22 | 同时,还可以通过设置连接池的参数来控制连接池中的初始连接数、连接的上下限数以及每个连接的最大使用次数、最大空闲时间等等。 23 | 也可以通过其自身的管理机制来监视数据库连接的数量、使用情况等。超出最大连接数会采用协程挂起,等到有连接关闭再恢复协程继续操作。 24 | 25 | ## 特性 26 | 27 | - 支持读写分离 28 | - 支持数据库连接池,能够有效解决 PHP 带来的数据库连接瓶颈 29 | - 支持 SQL92 标准 30 | - 采用协程调度 31 | - 支持多个数据库连接,多个数据库,多个用户,灵活搭配 32 | - 遵守 MySQL 原生协议,跨语言,跨平台的通用中间件代理 33 | - 支持 MySQL 事务 34 | - 支持 HandshakeV10 协议版本 35 | - 完美兼容 MySQL5.5 - 8.0 36 | - 兼容各大框架,无缝提升性能 37 | 38 | ## 设计初衷 39 | 40 | PHP 没有连接池,所以高并发时数据库会出现连接打满的情况,Mycat 等数据库中间件会出现部分 SQL 无法使用,例如不支持批量添加等,而且过于臃肿。 41 | 所以就自己编写了这个仅支持连接池和读写分离的轻量级中间件,使用 Swoole 协程调度 HandshakeV10 协议转发使程序更加稳定,不用像 Mycat 一样解析所有 SQL 包体,增加复杂度。 42 | 43 | ## 性能测试 44 | 45 | 请查阅 [docs/BENCHMARK.md](BENCHMARK.md)。 46 | 47 | ## 环境 48 | 49 | - Swoole >= 2.1.3 ![swoole_version](https://img.shields.io/badge/swoole->=2.1.3-yellow.svg?style=popout-square) 50 | - PHP >= 7.0 ![php_version](https://img.shields.io/badge/php->=7.0-blue.svg?style=popout-square) 51 | 52 | ## 安装 53 | 54 | (推荐)直接下载最新发行版的 PHAR 文件,解压即用: 55 | 56 | 57 | 58 | 或者使用 Git 切换任意版本: 59 | 60 | ```bash 61 | git clone https://github.com/louislivi/SMProxy.git 62 | composer install --no-dev # 如果你想贡献你的代码,请不要使用 --no-dev 参数。 63 | ``` 64 | 65 | ## 运行 66 | 67 | 需要给予 bin/SMProxy 执行权限。 68 | 69 | ```bash 70 | SMProxy [ start | stop | restart | status | reload ] [ -c | --config | --console | -f | --force ] 71 | SMProxy -h | --help 72 | SMProxy -v | --version 73 | ``` 74 | 75 | Options: 76 | - start 运行服务 77 | - stop 停止服务 78 | - restart 重启服务 79 | - status 查询服务运行状态 80 | - reload 平滑重启 81 | - -h --help 帮助 82 | - -v --version 查看当前服务版本 83 | - -c --config 设置配置项目录 84 | - --console 前台运行(SMProxy>=1.2.5) 85 | - -f --force 强制执行(SMProxy>=1.3.0) 86 | 87 | ## 配置 88 | 89 | - 配置文件位于 `smproxy/conf` 目录中,其中大写 `ROOT` 代表当前 SMProxy 根目录。 90 | 91 | ### database.json 92 | ```json 93 | { 94 | "database": { 95 | "account": { 96 | "自定义用户名": { 97 | "user": "必选,数据库账户", 98 | "password": "必选,数据库密码" 99 | }, 100 | "...": "必选1个,自定义用户名 与serverInfo中的account相对应" 101 | }, 102 | "serverInfo": { 103 | "自定义数据库连接信息": { 104 | "write": { 105 | "host": "必选,写库地址 多个用[]表示", 106 | "port": "必选,写库端口", 107 | "timeout": "必选,写库连接超时时间(秒)", 108 | "account": "必选,自定义用户名 与 account中的自定义用户名相对应", 109 | "maxConns": "重载,对应databases", 110 | "maxSpareConns": "重载,对应databases", 111 | "startConns": "重载,对应databases", 112 | "maxSpareExp": "重载,对应databases" 113 | }, 114 | "read": { 115 | "host": "可选,读库地址 多个用[]表示", 116 | "port": "可选,读库端口", 117 | "timeout": "可选,读库连接超时时间(秒)", 118 | "account": "可选,自定义用户名 与 account中的自定义用户名相对应", 119 | "maxConns": "重载,对应databases", 120 | "maxSpareConns": "重载,对应databases", 121 | "startConns": "重载,对应databases", 122 | "maxSpareExp": "重载,对应databases" 123 | } 124 | }, 125 | "...": "必选1个,自定义数据库连接信息 与databases中的serverInfo相对应,read读库可不配置" 126 | }, 127 | "databases": { 128 | "数据库别名": { 129 | "databaseName": "可选,指定真实链接数据库名称(默认不指定与别名相同)", 130 | "serverInfo": "必选,自定义数据库连接信息 与serverInfo中的自定义数据库连接信息相对应", 131 | "maxConns": "必选,该库服务最大连接数,支持计算", 132 | "maxSpareConns": "必选,该库服务最大空闲连接数,支持计算", 133 | "startConns": "可选,该库服务默认启动连接数,支持计算", 134 | "maxSpareExp": "可选,该库服务空闲连接数最大空闲时间(秒),默认为0,支持计算", 135 | "charset": "可选,该库编码格式" 136 | }, 137 | "...": "必选1个,数据库名称 多个数据库配置多个" 138 | } 139 | } 140 | } 141 | ``` 142 | 143 | | 参数名称 | 描述 | 144 | | ---- | ---- | 145 | | `maxSpareExp` | 活动连接的最大空闲时间,单位为秒 超过此时间的连接会被释放到连接池中,针对未被关闭的活动连接。| 146 | | `maxSpareConns` | 连接池中最多可空闲`maxConns`个连接 ,这里取值为20,表示即使没有数据库连接时依然可以保持20空闲的连接,而不被清除,随时处于待命状态。| 147 | | `maxConns` | 连接池支持的最大连接数,这里取值为20,表示同时最多有20个数据库连接。一般把`maxConns`设置成可能的并发量就行了。| 148 | | `startConns` | 初始化连接数目,服务启动时生成连接数。 | 149 | 150 | > - `maxConns`,`maxSpareConns`,`startConns` 151 | > - 推荐设置为`server.json`中配置的`worker_num`的倍数`swoole_cpu_num()*N` 152 | > - 多个读库,写库 153 | > - 目前采取的是随机获取连接,推荐将`maxConns`,`startConns`,`startConns`至少设置为`max(读库,写库)*worker_num` 的1倍以上 154 | > - `timeout` 155 | > - 设置2-5秒最佳。 156 | > - `databaseName` 157 | > - `databaseName`与`数据库别名`的区别在于,`数据库别名`是供链接`SMProxy`时指定的库名,`databaseName`为`SMProxy`链接到`MySQL`的库名。 158 | > - `Laravel`框架使用时需要注释`Illuminate/Database/Connectors/ConnectionFactory.php`中的的如下代码避免重复选择数据库导致出现数据库不存在的问题。 159 | ```php 160 | if (! empty($config['database'])) { 161 | $connection->exec("use `{$config['database']}`;"); 162 | } 163 | ``` 164 | > - `重载` 165 | > - 使用`重载`后会覆盖原对应参数的值,比如`maxConns`因读写使用频率不同,所以可以将读写设置不同的`maxConns`。 166 | 167 | ### server.json 168 | ```json 169 | { 170 | "server": { 171 | "user": "必选,SMProxy服务用户", 172 | "password": "必选,SMProxy服务密码", 173 | "charset": "可选,SMProxy编码,默认utf8mb4", 174 | "host": "可选,SMProxy地址,默认0.0.0.0", 175 | "port": "可选,SMProxy端口,默认3366 如需多个以`,`隔开", 176 | "mode": "可选,SMProxy运行模式,SWOOLE_PROCESS多进程模式(默认),SWOOLE_BASE基本模式", 177 | "sock_type": "可选,sock类型,SWOOLE_SOCK_TCP tcp", 178 | "logs": { 179 | "open":"必选,日志开关,true 开 false 关", 180 | "config": { 181 | "system": { 182 | "log_path": "必选,SMProxy系统日志目录", 183 | "log_file": "必选,SMProxy系统日志文件名", 184 | "format": "必选,SMProxy系统日志目录日期格式" 185 | }, 186 | "mysql": { 187 | "log_path": "必选,SMProxyMySQL日志目录", 188 | "log_file": "必选,SMProxyMySQL日志文件名", 189 | "format": "必选,SMProxyMySQL日志目录日期格式" 190 | } 191 | } 192 | }, 193 | "swoole": { 194 | "worker_num": "必选,SWOOLE worker进程数,支持计算", 195 | "max_coro_num": "必选,SWOOLE 协程数,推荐不低于3000", 196 | "pid_file": "必选,worker进程和manager进程pid目录", 197 | "open_tcp_nodelay": "可选,关闭Nagle合并算法", 198 | "daemonize": "可选,守护进程化,true 为守护进程 false 关闭守护进程", 199 | "heartbeat_check_interval": "可选,心跳检测", 200 | "heartbeat_idle_time": "可选,心跳检测最大空闲时间", 201 | "reload_async": "可选,异步重启,true 开启异步重启 false 关闭异步重启", 202 | "log_file": "可选,SWOOLE日志目录" 203 | }, 204 | "swoole_client_setting": { 205 | "package_max_length": "可选,SWOOLE Client 最大包长,默认16777216MySQL最大支持包长" 206 | }, 207 | "swoole_client_sock_setting": { 208 | "sock_type": "可选,SWOOLE Client sock 类型,默认tcp 仅支持tcp" 209 | } 210 | } 211 | } 212 | ``` 213 | > - `user`,`password`,`port,host` 214 | > - 为`SMProxy`的账户|密码|端口|地址(非Mysql数据库账户|密码|端口|地址) 215 | > - 可随意设置用于`SMProxy`登录验证 216 | > - 例如默认配置登录为`mysql -uroot -p123456 -P 3366 -h 127.0.0.1` 217 | > - `SMProxy`登录成功MySQL COMMIT会提示`Server version: 5.6.0-SMProxy` 218 | > - `worker_num` 219 | > - 推荐使用`swoole_cpu_num()` 或 `swoole_cpu_num()*N` 220 | 221 | ### 在项目中如何进行配置 222 | - Laravel 223 | - `.env` 224 | ```env 225 | DB_CONNECTION=mysql 226 | DB_HOST=server.json中配置的host 227 | DB_PORT=server.json中配置的port 228 | DB_DATABASE=databse.json中配置的数据库名称 229 | DB_USERNAME=server.json中配置的user 230 | DB_PASSWORD=server.json中配置的password 231 | ``` 232 | 233 | - ThinkPHP 234 | - `database.php` 235 | ```php 236 | 'type' => 'mysql', 237 | // 服务器地址 238 | 'hostname' => 'server.json中配置的host', 239 | // 数据库名 240 | 'database' => 'databse.json中配置的数据库名称', 241 | // 用户名 242 | 'username' => 'server.json中配置的user', 243 | // 密码 244 | 'password' => 'server.json中配置的password', 245 | // 端口 246 | 'hostport' => 'server.json中配置的port', 247 | ``` 248 | 249 | - WordPress 250 | - 除了配置数据库信息完成后还需要修改`wp-includes/wp-db.php`中的 251 | ```php 252 | mysqli_real_connect( $this->dbh, $host, $this->dbuser, $this->dbpassword, null, $port, $socket, $client_flags ); 253 | ``` 254 | 改为 255 | ```php 256 | mysqli_real_connect( $this->dbh, $host, $this->dbuser, $this->dbpassword, $this->dbname, $port, $socket, $client_flags ); 257 | ``` 258 | 259 | > - 其他框架以此类推,只需要配置代码中连接数据库的`host`,`port`,`user`,`password`与 `SMProxy`中`server.json`中一致即可。 260 | > - 切记连接数据库时一定要设置库信息,例如`mysqli_connect`,`new PDO`需要在连接时就设置库。 261 | 262 | ## 路由 263 | 264 | ### 注解 265 | - smproxy:db_type=[read | write] 266 | - 强制使用读库 267 | ```sql 268 | /** smproxy:db_type=read */select * from `user` limit 1 269 | ``` 270 | - 强制使用写库 271 | ```sql 272 | /** smproxy:db_type=write */select * from `user` limit 1 273 | ``` 274 | 275 | ## MySQL8.0 276 | 277 | - `SMProxy1.2.4`及以上可直接使用 278 | - `SMProxy1.2.4`以下需要做如下兼容处理: 279 | - `MySQL-8.0`默认使用了安全性更强的`caching_sha2_password`插件,其他版本如果是从`5.x`升级上来的, 可以直接使用所有`MySQL`功能, 如是新建的`MySQL`, 需要进入`MySQL`命令行执行以下操作来兼容: 280 | 281 | ```sql 282 | ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'password'; 283 | flush privileges; 284 | ``` 285 | 将语句中的 `'root'@'%'` 替换成你所使用的用户, `password` 替换成其密码. 286 | 287 | 如仍无法使用, 应在my.cnf中设置 `default_authentication_plugin = mysql_native_password` 288 | 289 | ## 常见问题 290 | - `SMProxy@access denied for user` 291 | - 请检查`serve.json`中的账号密码与业务代码中配置的是否一致。 292 | - 数据库`host`请勿配置`localhost`。 293 | - `SMProxy@Database config dbname write is not exists! ` 294 | - 请将`database.json`中的`dbname`项改为你的业务数据库名。 295 | - `Config serverInfo->*->account is not exists! ` 296 | - 请仔细比对`database.json`中`databse->serverInfo->*->*->account`是否在`database->account`下含有相对于的键值对。 297 | - `Reach max connections! Cann't pending fetch!` 298 | - 适当增加`maxSpareConns`或`maxConns`,或增加`database.json`中的`timeout`项。 299 | - `Must be connected before sending data!` 300 | - 检查`MySQL`是否有外网访问权限。 301 | - 检查`MySQL`验证插件是否为`mysql_native_password`或`caching_sha2_password` 302 | - 排查是否有服务冲突,推荐使用`Docker`进行运行排查环境问题。 303 | - `Connection * waiting timeout` 304 | - 检查`MySQL`是否有外网访问权限。 305 | - 启动出现数据库连接超时请检查数据库配置,若正常请降低`startConns`或增加`database.json`中的`timeout`项。 306 | - `The server is not running` 307 | - 查看`SMProxy`下的日志`mysql.log`和`system.log`。 308 | - 防止`SMProxy`异常退出,建议使用`Supervisor`或`docker`进行服务挂载。 309 | - `Supervisor` || `docker` 310 | - 使用`Supervisor`和`docker`时需要使用前台运行模式(v1.2.5+使用`--console`,或使用`daemonize`参数)否则无法正常启动。 311 | - `502 Bad Gateway` 312 | - MySQL异常崩溃后连接出现502或连接超时,请不要开启长连接模式。 313 | - SQL语句过大不要使用连接池,会导致连接阻塞,程序异常。 314 | - `启动SMProxy后CPU占用过高` 315 | - 因Swoole4.2.12及以下未开启协程Client读写分离所以CPU占比会比较大。 316 | - 升级Swoole版本到4.2.13及以上并升级SMProxy版本到1.2.8及以上。 317 | 318 | ## 交流 319 | 320 | QQ群:722124111 321 | 322 | ## 捐赠SMProxy项目 323 | 324 | 您的捐赠是对SMProxy项目开发组最大的鼓励和支持。我们会坚持开发维护下去。 您的捐赠将被用于: 325 | - 持续和深入地开发 326 | - 文档和社区的建设和维护 327 | 328 | ### 捐赠方式 329 | [开源中国-捐赠SMProxy项目](https://gitee.com/louislivi/smproxy?donate=true) 330 | 331 | ## 其他学习资料 332 | 333 | - MySQL协议分析 : 334 | - MySQL官方协议文档 : 335 | - Mycat源码 : 336 | - Swoole : 337 | -------------------------------------------------------------------------------- /docs/_coverpage.md: -------------------------------------------------------------------------------- 1 | ``` 2 | /$$$$$$ /$$ /$$ /$$$$$$$ 3 | /$$__ $$| $$$ /$$$| $$__ $$ 4 | | $$ \__/| $$$$ /$$$$| $$ \ $$ /$$$$$$ /$$$$$$ /$$ /$$ /$$ /$$ 5 | | $$$$$$ | $$ $$/$$ $$| $$$$$$$//$$__ $$ /$$__ $$| $$ /$$/| $$ | $$ 6 | \____ $$| $$ $$$| $$| $$____/| $$ \__/| $$ \ $$ \ $$$$/ | $$ | $$ 7 | /$$ \ $$| $$\ $ | $$| $$ | $$ | $$ | $$ >$$ $$ | $$ | $$ 8 | | $$$$$$/| $$ \/ | $$| $$ | $$ | $$$$$$/ /$$/\ $$| $$$$$$$ 9 | \______/ |__/ |__/|__/ |__/ \______/ |__/ \__/ \____ $$ 10 | /$$ | $$ 11 | | $$$$$$/ 12 | \______/ 13 | ``` 14 | 15 | # SMProxy 16 | 17 | > 一个基于 MySQL 协议,Swoole 开发的MySQL数据库连接池。 18 | 19 | - 支持读写分离 20 | - 支持数据库连接池,能够有效解决 PHP 带来的数据库连接瓶颈 21 | - 支持 SQL92 标准 22 | - 采用协程调度 23 | - 支持多个数据库连接,多个数据库,多个用户,灵活搭配 24 | - 遵守 MySQL 原生协议,跨语言,跨平台的通用中间件代理 25 | - 支持 MySQL 事务 26 | - 支持 HandshakeV10 协议版本 27 | - 完美兼容 MySQL5.5 - 8.0 28 | - 兼容各大框架,无缝提升性能 29 | 30 | [GitHub](https://github.com/louislivi/smproxy/) 31 | [开源中国](https://gitee.com/louislivi/smproxy/) 32 | [起步](README.md) -------------------------------------------------------------------------------- /docs/_navbar.md: -------------------------------------------------------------------------------- 1 | - [博客](http://www.louislivi.com) 2 | - Translations 3 | - [:cn: 中文](/) 4 | - [:uk: English](/en/) -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | - [SMProxy](README.md#SMProxy) 2 | - [Swoole MySQL Proxy](README.md#swoole-mysql-proxy) 3 | - [原理](README.md#%E5%8E%9F%E7%90%86) 4 | - [特性](README.md#%E7%89%B9%E6%80%A7) 5 | - [设计初衷](README.md#%E8%AE%BE%E8%AE%A1%E5%88%9D%E8%A1%B7) 6 | - [性能测试](BENCHMARK.md) 7 | - [SMProxy 连接测试](BENCHMARK.md#smproxy-%e8%bf%9e%e6%8e%a5%e6%b5%8b%e8%af%95) 8 | - [没用框架的 php7.2.6](BENCHMARK.md#%e6%b2%a1%e7%94%a8%e6%a1%86%e6%9e%b6%e7%9a%84-php-726) 9 | - [Thinkphp 5.0](BENCHMARK.md#thinkphp-50) 10 | - [MySQL 连接数](BENCHMARK.md#mysql-%e8%bf%9e%e6%8e%a5%e6%95%b0) 11 | - [环境](README.md#%E7%8E%AF%E5%A2%83) 12 | - [安装](README.md#%E5%AE%89%E8%A3%85) 13 | - [运行](README.md#%E8%BF%90%E8%A1%8C) 14 | - [配置](README.md#%E9%85%8D%E7%BD%AE) 15 | - [database.json](README.md#databasejson) 16 | - [server.json](README.md#serverjson) 17 | - [在项目中如何进行配置](README.md#%e5%9c%a8%e9%a1%b9%e7%9b%ae%e4%b8%ad%e5%a6%82%e4%bd%95%e8%bf%9b%e8%a1%8c%e9%85%8d%e7%bd%ae) 18 | - [路由](README.md#%e8%b7%af%e7%94%b1) 19 | - [注解](README.md#%e6%b3%a8%e8%a7%a3) 20 | - [MySQL8.0](README.md#mysql80) 21 | - [常见问题](README.md#%e5%b8%b8%e8%a7%81%e9%97%ae%e9%a2%98) 22 | - [交流](README.md#%E4%BA%A4%E6%B5%81) 23 | - [捐赠SMProxy项目](README.md#%e6%8d%90%e8%b5%a0SMProxy%e9%a1%b9%e7%9b%ae) 24 | - [其他学习资料](README.md#%E5%85%B6%E4%BB%96%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99) -------------------------------------------------------------------------------- /docs/en/BENCHMARK.md: -------------------------------------------------------------------------------- 1 | ## SMProxy Benchmark 2 | 3 | Benchmark for SMProxy is easily to execute. Connect and test it just like MySQL. 4 | 5 | It is recommended to test with the command line first (SMProxy<1.2.5 Do not use MySQL 8.0): 6 | 7 | ``` 8 | mysql -uroot -p123456 -P3366 -h127.0.0.1 9 | ``` 10 | 11 | Or connect with any GUI tool. 12 | 13 | ### PHP 7.2.6 Without Framework 14 | 15 | ![php7.2.6](https://file.gesmen.com.cn/smproxy/1542782011408.jpg) 16 | 17 | Native:0.15148401260376, With SMProxy:0.040808916091919 18 | 19 | Native: 20 | 21 | ![ab](https://file.gesmen.com.cn/smproxy/1542782075073.jpg) 22 | 23 | With SMProxy: 24 | 25 | ![ab](https://file.gesmen.com.cn/smproxy/1542782043730.jpg) 26 | 27 | ### ThinkPHP 5.0 28 | 29 | ![Thinkphp5](https://file.gesmen.com.cn/smproxy/8604B3D4-0AB0-4772-83E0-EEDA6B86F065.png) 30 | 31 | Native: 32 | 33 | ![ab](https://file.gesmen.com.cn/smproxy/1542685140126.jpg) 34 | 35 | With SMProxy: 36 | 37 | ![ab](https://file.gesmen.com.cn/smproxy/1542685109798.jpg) 38 | 39 | ### Laravel 5.7 40 | 41 | ![Laravel5.7](https://file.gesmen.com.cn/smproxy/3FE76B55-9422-40DB-B8CE-7024F36BB5A9.png) 42 | 43 | Native: 44 | 45 | ![ab](https://file.gesmen.com.cn/smproxy/1542686575874.jpg) 46 | 47 | With SMProxy: 48 | 49 | ![ab](https://file.gesmen.com.cn/smproxy/1542686580551.jpg) 50 | 51 | ### Number of MySQL Connections 52 | 53 | Native: 54 | 55 | ![MySQL](https://file.gesmen.com.cn/smproxy/1542625044913.jpg) 56 | 57 | With SMProxy: 58 | 59 | ![MySQL](https://file.gesmen.com.cn/smproxy/1542625037536.jpg) 60 | 61 | For more information, please visit [Benchmark result in Chinese](./BENCHMARK.md) -------------------------------------------------------------------------------- /docs/en/_coverpage.md: -------------------------------------------------------------------------------- 1 | ``` 2 | /$$$$$$ /$$ /$$ /$$$$$$$ 3 | /$$__ $$| $$$ /$$$| $$__ $$ 4 | | $$ \__/| $$$$ /$$$$| $$ \ $$ /$$$$$$ /$$$$$$ /$$ /$$ /$$ /$$ 5 | | $$$$$$ | $$ $$/$$ $$| $$$$$$$//$$__ $$ /$$__ $$| $$ /$$/| $$ | $$ 6 | \____ $$| $$ $$$| $$| $$____/| $$ \__/| $$ \ $$ \ $$$$/ | $$ | $$ 7 | /$$ \ $$| $$\ $ | $$| $$ | $$ | $$ | $$ >$$ $$ | $$ | $$ 8 | | $$$$$$/| $$ \/ | $$| $$ | $$ | $$$$$$/ /$$/\ $$| $$$$$$$ 9 | \______/ |__/ |__/|__/ |__/ \______/ |__/ \__/ \____ $$ 10 | /$$ | $$ 11 | | $$$$$$/ 12 | \______/ 13 | ``` 14 | 15 | # SMProxy 16 | 17 | > A MySQL database connection pool based on MySQL protocol and Swoole. 18 | 19 | - Read/Write Splitting 20 | - Connection Pool 21 | - SQL92 Standard 22 | - Coroutine Scheduling 23 | - Multiple database connections, multiple databases, multiple users... 24 | - Build with MySQL native protocol, cross-language, cross-platform. 25 | - Compatible with MySQL Transaction 26 | - Compatible with HandshakeV10 27 | - Compatible with MySQL 5.5 - 8.0 28 | - Compatible with Various Frameworks 29 | 30 | [GitHub](https://github.com/louislivi/smproxy/) 31 | [GitLab](https://gitlab.com/louislivi/smproxy/) 32 | [Get Started](en/README.md) -------------------------------------------------------------------------------- /docs/en/_navbar.md: -------------------------------------------------------------------------------- 1 | - [Blog](http://www.louislivi.com) 2 | - Translations 3 | - [:cn: 中文](/) 4 | - [:uk: English](/en/) -------------------------------------------------------------------------------- /docs/en/_sidebar.md: -------------------------------------------------------------------------------- 1 | - [SMProxy](en/README.md#SMProxy) 2 | - [Swoole MySQL Proxy](en/README.md#swoole-mysql-proxy) 3 | - [Principle](en/README.md#principle) 4 | - [Features](en/README.md#features) 5 | - [Why This](en/README.md#why-this) 6 | - [Benchmark](en/BENCHMARK.md) 7 | - [SMProxy Benchmark](en/BENCHMARK.md#smproxy-benchmark) 8 | - [PHP 7.2.6 Without Framework](en/BENCHMARK.md#php-726-without-framework) 9 | - [Thinkphp 5.0](en/BENCHMARK.md#thinkphp-50) 10 | - [Number of MySQL Connections](en/BENCHMARK.md#number-of-mysql-connections) 11 | - [Requirements](en/README.md#requirements) 12 | - [Installation](en/README.md#installation) 13 | - [Usage](en/README.md#usage) 14 | - [Configuration](en/README.md#configuration) 15 | - [database.json](en/README.md#databasejson) 16 | - [server.json](en/README.md#serverjson) 17 | - [Integration](en/README.md#integration) 18 | - [Route](en/README.md#Route) 19 | - [Annotation](en/README.md#Annotation) 20 | - [MySQL8.0](en/README.md#mysql80) 21 | - [Troubleshooting](en/README.md#troubleshooting) 22 | - [Community](en/README.md#community) 23 | - [More Documentation](en/README.md#more-documentation) 24 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/louislivi/SMProxy/743baea06dcf215a742c5772393d204a31179e0d/docs/favicon.ico -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SMProxy 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 23 | 24 | 25 | 29 |
30 | 31 | 32 | 33 | 34 | 48 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /docs/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: Baiduspider 2 | Disallow: 3 | User-agent: Sosospider 4 | Disallow: 5 | User-agent: sogou spider 6 | Disallow: 7 | User-agent: YodaoBot 8 | Disallow: 9 | User-agent: Googlebot 10 | Disallow: 11 | User-agent: Bingbot 12 | Disallow: 13 | User-agent: Slurp 14 | Disallow: 15 | User-agent: Teoma 16 | Disallow: 17 | User-agent: ia_archiver 18 | Disallow: 19 | User-agent: twiceler 20 | Disallow: 21 | User-agent: MSNBot 22 | Disallow: 23 | User-agent: Scrubby 24 | Disallow: 25 | User-agent: Robozilla 26 | Disallow: 27 | User-agent: Gigabot 28 | Disallow: 29 | User-agent: googlebot-image 30 | Disallow: 31 | User-agent: googlebot-mobile 32 | Disallow: 33 | User-agent: yahoo-mmcrawler 34 | Disallow: 35 | User-agent: yahoo-blogs/v3.9 36 | Disallow: 37 | User-agent: psbot 38 | Disallow: 39 | User-agent: * 40 | Disallow: 41 | Sitemap: https://smproxy.louislivi.com/sitemap.xml 42 | -------------------------------------------------------------------------------- /docs/ror.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ROR Sitemap for https://smproxy.louislivi.com/ 5 | https://smproxy.louislivi.com/ 6 | 7 | 8 | https://smproxy.louislivi.com/ 9 | SMProxy Document 10 | 一个基于 MySQL 协议,Swoole 开发的MySQL数据库连接池。A MySQL database connection pool based on MySQL protocol and Swoole. 11 | 12 | 0 13 | sitemap 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/sitemap.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | smproxy.louislivi.com Site Map 7 | 8 | 111 | 112 | 113 | 114 |
115 | 116 |

117 | Last updated: 2018, December 13
118 | Total pages: 1
119 | smproxy.louislivi.com Homepage 120 |

121 |
122 | 128 |
129 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /docs/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | https://smproxy.louislivi.com/ 9 | 2018-12-13T10:55:38+00:00 10 | 11 | -------------------------------------------------------------------------------- /docs/sitemap.xml.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/louislivi/SMProxy/743baea06dcf215a742c5772393d204a31179e0d/docs/sitemap.xml.gz -------------------------------------------------------------------------------- /docs/urllist.txt: -------------------------------------------------------------------------------- 1 | https://smproxy.louislivi.com/ 2 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | bin 4 | src 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | error 24 | 25 | 26 | 27 | 28 | error 29 | 30 | 31 | 32 | 33 | config/* 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/Base.php: -------------------------------------------------------------------------------- 1 | 13 | * Date: 2018/10/30 14 | * Time: 上午11:06. 15 | */ 16 | class Base extends Context 17 | { 18 | /** 19 | * 协程执行处理异常. 20 | * 21 | * @param $function 22 | */ 23 | protected static function go(\Closure $function) 24 | { 25 | if (-1 !== \Swoole\Coroutine::getuid()) { 26 | $pool = self::$pool[\Swoole\Coroutine::getuid()] ?? false; 27 | } else { 28 | $pool = false; 29 | } 30 | \Swoole\Coroutine::create(function () use ($function, $pool) { 31 | try { 32 | if ($pool) { 33 | self::$pool[\Swoole\Coroutine::getuid()] = $pool; 34 | } 35 | $function(); 36 | if ($pool) { 37 | unset(self::$pool[\Swoole\Coroutine::getuid()]); 38 | } 39 | } catch (SMProxyException $SMProxyException) { 40 | self::writeErrorMessage($SMProxyException, 'system'); 41 | } catch (MySQLException $MySQLException) { 42 | self::writeErrorMessage($MySQLException, 'mysql'); 43 | } 44 | }); 45 | } 46 | 47 | /** 48 | * 写入日志 49 | * 50 | * @param $exception 51 | * @param string $tag 52 | */ 53 | protected static function writeErrorMessage($exception, string $tag = 'mysql') 54 | { 55 | $log = Log::getLogger($tag); 56 | $errLevel = $exception->getCode() ? array_search($exception->getCode(), Log::$levels) : 'warning'; 57 | $log->$errLevel($exception->errorMessage()); 58 | if (CONFIG['server']['swoole']['daemonize'] != true) { 59 | echo '[' . ucfirst($errLevel) . '] ', $exception->errorMessage(), PHP_EOL; 60 | } 61 | } 62 | 63 | /** 64 | * 格式化配置项. 65 | * 66 | * @param array $_config 67 | * 68 | * @return array 69 | * 70 | * @throws \SMProxy\SMProxyException 71 | */ 72 | public function parseDbConfig(array $_config) 73 | { 74 | $config = $_config['database'] ?? []; 75 | foreach ($config['databases'] as $key => $database) { 76 | if (isset($config['serverInfo'][$database['serverInfo']])) { 77 | //处理连接池参数 78 | $this ->setConnPoolParams($config['databases'][$key], $config['databases'][$key]); 79 | foreach ($config['serverInfo'][$database['serverInfo']] as $s_key => $value) { 80 | $database_result = &$config['databases'][$s_key . DB_DELIMITER . $key]; 81 | //处理连接参数 82 | if (isset($config['account'][$value['account']])) { 83 | $host = &$config['serverInfo'][$database['serverInfo']][$s_key]['host']; 84 | if (is_array($host)) { 85 | $host = $host[array_rand($host)]; 86 | } 87 | $database_result = $config['databases'][$key]; 88 | $database_result['serverInfo'] = 89 | $config['serverInfo'][$database['serverInfo']][$s_key]; 90 | $database_result['serverInfo']['account'] = 91 | $config['account'][$value['account']]; 92 | //重载连接池参数 93 | $this ->setConnPoolParams($value, $database_result, $database_result['serverInfo']); 94 | if (!isset($config['databases'][$s_key])) { 95 | $config['databases'][$s_key] = $config['databases'][$key]; 96 | $config['databases'][$s_key]['serverInfo'] = $database_result['serverInfo']; 97 | } 98 | } else { 99 | throw new SMProxyException('Config serverInfo->' . $s_key . '->account is not exists!'); 100 | } 101 | } 102 | } else { 103 | throw new SMProxyException('Config serverInfo key ' . $database['serverInfo'] . 'is not exists!'); 104 | } 105 | unset($config['databases'][$key]); 106 | } 107 | return $config['databases']; 108 | } 109 | 110 | /** 111 | * 设置连接池参数 112 | * 113 | * @param array $value 需要设置的值 114 | * @param array $new_params 被设置的参数 115 | * @param array $old_params 删除旧参数 116 | * 117 | */ 118 | private function setConnPoolParams(array $value, array &$new_params, array &$old_params = []) 119 | { 120 | if (isset($value['maxConns'])) { 121 | $new_params['maxConns'] = $this ->evalConfigParam($value['maxConns'], true); 122 | if (!empty($old_params)) { 123 | unset($old_params['maxConns']); 124 | } 125 | } 126 | if (isset($value['maxSpareConns'])) { 127 | $new_params['maxSpareConns'] = $this ->evalConfigParam($value['maxSpareConns'], true); 128 | if (!empty($old_params)) { 129 | unset($old_params['maxSpareConns']); 130 | } 131 | } 132 | if (isset($value['startConns'])) { 133 | $new_params['startConns'] = $this ->evalConfigParam($value['startConns']); 134 | if (!empty($old_params)) { 135 | unset($old_params['startConns']); 136 | } 137 | } 138 | if (isset($value['maxSpareExp'])) { 139 | $new_params['maxSpareExp'] = $this ->evalConfigParam($value['maxSpareExp']); 140 | if (!empty($old_params)) { 141 | unset($old_params['maxSpareExp']); 142 | } 143 | } 144 | } 145 | 146 | /** 147 | * 计算配置参数 148 | * 149 | * @param string $value 150 | * @param bool $floor_worker_num 151 | * 152 | * @return float|mixed 153 | */ 154 | private function evalConfigParam(string $value, bool $floor_worker_num = false) 155 | { 156 | if ($floor_worker_num) { 157 | $param = floor( 158 | eval('return ' . $value . ';') / CONFIG['server']['swoole']['worker_num'] 159 | ); 160 | } else { 161 | $param = eval('return ' . $value . ';'); 162 | } 163 | return $param; 164 | } 165 | 166 | /** 167 | * 协程pop 168 | * 169 | * @param $chan 170 | * @param int $timeout 171 | * 172 | * @return bool 173 | */ 174 | protected static function coPop(Channel $chan, int $timeout = 0) 175 | { 176 | if (version_compare(swoole_version(), '4.0.3', '>=')) { 177 | return $chan->pop($timeout); 178 | } else { 179 | if (0 == $timeout) { 180 | return $chan->pop(); 181 | } else { 182 | $writes = []; 183 | $reads = [$chan]; 184 | $result = $chan->select($reads, $writes, $timeout); 185 | if (false === $result || empty($reads)) { 186 | return false; 187 | } 188 | $readChannel = $reads[0]; 189 | return $readChannel->pop(); 190 | } 191 | } 192 | } 193 | 194 | protected static function writeErrMessage(int $id, string $msg, int $errno = 0, $sqlState = 'HY000') 195 | { 196 | $err = new ErrorPacket(); 197 | $err->packetId = $id; 198 | if ($errno) { 199 | $err->errno = $errno; 200 | } 201 | $err->sqlState = $sqlState; 202 | $err->message = array_iconv($msg); 203 | 204 | return $err->write(); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/BaseServer.php: -------------------------------------------------------------------------------- 1 | 13 | * Date: 2018/10/26 14 | * Time: 下午5:40. 15 | */ 16 | abstract class BaseServer extends Base 17 | { 18 | protected $connectReadState = []; 19 | protected $connectHasTransaction = []; 20 | protected $connectHasAutoCommit = []; 21 | protected $server; 22 | 23 | /** 24 | * BaseServer constructor. 25 | * 26 | */ 27 | public function __construct() 28 | { 29 | try { 30 | if (!(CONFIG['server']['swoole'] ?? false)) { 31 | $system_log = Log::getLogger('system'); 32 | $system_log->error('config [swoole] is not found !'); 33 | throw new SMProxyException('config [swoole] is not found !'); 34 | } 35 | if ((CONFIG['server']['port'] ?? false)) { 36 | $ports = explode(',', CONFIG['server']['port']); 37 | } else { 38 | $ports = [3366]; 39 | } 40 | $this->server = new \swoole_server( 41 | CONFIG['server']['host'], 42 | $ports[0], 43 | CONFIG['server']['mode'], 44 | CONFIG['server']['sock_type'] 45 | ); 46 | if (count($ports) > 1) { 47 | for ($i = 1; $i < count($ports); ++$i) { 48 | $this->server->addListener( 49 | CONFIG['server']['host'], 50 | $ports[$i], 51 | CONFIG['server']['sock_type'] 52 | ); 53 | } 54 | } 55 | $this->server->set(CONFIG['server']['swoole']); 56 | $this->server->on('connect', [$this, 'onConnect']); 57 | $this->server->on('receive', [$this, 'onReceive']); 58 | $this->server->on('close', [$this, 'onClose']); 59 | $this->server->on('start', [$this, 'onStart']); 60 | $this->server->on('WorkerStart', [$this, 'onWorkerStart']); 61 | $this->server->on('ManagerStart', [$this, 'onManagerStart']); 62 | $this->server->set(packageLengthSetting()); 63 | $result = $this->server->start(); 64 | if ($result) { 65 | smproxy_error('WARNING: Server is shutdown!'); 66 | } else { 67 | smproxy_error('ERROR: Server start failed!'); 68 | } 69 | } catch (\Swoole\Exception $exception) { 70 | smproxy_error('ERROR:' . $exception->getMessage()); 71 | } catch (\ErrorException $exception) { 72 | smproxy_error('ERROR:' . $exception->getMessage()); 73 | } catch (SMProxyException $exception) { 74 | smproxy_error('ERROR:' . $exception->errorMessage()); 75 | } 76 | } 77 | 78 | protected function onConnect(\swoole_server $server, int $fd) 79 | { 80 | } 81 | 82 | protected function onReceive(\swoole_server $server, int $fd, int $reactor_id, string $data) 83 | { 84 | } 85 | 86 | protected function onWorkerStart(\swoole_server $server, int $worker_id) 87 | { 88 | } 89 | 90 | public function onStart(\swoole_server $server) 91 | { 92 | \file_put_contents(CONFIG['server']['swoole']['pid_file'], $server->master_pid . ',' . $server->manager_pid); 93 | ProcessHelper::setProcessTitle('SMProxy master process'); 94 | } 95 | 96 | public function onManagerStart(\swoole_server $server) 97 | { 98 | ProcessHelper::setProcessTitle('SMProxy manager process'); 99 | } 100 | 101 | /** 102 | * 关闭连接 销毁协程变量. 103 | * 104 | * @param $server 105 | * @param $fd 106 | */ 107 | protected function onClose(\swoole_server $server, int $fd) 108 | { 109 | $cid = Coroutine::getuid(); 110 | if ($cid > 0 && isset(self::$pool[$cid])) { 111 | unset(self::$pool[$cid]); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Command/Command.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 2018/11/23 5 | * Time: 下午1:04. 6 | */ 7 | 8 | namespace SMProxy\Command; 9 | 10 | use function SMProxy\Helper\initConfig; 11 | use SMProxy\Helper\PhpHelper; 12 | use function SMProxy\Helper\smproxy_error; 13 | 14 | class Command 15 | { 16 | /** 17 | * 运行 18 | * 19 | * @param array $argv 20 | * 21 | * @throws \SMProxy\SMProxyException 22 | */ 23 | public function run(array $argv) 24 | { 25 | $command = count($argv) >= 2 ? $argv[1] : false; 26 | $this ->settingConfig($argv); 27 | $this ->commandHandler($command, $argv); 28 | } 29 | 30 | /** 31 | * 设置配置文件 32 | * 33 | * @param array $argv 34 | * 35 | * @throws \SMProxy\SMProxyException 36 | */ 37 | protected function settingConfig(array $argv) 38 | { 39 | //指定配置文件 40 | $configPath = ROOT . '/conf/'; 41 | $configKey = array_search('-c', $argv) ?: array_search('--config', $argv); 42 | if ($configKey) { 43 | if (!isset($argv[$configKey + 1])) { 44 | smproxy_error(HelpMessage::$version . PHP_EOL . HelpMessage::$usage); 45 | } 46 | $configPath = $argv[$configKey + 1]; 47 | } 48 | 49 | 50 | //前台运行 51 | $consoleKey = array_search('--console', $argv); 52 | if ($consoleKey) { 53 | define('CONSOLE', true); 54 | } else { 55 | define('CONSOLE', false); 56 | } 57 | 58 | if (file_exists($configPath)) { 59 | define('CONFIG_PATH', realpath($configPath) . '/'); 60 | define('CONFIG', initConfig(CONFIG_PATH)); 61 | } else { 62 | smproxy_error('ERROR: ' . $configPath . ' No such file or directory!'); 63 | } 64 | } 65 | 66 | /** 67 | * 处理命令 68 | * 69 | * @param string $command 70 | */ 71 | protected function commandHandler(string $command, array $argv) 72 | { 73 | $serverCommand = new ServerCommand(); 74 | $serverCommand->argv = $argv; 75 | 76 | if ('-h' == $command || '--help' == $command) { 77 | echo $serverCommand->desc, PHP_EOL; 78 | 79 | return; 80 | } 81 | 82 | if ('-v' == $command || '--version' == $command) { 83 | echo $serverCommand->logo, PHP_EOL; 84 | 85 | return; 86 | } 87 | 88 | if (!$command || !method_exists($serverCommand, $command)) { 89 | echo $serverCommand->usage, PHP_EOL; 90 | 91 | return; 92 | } 93 | 94 | PhpHelper::call([$serverCommand, $command]); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Command/HelpMessage.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 2018/11/28 5 | * Time: 下午2:14 6 | */ 7 | 8 | namespace SMProxy\Command; 9 | 10 | class HelpMessage 11 | { 12 | public static $logo = <<<'LOGO' 13 | 14 | /$$$$$$ /$$ /$$ /$$$$$$$ 15 | /$$__ $$| $$$ /$$$| $$__ $$ 16 | | $$ \__/| $$$$ /$$$$| $$ \ $$ /$$$$$$ /$$$$$$ /$$ /$$ /$$ /$$ 17 | | $$$$$$ | $$ $$/$$ $$| $$$$$$$//$$__ $$ /$$__ $$| $$ /$$/| $$ | $$ 18 | \____ $$| $$ $$$| $$| $$____/| $$ \__/| $$ \ $$ \ $$$$/ | $$ | $$ 19 | /$$ \ $$| $$\ $ | $$| $$ | $$ | $$ | $$ >$$ $$ | $$ | $$ 20 | | $$$$$$/| $$ \/ | $$| $$ | $$ | $$$$$$/ /$$/\ $$| $$$$$$$ 21 | \______/ |__/ |__/|__/ |__/ \______/ |__/ \__/ \____ $$ 22 | /$$ | $$ 23 | | $$$$$$/ 24 | \______/ 25 | 26 | 27 | LOGO; 28 | 29 | public static $version = 'SMProxy version: ' . SMPROXY_VERSION . PHP_EOL; 30 | 31 | public static $usage = <<<'USAGE' 32 | Usage: 33 | SMProxy [ start | stop | restart | status | reload ] [ -c | --config | --console ] 34 | SMProxy -h | --help 35 | SMProxy -v | --version 36 | 37 | USAGE; 38 | 39 | public static $desc = <<<'DESC' 40 | Options: 41 | start Start server 42 | stop Shutdown server 43 | restart Restart server 44 | status Show server status 45 | reload Reload configuration 46 | -h --help Display help 47 | -v --version Display version 48 | -c --config Specify configuration path 49 | --console Front desk operation 50 | 51 | DESC; 52 | 53 | public static $status = <<<'STATUS' 54 | SMProxy[${version}] - ${uname} 55 | Host: ${host}, Port: ${port}, PHPVerison: ${php_version} 56 | SwooleVersion: ${swoole_version}, WorkerNum: ${worker_num} 57 | STATUS; 58 | } 59 | -------------------------------------------------------------------------------- /src/Command/ServerCommand.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 2018/11/23 5 | * Time: 下午1:11. 6 | */ 7 | 8 | namespace SMProxy\Command; 9 | 10 | use SMProxy\Base; 11 | use SMProxy\MysqlPacket\SMProxyPacket; 12 | use function SMProxy\Helper\getString; 13 | use function SMProxy\Helper\initConfig; 14 | use function SMProxy\Helper\smproxy_error; 15 | use SMProxy\MysqlPool\MySQLException; 16 | use Swoole\Coroutine; 17 | 18 | class ServerCommand extends Base 19 | { 20 | public $logo; 21 | public $desc; 22 | public $usage; 23 | public $argv; 24 | public $serverSetting = []; 25 | const SMPROXY_VERSION = 'v1.3.1'; 26 | 27 | public function __construct() 28 | { 29 | $this->logo = HelpMessage::$logo . PHP_EOL . HelpMessage::$version; 30 | $this->desc = $this->logo . PHP_EOL . HelpMessage::$usage . PHP_EOL . HelpMessage::$desc; 31 | $this->usage = $this->logo . PHP_EOL . HelpMessage::$usage; 32 | } 33 | 34 | /** 35 | * 启动服务 36 | * 37 | * @throws \ErrorException 38 | */ 39 | public function start() 40 | { 41 | // 是否正在运行 42 | if ($this->isRunning()) { 43 | smproxy_error("The server have been running! (PID: {$this->serverSetting['masterPid']})"); 44 | } 45 | 46 | echo $this->logo, PHP_EOL; 47 | echo 'Server starting ...', PHP_EOL; 48 | new \SMProxy\SMProxyServer(); 49 | } 50 | 51 | /** 52 | * 停止服务. 53 | */ 54 | public function stop() 55 | { 56 | if (!$this->isRunning()) { 57 | smproxy_error('ERROR: The server is not running! cannot stop!'); 58 | } 59 | 60 | //强制停止 61 | $force = false; 62 | if (in_array('--force', $this->argv) || in_array('-f', $this->argv)) { 63 | $force = true; 64 | } 65 | 66 | echo 'SMProxy is stopping ...', PHP_EOL; 67 | 68 | $result = function () use ($force) { 69 | // 获取master进程ID 70 | $masterPid = $this->serverSetting['masterPid']; 71 | // 使用swoole_process::kill代替posix_kill 72 | if ($force) { 73 | \swoole_process::kill($masterPid, SIGKILL); 74 | } else { 75 | \swoole_process::kill($masterPid); 76 | } 77 | $timeout = 60; 78 | $startTime = time(); 79 | while (true) { 80 | // Check the process status 81 | if (\swoole_process::kill($masterPid, 0)) { 82 | // 判断是否超时 83 | if (time() - $startTime >= $timeout) { 84 | return false; 85 | } 86 | usleep(10000); 87 | continue; 88 | } 89 | return true; 90 | } 91 | return false; 92 | }; 93 | 94 | // 停止失败 95 | if (!$result()) { 96 | smproxy_error('SMProxy shutting down failed!'); 97 | } 98 | 99 | // 删除pid文件 100 | if (file_exists(CONFIG['server']['swoole']['pid_file'])) { 101 | @unlink(CONFIG['server']['swoole']['pid_file']); 102 | } 103 | 104 | echo 'SMProxy has been shutting down.', PHP_EOL; 105 | } 106 | 107 | /** 108 | * 重启服务 109 | * 110 | * @throws \ErrorException 111 | * @throws \SMProxy\SMProxyException 112 | */ 113 | public function restart() 114 | { 115 | // 是否已启动 116 | if ($this->isRunning()) { 117 | $this->stop(); 118 | } 119 | 120 | // 重启默认是守护进程 121 | $this->start(); 122 | } 123 | 124 | /** 125 | * 平滑重启. 126 | */ 127 | public function reload() 128 | { 129 | // 是否已启动 130 | if (!$this->isRunning()) { 131 | echo 'The server is not running! cannot reload', PHP_EOL; 132 | 133 | return; 134 | } 135 | 136 | echo 'Server is reloading...', PHP_EOL; 137 | \swoole_process::kill($this->serverSetting['managerPid'], SIGUSR1); 138 | echo 'Server reload success', PHP_EOL; 139 | } 140 | 141 | /** 142 | * 服务状态 143 | * 144 | * @throws \SMProxy\SMProxyException 145 | */ 146 | public function status() 147 | { 148 | // 是否已启动 149 | if ($this->isRunning()) { 150 | self::go(function () { 151 | //显示基础信息 152 | echo str_replace([ 153 | '${version}', 154 | '${uname}', 155 | '${php_version}', 156 | '${worker_num}', 157 | '${host}', 158 | '${port}', 159 | '${swoole_version}', 160 | ], [ 161 | self::SMPROXY_VERSION, 162 | php_uname(), 163 | PHP_VERSION, 164 | CONFIG['server']['swoole']['worker_num'], 165 | CONFIG['server']['host'], 166 | CONFIG['server']['port'], 167 | swoole_version(), 168 | ], HelpMessage::$status), PHP_EOL; 169 | $dbConfig = $this->parseDbConfig(initConfig(CONFIG_PATH)); 170 | $serverClient = new Coroutine\Client(SWOOLE_SOCK_TCP); 171 | $serverClient->connect(CONFIG['server']['host'], CONFIG['server']['port'], 0.5); 172 | $serverClient->recv(); 173 | $serverClient->send(getString([7, 0, 0, 0, SMProxyPacket::$SMPROXY, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73])); 174 | $result = ''; 175 | while ($nowData = $serverClient->recv()) { 176 | $result .= $nowData; 177 | }; 178 | $result = json_decode(base64_decode($result), true); 179 | $serverClient->close(); 180 | $clients = []; 181 | foreach ($dbConfig as $key => $value) { 182 | $database = explode(DB_DELIMITER, $key)[1] ?? false; 183 | $model = explode(DB_DELIMITER, $key)[0]; 184 | if ($database && !isset($clients[$key])) { 185 | $threadId = ""; 186 | foreach ($result as $index => $item) { 187 | $indexes = explode(DB_DELIMITER, $index); 188 | if (($indexes[0] . DB_DELIMITER . $indexes[1] == $key) || (count($indexes) == 2 && $indexes[0] == $model)) { 189 | $threadId .= $item['threadId'] . ","; 190 | } 191 | } 192 | if (empty($threadId)) { 193 | continue; 194 | } 195 | $threadId = substr($threadId, 0, strlen($threadId) - 1); 196 | $mysql = new Coroutine\MySQL(); 197 | $mysql->connect([ 198 | 'host' => CONFIG['server']['host'], 199 | 'user' => CONFIG['server']['user'], 200 | 'port' => CONFIG['server']['port'], 201 | 'password' => CONFIG['server']['password'], 202 | 'database' => $database, 203 | ]); 204 | $mysql->setDefer(); 205 | switch ($model) { 206 | case 'read': 207 | $mysql->query('/*SMProxy processlist sql*/select * from information_schema.processlist where id in (' . $threadId . ') order by id asc'); 208 | break; 209 | case 'write': 210 | $mysql->query('/** smproxy:db_type=write *//*SMProxy processlist sql*/select * from information_schema.processlist where id in (' . $threadId . ') order by id asc'); 211 | break; 212 | } 213 | $clients[$key] = $mysql; 214 | } 215 | } 216 | //绘制表格数据 217 | $table = new Table(); 218 | $table->setHeader(["ID", "USER", "HOST", "DB", "COMMAND", "TIME", "STATE", "INFO", "SERVER_VERSION", "PLUGIN_NAME", "SERVER_STATUS", "SERVER_KEY"]); 219 | $processlist = []; 220 | foreach ($clients as $key => $client) { 221 | $model = explode(DB_DELIMITER, $key)[0]; 222 | $data = $client->recv() ?: []; 223 | foreach ($data as $process) { 224 | $processlist[$process["COMMAND"]] = ($processlist[$process["COMMAND"]] ?? 0) + 1; 225 | foreach ($result as $index => $item) { 226 | $indexes = explode(DB_DELIMITER, $index); 227 | if ($process["ID"] == $item["threadId"]) { 228 | $process["SERVER_VERSION"] = $item["serverVersion"]; 229 | $process["PLUGIN_NAME"] = $item["pluginName"]; 230 | $process["SERVER_STATUS"] = $item["serverStatus"]; 231 | if ($indexes[0] . DB_DELIMITER . $indexes[1] == $key) { 232 | $process["SERVER_KEY"] = $key; 233 | } else if (count($indexes) == 2 && $indexes[0] == $model) { 234 | $process["SERVER_KEY"] = $model; 235 | } 236 | } 237 | } 238 | if (strpos($process["INFO"], "/*SMProxy processlist sql*/") !== false) { 239 | $process["INFO"] = "/*SMProxy processlist sql*/"; 240 | } 241 | $table->addRow(array_values($process)); 242 | } 243 | if ($client->errno) { 244 | throw new MySQLException($client->error); 245 | } 246 | $client->close(); 247 | } 248 | $processlistDetails = ''; 249 | foreach ($processlist as $key => $value) { 250 | $processlistDetails .= ', ' . $value . ' ' . strtolower($key); 251 | } 252 | echo 'Process : ' . $table->count() . ' total' . $processlistDetails, PHP_EOL; 253 | echo $table->render(), PHP_EOL; 254 | }); 255 | } else { 256 | echo 'The Server is not running', PHP_EOL; 257 | } 258 | } 259 | 260 | /** 261 | * 判断服务是否在运行中. 262 | * 263 | * @return bool 264 | */ 265 | private function isRunning() 266 | { 267 | $masterIsLive = false; 268 | $pFile = CONFIG['server']['swoole']['pid_file']; 269 | 270 | // 判断pid文件是否存在 271 | if (file_exists($pFile)) { 272 | // 获取pid文件内容 273 | $pidFile = file_get_contents($pFile); 274 | $pids = explode(',', $pidFile); 275 | 276 | $this->serverSetting['masterPid'] = $pids[0]; 277 | $this->serverSetting['managerPid'] = $pids[1]; 278 | $masterIsLive = $this->serverSetting['masterPid'] && @\swoole_process::kill($this->serverSetting['managerPid'], 0); 279 | } 280 | 281 | return $masterIsLive; 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/Command/Table.php: -------------------------------------------------------------------------------- 1 | [], 53 | 'default' => [ 54 | 'top' => ['+', '-', '+', '+'], 55 | 'cell' => ['|', ' ', '|', '|'], 56 | 'middle' => ['+', '-', '+', '+'], 57 | 'bottom' => ['+', '-', '+', '+'], 58 | 'cross-top' => ['+', '-', '-', '+'], 59 | 'cross-bottom' => ['+', '-', '-', '+'], 60 | ], 61 | 'markdown' => [ 62 | 'top' => [' ', ' ', ' ', ' '], 63 | 'cell' => ['|', ' ', '|', '|'], 64 | 'middle' => ['|', '-', '|', '|'], 65 | 'bottom' => [' ', ' ', ' ', ' '], 66 | 'cross-top' => ['|', ' ', ' ', '|'], 67 | 'cross-bottom' => ['|', ' ', ' ', '|'], 68 | ], 69 | 'borderless' => [ 70 | 'top' => ['=', '=', ' ', '='], 71 | 'cell' => [' ', ' ', ' ', ' '], 72 | 'middle' => ['=', '=', ' ', '='], 73 | 'bottom' => ['=', '=', ' ', '='], 74 | 'cross-top' => ['=', '=', ' ', '='], 75 | 'cross-bottom' => ['=', '=', ' ', '='], 76 | ], 77 | 'box' => [ 78 | 'top' => ['┌', '─', '┬', '┐'], 79 | 'cell' => ['│', ' ', '│', '│'], 80 | 'middle' => ['├', '─', '┼', '┤'], 81 | 'bottom' => ['└', '─', '┴', '┘'], 82 | 'cross-top' => ['├', '─', '┴', '┤'], 83 | 'cross-bottom' => ['├', '─', '┬', '┤'], 84 | ], 85 | 'box-double' => [ 86 | 'top' => ['╔', '═', '╤', '╗'], 87 | 'cell' => ['║', ' ', '│', '║'], 88 | 'middle' => ['╠', '─', '╪', '╣'], 89 | 'bottom' => ['╚', '═', '╧', '╝'], 90 | 'cross-top' => ['╠', '═', '╧', '╣'], 91 | 'cross-bottom' => ['╠', '═', '╤', '╣'], 92 | ], 93 | ]; 94 | /** 95 | * 设置表格头信息 以及对齐方式 96 | * @access public 97 | * @param array $header 要输出的Header信息 98 | * @param int $align 对齐方式 默认1 ALGIN_LEFT 0 ALIGN_RIGHT 2 ALIGN_CENTER 99 | * @return void 100 | */ 101 | public function setHeader(array $header, int $align = 1) 102 | { 103 | $this->header = $header; 104 | $this->headerAlign = $align; 105 | $this->checkColWidth($header); 106 | } 107 | /** 108 | * 设置输出表格数据 及对齐方式 109 | * @access public 110 | * @param array $rows 要输出的表格数据(二维数组) 111 | * @param int $align 对齐方式 默认1 ALGIN_LEFT 0 ALIGN_RIGHT 2 ALIGN_CENTER 112 | * @return void 113 | */ 114 | public function setRows(array $rows, int $align = 1) 115 | { 116 | $this->rows = $rows; 117 | $this->cellAlign = $align; 118 | foreach ($rows as $row) { 119 | $this->checkColWidth($row); 120 | } 121 | } 122 | /** 123 | * 检查列数据的显示宽度 124 | * @access public 125 | * @param mixed $row 行数据 126 | * @return void 127 | */ 128 | protected function checkColWidth($row) 129 | { 130 | if (is_array($row)) { 131 | foreach ($row as $key => $cell) { 132 | if (!isset($this->colWidth[$key]) || strlen($cell) > $this->colWidth[$key]) { 133 | $this->colWidth[$key] = strlen($cell); 134 | } 135 | } 136 | } 137 | } 138 | /** 139 | * 增加一行表格数据 140 | * @access public 141 | * @param mixed $row 行数据 142 | * @param bool $first 是否在开头插入 143 | * @return void 144 | */ 145 | public function addRow($row, bool $first = false) 146 | { 147 | if ($first) { 148 | array_unshift($this->rows, $row); 149 | } else { 150 | $this->rows[] = $row; 151 | } 152 | $this->checkColWidth($row); 153 | } 154 | 155 | /** 156 | * 获取总记录数 157 | * @return int 158 | */ 159 | public function count() 160 | { 161 | return count($this->rows); 162 | } 163 | /** 164 | * 设置输出表格的样式 165 | * @access public 166 | * @param string $style 样式名 167 | * @return void 168 | */ 169 | public function setStyle(string $style) 170 | { 171 | $this->style = isset($this->format[$style]) ? $style : 'default'; 172 | } 173 | /** 174 | * 输出分隔行 175 | * @access public 176 | * @param string $pos 位置 177 | * @return string 178 | */ 179 | protected function renderSeparator(string $pos): string 180 | { 181 | $style = $this->getStyle($pos); 182 | $array = []; 183 | foreach ($this->colWidth as $width) { 184 | $array[] = str_repeat($style[1], $width + 2); 185 | } 186 | return $style[0] . implode($style[2], $array) . $style[3] . PHP_EOL; 187 | } 188 | /** 189 | * 输出表格头部 190 | * @access public 191 | * @return string 192 | */ 193 | protected function renderHeader(): string 194 | { 195 | $style = $this->getStyle('cell'); 196 | $content = $this->renderSeparator('top'); 197 | foreach ($this->header as $key => $header) { 198 | $array[] = ' ' . str_pad($header, $this->colWidth[$key], $style[1], $this->headerAlign); 199 | } 200 | if (!empty($array)) { 201 | $content .= $style[0] . implode(' ' . $style[2], $array) . ' ' . $style[3] . PHP_EOL; 202 | if (!empty($this->rows)) { 203 | $content .= $this->renderSeparator('middle'); 204 | } 205 | } 206 | return $content; 207 | } 208 | protected function getStyle(string $style): array 209 | { 210 | if ($this->format[$this->style]) { 211 | $style = $this->format[$this->style][$style]; 212 | } else { 213 | $style = [' ', ' ', ' ', ' ']; 214 | } 215 | return $style; 216 | } 217 | /** 218 | * 输出表格 219 | * @access public 220 | * @param array $dataList 表格数据 221 | * @return string 222 | */ 223 | public function render(array $dataList = []): string 224 | { 225 | if (!empty($dataList)) { 226 | $this->setRows($dataList); 227 | } 228 | // 输出头部 229 | $content = $this->renderHeader(); 230 | $style = $this->getStyle('cell'); 231 | if (!empty($this->rows)) { 232 | foreach ($this->rows as $row) { 233 | if (is_string($row) && '-' === $row) { 234 | $content .= $this->renderSeparator('middle'); 235 | } elseif (is_scalar($row)) { 236 | $content .= $this->renderSeparator('cross-top'); 237 | $array = str_pad($row, 3 * (count($this->colWidth) - 1) + array_reduce($this->colWidth, function ($a, $b) { 238 | return $a + $b; 239 | })); 240 | $content .= $style[0] . ' ' . $array . ' ' . $style[3] . PHP_EOL; 241 | $content .= $this->renderSeparator('cross-bottom'); 242 | } else { 243 | $array = []; 244 | foreach ($row as $key => $val) { 245 | $array[] = ' ' . str_pad($val, $this->colWidth[$key], ' ', $this->cellAlign); 246 | } 247 | $content .= $style[0] . implode(' ' . $style[2], $array) . ' ' . $style[3] . PHP_EOL; 248 | } 249 | } 250 | } 251 | $content .= $this->renderSeparator('bottom'); 252 | return $content; 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/Context.php: -------------------------------------------------------------------------------- 1 | 9 | * Date: 2018/10/30 10 | * Time: 下午4:48. 11 | */ 12 | class Context 13 | { 14 | protected static $pool = []; 15 | 16 | public static function get(string $key) 17 | { 18 | $cid = Coroutine::getuid(); 19 | if ($cid < 0) { 20 | return null; 21 | } 22 | if (isset(self::$pool[$cid][$key])) { 23 | return self::$pool[$cid][$key]; 24 | } 25 | 26 | return null; 27 | } 28 | 29 | public static function put(string $key, $item) 30 | { 31 | $cid = Coroutine::getuid(); 32 | if ($cid > 0) { 33 | self::$pool[$cid][$key] = $item; 34 | } 35 | } 36 | 37 | public static function delete(string $key = null) 38 | { 39 | $cid = Coroutine::getuid(); 40 | if ($cid > 0) { 41 | if ($key) { 42 | unset(self::$pool[$cid][$key]); 43 | } else { 44 | unset(self::$pool[$cid]); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Handler/Backend/BackendAuthenticator.php: -------------------------------------------------------------------------------- 1 | 9 | * Date: 2018/10/31 10 | * Time: 下午4:07. 11 | */ 12 | class BackendAuthenticator 13 | { 14 | /** 15 | * 与MySQL连接时的一些特性指定. 16 | */ 17 | public static function getClientFlags() 18 | { 19 | $flag = 0; 20 | 21 | $flag |= Capabilities::CLIENT_LONG_PASSWORD; 22 | $flag |= Capabilities::CLIENT_FOUND_ROWS; 23 | $flag |= Capabilities::CLIENT_LONG_FLAG; 24 | $flag |= Capabilities::CLIENT_CONNECT_WITH_DB; 25 | // flag |= Capabilities::CLIENT_NO_SCHEMA; 26 | // flag |= Capabilities::CLIENT_COMPRESS; 27 | $flag |= Capabilities::CLIENT_ODBC; 28 | // flag |= Capabilities::CLIENT_LOCAL_FILES; 29 | $flag |= Capabilities::CLIENT_IGNORE_SPACE; 30 | $flag |= Capabilities::CLIENT_PROTOCOL_41; 31 | $flag |= Capabilities::CLIENT_INTERACTIVE; 32 | // flag |= Capabilities::CLIENT_SSL; 33 | $flag |= Capabilities::CLIENT_IGNORE_SIGPIPE; 34 | $flag |= Capabilities::CLIENT_TRANSACTIONS; 35 | // flag |= Capabilities::CLIENT_RESERVED; 36 | $flag |= Capabilities::CLIENT_SECURE_CONNECTION; 37 | $flag |= Capabilities::CLIENT_PLUGIN_AUTH; 38 | // client extension 39 | // 不允许MULTI协议 40 | // flag |= Capabilities::CLIENT_MULTI_STATEMENTS; 41 | // flag |= Capabilities::CLIENT_MULTI_RESULTS; 42 | return $flag; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Handler/Frontend/FrontendAuthenticator.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 2018/11/9 5 | * Time: 上午10:01. 6 | */ 7 | 8 | namespace SMProxy\Handler\Frontend; 9 | 10 | use function SMProxy\Helper\getString; 11 | use SMProxy\MysqlPacket\HandshakePacket; 12 | use SMProxy\MysqlPacket\Util\Capabilities; 13 | use SMProxy\MysqlPacket\Util\CharsetUtil; 14 | use SMProxy\MysqlPacket\Util\RandomUtil; 15 | use SMProxy\MysqlPacket\Util\SecurityUtil; 16 | use SMProxy\MysqlPacket\Util\Versions; 17 | 18 | class FrontendAuthenticator 19 | { 20 | public $seed = []; 21 | public $auth = false; 22 | public $database; 23 | public $user; 24 | 25 | public function getHandshakePacket(int $server_id) 26 | { 27 | $rand1 = RandomUtil::randomBytes(8); 28 | $rand2 = RandomUtil::randomBytes(12); 29 | $this->seed = array_merge($rand1, $rand2); 30 | $hs = new HandshakePacket(); 31 | $hs->packetId = 0; 32 | $hs->protocolVersion = Versions::PROTOCOL_VERSION; 33 | $hs->serverVersion = Versions::SERVER_VERSION; 34 | $hs->threadId = $server_id; 35 | $hs->seed = $rand1; 36 | $hs->serverCapabilities = $this->getServerCapabilities(); 37 | $hs->serverCharsetIndex = (CharsetUtil::getIndex(CONFIG['server']['charset'] ?? 'utf8mb4') & 0xff); 38 | $hs->serverStatus = 2; 39 | $hs->restOfScrambleBuff = $rand2; 40 | 41 | return getString($hs->write()); 42 | } 43 | 44 | public function checkPassword(array $password, string $pass) 45 | { 46 | // check null 47 | if (null == $pass || 0 == strlen($pass)) { 48 | if (null == $password || 0 == count($password)) { 49 | return true; 50 | } else { 51 | return false; 52 | } 53 | } 54 | if (null == $password || 0 == count($password)) { 55 | return false; 56 | } 57 | 58 | // encrypt 59 | $encryptPass = null; 60 | try { 61 | $encryptPass = SecurityUtil::scramble411($pass, $this->seed); 62 | } catch (\Exception $e) { 63 | return false; 64 | } 65 | if (null != $encryptPass && (count($encryptPass) == count($password))) { 66 | $i = count($encryptPass); 67 | while (0 != $i--) { 68 | if ($encryptPass[$i] != $password[$i]) { 69 | return false; 70 | } 71 | } 72 | } else { 73 | return false; 74 | } 75 | 76 | return true; 77 | } 78 | 79 | protected function getServerCapabilities() 80 | { 81 | $flag = 0; 82 | $flag |= Capabilities::CLIENT_LONG_PASSWORD; 83 | $flag |= Capabilities::CLIENT_FOUND_ROWS; 84 | $flag |= Capabilities::CLIENT_LONG_FLAG; 85 | $flag |= Capabilities::CLIENT_CONNECT_WITH_DB; 86 | // flag |= Capabilities::CLIENT_NO_SCHEMA; 87 | // flag |= Capabilities::CLIENT_COMPRESS; 88 | $flag |= Capabilities::CLIENT_ODBC; 89 | // flag |= Capabilities::CLIENT_LOCAL_FILES; 90 | $flag |= Capabilities::CLIENT_IGNORE_SPACE; 91 | $flag |= Capabilities::CLIENT_PROTOCOL_41; 92 | $flag |= Capabilities::CLIENT_INTERACTIVE; 93 | // flag |= Capabilities::CLIENT_SSL; 94 | $flag |= Capabilities::CLIENT_IGNORE_SIGPIPE; 95 | $flag |= Capabilities::CLIENT_TRANSACTIONS; 96 | // flag |= ServerDefs.CLIENT_RESERVED; 97 | $flag |= Capabilities::CLIENT_SECURE_CONNECTION; 98 | // $flag |= Capabilities::CLIENT_PLUGIN_AUTH; 99 | 100 | return $flag; 101 | } 102 | 103 | protected function failure(int $errno, string $info) 104 | { 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Handler/Frontend/FrontendConnection.php: -------------------------------------------------------------------------------- 1 | 11 | * Date: 2018/11/3 12 | * Time: 上午10:18. 13 | */ 14 | class FrontendConnection 15 | { 16 | protected $queryHandler; 17 | 18 | public function __construct() 19 | { 20 | $this->setQueryHandler(new ServerQueryHandler()); 21 | } 22 | 23 | public function setQueryHandler(FrontendQueryHandler $queryHandler) 24 | { 25 | $this->queryHandler = $queryHandler; 26 | } 27 | 28 | /** 29 | * @param BinaryPacket $bin 30 | * 31 | * @return mixed 32 | * @throws MySQLException 33 | */ 34 | public function query(BinaryPacket $bin) 35 | { 36 | // 取得语句 37 | $mm = new MySQLMessage($bin->data); 38 | $mm->position(5); 39 | $sql = $mm->readString(); 40 | if (null == $sql || 0 == strlen($sql)) { 41 | throw new MySQLException('Empty SQL'); 42 | } 43 | // 执行查询 44 | return $this->queryHandler->query($sql); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Handler/Frontend/FrontendQueryHandler.php: -------------------------------------------------------------------------------- 1 | 7 | * Date: 2018/11/3 8 | * Time: 上午10:42. 9 | */ 10 | interface FrontendQueryHandler 11 | { 12 | public function query(string $sql); 13 | } 14 | -------------------------------------------------------------------------------- /src/Handler/Frontend/ServerQueryHandler.php: -------------------------------------------------------------------------------- 1 | 9 | * Date: 2018/11/3 10 | * Time: 上午10:48. 11 | */ 12 | class ServerQueryHandler implements FrontendQueryHandler 13 | { 14 | public function query(string $sql) 15 | { 16 | $rs = ServerParse::parse($sql); 17 | 18 | return $rs & 0xff; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Helper/PhpHelper.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 2018/11/23 5 | * Time: 下午12:18 6 | */ 7 | namespace SMProxy\Helper; 8 | 9 | /** 10 | * php帮助类 11 | */ 12 | class PhpHelper 13 | { 14 | /** 15 | * is Cli 16 | * 17 | * @return boolean 18 | */ 19 | public static function isCli(): bool 20 | { 21 | return PHP_SAPI === 'cli'; 22 | } 23 | 24 | /** 25 | * 是否是mac环境 26 | * 27 | * @return bool 28 | */ 29 | public static function isMac(): bool 30 | { 31 | return php_uname('s') === 'Darwin'; 32 | } 33 | 34 | /** 35 | * 调用 36 | * 37 | * @param mixed $cb callback函数,多种格式 38 | * @param array $args 参数 39 | * 40 | * @return mixed 41 | */ 42 | public static function call($cb, array $args = []) 43 | { 44 | if (version_compare(SWOOLE_VERSION, '4.0', '>=')) { 45 | $ret = call_user_func_array($cb, $args); 46 | } else { 47 | $ret = \Swoole\Coroutine::call_user_func_array($cb, $args); 48 | } 49 | 50 | return $ret; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Helper/ProcessHelper.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 2018/11/23 5 | * Time: 下午12:16 6 | */ 7 | 8 | namespace SMProxy\Helper; 9 | 10 | /** 11 | * 进程帮助类 12 | * 13 | */ 14 | class ProcessHelper 15 | { 16 | /** 17 | * 设置当前进程名称 18 | * 19 | * @param string $title 名称 20 | * 21 | * @return bool 22 | */ 23 | public static function setProcessTitle(string $title): bool 24 | { 25 | if (PhpHelper::isMac()) { 26 | return false; 27 | } 28 | 29 | if (\function_exists('cli_set_process_title')) { 30 | return @cli_set_process_title($title); 31 | } 32 | 33 | return true; 34 | } 35 | 36 | 37 | /** 38 | * run a command. it is support windows 39 | * @param string $command 40 | * @param string|null $cwd 41 | * @return array 42 | * @throws \RuntimeException 43 | */ 44 | public static function run(string $command, string $cwd = null): array 45 | { 46 | $descriptors = [ 47 | 0 => ['pipe', 'r'], // stdin - read channel 48 | 1 => ['pipe', 'w'], // stdout - write channel 49 | 2 => ['pipe', 'w'], // stdout - error channel 50 | 3 => ['pipe', 'r'], // stdin - This is the pipe we can feed the password into 51 | ]; 52 | 53 | $process = proc_open($command, $descriptors, $pipes, $cwd); 54 | 55 | if (!\is_resource($process)) { 56 | throw new \RuntimeException('Can\'t open resource with proc_open.'); 57 | } 58 | 59 | // Nothing to push to input. 60 | fclose($pipes[0]); 61 | 62 | $output = stream_get_contents($pipes[1]); 63 | fclose($pipes[1]); 64 | 65 | $error = stream_get_contents($pipes[2]); 66 | fclose($pipes[2]); 67 | 68 | fclose($pipes[3]); 69 | 70 | // Close all pipes before proc_close! $code === 0 is success. 71 | $code = proc_close($process); 72 | 73 | return [trim($code), trim($output), trim($error)]; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Helper/functions.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 2018/10/26 5 | * Time: 下午5:33. 6 | */ 7 | 8 | namespace SMProxy\Helper; 9 | 10 | use SMProxy\Command\ServerCommand; 11 | use SMProxy\Log\Log; 12 | use SMProxy\MysqlPacket\Util\CharsetUtil; 13 | 14 | /** 15 | * 获取bytes 数组. 16 | * 17 | * @param $data 18 | * 19 | * @return array 20 | */ 21 | function getBytes(string $data) 22 | { 23 | $bytes = []; 24 | $count = strlen($data); 25 | for ($i = 0; $i < $count; ++$i) { 26 | $byte = ord($data[$i]); 27 | $bytes[] = $byte; 28 | } 29 | 30 | return $bytes; 31 | } 32 | 33 | /** 34 | * 打印bytes 数组. 35 | * 36 | * @param $data 37 | * 38 | */ 39 | function printBytes(string $data) 40 | { 41 | print_r("print bytes:\n"); 42 | var_dump(implode(",", getBytes($data))); 43 | } 44 | 45 | /** 46 | * 获取 string. 47 | * 48 | * @param array $bytes 49 | * 50 | * @return string 51 | */ 52 | function getString(array $bytes) 53 | { 54 | return implode(array_map('chr', $bytes)); 55 | } 56 | 57 | /** 58 | * 数组复制. 59 | * 60 | * @param $array 61 | * @param $start 62 | * @param $len 63 | * 64 | * @return array 65 | */ 66 | function array_copy(array $array, int $start, int $len) 67 | { 68 | return array_slice($array, $start, $len); 69 | } 70 | 71 | /** 72 | * 转换长度. 73 | * 74 | * @param int $size 75 | * @param int $length 76 | * 77 | * @return array 78 | */ 79 | function getMysqlPackSize(int $size, int $length = 3) 80 | { 81 | $sizeData[] = $size & 0xff; 82 | $sizeData[] = shr16($size & 0xff << 8, 8); 83 | $sizeData[] = shr16($size & 0xff << 16, 16); 84 | if ($length > 3) { 85 | $sizeData[] = shr16($size & 0xff << 24, 24); 86 | } 87 | return $sizeData; 88 | } 89 | 90 | /** 91 | * 无符号16位右移. 92 | * 93 | * @param int $x 要进行操作的数字 94 | * @param int $bits 右移位数 95 | * 96 | * @return int 97 | */ 98 | function shr16(int $x, int $bits) 99 | { 100 | return ((2147483647 >> ($bits - 1)) & ($x >> $bits)) > 255 ? 255 : ((2147483647 >> ($bits - 1)) & ($x >> $bits)); 101 | } 102 | 103 | /** 104 | * 初始化配置文件. 105 | * 106 | * @param string $dir 107 | * 108 | * @return array 109 | * 110 | * @throws \SMProxy\SMProxyException 111 | */ 112 | function initConfig(string $dir) 113 | { 114 | $config = []; 115 | 116 | $dir = realpath($dir); 117 | if (!is_dir($dir)) { 118 | throw new \RuntimeException('Cannot find config dir.'); 119 | } 120 | 121 | $paths = glob($dir . '/*.json'); 122 | 123 | foreach ($paths as $path) { 124 | $item = json_decode(file_get_contents($path), true); 125 | if (is_array($item)) { 126 | $config = array_merge($config, $item); 127 | } else { 128 | throw new \InvalidArgumentException('Invalid config.'); 129 | } 130 | } 131 | 132 | if (!isset($config['server']['host'])) { 133 | $config['server']['host'] = '0.0.0.0'; 134 | } 135 | 136 | if (!isset($config['server']['port'])) { 137 | $config['server']['port'] = 3366; 138 | } 139 | 140 | //是否为前端运行 141 | if (CONSOLE) { 142 | $config['server']['swoole']['daemonize'] = false; 143 | } 144 | 145 | //计算worker_num 146 | if (isset($config['server']['swoole']['worker_num'])) { 147 | $config['server']['swoole']['worker_num'] = eval('return ' . $config['server']['swoole']['worker_num'] . ';'); 148 | } else { 149 | $config['server']['swoole']['worker_num'] = 1; 150 | } 151 | 152 | //替换swoole 常量 153 | if (isset($config['server']['mode'])) { 154 | replace_constant($config['server']['mode'], SWOOLE_PROCESS); 155 | } else { 156 | $config['server']['mode'] = SWOOLE_PROCESS; 157 | } 158 | 159 | if (isset($config['server']['sock_type'])) { 160 | replace_constant($config['server']['sock_type'], SWOOLE_SOCK_TCP); 161 | } else { 162 | $config['server']['sock_type'] = SWOOLE_SOCK_TCP; 163 | } 164 | 165 | if (isset($config['server']['swoole_client_sock_setting']['sock_type'])) { 166 | replace_constant($config['server']['swoole_client_sock_setting']['sock_type'], SWOOLE_SOCK_TCP); 167 | } else { 168 | $config['server']['swoole_client_sock_setting']['sock_type'] = SWOOLE_SOCK_TCP; 169 | } 170 | 171 | //生成日志目录 172 | if (isset($config['server']['logs']['config']['system']['log_path'])) { 173 | mk_log_dir($config['server']['logs']['config']['system']['log_path']); 174 | } else { 175 | throw new \SMProxy\SMProxyException('ERROR:server.logs.config.system.log_path 配置项不存在!'); 176 | } 177 | if (isset($config['server']['logs']['config']['mysql']['log_path'])) { 178 | mk_log_dir($config['server']['logs']['config']['mysql']['log_path']); 179 | } else { 180 | throw new \SMProxy\SMProxyException('ERROR:server.logs.config.mysql.log_path 配置项不存在!'); 181 | } 182 | if (isset($config['server']['swoole']['log_file'])) { 183 | mk_log_dir($config['server']['swoole']['log_file']); 184 | } else { 185 | throw new \SMProxy\SMProxyException('ERROR:server.swoole.log_file 配置项不存在!'); 186 | } 187 | if (isset($config['server']['swoole']['pid_file'])) { 188 | mk_log_dir($config['server']['swoole']['pid_file']); 189 | } else { 190 | throw new \SMProxy\SMProxyException('ERROR:server.swoole.pid_file 配置项不存在!'); 191 | } 192 | 193 | return $config; 194 | } 195 | 196 | /** 197 | * 替换常量值 198 | * 199 | * @param string $const 200 | * @param string $default 201 | */ 202 | function replace_constant(string &$const, string $default = '') 203 | { 204 | if (defined($const)) { 205 | $const = constant($const); 206 | } else { 207 | $const = $default; 208 | } 209 | } 210 | 211 | /** 212 | * 创建日志目录. 213 | * 214 | * @param string $path 215 | */ 216 | function mk_log_dir(string &$path) 217 | { 218 | $path = str_replace('ROOT', ROOT, $path); 219 | if (!file_exists(dirname($path))) { 220 | mkdir(dirname($path), 0755, true); 221 | } 222 | } 223 | 224 | /** 225 | * 对数据进行编码转换. 226 | * 227 | * @param array/string $data 数组 228 | * @param string $output 转换后的编码 229 | * 230 | * @return array|null|string|string[] 231 | */ 232 | function array_iconv($data, string $output = 'utf-8') 233 | { 234 | $output = CharsetUtil::charsetToEncoding($output); 235 | $encode_arr = ['UTF-8', 'ASCII', 'GBK', 'GB2312', 'BIG5', 'JIS', 'eucjp-win', 'sjis-win', 'EUC-JP']; 236 | $encoded = mb_detect_encoding($data, $encode_arr); 237 | 238 | if (!is_array($data)) { 239 | return mb_convert_encoding($data, $output, $encoded); 240 | } else { 241 | foreach ($data as $key => $val) { 242 | $key = array_iconv($key, $output); 243 | if (is_array($val)) { 244 | $data[$key] = array_iconv($val, $output); 245 | } else { 246 | $data[$key] = mb_convert_encoding($data, $output, $encoded); 247 | } 248 | } 249 | 250 | return $data; 251 | } 252 | } 253 | 254 | /** 255 | * get version. 256 | * 257 | * @return string 258 | */ 259 | function absorb_version_from_git() 260 | { 261 | $defaultVersion = ServerCommand::SMPROXY_VERSION; 262 | $hasGit = \SMProxy\Helper\ProcessHelper::run('type git >/dev/null 2>&1 || { echo >&2 "false" ;}', ROOT)[2]; 263 | if ($hasGit !== "false") { 264 | $tagInfo = \SMProxy\Helper\ProcessHelper::run('git describe --tags HEAD', ROOT)[1]; 265 | if (preg_match('/^(?.+)-\d+-g(?[a-f0-9]{7})$/', $tagInfo, $matches)) { 266 | return sprintf('%s@%s', $matches['tag'], $matches['hash']); 267 | } elseif ($tagInfo) { 268 | return $tagInfo; 269 | } 270 | } 271 | return $defaultVersion; 272 | } 273 | 274 | /** 275 | * error. 276 | * 277 | * @param $message 278 | * @param int $exitCode 279 | */ 280 | function smproxy_error($message, $exitCode = 0) 281 | { 282 | $parts = explode(':', $message, 2); 283 | 284 | $parts[0] = strtoupper($parts[0]); 285 | 286 | $prefixExists = in_array($parts[0], [ 287 | 'ERROR', 'WARNING', 'NOTICE', 288 | ]); 289 | 290 | if ($prefixExists) { 291 | $message = $parts[0] . ': ' . trim($parts[1]); 292 | } else { 293 | $message = 'ERROR: ' . $message; 294 | } 295 | 296 | error_log($message); 297 | 298 | if (!$prefixExists || 'ERROR' == $parts[0]) { 299 | exit($exitCode); 300 | } 301 | } 302 | 303 | /** 304 | * 获取包长配置 305 | * 306 | * @return array 307 | */ 308 | function packageLengthSetting() 309 | { 310 | $package_length_func = function ($data) { 311 | if (strlen($data) < 4) { 312 | return 0; 313 | } 314 | $length = ord($data[0]) | (ord($data[1]) << 8) | (ord($data[2]) << 16); 315 | if ($length <= 0) { 316 | return -1; 317 | } 318 | return $length + 4; 319 | }; 320 | return [ 321 | 'open_length_check' => true, 322 | 'package_length_func' => $package_length_func, 323 | ]; 324 | } 325 | 326 | /** 327 | * 获取包长 328 | * 329 | * @param string $data 330 | * @param int $step 331 | * @param int $offset 332 | * 333 | * @return int 334 | */ 335 | function getPackageLength(string $data, int $step, int $offset) 336 | { 337 | $i = ord($data[$step]); 338 | $i |= ord($data[$step + 1]) << 8; 339 | $i |= ord($data[$step + 2]) << 16; 340 | if ($offset >= 4) { 341 | $i |= ord($data[$step + 3]) << 24; 342 | } 343 | 344 | return $i + $offset; 345 | } 346 | 347 | /** 348 | * 处理异常 349 | * 350 | * @param int $errno 351 | * @param string $errstr 352 | * @param string $errfile 353 | * @param int $errline 354 | */ 355 | function _error_handler(int $errno, string $errstr, string $errfile, int $errline) 356 | { 357 | $errCode = strlen($errstr) > 3 ? substr($errstr, -4, 3) : 0; 358 | $errMethod = explode(': ', $errstr)[0] ?? ''; 359 | if (strrpos($errMethod, 'Swoole\Coroutine\Client') === false && $errCode != '110' && $errCode != '111' 360 | && !(basename($errfile) == 'ServerParse.php' && $errno == E_NOTICE)) { 361 | $system_log = Log::getLogger('system'); 362 | $message = sprintf('%s (%s:%s)', $errstr, $errfile, $errline); 363 | $errLevel = $errno ? (array_search($errno + 1, Log::$levels) ?: 'error') : 'error'; 364 | $system_log->$errLevel($message); 365 | if (CONFIG['server']['swoole']['daemonize'] != true) { 366 | echo '[' . ucfirst($errLevel) . '] ', trim($message), PHP_EOL; 367 | } 368 | } 369 | } 370 | 371 | function startsWith($haystack, $needle) 372 | { 373 | // search backwards starting from haystack length characters from the end 374 | return $needle === "" || strrpos($haystack, $needle, -strlen($haystack)) !== false; 375 | } 376 | 377 | function endsWith($haystack, $needle) 378 | { 379 | // search forward starting from end minus needle length characters 380 | return $needle === "" || (($temp = strlen($haystack) - strlen($needle)) >= 0 && strpos($haystack, $needle, $temp) !== false); 381 | } 382 | -------------------------------------------------------------------------------- /src/Log/Log.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 2018/11/12 5 | * Time: 上午9:56. 6 | */ 7 | 8 | namespace SMProxy\Log; 9 | 10 | use Swoole\Coroutine; 11 | use Psr\Log\AbstractLogger; 12 | use Psr\Log\LogLevel; 13 | use Psr\Log\InvalidArgumentException; 14 | 15 | /** 16 | * 日志类. 17 | */ 18 | class Log extends AbstractLogger 19 | { 20 | // 日志根目录 21 | private $logPath = '.'; 22 | 23 | // 日志文件 24 | private $logFile = 'system.log'; 25 | 26 | // 日志自定义目录 27 | private $format = 'Y/m/d'; 28 | 29 | // 日志标签 30 | private $tag = 'system'; 31 | 32 | // 总配置设定 33 | private static $CONFIG = []; 34 | 35 | public static $open = true; 36 | 37 | public static $levels = [ 38 | LogLevel::DEBUG => 0, 39 | LogLevel::INFO => 1, 40 | LogLevel::NOTICE => 2, 41 | LogLevel::WARNING => 3, 42 | LogLevel::ERROR => 4, 43 | LogLevel::CRITICAL => 5, 44 | LogLevel::ALERT => 6, 45 | LogLevel::EMERGENCY => 7, 46 | ]; 47 | 48 | private $minLevelIndex; 49 | 50 | /** 51 | * Log constructor. 52 | * 53 | * @param array $config 54 | * @param null $minLevel 55 | */ 56 | public function __construct(array $config, $minLevel = null) 57 | { 58 | // 日志根目录 59 | if (isset($config['log_path'])) { 60 | $this->logPath = $config['log_path']; 61 | } 62 | 63 | // 日志文件 64 | if (isset($config['log_file'])) { 65 | $this->logFile = $config['log_file']; 66 | } 67 | 68 | // 日志自定义目录 69 | if (isset($config['format'])) { 70 | $this->format = $config['format']; 71 | } 72 | 73 | // 日志标签 74 | if (isset($config['tag'])) { 75 | $this->tag = $config['tag']; 76 | } 77 | 78 | if (null === $minLevel) { 79 | $minLevel = LogLevel::WARNING; 80 | 81 | if (isset($_ENV['SHELL_VERBOSITY']) || isset($_SERVER['SHELL_VERBOSITY'])) { 82 | switch ((int) (isset($_ENV['SHELL_VERBOSITY']) ? $_ENV['SHELL_VERBOSITY'] : $_SERVER['SHELL_VERBOSITY'])) { 83 | case -1: 84 | $minLevel = LogLevel::ERROR; 85 | break; 86 | case 1: 87 | $minLevel = LogLevel::NOTICE; 88 | break; 89 | case 2: 90 | $minLevel = LogLevel::INFO; 91 | break; 92 | case 3: 93 | $minLevel = LogLevel::DEBUG; 94 | break; 95 | } 96 | } 97 | } 98 | 99 | if (!isset(self::$levels[$minLevel])) { 100 | throw new InvalidArgumentException(sprintf('The log level "%s" does not exist.', $minLevel)); 101 | } 102 | 103 | $this->minLevelIndex = self::$levels[$minLevel]; 104 | } 105 | 106 | /** 107 | * 添加日志。 108 | * 109 | * @param mixed $level 110 | * @param string $message 111 | * @param array $context 112 | */ 113 | public function log($level, $message, array $context = []) 114 | { 115 | if (self::$open) { 116 | if (!isset(self::$levels[$level])) { 117 | throw new InvalidArgumentException(sprintf('The log level "%s" does not exist.', $level)); 118 | } 119 | 120 | if (self::$levels[$level] < $this->minLevelIndex) { 121 | return; 122 | } 123 | 124 | $log_data = $this ->format($level, $message, $context); 125 | 126 | // 获取日志文件 127 | $log_file = $this->getLogFile(); 128 | 129 | // 创建日志目录 130 | $is_create = $this->createLogPath(dirname($log_file)); 131 | // 写入日志文件 132 | if ($is_create) { 133 | if (Coroutine::getuid() > 0) { 134 | // 协程写 135 | $this->coWrite($log_file, $log_data); 136 | } else { 137 | $this->syncWrite($log_file, $log_data); 138 | } 139 | } 140 | } 141 | } 142 | 143 | /** 144 | * 格式化输出信息。 145 | * 146 | * @param string $level 147 | * @param string $message 148 | * @param array $context 149 | * 150 | * @return string 151 | */ 152 | private function format(string $level, string $message, array $context): string 153 | { 154 | if (false !== strpos($message, '{')) { 155 | $replacements = []; 156 | foreach ($context as $key => $val) { 157 | if (null === $val || is_scalar($val) || (\is_object($val) && method_exists($val, '__toString'))) { 158 | $replacements["{{$key}}"] = $val; 159 | } elseif ($val instanceof \DateTimeInterface) { 160 | $replacements["{{$key}}"] = $val->format('Y-m-d H:i:s'); 161 | } elseif (\is_object($val)) { 162 | $replacements["{{$key}}"] = '[object ' . \get_class($val) . ']'; 163 | } else { 164 | $replacements["{{$key}}"] = '[' . \gettype($val) . ']'; 165 | } 166 | } 167 | 168 | $message = strtr($message, $replacements); 169 | } 170 | 171 | return sprintf('%s [%s] %s', date('Y-m-d H:i:s'), $level, $message) . \PHP_EOL; 172 | } 173 | 174 | /** 175 | * 获取日志类对象。 176 | * 177 | * @param string $tag 178 | * 179 | * @return Log 180 | */ 181 | public static function getLogger(string $tag = 'system') 182 | { 183 | if (!is_array(self::$CONFIG) || empty(self::$CONFIG)) { 184 | self::$CONFIG = CONFIG['server']['logs']['config']; 185 | self::$open = CONFIG['server']['logs']['open']; 186 | } 187 | 188 | // 根据tag从总配置中获取对应设定,如不存在使用system设定 189 | $config = isset(self::$CONFIG[$tag]) ? self::$CONFIG[$tag] : 190 | (isset(self::$CONFIG['system']) ? self::$CONFIG['system'] : []); 191 | 192 | // 设置标签 193 | $config['tag'] = '' != $tag && 'system' != $tag ? $tag : '-'; 194 | 195 | // 返回日志类对象 196 | return new Log($config, LogLevel::DEBUG); 197 | } 198 | 199 | /** 200 | * 创建日志目录. 201 | * 202 | * @param string $log_path 日志目录 203 | * 204 | * @return bool 205 | */ 206 | private function createLogPath(string $log_path) 207 | { 208 | $dirs = explode("/", $log_path); 209 | $current_dir = ""; 210 | foreach ($dirs as $dir) { 211 | $current_dir .= $dir; 212 | if (file_exists($current_dir) && !is_dir($current_dir)) { 213 | @unlink($current_dir); 214 | } 215 | $current_dir .= "/"; 216 | if (!file_exists($current_dir)) { 217 | @mkdir($current_dir, 0755); 218 | } 219 | } 220 | return true; 221 | } 222 | 223 | /** 224 | * 获取日志文件名称. 225 | * 226 | * @return string 227 | */ 228 | private function getLogFile() 229 | { 230 | // 创建日期时间对象writeFile 231 | $dt = new \DateTime(); 232 | // 计算日志目录格式 233 | return sprintf('%s/%s/%s', $this->logPath, $dt->format($this->format), $this->logFile); 234 | } 235 | 236 | /** 237 | * 协程写文件 238 | * 239 | * @param string $logFile 日志路径 240 | * @param string $messageText 文本信息 241 | */ 242 | private function coWrite(string $logFile, string $messageText) 243 | { 244 | \Swoole\Coroutine::create(function () use ($logFile, $messageText) { 245 | $res = Coroutine::writeFile($logFile, $messageText, FILE_APPEND); 246 | if ($res === false) { 247 | throw new \InvalidArgumentException("Unable to append to log file: {$this->logFile}"); 248 | } 249 | }); 250 | } 251 | 252 | /** 253 | * 同步写文件 254 | * 255 | * @param string $logFile 日志路径 256 | * @param string $messageText 文本信息 257 | */ 258 | private function syncWrite(string $logFile, string $messageText) 259 | { 260 | $fp = fopen($logFile, 'a'); 261 | if ($fp === false) { 262 | throw new \InvalidArgumentException("Unable to append to log file: {$this->logFile}"); 263 | } 264 | flock($fp, LOCK_EX); 265 | fwrite($fp, $messageText); 266 | flock($fp, LOCK_UN); 267 | fclose($fp); 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/MysqlClient.php: -------------------------------------------------------------------------------- 1 | 7 | * Date: 2018/10/26 8 | * Time: 下午5:45. 9 | */ 10 | abstract class MysqlClient extends Base 11 | { 12 | public $connName; 13 | public $client; 14 | public $database; 15 | public $model; 16 | public $ssl = false; 17 | 18 | public function connect(string $host, int $port, float $timeout = 0.1) 19 | { 20 | } 21 | 22 | public function onClientReceive(\Swoole\Coroutine\Client $cli, string $data) 23 | { 24 | } 25 | 26 | public function onClientClose(\Swoole\Coroutine\Client $cli) 27 | { 28 | } 29 | 30 | public function onClientError(\Swoole\Coroutine\Client $cli) 31 | { 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/MysqlPacket/AuthPacket.php: -------------------------------------------------------------------------------- 1 | 13 | * Date: 2018/10/31 14 | * Time: 上午10:32. 15 | */ 16 | class AuthPacket extends MySQLPacket 17 | { 18 | private static $FILLER = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; //23位array 19 | 20 | public $clientFlags; 21 | public $maxPacketSize; 22 | public $charsetIndex; 23 | public $extra; // from FILLER(23) 24 | public $user; 25 | public $password; 26 | public $database = 0; 27 | public $pluginName = 'mysql_native_password'; 28 | public $serverCapabilities; 29 | 30 | public function read(BinaryPacket $bin) 31 | { 32 | $this->packetLength = $bin->packetLength; 33 | $this->packetId = $bin->packetId; 34 | $mm = new MySQLMessage($bin->data); 35 | $mm->move(4); 36 | $this->clientFlags = $mm->readUB4(); 37 | $this->maxPacketSize = $mm->readUB4(); 38 | $this->charsetIndex = ($mm->read() & 0xff); 39 | $current = $mm->position(); 40 | $len = (int) $mm->readLength(); 41 | if ($len > 0 && $len < count(self::$FILLER)) { 42 | $this->extra = array_copy($mm->bytes(), $mm->position(), $len); 43 | } 44 | $mm->position($current + count(self::$FILLER)); 45 | $this->user = $mm->readStringWithNull(); 46 | $this->password = $mm->readBytesWithLength(); 47 | if ((0 != ($this->clientFlags & Capabilities::CLIENT_CONNECT_WITH_DB)) && $mm->hasRemaining()) { 48 | $this->database = $mm->readStringWithNull(); 49 | } 50 | $this->pluginName = $mm->readStringWithNull(); 51 | 52 | return $this; 53 | } 54 | 55 | public function write() 56 | { 57 | $data = getMysqlPackSize($this ->calcPacketSize()); 58 | $data[] = $this->packetId; 59 | BufferUtil::writeUB4($data, $this->clientFlags); 60 | BufferUtil::writeUB4($data, $this->maxPacketSize); 61 | $data[] = $this->charsetIndex; 62 | 63 | $data = array_merge($data, self::$FILLER); 64 | 65 | if (null == $this->user) { 66 | $data[] = 0; 67 | } else { 68 | BufferUtil::writeWithNull($data, getBytes($this->user)); 69 | } 70 | if (null == $this->password) { 71 | $authResponseLength = 0; 72 | $authResponse = 0; 73 | } else { 74 | $authResponseLength = count($this->password); 75 | $authResponse = $this->password; 76 | } 77 | if ($this ->clientFlags & Capabilities::CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA) { 78 | BufferUtil::writeLength($data, $authResponseLength); 79 | BufferUtil::writeWithNull($data, $authResponse, false); 80 | } else if ($this ->clientFlags & Capabilities::CLIENT_SECURE_CONNECTION) { 81 | $data[] = $authResponseLength; 82 | BufferUtil::writeWithNull($data, $authResponse, false); 83 | } else { 84 | BufferUtil::writeWithNull($data, $authResponse); 85 | } 86 | 87 | if ($this ->clientFlags & Capabilities::CLIENT_CONNECT_WITH_DB) { 88 | $database = getBytes($this->database); 89 | BufferUtil::writeWithNull($data, $database); 90 | } 91 | if ($this ->clientFlags & Capabilities::CLIENT_PLUGIN_AUTH) { 92 | BufferUtil::writeWithNull($data, getBytes($this->pluginName)); 93 | } 94 | return $data; 95 | } 96 | 97 | public function calcPacketSize() 98 | { 99 | $size = 32; // 4+4+1+23; 100 | $size += (null == $this->user) ? 1 : strlen($this->user) + 1; 101 | if ($this ->clientFlags & Capabilities::CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA) { 102 | $size += BufferUtil::getLength(count($this->password)) - 1; 103 | } 104 | $size += (null == $this->password) ? 1 : BufferUtil::getLength($this->password); 105 | if ($this ->clientFlags & Capabilities::CLIENT_CONNECT_WITH_DB) { 106 | $size += (null == $this->database) ? 1 : strlen($this->database) + 1; 107 | } 108 | if ($this ->clientFlags & Capabilities::CLIENT_PLUGIN_AUTH) { 109 | $size += strlen($this ->pluginName) + 1; 110 | } 111 | 112 | return $size; 113 | } 114 | 115 | protected function getPacketInfo() 116 | { 117 | return 'MySQL Authentication Packet'; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/MysqlPacket/BinaryPacket.php: -------------------------------------------------------------------------------- 1 | 7 | * Date: 2018/10/25 8 | * Time: 下午6:35. 9 | */ 10 | 11 | /** 12 | * MySql包 外层结构. 13 | * 14 | * @Author Louis Livi <574747417@qq.com> 15 | */ 16 | class BinaryPacket extends MySQLPacket 17 | { 18 | public static $OK = 1; 19 | public static $ERROR = 2; 20 | public static $HEADER = 3; 21 | public static $FIELD = 4; 22 | public static $FIELD_EOF = 5; 23 | public static $ROW = 6; 24 | public static $PACKET_EOF = 7; 25 | public $data; 26 | 27 | public function calcPacketSize() 28 | { 29 | return null == $this->data ? 0 : count($this->data); 30 | } 31 | 32 | protected function getPacketInfo() 33 | { 34 | return 'MySQL Binary Packet'; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/MysqlPacket/ErrorPacket.php: -------------------------------------------------------------------------------- 1 | 13 | * Date: 2018/10/29 14 | * Time: 下午4:11. 15 | */ 16 | class ErrorPacket extends MySQLPacket 17 | { 18 | public static $FIELD_COUNT = 255; 19 | public $marker = '#'; 20 | public $sqlState = 'HY000'; 21 | public $errno = ErrorCode::ER_NO_SUCH_USER; 22 | public $message; 23 | 24 | public function read(BinaryPacket $bin) 25 | { 26 | $this->packetLength = $bin->packetLength; 27 | $this->packetId = $bin->packetId; 28 | $mm = new MySQLMessage($bin->data); 29 | $mm->move(4); 30 | $this->fieldCount = $mm->read(); 31 | $this->errno = $mm->readUB2(); 32 | if ($mm->hasRemaining() && chr($mm->read($mm->position()) == $this->marker)) { 33 | $mm->read(); 34 | $this->sqlState = getString($mm->readBytes(5)); 35 | } 36 | $this->message = getString($mm->readBytes()); 37 | 38 | return $this; 39 | } 40 | 41 | public function write() 42 | { 43 | $data = []; 44 | $size = $this->calcPacketSize(); 45 | $data = array_merge($data, $size); 46 | $data[] = $this->packetId; 47 | $data[] = self::$FIELD_COUNT; 48 | BufferUtil::writeUB2($data, $this->errno); 49 | $data[] = ord($this->marker); 50 | $data = array_merge($data, getBytes($this->sqlState)); 51 | if (null != $this->message) { 52 | $data = array_merge($data, getBytes($this->message)); 53 | } 54 | 55 | return $data; 56 | } 57 | 58 | public function calcPacketSize() 59 | { 60 | $size = 9; 61 | if (null != $this->message) { 62 | $sizeData = getMysqlPackSize($size + strlen($this->message)); 63 | } else { 64 | $sizeData[] = $size; 65 | $sizeData[] = 0; 66 | $sizeData[] = 0; 67 | } 68 | 69 | return $sizeData; 70 | } 71 | 72 | protected function getPacketInfo() 73 | { 74 | return 'MySQL Error Packet'; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/MysqlPacket/HandshakePacket.php: -------------------------------------------------------------------------------- 1 | packetLength = $bin->packetLength; 32 | $this->packetId = $bin->packetId; 33 | $mm = new MySQLMessage($bin->data); 34 | $mm->length = $this->packetLength; 35 | $mm->move(4); 36 | $this->protocolVersion = $mm->read(); 37 | $this->serverVersion = $mm->readStringWithNull(); 38 | $this->threadId = $mm->readUB4(); 39 | $this->seed = $mm->readBytesWithNull(); 40 | $this->serverCapabilities = $mm->readUB2(); 41 | $this->serverCharsetIndex = $mm->read(); 42 | $this->serverStatus = $mm->readUB2(); 43 | $this->serverCapabilities |= $mm->readUB2(); 44 | $this->authDataLength = $mm->read(); 45 | $mm->move(10); 46 | if ($this ->serverCapabilities & Capabilities::CLIENT_SECURE_CONNECTION) { 47 | $this->restOfScrambleBuff = $mm->readBytesWithNull(); 48 | } 49 | $this->pluginName = $mm->readStringWithNull() ?: $this->pluginName; 50 | return $this; 51 | } 52 | 53 | public function write() 54 | { 55 | // default init 256,so it can avoid buff extract 56 | $buffer = []; 57 | BufferUtil::writeUB3($buffer, $this->calcPacketSize()); 58 | $buffer[] = $this->packetId; 59 | $buffer[] = $this->protocolVersion; 60 | BufferUtil::writeWithNull($buffer, getBytes($this->serverVersion)); 61 | BufferUtil::writeUB4($buffer, $this->threadId); 62 | BufferUtil::writeWithNull($buffer, $this->seed); 63 | BufferUtil::writeUB2($buffer, $this->serverCapabilities); 64 | $buffer[] = $this->serverCharsetIndex; 65 | BufferUtil::writeUB2($buffer, $this->serverStatus); 66 | if ($this ->serverCapabilities & Capabilities::CLIENT_PLUGIN_AUTH) { 67 | BufferUtil::writeUB2($buffer, $this->serverCapabilities); 68 | $buffer[] = max(13, count($this->seed) + count($this->restOfScrambleBuff) + 1); 69 | $buffer = array_merge($buffer, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); 70 | } else { 71 | $buffer = array_merge($buffer, self::$FILLER_13); 72 | } 73 | if ($this ->serverCapabilities & Capabilities::CLIENT_SECURE_CONNECTION) { 74 | BufferUtil::writeWithNull($buffer, $this->restOfScrambleBuff); 75 | } 76 | if ($this ->serverCapabilities & Capabilities::CLIENT_PLUGIN_AUTH) { 77 | BufferUtil::writeWithNull($buffer, getBytes($this->pluginName)); 78 | } 79 | return $buffer; 80 | } 81 | 82 | public function calcPacketSize() 83 | { 84 | $size = 1; 85 | $size += strlen($this->serverVersion); // n 86 | $size += 5; // 1+4 87 | $size += count($this->seed); // 8 88 | $size += 19; // 1+2+1+2+13 89 | if ($this ->serverCapabilities & Capabilities::CLIENT_SECURE_CONNECTION) { 90 | $size += count($this->restOfScrambleBuff); // 12 91 | ++$size; // 1 92 | } 93 | if ($this ->serverCapabilities & Capabilities::CLIENT_PLUGIN_AUTH) { 94 | $size += strlen($this->pluginName); 95 | ++$size; // 1 96 | } 97 | return $size; 98 | } 99 | 100 | protected function getPacketInfo() 101 | { 102 | return 'MySQL Handshake Packet'; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/MysqlPacket/MySQLMessage.php: -------------------------------------------------------------------------------- 1 | 10 | * Date: 2018/10/25 11 | * Time: 下午6:42. 12 | */ 13 | 14 | /** 15 | * For netty MySql. 16 | * 17 | * @author lizhuyang 18 | */ 19 | class MySQLMessage 20 | { 21 | public static $NULL_LENGTH = -1; 22 | private static $EMPTY_BYTES = 0; 23 | 24 | private $data; 25 | public $length; 26 | private $position; 27 | 28 | public function __construct(array $data) 29 | { 30 | $this->data = $data; 31 | $this->length = count($data); 32 | $this->position = 0; 33 | } 34 | 35 | public function length() 36 | { 37 | return $this->length; 38 | } 39 | 40 | public function position(int $i = 0) 41 | { 42 | if ($i) { 43 | $this->position = $i; 44 | } else { 45 | return $this->position; 46 | } 47 | } 48 | 49 | public function bytes() 50 | { 51 | return $this->data; 52 | } 53 | 54 | public function move(int $i) 55 | { 56 | $this->position += $i; 57 | } 58 | 59 | public function hasRemaining() 60 | { 61 | return $this->length > $this->position; 62 | } 63 | 64 | public function read(int $i = 0) 65 | { 66 | if ($i) { 67 | return $this->data[$i]; 68 | } 69 | 70 | return $this->data[$this->position++]; 71 | } 72 | 73 | public function readUB2() 74 | { 75 | $b = $this->data; 76 | $i = $b[$this->position++]; 77 | $i |= ($b[$this->position++]) << 8; 78 | 79 | return $i; 80 | } 81 | 82 | public function readUB3() 83 | { 84 | $b = $this->data; 85 | $i = $b[$this->position++]; 86 | $i |= ($b[$this->position++]) << 8; 87 | $i |= ($b[$this->position++]) << 16; 88 | 89 | return $i; 90 | } 91 | 92 | public function readUB4() 93 | { 94 | $b = $this->data; 95 | $l = $b[$this->position++]; 96 | $l |= $b[$this->position++] << 8; 97 | $l |= $b[$this->position++] << 16; 98 | $l |= $b[$this->position++] << 24; 99 | 100 | return $l; 101 | } 102 | 103 | public function readInt() 104 | { 105 | $b = $this->data; 106 | $i = $b[$this->position++]; 107 | $i |= ($b[$this->position++]) << 8; 108 | $i |= ($b[$this->position++]) << 16; 109 | $i |= ($b[$this->position++]) << 24; 110 | 111 | return $i; 112 | } 113 | 114 | public function readFloat() 115 | { 116 | return (float) ($this->readInt()); 117 | } 118 | 119 | public function readLong() 120 | { 121 | $b = $this->data; 122 | $l = $b[$this->position++]; 123 | $l |= $b[$this->position++] << 8; 124 | $l |= $b[$this->position++] << 16; 125 | $l |= $b[$this->position++] << 24; 126 | $l |= $b[$this->position++] << 32; 127 | $l |= $b[$this->position++] << 40; 128 | $l |= $b[$this->position++] << 48; 129 | $l |= $b[$this->position++] << 56; 130 | 131 | return $l; 132 | } 133 | 134 | public function readDouble() 135 | { 136 | return $this->readLong(); 137 | } 138 | 139 | public function readLength() 140 | { 141 | $length = ($this->data[$this->position++] ?? 0) & 0xff; 142 | switch ($length) { 143 | case 251: 144 | return self::$NULL_LENGTH; 145 | case 252: 146 | return $this->readUB2(); 147 | case 253: 148 | return $this->readUB3(); 149 | case 254: 150 | return $this->readLong(); 151 | default: 152 | return $length; 153 | } 154 | } 155 | 156 | public function readBytes(int $length = 0) 157 | { 158 | if ($length) { 159 | return array_copy($this->data, $this->position, $length); 160 | } else { 161 | if ($this->position >= $this->length) { 162 | return self::$EMPTY_BYTES; 163 | } 164 | 165 | return array_copy($this->data, $this->position, $this->length - $this->position); 166 | } 167 | } 168 | 169 | public function readBytesWithNull() 170 | { 171 | $b = $this->data; 172 | if ($this->position >= $this->length) { 173 | return self::$EMPTY_BYTES; 174 | } 175 | $offset = -1; 176 | for ($i = $this->position; $i < $this->length; ++$i) { 177 | if (0 == $b[$i]) { 178 | $offset = $i; 179 | break; 180 | } 181 | } 182 | switch ($offset) { 183 | case -1: 184 | $ab1 = array_copy($b, $this->position, $this->length - $this->position); 185 | $this->position = $this->length; 186 | 187 | return $ab1; 188 | case 0: 189 | $this->position++; 190 | 191 | return self::$EMPTY_BYTES; 192 | default: 193 | $ab2 = array_copy($b, $this->position, $offset - $this->position); 194 | $this->position = $offset + 1; 195 | 196 | return $ab2; 197 | } 198 | } 199 | 200 | public function readBytesWithLength() 201 | { 202 | $length = (int) $this->readLength(); 203 | if ($length <= 0) { 204 | return [self::$EMPTY_BYTES]; 205 | } 206 | $ab = array_copy($this->data, $this->position, $length); 207 | $this->position += $length; 208 | 209 | return $ab; 210 | } 211 | 212 | public function readStringWithNull(string $charset = '') 213 | { 214 | $b = $this->data; 215 | if ($this->position >= $this->length) { 216 | return null; 217 | } 218 | $offset = -1; 219 | for ($i = $this->position; $i < $this->length; ++$i) { 220 | if (0 == $b[$i]) { 221 | $offset = $i; 222 | break; 223 | } 224 | } 225 | if ($charset) { 226 | switch ($offset) { 227 | case -1: 228 | $s1 = getString(array_copy($b, $this->position, $this->length - $this->position)); 229 | $this->position = $this->length; 230 | 231 | return $s1; 232 | case 0: 233 | $this->position++; 234 | 235 | return null; 236 | default: 237 | $s2 = getString(array_copy($b, $this->position, $offset - $this->position)); 238 | $this->position = $offset + 1; 239 | 240 | return $s2; 241 | } 242 | } else { 243 | if (-1 == $offset) { 244 | $s = getString(array_copy($b, $this->position, $this->length - $this->position)); 245 | $this->position = $this->length; 246 | 247 | return $s; 248 | } 249 | if ($offset > $this->position) { 250 | $s = getString(array_copy($b, $this->position, $offset - $this->position)); 251 | $this->position = $offset + 1; 252 | 253 | return $s; 254 | } else { 255 | ++$this->position; 256 | 257 | return null; 258 | } 259 | } 260 | } 261 | 262 | public function readString() 263 | { 264 | if ($this->position >= $this->length) { 265 | return null; 266 | } 267 | $s = getString(array_copy($this->data, $this->position, $this->length - $this->position)); 268 | $this->position = $this->length; 269 | 270 | return $s; 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/MysqlPacket/MySQLPacket.php: -------------------------------------------------------------------------------- 1 | 7 | * Date: 2018/10/25 8 | * Time: 下午6:26. 9 | */ 10 | 11 | /** 12 | * MySqlPacket. 13 | * 14 | * @Author Louis Livi <574747417@qq.com> 15 | */ 16 | abstract class MySQLPacket 17 | { 18 | /** 19 | * none, this is an internal thread state. 20 | */ 21 | public static $COM_SLEEP = 0; 22 | 23 | /** 24 | * mysql_close. 25 | */ 26 | public static $COM_QUIT = 1; 27 | 28 | /** 29 | * mysql_select_db. 30 | */ 31 | public static $COM_INIT_DB = 2; 32 | 33 | /** 34 | * mysql_real_query. 35 | */ 36 | public static $COM_QUERY = 3; 37 | 38 | /** 39 | * mysql_list_fields. 40 | */ 41 | public static $COM_FIELD_LIST = 4; 42 | 43 | /** 44 | * mysql_create_db (deprecated). 45 | */ 46 | public static $COM_CREATE_DB = 5; 47 | 48 | /** 49 | * mysql_drop_db (deprecated). 50 | */ 51 | public static $COM_DROP_DB = 6; 52 | 53 | /** 54 | * mysql_refresh. 55 | */ 56 | public static $COM_REFRESH = 7; 57 | 58 | /** 59 | * mysql_shutdown. 60 | */ 61 | public static $COM_SHUTDOWN = 8; 62 | 63 | /** 64 | * mysql_stat. 65 | */ 66 | public static $COM_STATISTICS = 9; 67 | 68 | /** 69 | * mysql_list_processes. 70 | */ 71 | public static $COM_PROCESS_INFO = 10; 72 | 73 | /** 74 | * none, this is an internal thread state. 75 | */ 76 | public static $COM_CONNECT = 11; 77 | 78 | /** 79 | * mysql_kill. 80 | */ 81 | public static $COM_PROCESS_KILL = 12; 82 | 83 | /** 84 | * mysql_dump_debug_info. 85 | */ 86 | public static $COM_DEBUG = 13; 87 | 88 | /** 89 | * mysql_ping. 90 | */ 91 | public static $COM_PING = 14; 92 | 93 | /** 94 | * none, this is an internal thread state. 95 | */ 96 | public static $COM_TIME = 15; 97 | 98 | /** 99 | * none, this is an internal thread state. 100 | */ 101 | public static $COM_DELAYED_INSERT = 16; 102 | 103 | /** 104 | * mysql_change_user. 105 | */ 106 | public static $COM_CHANGE_USER = 17; 107 | 108 | /** 109 | * used by slave server mysqlbinlog. 110 | */ 111 | public static $COM_BINLOG_DUMP = 18; 112 | 113 | /** 114 | * used by slave server to get master table. 115 | */ 116 | public static $COM_TABLE_DUMP = 19; 117 | 118 | /** 119 | * used by slave to log connection to master. 120 | */ 121 | public static $COM_CONNECT_OUT = 20; 122 | 123 | /** 124 | * used by slave to register to master. 125 | */ 126 | public static $COM_REGISTER_SLAVE = 21; 127 | 128 | /** 129 | * mysql_stmt_prepare. 130 | */ 131 | public static $COM_STMT_PREPARE = 22; 132 | 133 | /** 134 | * mysql_stmt_execute. 135 | */ 136 | public static $COM_STMT_EXECUTE = 23; 137 | 138 | /** 139 | * mysql_stmt_send_long_data. 140 | */ 141 | public static $COM_STMT_SEND_LONG_DATA = 24; 142 | 143 | /** 144 | * mysql_stmt_close. 145 | */ 146 | public static $COM_STMT_CLOSE = 25; 147 | 148 | /** 149 | * mysql_stmt_reset. 150 | */ 151 | public static $COM_STMT_RESET = 26; 152 | 153 | /** 154 | * mysql_set_server_option. 155 | */ 156 | public static $COM_SET_OPTION = 27; 157 | 158 | /** 159 | * mysql_stmt_fetch. 160 | */ 161 | public static $COM_STMT_FETCH = 28; 162 | 163 | /** 164 | * cobar heartbeat. 165 | */ 166 | public static $COM_HEARTBEAT = 64; 167 | 168 | /** 169 | * MORE RESULTS. 170 | */ 171 | public static $SERVER_MORE_RESULTS_EXISTS = 8; 172 | 173 | public $packetLength; 174 | public $packetId = 1; 175 | 176 | /** 177 | * 把数据包写到buffer中,如果buffer满了就把buffer通过前端连接写出。 178 | * 179 | * @throws \Exception 180 | */ 181 | public function write() 182 | { 183 | throw new \Exception(); 184 | } 185 | 186 | /** 187 | * @param $buffer 188 | * @param $ctx 189 | * 190 | * @throws \Exception 191 | */ 192 | public function writeBuf($buffer, $ctx) 193 | { 194 | throw new \Exception(); 195 | } 196 | 197 | /** 198 | * 计算数据包大小,不包含包头长度。 199 | */ 200 | abstract public function calcPacketSize(); 201 | 202 | /** 203 | * 取得数据包信息. 204 | */ 205 | abstract protected function getPacketInfo(); 206 | 207 | protected function toString() 208 | { 209 | return $this->getPacketInfo() . '{length=' . $this->packetLength . ',id=' 210 | . $this->packetId . '}'; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/MysqlPacket/MySqlPacketDecoder.php: -------------------------------------------------------------------------------- 1 | 12 | * Date: 2018/10/27 13 | * Time: 上午10:37. 14 | */ 15 | class MySqlPacketDecoder 16 | { 17 | private $packetHeaderSize = 4; 18 | private $maxPacketSize = 16777216; 19 | 20 | /** 21 | * MySql外层结构解包. 22 | * 23 | * @param string $data 24 | * 25 | * @return \SMProxy\MysqlPacket\BinaryPacket 26 | * @throws \SMProxy\SMProxyException 27 | */ 28 | public function decode(string $data) 29 | { 30 | $data = getBytes($data); 31 | // 4 bytes:3 length + 1 packetId 32 | if (count($data) < $this->packetHeaderSize) { 33 | throw new SMProxyException('Packet is empty'); 34 | } 35 | $packetLength = ByteUtil::readUB3($data); 36 | // // 过载保护 37 | if ($packetLength > $this->maxPacketSize) { 38 | throw new SMProxyException('Packet size over the limit ' . $this->maxPacketSize); 39 | } 40 | $packetId = $data[3]; 41 | // if (in.readableBytes() < packetLength) { 42 | // // 半包回溯 43 | // in.resetReaderIndex(); 44 | // return; 45 | // } 46 | $packet = new BinaryPacket(); 47 | $packet->packetLength = $packetLength; 48 | $packet->packetId = $packetId; 49 | // data will not be accessed any more,so we can use this array safely 50 | $packet->data = $data; 51 | if (null == $packet->data || 0 == count($packet->data)) { 52 | throw new SMProxyException('get data errorMessage,packetLength=' . $packet->packetLength); 53 | } 54 | 55 | return $packet; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/MysqlPacket/OkPacket.php: -------------------------------------------------------------------------------- 1 | 9 | * Date: 2018/10/27 10 | * Time: 上午9:36. 11 | */ 12 | 13 | /** 14 | * MySql OkPacket. 15 | * 16 | * @Author lizhuyang 17 | */ 18 | class OkPacket extends MySQLPacket 19 | { 20 | public static $FIELD_COUNT = 0x00; 21 | public static $OK = [7, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0]; 22 | public static $AUTH_OK = [7, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0]; 23 | public static $FAST_AUTH_OK = [7, 0, 0, 3, 0, 0, 0, 2, 0, 0, 0]; 24 | public static $SWITCH_AUTH_OK = [7, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0]; 25 | public static $FULL_AUTH_OK = [7, 0, 0, 6, 0, 0, 0, 2, 0, 0, 0]; 26 | 27 | public $fieldCount = 0x00; 28 | public $affectedRows; 29 | public $insertId; 30 | public $serverStatus; 31 | public $warningCount; 32 | public $message; 33 | 34 | public function read(BinaryPacket $bin) 35 | { 36 | $this->packetLength = $bin->packetLength; 37 | $this->packetId = $bin->packetId; 38 | $mm = new MySQLMessage($bin->data); 39 | $this->fieldCount = $mm->read(); 40 | $this->affectedRows = $mm->readLength(); 41 | $this->insertId = $mm->readLength(); 42 | $this->serverStatus = $mm->readUB2(); 43 | $this->warningCount = $mm->readUB2(); 44 | if ($mm->hasRemaining()) { 45 | $this->message = $mm->readBytesWithLength(); 46 | } 47 | } 48 | 49 | public function calcPacketSize() 50 | { 51 | $i = 1; 52 | $i += BufferUtil::getLength($this->affectedRows); 53 | $i += BufferUtil::getLength($this->insertId); 54 | $i += 4; 55 | if (null != $this->message) { 56 | $i += BufferUtil::getLength($this->message); 57 | } 58 | 59 | return $i; 60 | } 61 | 62 | protected function getPacketInfo() 63 | { 64 | return 'MySQL OK Packet'; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/MysqlPacket/SMProxyPacket.php: -------------------------------------------------------------------------------- 1 | 7 | * Date: 2020/1/11 8 | * Time: 上午11:07 9 | */ 10 | class SMProxyPacket 11 | { 12 | public static $SMPROXY = 83; 13 | } 14 | -------------------------------------------------------------------------------- /src/MysqlPacket/Util/BufferUtil.php: -------------------------------------------------------------------------------- 1 | 9 | * Date: 2018/10/25 10 | * Time: 下午7:22. 11 | */ 12 | 13 | /** 14 | * @Author lizhuyang 15 | */ 16 | class BufferUtil 17 | { 18 | public static function writeUB2(array &$buffer, int $i) 19 | { 20 | $buffer[] = $i & 0xff; 21 | $buffer[] = shr16($i & 0xff << 8, 8); 22 | } 23 | 24 | public static function writeUB3(array &$buffer, int $i) 25 | { 26 | $buffer[] = $i & 0xff; 27 | $buffer[] = shr16($i & 0xff << 8, 8); 28 | $buffer[] = shr16($i & 0xff << 16, 16); 29 | } 30 | 31 | public static function writeInt(array &$buffer, int $i) 32 | { 33 | $buffer[] = $i & 0xff; 34 | $buffer[] = shr16($i & 0xff << 8, 8); 35 | $buffer[] = shr16($i & 0xff << 16, 16); 36 | $buffer[] = shr16($i & 0xff << 24, 24); 37 | } 38 | 39 | public static function writeFloat(array &$buffer, int $f) 40 | { 41 | self::writeInt($buffer, (int) ($f)); 42 | } 43 | 44 | public static function writeUB4(array &$buffer, int $l) 45 | { 46 | $buffer[] = $l & 0xff; 47 | $buffer[] = shr16($l & 0xff << 8, 8); 48 | $buffer[] = shr16($l & 0xff << 16, 16); 49 | $buffer[] = shr16($l & 0xff << 24, 24); 50 | } 51 | 52 | public static function writeLong(array &$buffer, int $l) 53 | { 54 | $buffer[] = $l & 0xff; 55 | $buffer[] = shr16($l & 0xff << 8, 8); 56 | $buffer[] = shr16($l & 0xff << 16, 16); 57 | $buffer[] = shr16($l & 0xff << 24, 24); 58 | $buffer[] = shr16($l & 0xff << 32, 32); 59 | $buffer[] = shr16($l & 0xff << 40, 40); 60 | $buffer[] = shr16($l & 0xff << 48, 48); 61 | $buffer[] = shr16($l & 0xff << 56, 56); 62 | } 63 | 64 | public static function writeDouble(array &$buffer, int $d) 65 | { 66 | self::writeLong($buffer, (float) ($d)); 67 | } 68 | 69 | public static function writeLength(array &$buffer, int $l) 70 | { 71 | if ($l < 251) { 72 | $buffer[] = $l; 73 | } elseif ($l < 0x10000) { 74 | $buffer[] = 252; 75 | self::writeUB2($buffer, (int) $l); 76 | } elseif ($l < 0x1000000) { 77 | $buffer[] = 253; 78 | self::writeUB3($buffer, (int) $l); 79 | } else { 80 | $buffer[] = 254; 81 | 82 | self::writeLong($buffer, $l); 83 | } 84 | } 85 | 86 | public static function writeWithNull(array &$buffer, $src, $null = true) 87 | { 88 | $src = is_array($src) ? $src : [$src]; 89 | $buffer = array_merge($buffer, $src); 90 | if ($null) { 91 | $buffer[] = 0; 92 | } 93 | } 94 | 95 | public static function writeWithLength(array &$buffer, $src, int $nullValue = 0) 96 | { 97 | if (null == $src) { 98 | $buffer[] = $nullValue; 99 | } else { 100 | $length = count($src); 101 | if ($length < 251) { 102 | $buffer[] = $length; 103 | } elseif ($length < 0x10000) { 104 | $buffer[] = 252; 105 | self::writeUB2($buffer, $length); 106 | } elseif ($length < 0x1000000) { 107 | $buffer[] = 253; 108 | self::writeUB3($buffer, $length); 109 | } else { 110 | $buffer[] = 254; 111 | self::writeLong($buffer, $length); 112 | } 113 | $buffer = array_merge($buffer, $src); 114 | } 115 | } 116 | 117 | public static function getLength($length) 118 | { 119 | if (is_array($length)) { 120 | $length = count($length); 121 | if ($length < 251) { 122 | return 1 + $length; 123 | } elseif ($length < 0x10000) { 124 | return 3 + $length; 125 | } elseif ($length < 0x1000000) { 126 | return 4 + $length; 127 | } else { 128 | return 9 + $length; 129 | } 130 | } else { 131 | if ($length < 251) { 132 | return 1; 133 | } elseif ($length < 0x10000) { 134 | return 3; 135 | } elseif ($length < 0x1000000) { 136 | return 4; 137 | } else { 138 | return 9; 139 | } 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/MysqlPacket/Util/ByteUtil.php: -------------------------------------------------------------------------------- 1 | 9 | * Date: 2018/10/27 10 | * Time: 上午10:44. 11 | */ 12 | class ByteUtil 13 | { 14 | public static function readUB2(array $data) 15 | { 16 | $i = ($data[0]); 17 | $i |= ($data[1] << 8); 18 | 19 | return $i; 20 | } 21 | 22 | public static function readUB3(array $data) 23 | { 24 | $i = ($data[0]); 25 | $i |= ($data[1] << 8); 26 | $i |= ($data[2] << 16); 27 | 28 | return $i; 29 | } 30 | 31 | public static function readUB4(array $data) 32 | { 33 | $i = ($data[0]); 34 | $i |= ($data[1] << 8); 35 | $i |= ($data[2] << 16); 36 | $i |= ($data[3] << 24); 37 | 38 | return $i; 39 | } 40 | 41 | public static function readLong(array $data) 42 | { 43 | $l = ($data[0]); 44 | $l |= ($data[1]) << 8; 45 | $l |= ($data[2]) << 16; 46 | $l |= ($data[3]) << 24; 47 | $l |= ($data[4]) << 32; 48 | $l |= ($data[5]) << 40; 49 | $l |= ($data[6]) << 48; 50 | $l |= ($data[7]) << 56; 51 | 52 | return $l; 53 | } 54 | 55 | /** 56 | * this is for the String. 57 | * 58 | * @param array $data 59 | * 60 | * @return int|mixed 61 | */ 62 | public static function readLength(array $data) 63 | { 64 | $length = $data[0]; 65 | switch ($length) { 66 | case 251: 67 | return MySQLMessage::$NULL_LENGTH; 68 | case 252: 69 | return self::readUB2($data); 70 | case 253: 71 | return self::readUB3($data); 72 | case 254: 73 | return self::readLong($data); 74 | default: 75 | return $length; 76 | } 77 | } 78 | 79 | public static function decodeLength($src) 80 | { 81 | if (is_array($src)) { 82 | $length = count($src); 83 | if ($length < 251) { 84 | return 1 + $length; 85 | } elseif ($length < 0x10000) { 86 | return 3 + $length; 87 | } elseif ($length < 0x1000000) { 88 | return 4 + $length; 89 | } else { 90 | return 9 + $length; 91 | } 92 | } else { 93 | if ($src < 251) { 94 | return 1; 95 | } elseif ($src < 0x10000) { 96 | return 3; 97 | } elseif ($src < 0x1000000) { 98 | return 4; 99 | } else { 100 | return 9; 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/MysqlPacket/Util/Capabilities.php: -------------------------------------------------------------------------------- 1 | 7 | * Date: 2018/10/31 8 | * Time: 上午10:39. 9 | */ 10 | interface Capabilities 11 | { 12 | /** 13 | * server capabilities 14 | *

15 | *

 16 |      * server:        11110111 11111111
 17 |      * client_cmd: 11 10100110 10000101
 18 |      * client_jdbc:10 10100010 10001111.
 19 |      *
 20 |      * 
21 | */ 22 | // new more secure passwords 23 | const CLIENT_LONG_PASSWORD = 1; 24 | 25 | // Found instead of affected rows 26 | // 返回找到(匹配)的行数,而不是改变了的行数。 27 | const CLIENT_FOUND_ROWS = 2; 28 | 29 | // Get all column flags 30 | const CLIENT_LONG_FLAG = 4; 31 | 32 | // One can specify db on connect 33 | const CLIENT_CONNECT_WITH_DB = 8; 34 | 35 | // Don't allow database.table.column 36 | // 不允许“数据库名.表名.列名”这样的语法。这是对于ODBC的设置。 37 | // 当使用这样的语法时解析器会产生一个错误,这对于一些ODBC的程序限制bug来说是有用的。 38 | const CLIENT_NO_SCHEMA = 16; 39 | 40 | // Can use compression protocol 41 | // 使用压缩协议 42 | const CLIENT_COMPRESS = 32; 43 | 44 | // Odbc client 45 | const CLIENT_ODBC = 64; 46 | 47 | // Can use LOAD DATA LOCAL 48 | const CLIENT_LOCAL_FILES = 128; 49 | 50 | // Ignore spaces before '(' 51 | // 允许在函数名后使用空格。所有函数名可以预留字。 52 | const CLIENT_IGNORE_SPACE = 256; 53 | 54 | // New 4.1 protocol This is an interactive client 55 | const CLIENT_PROTOCOL_41 = 512; 56 | 57 | // This is an interactive client 58 | // 允许使用关闭连接之前的不活动交互超时的描述,而不是等待超时秒数。 59 | // 客户端的会话等待超时变量变为交互超时变量。 60 | const CLIENT_INTERACTIVE = 1024; 61 | 62 | // Switch to SSL after handshake 63 | // 使用SSL。这个设置不应该被应用程序设置,他应该是在客户端库内部是设置的。 64 | // 可以在调用mysql_real_connect()之前调用mysql_ssl_set()来代替设置。 65 | const CLIENT_SSL = 2048; 66 | 67 | // IGNORE sigpipes 68 | // 阻止客户端库安装一个SIGPIPE信号处理器。 69 | // 这个可以用于当应用程序已经安装该处理器的时候避免与其发生冲突。 70 | const CLIENT_IGNORE_SIGPIPE = 4096; 71 | 72 | // Client knows about transactions 73 | const CLIENT_TRANSACTIONS = 8192; 74 | 75 | // Old flag for 4.1 protocol 76 | const CLIENT_RESERVED = 16384; 77 | 78 | // New 4.1 authentication 79 | const CLIENT_SECURE_CONNECTION = 32768; 80 | 81 | // Enable/disable multi-stmt support 82 | // 通知服务器客户端可以发送多条语句(由分号分隔)。如果该标志为没有被设置,多条语句执行。 83 | const CLIENT_MULTI_STATEMENTS = 65536; 84 | 85 | // Enable/disable multi-results 86 | // 通知服务器客户端可以处理由多语句或者存储过程执行生成的多结果集。 87 | // 当打开CLIENT_MULTI_STATEMENTS时,这个标志自动的被打开。 88 | const CLIENT_MULTI_RESULTS = 131072; 89 | 90 | const CLIENT_PLUGIN_AUTH = 524288; 91 | 92 | const CLIENT_CONNECT_ATTRS = 1048576; 93 | 94 | const CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA = 2097152; 95 | 96 | const CLIENT_CAPABILITIES = ( 97 | self::CLIENT_LONG_PASSWORD | self::CLIENT_FOUND_ROWS | self::CLIENT_ODBC | self::CLIENT_LONG_FLAG | 98 | self::CLIENT_IGNORE_SPACE | self::CLIENT_PROTOCOL_41 | self::CLIENT_TRANSACTIONS | 99 | self::CLIENT_INTERACTIVE | self::CLIENT_SECURE_CONNECTION | self::CLIENT_MULTI_RESULTS | 100 | self::CLIENT_PLUGIN_AUTH | self::CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA 101 | ); 102 | # Not done yet 103 | const CLIENT_HANDLE_EXPIRED_PASSWORDS = 4194304; 104 | 105 | const CLIENT_SESSION_TRACK = 8388608; 106 | 107 | const CLIENT_DEPRECATE_EOF = 16777216; 108 | } 109 | -------------------------------------------------------------------------------- /src/MysqlPacket/Util/CharsetUtil.php: -------------------------------------------------------------------------------- 1 | 7 | * Date: 2018/10/31 8 | * Time: 下午4:13. 9 | */ 10 | class CharsetUtil 11 | { 12 | private static $CHARSET_TO_INDEX = []; 13 | const INDEX_TO_CHARSET = [ 14 | 1 => ['big5', 'big5_chinese_ci', true], 15 | 2 => ['latin2', 'latin2_czech_cs', false], 16 | 3 => ['dec8', 'dec8_swedish_ci', true], 17 | 4 => ['cp850', 'cp850_general_ci', true], 18 | 5 => ['latin1', 'latin1_german1_ci', false], 19 | 6 => ['hp8', 'hp8_english_ci', true], 20 | 7 => ['koi8r', 'koi8r_general_ci', true], 21 | 8 => ['latin1', 'latin1_swedish_ci', true], 22 | 9 => ['latin2', 'latin2_general_ci', true], 23 | 10 => ['swe7', 'swe7_swedish_ci', true], 24 | 11 => ['ascii', 'ascii_general_ci', true], 25 | 12 => ['ujis', 'ujis_japanese_ci', true], 26 | 13 => ['sjis', 'sjis_japanese_ci', true], 27 | 14 => ['cp1251', 'cp1251_bulgarian_ci', false], 28 | 15 => ['latin1', 'latin1_danish_ci', false], 29 | 16 => ['hebrew', 'hebrew_general_ci', true], 30 | 18 => ['tis620', 'tis620_thai_ci', true], 31 | 19 => ['euckr', 'euckr_korean_ci', true], 32 | 20 => ['latin7', 'latin7_estonian_cs', false], 33 | 21 => ['latin2', 'latin2_hungarian_ci', false], 34 | 22 => ['koi8u', 'koi8u_general_ci', true], 35 | 23 => ['cp1251', 'cp1251_ukrainian_ci', false], 36 | 24 => ['gb2312', 'gb2312_chinese_ci', true], 37 | 25 => ['greek', 'greek_general_ci', true], 38 | 26 => ['cp1250', 'cp1250_general_ci', true], 39 | 27 => ['latin2', 'latin2_croatian_ci', false], 40 | 28 => ['gbk', 'gbk_chinese_ci', true], 41 | 29 => ['cp1257', 'cp1257_lithuanian_ci', false], 42 | 30 => ['latin5', 'latin5_turkish_ci', true], 43 | 31 => ['latin1', 'latin1_german2_ci', false], 44 | 32 => ['armscii8', 'armscii8_general_ci', true], 45 | 33 => ['utf8', 'utf8_general_ci', true], 46 | 34 => ['cp1250', 'cp1250_czech_cs', false], 47 | 35 => ['ucs2', 'ucs2_general_ci', true], 48 | 36 => ['cp866', 'cp866_general_ci', true], 49 | 37 => ['keybcs2', 'keybcs2_general_ci', true], 50 | 38 => ['macce', 'macce_general_ci', true], 51 | 39 => ['macroman', 'macroman_general_ci', true], 52 | 40 => ['cp852', 'cp852_general_ci', true], 53 | 41 => ['latin7', 'latin7_general_ci', true], 54 | 42 => ['latin7', 'latin7_general_cs', false], 55 | 43 => ['macce', 'macce_bin', false], 56 | 44 => ['cp1250', 'cp1250_croatian_ci', false], 57 | 45 => ['utf8mb4', 'utf8mb4_general_ci', true], 58 | 46 => ['utf8mb4', 'utf8mb4_bin', false], 59 | 47 => ['latin1', 'latin1_bin', false], 60 | 48 => ['latin1', 'latin1_general_ci', false], 61 | 49 => ['latin1', 'latin1_general_cs', false], 62 | 50 => ['cp1251', 'cp1251_bin', false], 63 | 51 => ['cp1251', 'cp1251_general_ci', true], 64 | 52 => ['cp1251', 'cp1251_general_cs', false], 65 | 53 => ['macroman', 'macroman_bin', false], 66 | 54 => ['utf16', 'utf16_general_ci', true], 67 | 55 => ['utf16', 'utf16_bin', false], 68 | 57 => ['cp1256', 'cp1256_general_ci', true], 69 | 58 => ['cp1257', 'cp1257_bin', false], 70 | 59 => ['cp1257', 'cp1257_general_ci', true], 71 | 60 => ['utf32', 'utf32_general_ci', true], 72 | 61 => ['utf32', 'utf32_bin', false], 73 | 63 => ['binary', 'binary', true], 74 | 64 => ['armscii8', 'armscii8_bin', false], 75 | 65 => ['ascii', 'ascii_bin', false], 76 | 66 => ['cp1250', 'cp1250_bin', false], 77 | 67 => ['cp1256', 'cp1256_bin', false], 78 | 68 => ['cp866', 'cp866_bin', false], 79 | 69 => ['dec8', 'dec8_bin', false], 80 | 70 => ['greek', 'greek_bin', false], 81 | 71 => ['hebrew', 'hebrew_bin', false], 82 | 72 => ['hp8', 'hp8_bin', false], 83 | 73 => ['keybcs2', 'keybcs2_bin', false], 84 | 74 => ['koi8r', 'koi8r_bin', false], 85 | 75 => ['koi8u', 'koi8u_bin', false], 86 | 77 => ['latin2', 'latin2_bin', false], 87 | 78 => ['latin5', 'latin5_bin', false], 88 | 79 => ['latin7', 'latin7_bin', false], 89 | 80 => ['cp850', 'cp850_bin', false], 90 | 81 => ['cp852', 'cp852_bin', false], 91 | 82 => ['swe7', 'swe7_bin', false], 92 | 83 => ['utf8', 'utf8_bin', false], 93 | 84 => ['big5', 'big5_bin', false], 94 | 85 => ['euckr', 'euckr_bin', false], 95 | 86 => ['gb2312', 'gb2312_bin', false], 96 | 87 => ['gbk', 'gbk_bin', false], 97 | 88 => ['sjis', 'sjis_bin', false], 98 | 89 => ['tis620', 'tis620_bin', false], 99 | 90 => ['ucs2', 'ucs2_bin', false], 100 | 91 => ['ujis', 'ujis_bin', false], 101 | 92 => ['geostd8', 'geostd8_general_ci', true], 102 | 93 => ['geostd8', 'geostd8_bin', false], 103 | 94 => ['latin1', 'latin1_spanish_ci', false], 104 | 95 => ['cp932', 'cp932_japanese_ci', true], 105 | 96 => ['cp932', 'cp932_bin', false], 106 | 97 => ['eucjpms', 'eucjpms_japanese_ci', true], 107 | 98 => ['eucjpms', 'eucjpms_bin', false], 108 | 99 => ['cp1250', 'cp1250_polish_ci', false], 109 | 101 => ['utf16', 'utf16_unicode_ci', false], 110 | 102 => ['utf16', 'utf16_icelandic_ci', false], 111 | 103 => ['utf16', 'utf16_latvian_ci', false], 112 | 104 => ['utf16', 'utf16_romanian_ci', false], 113 | 105 => ['utf16', 'utf16_slovenian_ci', false], 114 | 106 => ['utf16', 'utf16_polish_ci', false], 115 | 107 => ['utf16', 'utf16_estonian_ci', false], 116 | 108 => ['utf16', 'utf16_spanish_ci', false], 117 | 109 => ['utf16', 'utf16_swedish_ci', false], 118 | 110 => ['utf16', 'utf16_turkish_ci', false], 119 | 111 => ['utf16', 'utf16_czech_ci', false], 120 | 112 => ['utf16', 'utf16_danish_ci', false], 121 | 113 => ['utf16', 'utf16_lithuanian_ci', false], 122 | 114 => ['utf16', 'utf16_slovak_ci', false], 123 | 115 => ['utf16', 'utf16_spanish2_ci', false], 124 | 116 => ['utf16', 'utf16_roman_ci', false], 125 | 117 => ['utf16', 'utf16_persian_ci', false], 126 | 118 => ['utf16', 'utf16_esperanto_ci', false], 127 | 119 => ['utf16', 'utf16_hungarian_ci', false], 128 | 120 => ['utf16', 'utf16_sinhala_ci', false], 129 | 128 => ['ucs2', 'ucs2_unicode_ci', false], 130 | 129 => ['ucs2', 'ucs2_icelandic_ci', false], 131 | 130 => ['ucs2', 'ucs2_latvian_ci', false], 132 | 131 => ['ucs2', 'ucs2_romanian_ci', false], 133 | 132 => ['ucs2', 'ucs2_slovenian_ci', false], 134 | 133 => ['ucs2', 'ucs2_polish_ci', false], 135 | 134 => ['ucs2', 'ucs2_estonian_ci', false], 136 | 135 => ['ucs2', 'ucs2_spanish_ci', false], 137 | 136 => ['ucs2', 'ucs2_swedish_ci', false], 138 | 137 => ['ucs2', 'ucs2_turkish_ci', false], 139 | 138 => ['ucs2', 'ucs2_czech_ci', false], 140 | 139 => ['ucs2', 'ucs2_danish_ci', false], 141 | 140 => ['ucs2', 'ucs2_lithuanian_ci', false], 142 | 141 => ['ucs2', 'ucs2_slovak_ci', false], 143 | 142 => ['ucs2', 'ucs2_spanish2_ci', false], 144 | 143 => ['ucs2', 'ucs2_roman_ci', false], 145 | 144 => ['ucs2', 'ucs2_persian_ci', false], 146 | 145 => ['ucs2', 'ucs2_esperanto_ci', false], 147 | 146 => ['ucs2', 'ucs2_hungarian_ci', false], 148 | 147 => ['ucs2', 'ucs2_sinhala_ci', false], 149 | 159 => ['ucs2', 'ucs2_general_mysql500_ci', false], 150 | 160 => ['utf32', 'utf32_unicode_ci', false], 151 | 161 => ['utf32', 'utf32_icelandic_ci', false], 152 | 162 => ['utf32', 'utf32_latvian_ci', false], 153 | 163 => ['utf32', 'utf32_romanian_ci', false], 154 | 164 => ['utf32', 'utf32_slovenian_ci', false], 155 | 165 => ['utf32', 'utf32_polish_ci', false], 156 | 166 => ['utf32', 'utf32_estonian_ci', false], 157 | 167 => ['utf32', 'utf32_spanish_ci', false], 158 | 168 => ['utf32', 'utf32_swedish_ci', false], 159 | 169 => ['utf32', 'utf32_turkish_ci', false], 160 | 170 => ['utf32', 'utf32_czech_ci', false], 161 | 171 => ['utf32', 'utf32_danish_ci', false], 162 | 172 => ['utf32', 'utf32_lithuanian_ci', false], 163 | 173 => ['utf32', 'utf32_slovak_ci', false], 164 | 174 => ['utf32', 'utf32_spanish2_ci', false], 165 | 175 => ['utf32', 'utf32_roman_ci', false], 166 | 176 => ['utf32', 'utf32_persian_ci', false], 167 | 177 => ['utf32', 'utf32_esperanto_ci', false], 168 | 178 => ['utf32', 'utf32_hungarian_ci', false], 169 | 179 => ['utf32', 'utf32_sinhala_ci', false], 170 | 192 => ['utf8', 'utf8_unicode_ci', false], 171 | 193 => ['utf8', 'utf8_icelandic_ci', false], 172 | 194 => ['utf8', 'utf8_latvian_ci', false], 173 | 195 => ['utf8', 'utf8_romanian_ci', false], 174 | 196 => ['utf8', 'utf8_slovenian_ci', false], 175 | 197 => ['utf8', 'utf8_polish_ci', false], 176 | 198 => ['utf8', 'utf8_estonian_ci', false], 177 | 199 => ['utf8', 'utf8_spanish_ci', false], 178 | 200 => ['utf8', 'utf8_swedish_ci', false], 179 | 201 => ['utf8', 'utf8_turkish_ci', false], 180 | 202 => ['utf8', 'utf8_czech_ci', false], 181 | 203 => ['utf8', 'utf8_danish_ci', false], 182 | 204 => ['utf8', 'utf8_lithuanian_ci', false], 183 | 205 => ['utf8', 'utf8_slovak_ci', false], 184 | 206 => ['utf8', 'utf8_spanish2_ci', false], 185 | 207 => ['utf8', 'utf8_roman_ci', false], 186 | 208 => ['utf8', 'utf8_persian_ci', false], 187 | 209 => ['utf8', 'utf8_esperanto_ci', false], 188 | 210 => ['utf8', 'utf8_hungarian_ci', false], 189 | 211 => ['utf8', 'utf8_sinhala_ci', false], 190 | 223 => ['utf8', 'utf8_general_mysql500_ci', false], 191 | 224 => ['utf8mb4', 'utf8mb4_unicode_ci', false], 192 | 225 => ['utf8mb4', 'utf8mb4_icelandic_ci', false], 193 | 226 => ['utf8mb4', 'utf8mb4_latvian_ci', false], 194 | 227 => ['utf8mb4', 'utf8mb4_romanian_ci', false], 195 | 228 => ['utf8mb4', 'utf8mb4_slovenian_ci', false], 196 | 229 => ['utf8mb4', 'utf8mb4_polish_ci', false], 197 | 230 => ['utf8mb4', 'utf8mb4_estonian_ci', false], 198 | 231 => ['utf8mb4', 'utf8mb4_spanish_ci', false], 199 | 232 => ['utf8mb4', 'utf8mb4_swedish_ci', false], 200 | 233 => ['utf8mb4', 'utf8mb4_turkish_ci', false], 201 | 234 => ['utf8mb4', 'utf8mb4_czech_ci', false], 202 | 235 => ['utf8mb4', 'utf8mb4_danish_ci', false], 203 | 236 => ['utf8mb4', 'utf8mb4_lithuanian_ci', false], 204 | 237 => ['utf8mb4', 'utf8mb4_slovak_ci', false], 205 | 238 => ['utf8mb4', 'utf8mb4_spanish2_ci', false], 206 | 239 => ['utf8mb4', 'utf8mb4_roman_ci', false], 207 | 240 => ['utf8mb4', 'utf8mb4_persian_ci', false], 208 | 241 => ['utf8mb4', 'utf8mb4_esperanto_ci', false], 209 | 242 => ['utf8mb4', 'utf8mb4_hungarian_ci', false], 210 | 243 => ['utf8mb4', 'utf8mb4_sinhala_ci', false], 211 | 244 => ['utf8mb4', 'utf8mb4_german2_ci', false], 212 | 245 => ['utf8mb4', 'utf8mb4_croatian_ci', false], 213 | 246 => ['utf8mb4', 'utf8mb4_unicode_520_ci', false], 214 | 247 => ['utf8mb4', 'utf8mb4_vietnamese_ci', false], 215 | ]; 216 | 217 | public static function init() 218 | { 219 | // charset --> index 220 | foreach (self::INDEX_TO_CHARSET as $index => $charset) { 221 | if (!isset(self::$CHARSET_TO_INDEX[$charset[0]]) || $charset[2]) { 222 | self::$CHARSET_TO_INDEX[$charset[0]] = $index; 223 | } 224 | } 225 | 226 | self::$CHARSET_TO_INDEX['iso-8859-1'] = 14; 227 | self::$CHARSET_TO_INDEX['utf-8'] = 33; 228 | } 229 | 230 | public static function getCharset(int $index) 231 | { 232 | self::init(); 233 | return self::INDEX_TO_CHARSET[$index][0]; 234 | } 235 | 236 | public static function getIndex(string $charset) 237 | { 238 | self::init(); 239 | if (null == $charset || 0 == strlen($charset)) { 240 | return 0; 241 | } else { 242 | $i = self::$CHARSET_TO_INDEX[strtolower($charset)]; 243 | 244 | return (null == $i) ? 0 : $i; 245 | } 246 | } 247 | 248 | public static function charsetToEncoding($charset) 249 | { 250 | if ($charset == 'utf8mb4') { 251 | return 'utf-8'; 252 | } elseif ($charset == 'utf8') { 253 | return 'utf-8'; 254 | } 255 | return $charset; 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/MysqlPacket/Util/RandomUtil.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 2018/11/9 5 | * Time: 上午9:41. 6 | */ 7 | 8 | namespace SMProxy\MysqlPacket\Util; 9 | 10 | use function SMProxy\Helper\shr16; 11 | 12 | /** 13 | * 随机数类. 14 | * 15 | * Class RandomUtil 16 | */ 17 | class RandomUtil 18 | { 19 | private static $bytes = [ 20 | '1', 21 | '2', 22 | '3', 23 | '4', 24 | '5', 25 | '6', 26 | '7', 27 | '8', 28 | '9', 29 | '0', 30 | 'q', 31 | 'w', 32 | 'e', 33 | 'r', 34 | 't', 35 | 'y', 'u', 'i', 'o', 'p', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'z', 'x', 'c', 'v', 'b', 'n', 'm', 36 | 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'Z', 'X', 37 | 'C', 'V', 'B', 'N', 'M', 38 | ]; 39 | private static $multiplier = 0x5DEECE66D; 40 | private static $addend = 0xB; 41 | private static $mask = (1 << 48) - 1; 42 | private static $integerMask = (1 << 33) - 1; 43 | private static $seedUniquifier = 8682522807148012; 44 | 45 | private static $seed; 46 | 47 | public function __construct() 48 | { 49 | $s = self::$seedUniquifier + system('date +%s%N'); 50 | $s = ($s ^ self::$multiplier) & self::$mask; 51 | $this->seed = $s; 52 | } 53 | 54 | public static function randomBytes(int $size) 55 | { 56 | $bb = self::$bytes; 57 | $ab = new \SplFixedArray($size); 58 | for ($i = 0; $i < $size; ++$i) { 59 | $ab[$i] = array_rand($bb); 60 | } 61 | 62 | return $ab->toArray(); 63 | } 64 | 65 | private static function randomByte(array $b) 66 | { 67 | $ran = (int) (shr16((self::next() & self::$integerMask) & 0xff << 16, 16)); 68 | 69 | return $b[$ran % count($b)]; 70 | } 71 | 72 | private static function next() 73 | { 74 | $oldSeed = self::$seed; 75 | $nextSeed = 0; 76 | do { 77 | $nextSeed = ($oldSeed * self::$multiplier + self::$addend) & self::$mask; 78 | } while ($oldSeed == $nextSeed); 79 | self::$seed = $nextSeed; 80 | 81 | return $nextSeed; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/MysqlPacket/Util/SecurityUtil.php: -------------------------------------------------------------------------------- 1 | 10 | * Date: 2018/10/31 11 | * Time: 下午2:18. 12 | */ 13 | class SecurityUtil 14 | { 15 | public static function scramble411(string $pass, array $seed) 16 | { 17 | $pass1 = getBytes(sha1($pass, true)); 18 | $pass2 = getBytes(sha1(getString($pass1), true)); 19 | $pass3 = getBytes(sha1(getString($seed) . getString($pass2), true)); 20 | for ($i = 0, $count = count($pass3); $i < $count; ++$i) { 21 | $pass3[$i] = ($pass3[$i] ^ $pass1[$i]); 22 | } 23 | 24 | return $pass3; 25 | } 26 | 27 | public static function scrambleSha256(string $pass, array $seed) 28 | { 29 | $pass1 = getBytes(hash('sha256', $pass, true)); 30 | $pass2 = getBytes(hash('sha256', getString($pass1), true)); 31 | $pass3 = getBytes(hash('sha256', getString($pass2) . getString($seed), true)); 32 | for ($i = 0, $count = count($pass3); $i < $count; ++$i) { 33 | $pass1[$i] ^= $pass3[$i]; 34 | } 35 | return $pass1; 36 | } 37 | 38 | private static function xorPassword($password, $salt) 39 | { 40 | $password_bytes = getBytes($password); 41 | $password_bytes[] = 0; 42 | $salt_len = count($salt); 43 | for ($i = 0, $count = count($password_bytes); $i < $count; ++$i) { 44 | $password_bytes[$i] ^= $salt[$i % $salt_len]; 45 | } 46 | return getString($password_bytes); 47 | } 48 | 49 | 50 | public static function sha2RsaEncrypt($password, $salt, $publicKey) 51 | { 52 | /*Encrypt password with salt and public_key. 53 | 54 | Used for sha256_password and caching_sha2_password. 55 | */ 56 | $message = self::xorPassword($password, $salt); 57 | openssl_public_encrypt($message, $encrypted, $publicKey, OPENSSL_PKCS1_OAEP_PADDING); 58 | return $encrypted; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/MysqlPacket/Util/Versions.php: -------------------------------------------------------------------------------- 1 | 4 | * Date: 2018/11/9 5 | * Time: 上午10:11. 6 | */ 7 | 8 | namespace SMProxy\MysqlPacket\Util; 9 | 10 | interface Versions 11 | { 12 | /** 协议版本 */ 13 | const PROTOCOL_VERSION = 10; 14 | 15 | /** 服务器版本 */ 16 | const SERVER_VERSION = '5.6.0-SMProxy'; 17 | } 18 | -------------------------------------------------------------------------------- /src/MysqlPool/MySQLException.php: -------------------------------------------------------------------------------- 1 | 7 | * Date: 2018/11/6 8 | * Time: 上午10:53. 9 | */ 10 | class MySQLException extends \Exception 11 | { 12 | public function errorMessage() 13 | { 14 | return sprintf('%s (%s:%s)', trim($this->getMessage()), $this->getFile(), $this->getLine()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/MysqlProxy.php: -------------------------------------------------------------------------------- 1 | 25 | * Date: 2018/10/26 26 | * Time: 下午5:51. 27 | */ 28 | class MysqlProxy extends MysqlClient 29 | { 30 | private $isDuplex; 31 | public $server; 32 | public $serverFd; 33 | public $charset; 34 | public $account; 35 | public $auth = false; 36 | public $chan; 37 | public $serverPublicKey; 38 | public $salt; 39 | public $connected = false; 40 | public $timeout = 0.1; 41 | public $mysqlClient; 42 | public $mysqlServer; 43 | 44 | /** 45 | * MysqlClient constructor. 46 | * 47 | */ 48 | public function __construct(\swoole_server $server, int $fd, \Swoole\Coroutine\Channel $chan) 49 | { 50 | $this->server = $server; 51 | $this->serverFd = $fd; 52 | $this->chan = $chan; 53 | $this->client = new Client(CONFIG['server']['swoole_client_sock_setting']['sock_type'] ?? SWOOLE_SOCK_TCP); 54 | $this->client->set(CONFIG['server']['swoole_client_setting'] ?? []); 55 | $this->client->set(packageLengthSetting()); 56 | $this->isDuplex = version_compare(SWOOLE_VERSION, '4.2.13', '>='); 57 | if (!$this->isDuplex) { 58 | $this->mysqlClient = new Channel(1); 59 | } 60 | } 61 | 62 | /** 63 | * connect. 64 | * 65 | * @param string $host 66 | * @param int $port 67 | * @param float $timeout 68 | * @param int $tryStep 69 | * 70 | * @return bool|Client 71 | */ 72 | public function connect(string $host, int $port, float $timeout = 0.1, int $tryStep = 0) 73 | { 74 | $this->timeout = $timeout; 75 | if (!$this->client->connect($host, $port, $timeout)) { 76 | if ($tryStep < 3) { 77 | $this->client->close(); 78 | return $this->connect($host, $port, $timeout, ++$tryStep); 79 | } else { 80 | $this->onClientError($this->client); 81 | return false; 82 | } 83 | } else { 84 | if (!$this->isDuplex) { 85 | $this->mysqlClient->push($this->client); 86 | } 87 | self::go(function () { 88 | $remain = ''; 89 | while (true) { 90 | $data = $this->recv($remain); 91 | if ($data === '' || $data === false) { 92 | break; 93 | } 94 | } 95 | }); 96 | return $this->client; 97 | } 98 | } 99 | 100 | /** 101 | * mysql 客户端消息转发. 102 | * 103 | * @param $cli 104 | * @param $data 105 | */ 106 | public function onClientReceive(\Swoole\Coroutine\Client $cli, string $data) 107 | { 108 | self::go(function () use ($cli, $data) { 109 | $fd = $this->serverFd; 110 | $binaryPacket = new BinaryPacket(); 111 | $binaryPacket->data = getBytes($data); 112 | $binaryPacket->packetLength = $binaryPacket->calcPacketSize(); 113 | if (isset($binaryPacket->data[4])) { 114 | $send = true; 115 | //ERROR Packet 116 | if ($binaryPacket->data[4] == ErrorPacket::$FIELD_COUNT) { 117 | $errorPacket = new ErrorPacket(); 118 | $errorPacket->read($binaryPacket); 119 | //$errorPacket->errno = ErrorCode::ER_SYNTAX_ERROR; 120 | $data = getString($errorPacket->write()); 121 | } elseif (!$this->connected) { 122 | //OK Packet 123 | if ($binaryPacket->data[4] == OkPacket::$FIELD_COUNT) { 124 | $send = false; 125 | $this->connected = true; 126 | $this->chan->push($this); 127 | # 快速认证 128 | } elseif ($binaryPacket->data[4] == 0x01) { 129 | # 请求公钥 130 | if ($binaryPacket->packetLength == 6) { 131 | if ($binaryPacket->data[$binaryPacket->packetLength - 1] == 4) { 132 | $data = getString(array_merge(getMysqlPackSize(1), [3, 2])); 133 | $this->send($data); 134 | } 135 | } else { 136 | $this->serverPublicKey = substr($data, 5, strlen($data) - 2); 137 | $encryptData = SecurityUtil::sha2RsaEncrypt($this->account['password'], $this->salt, $this->serverPublicKey); 138 | $data = getString(array_merge(getMysqlPackSize(strlen($encryptData)), [5])) . $encryptData; 139 | $this->send($data); 140 | } 141 | $send = false; 142 | //EOF Packet 143 | } elseif ($binaryPacket->data[4] == 0xfe) { 144 | $mm = new MySQLMessage($binaryPacket->data); 145 | $mm->move(5); 146 | $pluginName = $mm->readStringWithNull(); 147 | $this->salt = $mm->readBytesWithNull(); 148 | $password = $this->processAuth($pluginName ?: 'mysql_native_password'); 149 | $this->send(getString(array_merge(getMysqlPackSize(count($password)), [3], $password))); 150 | $send = false; 151 | //未授权 152 | } elseif (!$this->auth) { 153 | $handshakePacket = (new HandshakePacket())->read($binaryPacket); 154 | $this->mysqlServer = $handshakePacket; 155 | $this->salt = array_merge($handshakePacket->seed, $handshakePacket->restOfScrambleBuff); 156 | $password = $this->processAuth($handshakePacket->pluginName); 157 | $clientFlag = Capabilities::CLIENT_CAPABILITIES; 158 | $authPacket = new AuthPacket(); 159 | $authPacket->pluginName = $handshakePacket->pluginName; 160 | $authPacket->packetId = 1; 161 | if (isset($this->database) && $this->database) { 162 | $authPacket->database = $this->database; 163 | } else { 164 | $authPacket->database = 0; 165 | } 166 | if ($authPacket->database) { 167 | $clientFlag |= Capabilities::CLIENT_CONNECT_WITH_DB; 168 | } 169 | if (version_compare($handshakePacket->serverVersion, '5.0', '>=')) { 170 | $clientFlag |= Capabilities::CLIENT_MULTI_RESULTS; 171 | } 172 | $authPacket->clientFlags = $clientFlag; 173 | $authPacket->serverCapabilities = $handshakePacket->serverCapabilities; 174 | $authPacket->maxPacketSize = 175 | CONFIG['server']['swoole_client_setting']['package_max_length'] ?? 16777215; 176 | $authPacket->charsetIndex = CharsetUtil::getIndex($this->charset ?? 'utf8mb4'); 177 | $authPacket->user = $this->account['user']; 178 | $authPacket->password = $password; 179 | $this->auth = true; 180 | $this->send(getString($authPacket->write())); 181 | $send = false; 182 | } 183 | } 184 | if ($send && $this->server->exist($fd)) { 185 | $this->server->send($fd, $data); 186 | } 187 | } 188 | }); 189 | } 190 | 191 | /** 192 | * 认证 193 | * 194 | * @param string $pluginName 195 | * 196 | * @return array 197 | * @throws MySQLException 198 | */ 199 | public function processAuth(string $pluginName) 200 | { 201 | switch ($pluginName) { 202 | case 'mysql_native_password': 203 | $password = SecurityUtil::scramble411($this->account['password'], $this->salt); 204 | break; 205 | case 'caching_sha2_password': 206 | $password = SecurityUtil::scrambleSha256($this->account['password'], $this->salt); 207 | break; 208 | case 'sha256_password': 209 | throw new MySQLException('Sha256_password plugin is not supported yet'); 210 | break; 211 | case 'mysql_old_password': 212 | throw new MySQLException('mysql_old_password plugin is not supported yet'); 213 | break; 214 | case 'mysql_clear_password': 215 | $password = array_merge(getBytes($this->account['password']), [0]); 216 | break; 217 | default: 218 | $password = SecurityUtil::scramble411($this->account['password'], $this->salt); 219 | break; 220 | } 221 | return $password; 222 | } 223 | 224 | /** 225 | * send. 226 | * 227 | * @param mixed ...$data 228 | * 229 | * @return bool 230 | */ 231 | public function send(...$data) 232 | { 233 | if ($this->isDuplex) { 234 | if ($this->client->isConnected()) { 235 | return $this->client->send(...$data); 236 | } else { 237 | return false; 238 | } 239 | } else { 240 | $client = self::coPop($this->mysqlClient); 241 | if ($client === false) { 242 | //连接关闭或超时 243 | return false; 244 | } 245 | if ($client->isConnected()) { 246 | $result = $client->send(...$data); 247 | $this->mysqlClient->push($client); 248 | return $result; 249 | } 250 | return false; 251 | } 252 | } 253 | 254 | /** 255 | * recv. 256 | * 257 | * @return mixed 258 | */ 259 | public function recv(&$remain) 260 | { 261 | if ($this->isDuplex) { 262 | $client = $this->client; 263 | $data = $client->recv(-1); 264 | } else { 265 | $client = self::coPop($this->mysqlClient, $this->timeout); 266 | if ($client === false) { 267 | //连接关闭或超时 268 | return false; 269 | } 270 | if ($client->isConnected()) { 271 | $data = $client->recv($this->timeout / 500); 272 | } else { 273 | $data = ''; 274 | } 275 | $this->mysqlClient->push($client); 276 | if ($data === false && $client->errCode == 110) { 277 | //如果连接超时则不处理 278 | $data = true; 279 | } 280 | if ($data === '' || $data === false) { 281 | $this->mysqlClient->close(); 282 | } 283 | } 284 | if ($data === '' || $data === false) { 285 | $this->onClientClose($client); 286 | } elseif (is_string($data)) { 287 | $this->onClientReceive($client, $data); 288 | } 289 | return $data; 290 | } 291 | 292 | /** 293 | * close. 294 | * 295 | * @param Client $cli 296 | */ 297 | public function onClientClose(\Swoole\Coroutine\Client $cli) 298 | { 299 | MySQLPool::destruct($cli, $this->connName); 300 | } 301 | 302 | public function onClientError(\Swoole\Coroutine\Client $cli) 303 | { 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /src/Parser/Util/CharTypes.php: -------------------------------------------------------------------------------- 1 | 7 | * Date: 2018/11/3 8 | * Time: 上午9:48. 9 | */ 10 | class CharTypes 11 | { 12 | public static function isIdentifierChar(int $c2) 13 | { 14 | $identifierFlags = new \SplFixedArray(256); 15 | for ($c = 0; $c < count($identifierFlags); ++$c) { 16 | if ($c >= 'A' && $c <= 'Z') { 17 | $identifierFlags[$c] = true; 18 | } elseif ($c >= 'a' && $c <= 'z') { 19 | $identifierFlags[$c] = true; 20 | } elseif ($c >= '0' && $c <= '9') { 21 | $identifierFlags[$c] = true; 22 | } 23 | } 24 | // identifierFlags['`'] = true; 25 | $identifierFlags['_'] = true; 26 | $identifierFlags['$'] = true; 27 | 28 | return $c2 > count($identifierFlags) || $identifierFlags[$c2]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Parser/Util/ParseUtil.php: -------------------------------------------------------------------------------- 1 | 7 | * Date: 2018/11/3 8 | * Time: 上午9:30. 9 | */ 10 | final class ParseUtil 11 | { 12 | public static function isEOF($c) 13 | { 14 | return ' ' == $c || "\t" == $c || "\n" == $c || "\r" == $c || ';' == $c; 15 | } 16 | 17 | public static function getSQLId(string $stmt) 18 | { 19 | $offset = strpos($stmt, '='); 20 | if (-1 != $offset && strlen($stmt) > ++$offset) { 21 | $id = trim(substr($stmt, $offset)); 22 | 23 | return $id; 24 | } 25 | 26 | return 0; 27 | } 28 | 29 | /** 30 | * 'abc'. 31 | * 32 | * @param string $stmt 33 | * @param int offset stmt.charAt(offset) == first ' 34 | * 35 | * @return string 36 | */ 37 | private static function parsestring(string $stmt, int $offset) 38 | { 39 | $sb = ''; 40 | $stmtLen = strlen($stmt); 41 | for (++$offset; $offset < $stmtLen; ++$offset) { 42 | $c = $stmt[$offset]; 43 | if ('\\' == $c) { 44 | switch ($c = $stmt[++$offset]) { 45 | case '0': 46 | $sb .= "\0"; 47 | break; 48 | case 'b': 49 | $sb .= "\b"; 50 | break; 51 | case 'n': 52 | $sb .= "\n"; 53 | break; 54 | case 'r': 55 | $sb .= "\r"; 56 | break; 57 | case 't': 58 | $sb .= "\t"; 59 | break; 60 | case 'Z': 61 | $sb .= chr(26); 62 | break; 63 | default: 64 | $sb .= $c; 65 | } 66 | } elseif ('\'' == $c) { 67 | if ($offset + 1 < strlen($stmt) && '\'' == $stmt[$offset + 1]) { 68 | ++$offset; 69 | $sb .= '\''; 70 | } else { 71 | break; 72 | } 73 | } else { 74 | $sb .= $c; 75 | } 76 | } 77 | 78 | return $sb; 79 | } 80 | 81 | /** 82 | * "abc". 83 | * 84 | * @param string $stmt 85 | * @param int offset stmt.charAt(offset) == first " 86 | * 87 | * @return string 88 | */ 89 | private static function parsestring2(string $stmt, int $offset) 90 | { 91 | $sb = ''; 92 | $stmtLen = strlen($stmt); 93 | for (++$offset; $offset < $stmtLen; ++$offset) { 94 | $c = $stmt = [$offset]; 95 | if ('\\' == $c) { 96 | switch ($c = $stmt[++$offset]) { 97 | case '0': 98 | $sb .= "\0"; 99 | break; 100 | case 'b': 101 | $sb .= "\b"; 102 | break; 103 | case 'n': 104 | $sb .= "\n"; 105 | break; 106 | case 'r': 107 | $sb .= "\r"; 108 | break; 109 | case 't': 110 | $sb .= "\t"; 111 | break; 112 | case 'Z': 113 | $sb .= chr(26); 114 | break; 115 | default: 116 | $sb .= $c; 117 | } 118 | } elseif ('"' == $c) { 119 | if ($offset + 1 < strlen($stmt) && '"' == $stmt[$offset + 1]) { 120 | ++$offset; 121 | $sb .= '"'; 122 | } else { 123 | break; 124 | } 125 | } else { 126 | $sb .= $c; 127 | } 128 | } 129 | 130 | return $sb; 131 | } 132 | 133 | /** 134 | * AS `abc`. 135 | * 136 | * @param string $stmt 137 | * @param int $offset 138 | * 139 | * @return string 140 | */ 141 | private static function parseIdentifierEscape(string $stmt, int $offset) 142 | { 143 | $sb = ''; 144 | for (++$offset,$stemLen = strlen($stmt); $offset < $stemLen; ++$offset) { 145 | $c = $stmt[$offset]; 146 | if ('`' == $c) { 147 | if ($offset + 1 < strlen($stmt) && '`' == $stmt[$offset + 1]) { 148 | ++$offset; 149 | $sb .= '`'; 150 | } else { 151 | break; 152 | } 153 | } else { 154 | $sb .= $c; 155 | } 156 | } 157 | 158 | return $sb; 159 | } 160 | 161 | /** 162 | * @param string $stmt 163 | * @param int $aliasIndex 164 | * @return bool|null|string 165 | */ 166 | public static function parseAlias(string $stmt, int $aliasIndex) 167 | { 168 | if ($aliasIndex < 0 || $aliasIndex >= strlen($stmt)) { 169 | return null; 170 | } 171 | switch ($stmt[$aliasIndex]) { 172 | case '\'': 173 | return self::parsestring($stmt, $aliasIndex); 174 | case '"': 175 | return self::parsestring2($stmt, $aliasIndex); 176 | case '`': 177 | return self::parseIdentifierEscape($stmt, $aliasIndex); 178 | default: 179 | $offset = $aliasIndex; 180 | $stmtLen = strlen($stmt); 181 | while ($offset < $stmtLen && CharTypes::isIdentifierChar($stmt[$offset])) { 182 | $offset++; 183 | } 184 | 185 | return substr($stmt, $aliasIndex, $offset); 186 | } 187 | } 188 | 189 | public static function comment(string $stmt, int $offset) 190 | { 191 | $len = strlen($stmt); 192 | $n = $offset; 193 | switch ($stmt[$n]) { 194 | case '/': 195 | if ($len > ++$n && '*' == $stmt[$n++] && $len > $n + 1 && '!' != $stmt[$n]) { 196 | for ($i = $n; $i < $len; ++$i) { 197 | if ('*' == $stmt[$i]) { 198 | $m = $i + 1; 199 | if ($len > $m && '/' == $stmt[$m]) { 200 | return $m; 201 | } 202 | } 203 | } 204 | } 205 | break; 206 | case '#': 207 | for ($i = $n + 1; $i < $len; ++$i) { 208 | if ("\n" == $stmt[$i]) { 209 | return $i; 210 | } 211 | } 212 | break; 213 | } 214 | 215 | return $offset; 216 | } 217 | 218 | public static function currentCharIsSep(string $stmt, int $offset) 219 | { 220 | if (strlen($stmt) > $offset) { 221 | switch ($stmt[$offset]) { 222 | case ' ': 223 | case "\t": 224 | case "\r": 225 | case "\n": 226 | return true; 227 | default: 228 | return false; 229 | } 230 | } 231 | 232 | return true; 233 | } 234 | 235 | /***** 236 | * 检查下一个字符是否为分隔符,并把偏移量加1 237 | * @param string $stmt 238 | * @param int $offset 239 | * @return bool 240 | */ 241 | public static function nextCharIsSep(string $stmt, int $offset) 242 | { 243 | return self::currentCharIsSep($stmt, ++$offset); 244 | } 245 | 246 | /***** 247 | * 检查下一个字符串是否为期望的字符串,并把偏移量移到从offset开始计算,expectValue之后的位置 248 | * 249 | * @param string $stmt 被解析的sql 250 | * @param int $offset 被解析的sql的当前位置 251 | * @param string $nextExpectedstring 在stmt中准备查找的字符串 252 | * @param bool $checkSepChar 当找到expectValue值时,是否检查其后面字符为分隔符号 253 | * 254 | * @return int 如果包含指定的字符串,则移动相应的偏移量,否则返回值=offset 255 | */ 256 | public static function nextstringIsExpectedWithIgnoreSepChar( 257 | string $stmt, 258 | int $offset, 259 | string $nextExpectedstring, 260 | bool $checkSepChar 261 | ) { 262 | if (null == $nextExpectedstring || strlen($nextExpectedstring) < 1) { 263 | return $offset; 264 | } 265 | $i = $offset; 266 | $index = 0; 267 | for ($stmtLen = strlen($stmt); $i < $stmtLen && $index < strlen($nextExpectedstring); ++$i) { 268 | if (0 == $index) { 269 | $isSep = self::currentCharIsSep($stmt, $i); 270 | if ($isSep) { 271 | continue; 272 | } 273 | } 274 | $actualChar = $stmt[$i]; 275 | $expectedChar = $nextExpectedstring[$index++]; 276 | if ($actualChar != $expectedChar) { 277 | return $offset; 278 | } 279 | } 280 | if ($index == strlen($nextExpectedstring)) { 281 | $ok = true; 282 | if ($checkSepChar) { 283 | $ok = self::nextCharIsSep($stmt, $i); 284 | } 285 | if ($ok) { 286 | return $i; 287 | } 288 | } 289 | 290 | return $offset; 291 | } 292 | 293 | const JSON = 'json'; 294 | const EQ = '='; 295 | 296 | //private static final string WHERE = "where"; 297 | //private static final string SET = "set"; 298 | 299 | /********** 300 | * 检查下一个字符串是否json= * 301 | * 302 | * @param string $stmt 被解析的sql 303 | * @param int $offset 304 | * @return int 如果包含指定的字符串,则移动相应的偏移量,否则返回值=offset 305 | */ 306 | public static function nextstringIsJsonEq(string $stmt, int $offset) 307 | { 308 | $i = $offset; 309 | 310 | // / drds 之后的符号 311 | if (!self::currentCharIsSep($stmt, ++$i)) { 312 | return $offset; 313 | } 314 | 315 | // json 串 316 | $k = self::nextstringIsExpectedWithIgnoreSepChar($stmt, $i, self::JSON, false); 317 | if ($k <= $i) { 318 | return $offset; 319 | } 320 | $i = $k; 321 | 322 | // 等于符号 323 | $k = self::nextstringIsExpectedWithIgnoreSepChar($stmt, $i, self::EQ, false); 324 | if ($k <= $i) { 325 | return $offset; 326 | } 327 | 328 | return $i; 329 | } 330 | 331 | public static function move(string $stmt, int $offset, int $length) 332 | { 333 | $stmtLen = strlen($stmt); 334 | for ($i = $offset; $i < $stmtLen; ++$i) { 335 | switch ($stmt[$i]) { 336 | case ' ': 337 | case "\t": 338 | case "\r": 339 | case "\n": 340 | continue 2; 341 | case '/': 342 | case '#': 343 | $i = self::comment($stmt, $i); 344 | continue 2; 345 | default: 346 | return $i + $length; 347 | } 348 | } 349 | 350 | return $i; 351 | } 352 | 353 | public static function compare(string $s, int $offset, $keyword) 354 | { 355 | if (strlen($s) >= $offset + count($keyword)) { 356 | for ($i = 0; $i < count($keyword); ++$i, ++$offset) { 357 | if (strtoupper($s[$offset]) != $keyword[$i]) { 358 | return false; 359 | } 360 | } 361 | 362 | return true; 363 | } 364 | 365 | return false; 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /src/Route/RouteService.php: -------------------------------------------------------------------------------- 1 | 7 | * Date: 2018/12/10 8 | * Time: 下午4:11 9 | */ 10 | class RouteService 11 | { 12 | const HINT_SPLIT = '='; 13 | const SMPROXY_HINT_TYPE = "_smproxyHintType"; 14 | 15 | public static function route(string $stmt) 16 | { 17 | $hintLength = self::isHintSql($stmt); 18 | if ($hintLength != -1) { 19 | $endPos = strpos($stmt, "*/"); 20 | if ($endPos > 0) { 21 | $hint = trim(substr($stmt, $hintLength, $endPos - $hintLength)); 22 | $firstSplitPos = strpos($hint, self::HINT_SPLIT); 23 | if ($firstSplitPos > 0) { 24 | $hintArr = self::parseHint($hint); 25 | return $hintArr; 26 | } 27 | } 28 | } 29 | return []; 30 | } 31 | 32 | public static function isHintSql(string $sql) 33 | { 34 | $j = 0; 35 | $len = strlen($sql); 36 | if ($sql[$j++] == '/' && $sql[$j++] == '*') { 37 | $c = $sql[$j]; 38 | // 过滤掉 空格 和 * 两种字符, 支持: "/** !smproxy: */" 和 "/** #smproxy: */" 形式的注解 39 | while ($j < $len && $c != '!' && $c != '#' && ($c == ' ' || $c == '*')) { 40 | $c = $sql[++$j]; 41 | } 42 | if ($sql[$j] == 's') { 43 | $j--; 44 | } 45 | if ($j + 6 >= $len) { 46 | return -1; 47 | } 48 | if ($sql[++$j] == 's' && $sql[++$j] == 'm' && $sql[++$j] == 'p' 49 | && $sql[++$j] == 'r' && $sql[++$j] == 'o' && $sql[++$j] == 'x' && $sql[++$j] == 'y' && ($sql[++$j] == ':' || $sql[++$j] == '#')) { 50 | return $j + 1; // true,同时返回注解部分的长度 51 | } 52 | } 53 | return -1; // false 54 | } 55 | 56 | private static function parseHint(string $sql) 57 | { 58 | $arr = []; 59 | $y = 0; 60 | $begin = 0; 61 | for ($i = 0; $i < strlen($sql); $i++) { 62 | $cur = $sql[$i]; 63 | if ($cur == ',' && $y % 2 == 0) { 64 | $substring = substr($sql, $begin, $i); 65 | 66 | self::parseKeyValue($arr, $substring); 67 | $begin = $i + 1; 68 | } elseif ($cur == '\'') { 69 | $y++; 70 | } 71 | if ($i == strlen($sql) - 1) { 72 | self::parseKeyValue($arr, substr($sql, $begin)); 73 | } 74 | } 75 | return $arr; 76 | } 77 | 78 | private static function parseKeyValue(array &$arr, string $substring) 79 | { 80 | $indexOf = strpos($substring, '='); 81 | if ($indexOf != -1) { 82 | $key = strtolower(trim(substr($substring, 0, $indexOf))); 83 | $value = substr($substring, $indexOf + 1, strlen($substring)); 84 | if (\SMProxy\Helper\endsWith($value, "'") && \SMProxy\Helper\startsWith($value, "'")) { 85 | $value = substr($value, 1, strlen($value) - 1); 86 | } 87 | if ($value == '') { 88 | $arr[self::SMPROXY_HINT_TYPE] = $key; 89 | } 90 | $arr[$key] = trim($value); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/SMProxyException.php: -------------------------------------------------------------------------------- 1 | 7 | * Date: 2018/10/26 8 | * Time: 下午5:56. 9 | */ 10 | class SMProxyException extends \Exception 11 | { 12 | public function errorMessage() 13 | { 14 | return sprintf('%s (%s:%s)', trim($this->getMessage()), $this->getFile(), $this->getLine()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/conf/database.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "account": { 4 | "root": { 5 | "user": "root", 6 | "password": "123456" 7 | } 8 | }, 9 | "serverInfo": { 10 | "server1": { 11 | "write": { 12 | "host": ["127.0.0.1"], 13 | "port": 3306, 14 | "timeout": 0.5, 15 | "account": "root" 16 | }, 17 | "read": { 18 | "host": ["127.0.0.1"], 19 | "port": 3306, 20 | "timeout": 0.5, 21 | "account": "root" 22 | } 23 | } 24 | }, 25 | "databases": { 26 | "mysql": { 27 | "serverInfo": "server1", 28 | "startConns": "swoole_cpu_num()*10", 29 | "maxSpareConns": "swoole_cpu_num()*10", 30 | "maxSpareExp": 3600, 31 | "maxConns": "swoole_cpu_num()*20", 32 | "charset": "utf-8" 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/conf/server.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "user": "root", 4 | "password": "123456", 5 | "charset": "utf8mb4", 6 | "host": "0.0.0.0", 7 | "port": "3366", 8 | "mode": "SWOOLE_PROCESS", 9 | "sock_type": "SWOOLE_SOCK_TCP", 10 | "logs": { 11 | "open":true, 12 | "config": { 13 | "system": { 14 | "log_path": "ROOT/logs", 15 | "log_file": "system.log", 16 | "format": "Y/m/d" 17 | }, 18 | "mysql": { 19 | "log_path": "ROOT/logs", 20 | "log_file": "mysql.log", 21 | "format": "Y/m/d" 22 | } 23 | } 24 | }, 25 | "swoole": { 26 | "worker_num": "swoole_cpu_num()", 27 | "max_coro_num": 6000, 28 | "open_tcp_nodelay": true, 29 | "daemonize": true, 30 | "heartbeat_check_interval": 60, 31 | "heartbeat_idle_time": 600, 32 | "reload_async": true, 33 | "log_file": "ROOT/logs/swoole.log", 34 | "pid_file": "ROOT/logs/pid/server.pid" 35 | }, 36 | "swoole_client_setting": { 37 | "package_max_length": 16777216 38 | }, 39 | "swoole_client_sock_setting": { 40 | "sock_type": "SWOOLE_SOCK_TCP" 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /tests/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | EXEC="`dirname $0`/../bin/SMProxy" 4 | CONFIG="`dirname $0`/../tests/conf" 5 | 6 | $EXEC start -c $CONFIG -------------------------------------------------------------------------------- /tests/test.php: -------------------------------------------------------------------------------- 1 | query($sql); 20 | fwrite(STDOUT, 'Executed query:' . $sql . PHP_EOL); 21 | if ($result->rowCount()) { 22 | fwrite(STDOUT, 'Result: ' . json_encode($result->fetch()) . PHP_EOL); 23 | } else { 24 | fwrite(STDERR, "Result empty!" . PHP_EOL); 25 | } 26 | unset($conn); 27 | } catch (\Exception $exception) { 28 | fwrite(STDERR, $exception ->getMessage()); 29 | } 30 | ?> 31 | --------------------------------------------------------------------------------