Merge branch 'release/5.0.2.1'
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
composer.lock
|
||||
vendor/
|
||||
.idea/
|
||||
201
LICENSE
Normal file
201
LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright {yyyy} {name of copyright owner}
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
139
README.md
139
README.md
@@ -1,2 +1,139 @@
|
||||
# think-worker
|
||||
ThinkPHP Workerman 扩展
|
||||
===============
|
||||
|
||||
交流群:981069000 [](https://qm.qq.com/q/A8YNpzrzC8)
|
||||
|
||||
## 安装
|
||||
```
|
||||
composer require topthink/think-worker
|
||||
```
|
||||
|
||||
## 说明
|
||||
> 由于windows下无法在一个文件里启动多个worker,所以本扩展不支持windows平台
|
||||
|
||||
## 使用方法
|
||||
|
||||
### HttpServer
|
||||
|
||||
在命令行启动服务端
|
||||
~~~
|
||||
php think worker
|
||||
~~~
|
||||
|
||||
然后就可以通过浏览器直接访问当前应用
|
||||
|
||||
~~~
|
||||
http://localhost:8080
|
||||
~~~
|
||||
|
||||
如果需要使用守护进程方式运行,建议使用supervisor来管理进程
|
||||
|
||||
## 访问静态文件
|
||||
> 建议使用nginx来支持静态文件访问,也可使用路由输出文件内容,下面是示例,可参照修改
|
||||
1. 添加静态文件路由:
|
||||
|
||||
```php
|
||||
Route::get('static/:path', function (string $path) {
|
||||
$filename = public_path() . $path;
|
||||
return new \think\worker\response\File($filename);
|
||||
})->pattern(['path' => '.*\.\w+$']);
|
||||
```
|
||||
|
||||
2. 访问路由 `http://localhost/static/文件路径`
|
||||
|
||||
## 队列支持
|
||||
|
||||
使用方法见 [think-queue](https://github.com/top-think/think-queue)
|
||||
|
||||
以下配置代替think-queue里的最后一步:`监听任务并执行`,无需另外起进程执行队列
|
||||
|
||||
```php
|
||||
return [
|
||||
// ...
|
||||
'queue' => [
|
||||
'enable' => true,
|
||||
//键名是队列名称
|
||||
'workers' => [
|
||||
//下面参数是不设置时的默认配置
|
||||
'default' => [
|
||||
'delay' => 0,
|
||||
'sleep' => 3,
|
||||
'tries' => 0,
|
||||
'timeout' => 60,
|
||||
'worker_num' => 1,
|
||||
],
|
||||
//使用@符号后面可指定队列使用驱动
|
||||
'default@connection' => [
|
||||
//此处可不设置任何参数,使用上面的默认配置
|
||||
],
|
||||
],
|
||||
],
|
||||
// ...
|
||||
];
|
||||
|
||||
```
|
||||
|
||||
### websocket
|
||||
|
||||
> 使用路由调度的方式,可以让不同路径的websocket服务响应不同的事件
|
||||
|
||||
#### 配置
|
||||
|
||||
```
|
||||
worker.websocket = true 时开启
|
||||
```
|
||||
|
||||
#### 路由定义
|
||||
```php
|
||||
Route::get('path1','controller/action1');
|
||||
Route::get('path2','controller/action2');
|
||||
```
|
||||
|
||||
#### 控制器
|
||||
|
||||
```php
|
||||
use \think\worker\Websocket;
|
||||
use \think\worker\websocket\Frame;
|
||||
|
||||
class Controller {
|
||||
|
||||
public function action1(){
|
||||
|
||||
return (new \think\worker\response\Websocket())
|
||||
->onOpen(...)
|
||||
->onMessage(function(Websocket $websocket, Frame $frame){
|
||||
...
|
||||
})
|
||||
->onClose(...);
|
||||
}
|
||||
|
||||
public function action2(){
|
||||
|
||||
return (new \think\worker\response\Websocket())
|
||||
->onOpen(...)
|
||||
->onMessage(function(Websocket $websocket, Frame $frame){
|
||||
...
|
||||
})
|
||||
->onClose(...);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 自定义worker
|
||||
监听`worker.init`事件 注入`Manager`对象,调用addWorker方法添加
|
||||
~~~php
|
||||
use think\worker\Manager;
|
||||
use \think\worker\Worker;
|
||||
|
||||
//...
|
||||
|
||||
public function handle(Manager $manager){
|
||||
$worker = $manager->addWorker(function(Worker $worker){
|
||||
//..其他回调或处理
|
||||
//动态添加监听可参考 https://www.workerman.net/doc/workerman/worker/listen.html
|
||||
});
|
||||
}
|
||||
|
||||
//...
|
||||
~~~
|
||||
|
||||
55
composer.json
Normal file
55
composer.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "topthink/think-worker",
|
||||
"description": "workerman extend for thinkphp",
|
||||
"license": "Apache-2.0",
|
||||
"authors": [
|
||||
{
|
||||
"name": "liu21st",
|
||||
"email": "liu21st@gmail.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"workerman/workerman": "~5.0.0",
|
||||
"topthink/framework": "^8.0",
|
||||
"revolt/event-loop": "^1.0",
|
||||
"workerman/redis": "^2.0",
|
||||
"symfony/finder": ">=4.3"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"think\\worker\\": "src"
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"think": {
|
||||
"services": [
|
||||
"think\\worker\\Service"
|
||||
],
|
||||
"config": {
|
||||
"worker": "src/config/worker.php"
|
||||
}
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"pestphp/pest": "^3.7",
|
||||
"guzzlehttp/guzzle": "^7.0",
|
||||
"topthink/think-queue": "^3.0",
|
||||
"phpstan/phpstan": "^2.0",
|
||||
"ratchet/pawl": "^0.4.1"
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"app\\": "tests/stub/app"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"analyze": "phpstan --memory-limit=1G",
|
||||
"test": "pest --colors=always"
|
||||
}
|
||||
}
|
||||
16
phpstan.neon
Normal file
16
phpstan.neon
Normal file
@@ -0,0 +1,16 @@
|
||||
parameters:
|
||||
level: 5
|
||||
paths:
|
||||
- src
|
||||
- tests
|
||||
universalObjectCratesClasses:
|
||||
- PHPUnit\Framework\TestCase
|
||||
ignoreErrors:
|
||||
- '#Function root_path not found.#'
|
||||
- '#Function env not found.#'
|
||||
- '#Function app_path not found.#'
|
||||
- '#Function config_path not found.#'
|
||||
- '#Function public_path not found.#'
|
||||
- '#Function json not found.#'
|
||||
- '#Function runtime_path not found.#'
|
||||
- '#Constant STUB_DIR not found.#'
|
||||
18
phpunit.xml
Normal file
18
phpunit.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Test Suite">
|
||||
<directory suffix="Test.php">./tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<coverage/>
|
||||
<source>
|
||||
<include>
|
||||
<directory suffix=".php">./src</directory>
|
||||
</include>
|
||||
</source>
|
||||
</phpunit>
|
||||
23
src/App.php
Normal file
23
src/App.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker;
|
||||
|
||||
class App extends \think\App
|
||||
{
|
||||
protected $inConsole = true;
|
||||
|
||||
public function setInConsole($inConsole = true)
|
||||
{
|
||||
$this->inConsole = $inConsole;
|
||||
}
|
||||
|
||||
public function runningInConsole(): bool
|
||||
{
|
||||
return $this->inConsole;
|
||||
}
|
||||
|
||||
public function clearInstances()
|
||||
{
|
||||
$this->instances = [];
|
||||
}
|
||||
}
|
||||
22
src/Conduit.php
Normal file
22
src/Conduit.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker;
|
||||
|
||||
/**
|
||||
* @mixin \think\worker\conduit\Driver
|
||||
*/
|
||||
class Conduit extends \think\Manager
|
||||
{
|
||||
|
||||
protected $namespace = "\\think\\worker\\conduit\\driver\\";
|
||||
|
||||
protected function resolveConfig(string $name)
|
||||
{
|
||||
return $this->app->config->get("worker.conduit.{$name}", []);
|
||||
}
|
||||
|
||||
public function getDefaultDriver()
|
||||
{
|
||||
return $this->app->config->get('worker.conduit.type', 'socket');
|
||||
}
|
||||
}
|
||||
17
src/Http.php
Normal file
17
src/Http.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker;
|
||||
|
||||
class Http extends \think\Http
|
||||
{
|
||||
|
||||
protected function loadMiddleware(): void
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected function loadRoutes(): void
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
41
src/Ipc.php
Normal file
41
src/Ipc.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker;
|
||||
|
||||
class Ipc
|
||||
{
|
||||
protected $workerId;
|
||||
|
||||
public function __construct(protected Manager $manager, protected Conduit $conduit)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function listenMessage()
|
||||
{
|
||||
$this->subscribe();
|
||||
return $this->workerId;
|
||||
}
|
||||
|
||||
public function sendMessage($workerId, $message)
|
||||
{
|
||||
if ($workerId === $this->workerId) {
|
||||
$this->manager->triggerEvent('message', $message);
|
||||
} else {
|
||||
$this->publish($workerId, $message);
|
||||
}
|
||||
}
|
||||
|
||||
public function subscribe()
|
||||
{
|
||||
$this->workerId = $this->conduit->inc('ipc:worker');
|
||||
$this->conduit->subscribe("ipc:message:{$this->workerId}", function ($message) {
|
||||
$this->manager->triggerEvent('message', unserialize($message));
|
||||
});
|
||||
}
|
||||
|
||||
public function publish($workerId, $message)
|
||||
{
|
||||
$this->conduit->publish("ipc:message:{$workerId}", serialize($message));
|
||||
}
|
||||
}
|
||||
27
src/Manager.php
Normal file
27
src/Manager.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker;
|
||||
|
||||
use think\worker\concerns\InteractsWithConduit;
|
||||
use think\worker\concerns\InteractsWithHttp;
|
||||
use think\worker\concerns\InteractsWithQueue;
|
||||
use think\worker\concerns\InteractsWithServer;
|
||||
use think\worker\concerns\WithApplication;
|
||||
use think\worker\concerns\WithContainer;
|
||||
|
||||
class Manager
|
||||
{
|
||||
use InteractsWithServer,
|
||||
InteractsWithHttp,
|
||||
InteractsWithQueue,
|
||||
InteractsWithConduit,
|
||||
WithApplication,
|
||||
WithContainer;
|
||||
|
||||
protected function initialize(): void
|
||||
{
|
||||
$this->prepareHttp();
|
||||
$this->prepareQueue();
|
||||
$this->prepareConduit();
|
||||
}
|
||||
}
|
||||
210
src/Sandbox.php
Normal file
210
src/Sandbox.php
Normal file
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker;
|
||||
|
||||
use Closure;
|
||||
use InvalidArgumentException;
|
||||
use ReflectionObject;
|
||||
use RuntimeException;
|
||||
use think\Config;
|
||||
use think\Container;
|
||||
use think\Event;
|
||||
use think\exception\Handle;
|
||||
use think\worker\App;
|
||||
use think\worker\concerns\ModifyProperty;
|
||||
use think\worker\contract\ResetterInterface;
|
||||
use think\worker\resetters\ClearInstances;
|
||||
use think\worker\resetters\ResetConfig;
|
||||
use think\worker\resetters\ResetEvent;
|
||||
use think\worker\resetters\ResetModel;
|
||||
use think\worker\resetters\ResetPaginator;
|
||||
use think\worker\resetters\ResetService;
|
||||
use Throwable;
|
||||
use WeakMap;
|
||||
|
||||
class Sandbox
|
||||
{
|
||||
use ModifyProperty;
|
||||
|
||||
/** @var App|null */
|
||||
protected $snapshot;
|
||||
|
||||
/** @var WeakMap */
|
||||
protected $snapshots;
|
||||
|
||||
/** @var App */
|
||||
protected $app;
|
||||
|
||||
/** @var Config */
|
||||
protected $config;
|
||||
|
||||
/** @var Event */
|
||||
protected $event;
|
||||
|
||||
/** @var ResetterInterface[] */
|
||||
protected $resetters = [];
|
||||
protected $services = [];
|
||||
|
||||
public function __construct(App $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
$this->snapshots = new WeakMap();
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
protected function initialize()
|
||||
{
|
||||
Container::setInstance(function () {
|
||||
return $this->getSnapshot();
|
||||
});
|
||||
|
||||
$this->setInitialConfig();
|
||||
$this->setInitialServices();
|
||||
$this->setInitialEvent();
|
||||
$this->setInitialResetters();
|
||||
}
|
||||
|
||||
public function run(Closure $callable, ?object $key = null)
|
||||
{
|
||||
$this->snapshot = $this->createApp($key);
|
||||
try {
|
||||
$this->snapshot->invoke($callable, [$this]);
|
||||
} catch (Throwable $e) {
|
||||
$this->snapshot->make(Handle::class)->report($e);
|
||||
} finally {
|
||||
if (empty($key)) {
|
||||
$this->snapshot->clearInstances();
|
||||
}
|
||||
$this->snapshot = null;
|
||||
$this->setInstance($this->app);
|
||||
}
|
||||
}
|
||||
|
||||
protected function createApp(?object $key = null)
|
||||
{
|
||||
if (!empty($key)) {
|
||||
if (isset($this->snapshots[$key])) {
|
||||
return $this->snapshots[$key]->app;
|
||||
}
|
||||
}
|
||||
|
||||
$app = clone $this->app;
|
||||
$this->setInstance($app);
|
||||
$this->resetApp($app);
|
||||
|
||||
if (!empty($key)) {
|
||||
$this->snapshots[$key] = new class($app) {
|
||||
public function __construct(public App $app)
|
||||
{
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->app->clearInstances();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return $app;
|
||||
}
|
||||
|
||||
protected function resetApp(App $app)
|
||||
{
|
||||
foreach ($this->resetters as $resetter) {
|
||||
$resetter->handle($app, $this);
|
||||
}
|
||||
}
|
||||
|
||||
protected function setInstance(App $app)
|
||||
{
|
||||
$app->instance('app', $app);
|
||||
$app->instance(Container::class, $app);
|
||||
|
||||
$reflectObject = new ReflectionObject($app);
|
||||
$reflectProperty = $reflectObject->getProperty('services');
|
||||
$services = $reflectProperty->getValue($app);
|
||||
|
||||
foreach ($services as $service) {
|
||||
$this->modifyProperty($service, $app);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set initial config.
|
||||
*/
|
||||
protected function setInitialConfig()
|
||||
{
|
||||
$this->config = clone $this->app->config;
|
||||
}
|
||||
|
||||
protected function setInitialEvent()
|
||||
{
|
||||
$this->event = clone $this->app->event;
|
||||
}
|
||||
|
||||
protected function setInitialServices()
|
||||
{
|
||||
$services = $this->config->get('worker.services', []);
|
||||
|
||||
foreach ($services as $service) {
|
||||
if (class_exists($service) && !in_array($service, $this->services)) {
|
||||
$serviceObj = new $service($this->app);
|
||||
$this->services[$service] = $serviceObj;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize resetters.
|
||||
*/
|
||||
protected function setInitialResetters()
|
||||
{
|
||||
$resetters = [
|
||||
ClearInstances::class,
|
||||
ResetConfig::class,
|
||||
ResetEvent::class,
|
||||
ResetService::class,
|
||||
ResetModel::class,
|
||||
ResetPaginator::class,
|
||||
];
|
||||
|
||||
$resetters = array_merge($resetters, $this->config->get('worker.resetters', []));
|
||||
|
||||
foreach ($resetters as $resetter) {
|
||||
$resetterClass = $this->app->make($resetter);
|
||||
if (!$resetterClass instanceof ResetterInterface) {
|
||||
throw new RuntimeException("{$resetter} must implement " . ResetterInterface::class);
|
||||
}
|
||||
$this->resetters[$resetter] = $resetterClass;
|
||||
}
|
||||
}
|
||||
|
||||
public function getSnapshot()
|
||||
{
|
||||
$snapshot = $this->snapshot;
|
||||
if ($snapshot instanceof App) {
|
||||
return $snapshot;
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('The app object has not been initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get config snapshot.
|
||||
*/
|
||||
public function getConfig()
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
public function getEvent()
|
||||
{
|
||||
return $this->event;
|
||||
}
|
||||
|
||||
public function getServices()
|
||||
{
|
||||
return $this->services;
|
||||
}
|
||||
|
||||
}
|
||||
23
src/Service.php
Normal file
23
src/Service.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
namespace think\worker;
|
||||
|
||||
use think\worker\command\Server;
|
||||
|
||||
class Service extends \think\Service
|
||||
{
|
||||
public function boot()
|
||||
{
|
||||
$this->commands([
|
||||
Server::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
33
src/Watcher.php
Normal file
33
src/Watcher.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace think\worker;
|
||||
|
||||
use think\worker\watcher\Driver;
|
||||
|
||||
/**
|
||||
* @mixin Driver
|
||||
*/
|
||||
class Watcher extends \think\Manager
|
||||
{
|
||||
protected $namespace = '\\think\\worker\\watcher\\';
|
||||
|
||||
protected function getConfig(string $name, $default = null)
|
||||
{
|
||||
return $this->app->config->get('worker.hot_update.' . $name, $default);
|
||||
}
|
||||
|
||||
protected function resolveParams($name): array
|
||||
{
|
||||
return [
|
||||
array_filter($this->getConfig('include', []), function ($dir) {
|
||||
return is_dir($dir);
|
||||
}),
|
||||
$this->getConfig('exclude', []),
|
||||
$this->getConfig('name', []),
|
||||
];
|
||||
}
|
||||
|
||||
public function getDefaultDriver()
|
||||
{
|
||||
return $this->getConfig('type', 'scan');
|
||||
}
|
||||
}
|
||||
121
src/Websocket.php
Normal file
121
src/Websocket.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker;
|
||||
|
||||
use RuntimeException;
|
||||
use think\Event;
|
||||
use think\worker\websocket\Pusher;
|
||||
use think\worker\websocket\Room;
|
||||
use Workerman\Connection\TcpConnection;
|
||||
|
||||
/**
|
||||
* Class Websocket
|
||||
*/
|
||||
class Websocket
|
||||
{
|
||||
/**
|
||||
* @var \think\App
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
/**
|
||||
* @var Room
|
||||
*/
|
||||
protected $room;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $sender;
|
||||
|
||||
/** @var Event */
|
||||
protected $event;
|
||||
|
||||
/** @var TcpConnection|null */
|
||||
protected $connection;
|
||||
|
||||
protected $connected = true;
|
||||
|
||||
/**
|
||||
* Websocket constructor.
|
||||
*
|
||||
* @param \think\App $app
|
||||
* @param Room $room
|
||||
* @param Event $event
|
||||
*/
|
||||
public function __construct(\think\App $app, Room $room, Event $event, ?TcpConnection $connection = null)
|
||||
{
|
||||
$this->app = $app;
|
||||
$this->room = $room;
|
||||
$this->event = $event;
|
||||
$this->connection = $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Pusher
|
||||
*/
|
||||
protected function makePusher()
|
||||
{
|
||||
return $this->app->invokeClass(Pusher::class);
|
||||
}
|
||||
|
||||
public function to(...$values)
|
||||
{
|
||||
return $this->makePusher()->to(...$values);
|
||||
}
|
||||
|
||||
public function push($data)
|
||||
{
|
||||
$this->makePusher()->to($this->getSender())->push($data);
|
||||
}
|
||||
|
||||
public function emit(string $event, ...$data)
|
||||
{
|
||||
$this->makePusher()->to($this->getSender())->emit($event, ...$data);
|
||||
}
|
||||
|
||||
public function join(...$rooms): self
|
||||
{
|
||||
$this->room->add($this->getSender(), ...$rooms);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function leave(...$rooms): self
|
||||
{
|
||||
$this->room->delete($this->getSender(), ...$rooms);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setConnected($connected)
|
||||
{
|
||||
$this->connected = $connected;
|
||||
}
|
||||
|
||||
public function isEstablished()
|
||||
{
|
||||
return $this->connected;
|
||||
}
|
||||
|
||||
public function close()
|
||||
{
|
||||
if ($this->connection) {
|
||||
$this->connection->close();
|
||||
}
|
||||
}
|
||||
|
||||
public function setSender(string $fd)
|
||||
{
|
||||
$this->sender = $fd;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSender()
|
||||
{
|
||||
if (empty($this->sender)) {
|
||||
throw new RuntimeException('Cannot use websocket as current client before handshake!');
|
||||
}
|
||||
return $this->sender;
|
||||
}
|
||||
}
|
||||
19
src/Worker.php
Normal file
19
src/Worker.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker;
|
||||
|
||||
class Worker extends \Workerman\Worker
|
||||
{
|
||||
protected static function init(): void
|
||||
{
|
||||
static::$pidFile = runtime_path() . 'worker.pid';
|
||||
static::$statusFile = runtime_path() . 'worker.status';
|
||||
static::$logFile = runtime_path() . 'worker.log';
|
||||
parent::init();
|
||||
}
|
||||
|
||||
protected static function parseCommand(): void
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
35
src/command/Server.php
Normal file
35
src/command/Server.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace think\worker\command;
|
||||
|
||||
use think\console\Command;
|
||||
use think\worker\Manager;
|
||||
|
||||
/**
|
||||
* Worker Server 命令行类
|
||||
*/
|
||||
class Server extends Command
|
||||
{
|
||||
protected $config = [];
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('worker')
|
||||
->setDescription('Workerman Server for ThinkPHP');
|
||||
}
|
||||
|
||||
public function handle(Manager $manager)
|
||||
{
|
||||
$manager->start();
|
||||
}
|
||||
|
||||
}
|
||||
20
src/concerns/InteractsWithConduit.php
Normal file
20
src/concerns/InteractsWithConduit.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\concerns;
|
||||
|
||||
use think\worker\Conduit;
|
||||
|
||||
trait InteractsWithConduit
|
||||
{
|
||||
/** @var Conduit */
|
||||
protected $conduit;
|
||||
|
||||
protected function prepareConduit()
|
||||
{
|
||||
$this->conduit = $this->container->make(Conduit::class);
|
||||
$this->conduit->prepare();
|
||||
$this->onEvent('workerStart', function () {
|
||||
$this->app->instance(Conduit::class, $this->conduit);
|
||||
});
|
||||
}
|
||||
}
|
||||
321
src/concerns/InteractsWithHttp.php
Normal file
321
src/concerns/InteractsWithHttp.php
Normal file
@@ -0,0 +1,321 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\concerns;
|
||||
|
||||
use think\App;
|
||||
use think\Cookie;
|
||||
use think\Event;
|
||||
use think\exception\Handle;
|
||||
use think\helper\Str;
|
||||
use think\Http;
|
||||
use think\response\View;
|
||||
use think\worker\App as WorkerApp;
|
||||
use think\worker\Http as WorkerHttp;
|
||||
use think\worker\response\File as FileResponse;
|
||||
use think\worker\response\Iterator as IteratorResponse;
|
||||
use think\worker\websocket\Frame;
|
||||
use think\worker\Worker;
|
||||
use Throwable;
|
||||
use Workerman\Connection\TcpConnection;
|
||||
use Workerman\Protocols\Http\Chunk;
|
||||
use Workerman\Protocols\Http\Request as WorkerRequest;
|
||||
use Workerman\Protocols\Http\Response;
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
* Trait InteractsWithHttp
|
||||
* @property App $app
|
||||
* @property App $container
|
||||
*/
|
||||
trait InteractsWithHttp
|
||||
{
|
||||
use ModifyProperty, InteractsWithWebsocket;
|
||||
|
||||
protected $wsEnable = false;
|
||||
|
||||
protected function prepareHttp()
|
||||
{
|
||||
if ($this->getConfig('http.enable', true)) {
|
||||
|
||||
$this->wsEnable = $this->getConfig('websocket.enable', false);
|
||||
|
||||
if ($this->wsEnable) {
|
||||
$this->prepareWebsocket();
|
||||
}
|
||||
|
||||
$workerNum = $this->getConfig('http.worker_num', 4);
|
||||
$this->addWorker([$this, 'createHttpServer'], 'http server', $workerNum);
|
||||
}
|
||||
}
|
||||
|
||||
public function createHttpServer()
|
||||
{
|
||||
$this->preloadHttp();
|
||||
|
||||
$host = $this->getConfig('http.host');
|
||||
$port = $this->getConfig('http.port');
|
||||
$options = $this->getConfig('http.options', []);
|
||||
|
||||
$server = new Worker("\\think\\worker\\protocols\\FlexHttp://{$host}:{$port}", $options);
|
||||
|
||||
$server->reusePort = true;
|
||||
|
||||
$server->onMessage = function (TcpConnection $connection, $data) {
|
||||
if ($data instanceof WorkerRequest) {
|
||||
if ($this->wsEnable && $this->isWebsocketRequest($data)) {
|
||||
$this->onHandShake($connection, $data);
|
||||
} else {
|
||||
$this->onRequest($connection, $data);
|
||||
}
|
||||
} elseif ($data instanceof Frame) {
|
||||
$this->onMessage($connection, $data);
|
||||
}
|
||||
};
|
||||
|
||||
$server->onClose = function (TcpConnection $connection) {
|
||||
if ($this->wsEnable) {
|
||||
$this->onClose($connection);
|
||||
}
|
||||
};
|
||||
|
||||
$server->listen();
|
||||
}
|
||||
|
||||
protected function preloadHttp()
|
||||
{
|
||||
$http = $this->app->http;
|
||||
$this->app->invokeMethod([$http, 'loadMiddleware'], [], true);
|
||||
|
||||
if ($this->app->config->get('app.with_route', true)) {
|
||||
$this->app->invokeMethod([$http, 'loadRoutes'], [], true);
|
||||
$route = clone $this->app->route;
|
||||
unset($this->app->route);
|
||||
|
||||
$this->app->resolving(WorkerHttp::class, function ($http, App $app) use ($route) {
|
||||
$newRoute = clone $route;
|
||||
$this->modifyProperty($newRoute, $app);
|
||||
$app->instance('route', $newRoute);
|
||||
});
|
||||
}
|
||||
|
||||
$middleware = clone $this->app->middleware;
|
||||
unset($this->app->middleware);
|
||||
|
||||
$this->app->resolving(WorkerHttp::class, function ($http, App $app) use ($middleware) {
|
||||
$newMiddleware = clone $middleware;
|
||||
$this->modifyProperty($newMiddleware, $app);
|
||||
$app->instance('middleware', $newMiddleware);
|
||||
});
|
||||
|
||||
unset($this->app->http);
|
||||
$this->app->bind(Http::class, WorkerHttp::class);
|
||||
}
|
||||
|
||||
public function onRequest(TcpConnection $connection, WorkerRequest $wkRequest)
|
||||
{
|
||||
$this->runInSandbox(function (Http $http, Event $event, WorkerApp $app) use ($connection, $wkRequest) {
|
||||
|
||||
$app->setInConsole(false);
|
||||
|
||||
$request = $this->prepareRequest($wkRequest);
|
||||
|
||||
try {
|
||||
$response = $this->handleRequest($http, $request);
|
||||
$this->prepareResponse($response);
|
||||
} catch (Throwable $e) {
|
||||
$handle = $app->make(Handle::class);
|
||||
$handle->report($e);
|
||||
$response = $handle->render($request, $e);
|
||||
}
|
||||
|
||||
$this->sendResponse($connection, $request, $response, $app->cookie);
|
||||
|
||||
//关闭连接
|
||||
$connection->close();
|
||||
|
||||
$http->end($response);
|
||||
});
|
||||
}
|
||||
|
||||
protected function handleRequest(Http $http, $request)
|
||||
{
|
||||
$level = ob_get_level();
|
||||
ob_start();
|
||||
|
||||
$response = $http->run($request);
|
||||
|
||||
if (ob_get_length() > 0) {
|
||||
$content = $response->getContent();
|
||||
$response->content(ob_get_contents() . $content);
|
||||
}
|
||||
|
||||
while (ob_get_level() > $level) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
protected function prepareRequest(WorkerRequest $wkRequest)
|
||||
{
|
||||
$header = $wkRequest->header();
|
||||
$server = [];
|
||||
|
||||
foreach ($header as $key => $value) {
|
||||
$server['http_' . str_replace('-', '_', $key)] = $value;
|
||||
}
|
||||
|
||||
// 重新实例化请求对象 处理请求数据
|
||||
/** @var \think\Request $request */
|
||||
$request = $this->app->make('request', [], true);;
|
||||
|
||||
$queryString = $wkRequest->queryString();
|
||||
|
||||
return $request
|
||||
->setMethod($wkRequest->method())
|
||||
->withHeader($header)
|
||||
->withServer($server)
|
||||
->withGet($wkRequest->get())
|
||||
->withPost($wkRequest->post())
|
||||
->withCookie($wkRequest->cookie())
|
||||
->withFiles($wkRequest->file())
|
||||
->withInput($wkRequest->rawBody())
|
||||
->setBaseUrl($wkRequest->uri())
|
||||
->setUrl($wkRequest->uri() . (!empty($queryString) ? '?' . $queryString : ''))
|
||||
->setPathinfo(ltrim($wkRequest->path(), '/'));
|
||||
}
|
||||
|
||||
protected function prepareResponse(\think\Response $response)
|
||||
{
|
||||
switch (true) {
|
||||
case $response instanceof View:
|
||||
$response->getContent();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected function sendResponse(TcpConnection $connection, \think\Request $request, \think\Response $response, Cookie $cookie)
|
||||
{
|
||||
switch (true) {
|
||||
case $response instanceof IteratorResponse:
|
||||
$this->sendIterator($connection, $response, $cookie);
|
||||
break;
|
||||
case $response instanceof FileResponse:
|
||||
$this->sendFile($connection, $request, $response, $cookie);
|
||||
break;
|
||||
default:
|
||||
$this->sendContent($connection, $response, $cookie);
|
||||
}
|
||||
}
|
||||
|
||||
protected function sendIterator(TcpConnection $connection, IteratorResponse $response, Cookie $cookie)
|
||||
{
|
||||
$wkResponse = $this->createResponse($response, $cookie);
|
||||
$connection->send($wkResponse);
|
||||
|
||||
foreach ($response as $content) {
|
||||
$connection->send($content, true);
|
||||
}
|
||||
}
|
||||
|
||||
protected function sendFile(TcpConnection $connection, \think\Request $request, FileResponse $response, Cookie $cookie)
|
||||
{
|
||||
$ifNoneMatch = $request->header('If-None-Match');
|
||||
$ifRange = $request->header('If-Range');
|
||||
|
||||
$code = $response->getCode();
|
||||
$file = $response->getFile();
|
||||
$eTag = $response->getHeader('ETag');
|
||||
$lastModified = $response->getHeader('Last-Modified');
|
||||
|
||||
$fileSize = $file->getSize();
|
||||
$offset = 0;
|
||||
$length = -1;
|
||||
|
||||
if ($ifNoneMatch == $eTag) {
|
||||
$code = 304;
|
||||
} elseif (!$ifRange || $ifRange === $eTag || $ifRange === $lastModified) {
|
||||
$range = $request->header('Range', '');
|
||||
if (Str::startsWith($range, 'bytes=')) {
|
||||
[$start, $end] = explode('-', substr($range, 6), 2) + [0];
|
||||
|
||||
$end = ('' === $end) ? $fileSize - 1 : (int) $end;
|
||||
|
||||
if ('' === $start) {
|
||||
$start = $fileSize - $end;
|
||||
$end = $fileSize - 1;
|
||||
} else {
|
||||
$start = (int) $start;
|
||||
}
|
||||
|
||||
if ($start <= $end) {
|
||||
$end = min($end, $fileSize - 1);
|
||||
if ($start < 0 || $start > $end) {
|
||||
$code = 416;
|
||||
$response->header([
|
||||
'Content-Range' => sprintf('bytes */%s', $fileSize),
|
||||
]);
|
||||
} elseif ($end - $start < $fileSize - 1) {
|
||||
$length = $end < $fileSize ? $end - $start + 1 : -1;
|
||||
$offset = $start;
|
||||
$code = 206;
|
||||
$response->header([
|
||||
'Content-Range' => sprintf('bytes %s-%s/%s', $start, $end, $fileSize),
|
||||
'Content-Length' => $end - $start + 1,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$wkResponse = $this->createResponse($response, $cookie);
|
||||
|
||||
if ($code >= 200 && $code < 300 && $length !== 0) {
|
||||
$wkResponse->withFile($file->getPathname(), $offset, $length);
|
||||
}
|
||||
|
||||
$connection->send($wkResponse);
|
||||
}
|
||||
|
||||
protected function sendContent(TcpConnection $connection, \think\Response $response, Cookie $cookie)
|
||||
{
|
||||
$response->header(['Transfer-Encoding' => 'chunked']);
|
||||
|
||||
$wkResponse = $this->createResponse($response, $cookie);
|
||||
|
||||
$connection->send($wkResponse);
|
||||
|
||||
$content = $response->getContent();
|
||||
if ($content) {
|
||||
$contentSize = strlen($content);
|
||||
$chunkSize = 8192;
|
||||
|
||||
if ($contentSize > $chunkSize) {
|
||||
$sendSize = 0;
|
||||
do {
|
||||
if (!$connection->send(new Chunk(substr($content, $sendSize, $chunkSize)))) {
|
||||
break;
|
||||
}
|
||||
} while (($sendSize += $chunkSize) < $contentSize);
|
||||
} else {
|
||||
$connection->send(new Chunk($content));
|
||||
}
|
||||
}
|
||||
$connection->send(new Chunk(''));
|
||||
}
|
||||
|
||||
protected function createResponse(\think\Response $response, Cookie $cookie, $body = '')
|
||||
{
|
||||
$code = $response->getCode();
|
||||
$header = $response->getHeader();
|
||||
|
||||
$wkResponse = new Response($code, $header, $body);
|
||||
|
||||
foreach ($cookie->getCookie() as $name => $val) {
|
||||
[$value, $expire, $option] = $val;
|
||||
$wkResponse->cookie($name, $value, $expire, $option['path'], $option['domain'], (bool) $option['secure'], (bool) $option['httponly'], $option['samesite']);
|
||||
}
|
||||
|
||||
return $wkResponse;
|
||||
}
|
||||
}
|
||||
97
src/concerns/InteractsWithQueue.php
Normal file
97
src/concerns/InteractsWithQueue.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\concerns;
|
||||
|
||||
use think\helper\Arr;
|
||||
use think\queue\event\JobFailed;
|
||||
use think\queue\Worker;
|
||||
use Workerman\Timer;
|
||||
|
||||
trait InteractsWithQueue
|
||||
{
|
||||
protected function createQueueWorkers()
|
||||
{
|
||||
$workers = $this->getConfig('queue.workers', []);
|
||||
|
||||
foreach ($workers as $queue => $options) {
|
||||
|
||||
if (strpos($queue, '@') !== false) {
|
||||
[$queue, $connection] = explode('@', $queue);
|
||||
} else {
|
||||
$connection = null;
|
||||
}
|
||||
|
||||
$workerNum = Arr::get($options, 'worker_num', 1);
|
||||
|
||||
$this->addWorker(function () use ($options, $connection, $queue) {
|
||||
$delay = Arr::get($options, 'delay', 0);
|
||||
$sleep = Arr::get($options, 'sleep', 3);
|
||||
$tries = Arr::get($options, 'tries', 0);
|
||||
$timeout = Arr::get($options, 'timeout', 60);
|
||||
$qWorker = $this->app->make(Worker::class);
|
||||
|
||||
if ($this->supportsAsyncSignals()) {
|
||||
pcntl_signal(SIGALRM, function () {
|
||||
\think\worker\Worker::stopAll();
|
||||
});
|
||||
|
||||
pcntl_alarm($timeout);
|
||||
}
|
||||
|
||||
$this->createRunTimer(function () use ($connection, $queue, $delay, $sleep, $tries, $qWorker) {
|
||||
$this->runInSandbox(function () use ($connection, $queue, $delay, $sleep, $tries, $qWorker) {
|
||||
$qWorker->runNextJob($connection, $queue, $delay, $sleep, $tries);
|
||||
});
|
||||
if ($this->supportsAsyncSignals()) {
|
||||
pcntl_alarm(0);
|
||||
}
|
||||
});
|
||||
}, "queue [$queue]", $workerNum);
|
||||
}
|
||||
}
|
||||
|
||||
protected function supportsAsyncSignals()
|
||||
{
|
||||
return extension_loaded('pcntl');
|
||||
}
|
||||
|
||||
protected function createRunTimer($func)
|
||||
{
|
||||
Timer::add(0.1, function () use ($func) {
|
||||
$func();
|
||||
$this->createRunTimer($func);
|
||||
}, [], false);
|
||||
}
|
||||
|
||||
protected function prepareQueue()
|
||||
{
|
||||
if ($this->getConfig('queue.enable', false)) {
|
||||
$this->listenForEvents();
|
||||
$this->createQueueWorkers();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册事件
|
||||
*/
|
||||
protected function listenForEvents()
|
||||
{
|
||||
$this->container->event->listen(JobFailed::class, function (JobFailed $event) {
|
||||
$this->logFailedJob($event);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录失败任务
|
||||
* @param JobFailed $event
|
||||
*/
|
||||
protected function logFailedJob(JobFailed $event)
|
||||
{
|
||||
$this->container['queue.failer']->log(
|
||||
$event->connection,
|
||||
$event->job->getQueue(),
|
||||
$event->job->getRawBody(),
|
||||
$event->exception
|
||||
);
|
||||
}
|
||||
}
|
||||
119
src/concerns/InteractsWithServer.php
Normal file
119
src/concerns/InteractsWithServer.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\concerns;
|
||||
|
||||
use think\App;
|
||||
use think\worker\Ipc;
|
||||
use think\worker\Watcher;
|
||||
use think\worker\Worker;
|
||||
use Workerman\Redis\Client;
|
||||
|
||||
/**
|
||||
* Trait InteractsWithServer
|
||||
* @property App $container
|
||||
*/
|
||||
trait InteractsWithServer
|
||||
{
|
||||
/** @var Ipc */
|
||||
protected $ipc;
|
||||
|
||||
protected $workerId = null;
|
||||
protected $stopping = false;
|
||||
|
||||
public function addWorker(callable $func, $name = 'none', int $count = 1): Worker
|
||||
{
|
||||
$worker = new Worker();
|
||||
|
||||
$worker->name = $name;
|
||||
$worker->count = $count;
|
||||
|
||||
$worker->onWorkerStart = function (Worker $worker) use ($func) {
|
||||
$this->clearCache();
|
||||
$this->prepareApplication();
|
||||
|
||||
$this->conduit->connect();
|
||||
|
||||
$this->workerId = $this->ipc->listenMessage();
|
||||
|
||||
$this->triggerEvent('workerStart', $worker);
|
||||
|
||||
$func($worker);
|
||||
};
|
||||
|
||||
$worker->onWorkerReload = function () {
|
||||
$this->stopping = true;
|
||||
};
|
||||
|
||||
return $worker;
|
||||
}
|
||||
|
||||
public function getWorkerId()
|
||||
{
|
||||
return $this->workerId;
|
||||
}
|
||||
|
||||
public function isStopping()
|
||||
{
|
||||
return $this->stopping;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动服务
|
||||
*/
|
||||
public function start(): void
|
||||
{
|
||||
$this->initialize();
|
||||
$this->prepareIpc();
|
||||
$this->triggerEvent('init');
|
||||
|
||||
//热更新
|
||||
if ($this->getConfig('hot_update.enable', false)) {
|
||||
$this->addHotUpdateWorker();
|
||||
}
|
||||
|
||||
Worker::runAll();
|
||||
}
|
||||
|
||||
protected function prepareIpc()
|
||||
{
|
||||
$this->ipc = $this->container->make(Ipc::class);
|
||||
}
|
||||
|
||||
public function sendMessage($workerId, $message)
|
||||
{
|
||||
$this->ipc->sendMessage($workerId, $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 热更新
|
||||
*/
|
||||
protected function addHotUpdateWorker()
|
||||
{
|
||||
$worker = new Worker();
|
||||
|
||||
$worker->name = 'hot update';
|
||||
$worker->reloadable = false;
|
||||
|
||||
$worker->onWorkerStart = function () {
|
||||
$watcher = $this->container->make(Watcher::class);
|
||||
$watcher->watch(function () {
|
||||
posix_kill(posix_getppid(), SIGUSR1);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除apc、op缓存
|
||||
*/
|
||||
protected function clearCache()
|
||||
{
|
||||
if (extension_loaded('apc')) {
|
||||
apc_clear_cache();
|
||||
}
|
||||
|
||||
if (extension_loaded('Zend OPcache')) {
|
||||
opcache_reset();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
145
src/concerns/InteractsWithWebsocket.php
Normal file
145
src/concerns/InteractsWithWebsocket.php
Normal file
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\concerns;
|
||||
|
||||
use think\App;
|
||||
use think\Event;
|
||||
use think\helper\Arr;
|
||||
use think\Http;
|
||||
use think\worker\message\PushMessage;
|
||||
use think\worker\Websocket;
|
||||
use think\worker\contract\websocket\HandlerInterface;
|
||||
use think\worker\websocket\Frame;
|
||||
use think\worker\websocket\Handler;
|
||||
use Throwable;
|
||||
use Workerman\Connection\TcpConnection;
|
||||
use Workerman\Protocols\Http\Request as WorkerRequest;
|
||||
use Workerman\Protocols\Http\Response;
|
||||
|
||||
trait InteractsWithWebsocket
|
||||
{
|
||||
|
||||
protected $messageSender = [];
|
||||
|
||||
protected function prepareWebsocket()
|
||||
{
|
||||
$this->onEvent('workerStart', function () {
|
||||
$handlerClass = $this->getConfig('websocket.handler', Handler::class);
|
||||
$this->app->bind(HandlerInterface::class, $handlerClass);
|
||||
|
||||
$this->onEvent('message', function ($message) {
|
||||
if ($message instanceof PushMessage) {
|
||||
if (isset($this->messageSender[$message->to])) {
|
||||
$this->messageSender[$message->to]($message->data);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public function onHandShake(TcpConnection $connection, WorkerRequest $wkRequest)
|
||||
{
|
||||
$this->runInSandbox(function (App $app, Http $http, Event $event) use ($connection, $wkRequest) {
|
||||
$request = $this->prepareRequest($wkRequest);
|
||||
|
||||
$response = $http->run($request);
|
||||
if (!$response instanceof \think\worker\response\Websocket) {
|
||||
$connection->close();
|
||||
return;
|
||||
}
|
||||
|
||||
$event->subscribe([$response]);
|
||||
$this->upgrade($connection, $wkRequest);
|
||||
|
||||
$websocket = $app->make(Websocket::class, [$connection], true);
|
||||
$app->instance(Websocket::class, $websocket);
|
||||
|
||||
$id = "{$this->workerId}.{$connection->id}";
|
||||
|
||||
$websocket->setSender($id);
|
||||
$websocket->join($id);
|
||||
|
||||
$handler = $app->make(HandlerInterface::class);
|
||||
|
||||
$this->messageSender[$connection->id] = function ($data) use ($connection, $handler) {
|
||||
$connection->send($handler->encodeMessage($data));
|
||||
};
|
||||
|
||||
try {
|
||||
$handler->onOpen($request);
|
||||
} catch (Throwable $e) {
|
||||
$this->logServerError($e);
|
||||
}
|
||||
}, $connection);
|
||||
}
|
||||
|
||||
public function onMessage(TcpConnection $connection, Frame $frame)
|
||||
{
|
||||
$this->runInSandbox(function (App $app) use ($frame) {
|
||||
$handler = $app->make(HandlerInterface::class);
|
||||
try {
|
||||
$handler->onMessage($frame);
|
||||
} catch (Throwable $e) {
|
||||
$this->logServerError($e);
|
||||
}
|
||||
}, $connection);
|
||||
}
|
||||
|
||||
public function onClose(TcpConnection $connection)
|
||||
{
|
||||
$this->runInSandbox(function (App $app) use ($connection) {
|
||||
if ($app->exists(Websocket::class)) {
|
||||
$websocket = $app->make(Websocket::class);
|
||||
$handler = $app->make(HandlerInterface::class);
|
||||
try {
|
||||
$handler->onClose();
|
||||
} catch (Throwable $e) {
|
||||
$this->logServerError($e);
|
||||
}
|
||||
|
||||
// leave all rooms
|
||||
$websocket->leave();
|
||||
|
||||
unset($this->messageSender[$connection->id]);
|
||||
|
||||
$websocket->setConnected(false);
|
||||
}
|
||||
}, $connection);
|
||||
}
|
||||
|
||||
protected function isWebsocketRequest(WorkerRequest $request)
|
||||
{
|
||||
$header = $request->header();
|
||||
return strcasecmp(Arr::get($header, 'connection', ''), 'upgrade') === 0 &&
|
||||
strcasecmp(Arr::get($header, 'upgrade', ''), 'websocket') === 0;
|
||||
}
|
||||
|
||||
protected function upgrade(TcpConnection $connection, WorkerRequest $request)
|
||||
{
|
||||
$key = $request->header('Sec-WebSocket-Key');
|
||||
|
||||
$headers = [
|
||||
'Upgrade' => 'websocket',
|
||||
'Sec-WebSocket-Version' => '13',
|
||||
'Connection' => 'Upgrade',
|
||||
'Sec-WebSocket-Accept' => base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true)),
|
||||
];
|
||||
|
||||
if ($protocol = $request->header('Sec-Websocket-Protocol')) {
|
||||
$headers['Sec-WebSocket-Protocol'] = $protocol;
|
||||
}
|
||||
|
||||
$response = new Response(101, $headers);
|
||||
|
||||
$connection->send($response);
|
||||
|
||||
// Websocket data buffer.
|
||||
$connection->context->websocketDataBuffer = '';
|
||||
// Current websocket frame length.
|
||||
$connection->context->websocketCurrentFrameLength = 0;
|
||||
// Current websocket frame data.
|
||||
$connection->context->websocketCurrentFrameBuffer = '';
|
||||
|
||||
$connection->context->websocketHandshake = true;
|
||||
}
|
||||
}
|
||||
20
src/concerns/ModifyProperty.php
Normal file
20
src/concerns/ModifyProperty.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\concerns;
|
||||
|
||||
use ReflectionObject;
|
||||
|
||||
trait ModifyProperty
|
||||
{
|
||||
protected function modifyProperty($object, $value, $property = 'app')
|
||||
{
|
||||
$reflectObject = new ReflectionObject($object);
|
||||
if ($reflectObject->hasProperty($property)) {
|
||||
$reflectProperty = $reflectObject->getProperty($property);
|
||||
if(PHP_VERSION_ID < 80100) {
|
||||
$reflectProperty->setAccessible(true);
|
||||
}
|
||||
$reflectProperty->setValue($object, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
77
src/concerns/WithApplication.php
Normal file
77
src/concerns/WithApplication.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\concerns;
|
||||
|
||||
use Closure;
|
||||
use think\App;
|
||||
use think\worker\App as WorkerApp;
|
||||
use think\worker\Manager;
|
||||
use think\worker\Sandbox;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Trait WithApplication
|
||||
* @property App $container
|
||||
*/
|
||||
trait WithApplication
|
||||
{
|
||||
/**
|
||||
* @var WorkerApp
|
||||
*/
|
||||
protected $app;
|
||||
|
||||
protected function prepareApplication()
|
||||
{
|
||||
if (!$this->app instanceof WorkerApp) {
|
||||
$this->app = new WorkerApp($this->container->getRootPath());
|
||||
|
||||
$this->app->bind(WorkerApp::class, App::class);
|
||||
$this->app->bind(Manager::class, $this);
|
||||
|
||||
$this->app->initialize();
|
||||
$this->app->instance('request', $this->container->request);
|
||||
$this->prepareConcretes();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载
|
||||
*/
|
||||
protected function prepareConcretes()
|
||||
{
|
||||
$defaultConcretes = ['db', 'cache', 'event'];
|
||||
|
||||
$concretes = array_merge($defaultConcretes, $this->getConfig('concretes', []));
|
||||
|
||||
foreach ($concretes as $concrete) {
|
||||
$this->app->make($concrete);
|
||||
}
|
||||
}
|
||||
|
||||
public function getApp()
|
||||
{
|
||||
return $this->app;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取沙箱
|
||||
* @return Sandbox
|
||||
*/
|
||||
protected function getSandbox()
|
||||
{
|
||||
return $this->app->make(Sandbox::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在沙箱中执行
|
||||
* @param Closure $callable
|
||||
*/
|
||||
public function runInSandbox(Closure $callable, ?object $key = null)
|
||||
{
|
||||
try {
|
||||
$this->getSandbox()->run($callable, $key);
|
||||
} catch (Throwable $e) {
|
||||
$this->logServerError($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
75
src/concerns/WithContainer.php
Normal file
75
src/concerns/WithContainer.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\concerns;
|
||||
|
||||
use think\App;
|
||||
use think\exception\Handle;
|
||||
use Throwable;
|
||||
|
||||
trait WithContainer
|
||||
{
|
||||
|
||||
/**
|
||||
* @var App
|
||||
*/
|
||||
protected $container;
|
||||
|
||||
/**
|
||||
* Manager constructor.
|
||||
* @param App $container
|
||||
*/
|
||||
public function __construct(App $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
protected function getContainer()
|
||||
{
|
||||
return $this->container;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置
|
||||
* @param string $name
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function getConfig(string $name, $default = null)
|
||||
{
|
||||
return $this->container->config->get("worker.{$name}", $default);
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发事件
|
||||
* @param string $event
|
||||
* @param mixed $params
|
||||
*/
|
||||
public function triggerEvent(string $event, $params = null): void
|
||||
{
|
||||
$this->container->event->trigger("worker.{$event}", $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听事件
|
||||
* @param string $event
|
||||
* @param $listener
|
||||
* @param bool $first
|
||||
*/
|
||||
public function onEvent(string $event, $listener, bool $first = false): void
|
||||
{
|
||||
$this->container->event->listen("worker.{$event}", $listener, $first);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log server error.
|
||||
*
|
||||
* @param Throwable $e
|
||||
*/
|
||||
public function logServerError(Throwable $e)
|
||||
{
|
||||
/** @var Handle $handle */
|
||||
$handle = $this->container->make(Handle::class);
|
||||
|
||||
$handle->report($e);
|
||||
}
|
||||
}
|
||||
26
src/conduit/Driver.php
Normal file
26
src/conduit/Driver.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\conduit;
|
||||
|
||||
abstract class Driver
|
||||
{
|
||||
abstract public function prepare();
|
||||
|
||||
abstract public function connect();
|
||||
|
||||
abstract public function get(string $name);
|
||||
|
||||
abstract public function set(string $name, $value);
|
||||
|
||||
abstract public function inc(string $name, int $step = 1);
|
||||
|
||||
abstract public function sAdd(string $name, ...$value);
|
||||
|
||||
abstract public function sRem(string $name, $value);
|
||||
|
||||
abstract public function sMembers(string $name);
|
||||
|
||||
abstract public function publish(string $name, $value);
|
||||
|
||||
abstract public function subscribe(string $name, $callback);
|
||||
}
|
||||
186
src/conduit/driver/Socket.php
Normal file
186
src/conduit/driver/Socket.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\conduit\driver;
|
||||
|
||||
use Exception;
|
||||
use Revolt\EventLoop;
|
||||
use Revolt\EventLoop\Suspension;
|
||||
use think\worker\conduit\Driver;
|
||||
use think\worker\conduit\driver\socket\Command;
|
||||
use think\worker\conduit\driver\socket\Event;
|
||||
use think\worker\conduit\driver\socket\Result;
|
||||
use think\worker\conduit\driver\socket\Server;
|
||||
use think\worker\Manager;
|
||||
use Workerman\Connection\AsyncTcpConnection;
|
||||
use Workerman\Protocols\Frame;
|
||||
use Workerman\Timer;
|
||||
|
||||
class Socket extends Driver
|
||||
{
|
||||
protected $id = 0;
|
||||
protected $domain;
|
||||
|
||||
/** @var AsyncTcpConnection|null */
|
||||
protected $connection = null;
|
||||
protected $reconnectTimer;
|
||||
protected $pingInterval = 55;
|
||||
|
||||
/** @var array<int, array{0: Suspension, 1: int}> */
|
||||
protected $suspensions = [];
|
||||
protected $events = [];
|
||||
|
||||
public function __construct(protected Manager $manager)
|
||||
{
|
||||
$filename = runtime_path() . 'conduit.sock';
|
||||
@unlink($filename);
|
||||
$this->domain = "unix://{$filename}";
|
||||
}
|
||||
|
||||
public function prepare()
|
||||
{
|
||||
//启动服务端
|
||||
Server::run($this->domain);
|
||||
}
|
||||
|
||||
public function connect()
|
||||
{
|
||||
$suspension = EventLoop::getSuspension();
|
||||
$this->connection = $this->createConnection($suspension);
|
||||
$suspension->suspend();
|
||||
|
||||
Timer::add($this->pingInterval, function () {
|
||||
if ($this->connection) {
|
||||
$this->connection->send('');
|
||||
}
|
||||
});
|
||||
|
||||
Timer::add(1, function () {
|
||||
//检查是否超时
|
||||
foreach ($this->suspensions as $id => $suspension) {
|
||||
if (time() - $suspension[1] > 10) {
|
||||
$suspension[0]->throw(new Exception('conduit connection is timeout'));
|
||||
unset($this->suspensions[$id]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function get(string $name)
|
||||
{
|
||||
return $this->sendAndRecv(Command::create('get', $name));
|
||||
}
|
||||
|
||||
public function set(string $name, $value)
|
||||
{
|
||||
$this->send(Command::create('set', $name, $value));
|
||||
}
|
||||
|
||||
public function inc(string $name, int $step = 1)
|
||||
{
|
||||
return $this->sendAndRecv(Command::create('inc', $name, $step));
|
||||
}
|
||||
|
||||
public function sAdd(string $name, ...$value)
|
||||
{
|
||||
$this->send(Command::create('sAdd', $name, $value));
|
||||
}
|
||||
|
||||
public function sRem(string $name, $value)
|
||||
{
|
||||
$this->send(Command::create('sRem', $name, $value));
|
||||
}
|
||||
|
||||
public function sMembers(string $name)
|
||||
{
|
||||
return $this->sendAndRecv(Command::create('sMembers', $name));
|
||||
}
|
||||
|
||||
public function publish(string $name, $value)
|
||||
{
|
||||
$this->send(Command::create('publish', $name, $value));
|
||||
}
|
||||
|
||||
public function subscribe(string $name, $callback)
|
||||
{
|
||||
$this->send(Command::create('subscribe', $name));
|
||||
$this->events[$name] = $callback;
|
||||
}
|
||||
|
||||
protected function sendAndRecv(Command $command)
|
||||
{
|
||||
$suspension = EventLoop::getSuspension();
|
||||
|
||||
$id = $this->id++;
|
||||
|
||||
$command->id = $id;
|
||||
|
||||
$this->suspensions[$id] = [$suspension, time()];
|
||||
|
||||
$this->send($command);
|
||||
|
||||
return $suspension->suspend();
|
||||
}
|
||||
|
||||
protected function send(Command $command)
|
||||
{
|
||||
if (!$this->connection) {
|
||||
throw new Exception('conduit connection is disconnected');
|
||||
}
|
||||
|
||||
$this->connection->send(serialize($command));
|
||||
}
|
||||
|
||||
protected function createConnection(?Suspension $suspension = null)
|
||||
{
|
||||
$connection = new AsyncTcpConnection($this->domain);
|
||||
|
||||
$connection->protocol = Frame::class;
|
||||
|
||||
$connection->onConnect = function () use ($suspension) {
|
||||
$this->clearTimer();
|
||||
if ($suspension) {
|
||||
$suspension->resume();
|
||||
}
|
||||
//补订阅
|
||||
foreach ($this->events as $name => $callback) {
|
||||
$this->send(Command::create('subscribe', $name));
|
||||
}
|
||||
};
|
||||
|
||||
$connection->onMessage = function ($connection, $buffer) {
|
||||
/** @var Result|Event $result */
|
||||
$result = unserialize($buffer);
|
||||
|
||||
if ($result instanceof Event) {
|
||||
if (isset($this->events[$result->name])) {
|
||||
$this->events[$result->name]($result->data);
|
||||
}
|
||||
} elseif (isset($result->id) && isset($this->suspensions[$result->id])) {
|
||||
[$suspension] = $this->suspensions[$result->id];
|
||||
$suspension->resume($result->data);
|
||||
unset($this->suspensions[$result->id]);
|
||||
}
|
||||
};
|
||||
|
||||
$connection->onClose = function () {
|
||||
$this->connection = null;
|
||||
//重连
|
||||
$this->clearTimer();
|
||||
$this->reconnectTimer = Timer::add(1, function () {
|
||||
$this->connection = $this->createConnection();
|
||||
});
|
||||
};
|
||||
|
||||
$connection->connect();
|
||||
|
||||
return $connection;
|
||||
}
|
||||
|
||||
protected function clearTimer()
|
||||
{
|
||||
if ($this->reconnectTimer) {
|
||||
Timer::del($this->reconnectTimer);
|
||||
$this->reconnectTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/conduit/driver/socket/Command.php
Normal file
22
src/conduit/driver/socket/Command.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\conduit\driver\socket;
|
||||
|
||||
class Command
|
||||
{
|
||||
public $id;
|
||||
public $name;
|
||||
public $key;
|
||||
public $data;
|
||||
|
||||
public static function create($name, $key, $data = null)
|
||||
{
|
||||
$packet = new self();
|
||||
|
||||
$packet->name = $name;
|
||||
$packet->key = $key;
|
||||
$packet->data = $data;
|
||||
|
||||
return $packet;
|
||||
}
|
||||
}
|
||||
15
src/conduit/driver/socket/Event.php
Normal file
15
src/conduit/driver/socket/Event.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\conduit\driver\socket;
|
||||
|
||||
class Event
|
||||
{
|
||||
public function __construct(public $name, public $data)
|
||||
{
|
||||
}
|
||||
|
||||
public static function create($name, $data)
|
||||
{
|
||||
return new self($name, $data);
|
||||
}
|
||||
}
|
||||
16
src/conduit/driver/socket/Result.php
Normal file
16
src/conduit/driver/socket/Result.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\conduit\driver\socket;
|
||||
|
||||
class Result
|
||||
{
|
||||
public function __construct(public $id = null, public $data = null)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public static function create($id = null)
|
||||
{
|
||||
return new self($id);
|
||||
}
|
||||
}
|
||||
102
src/conduit/driver/socket/Server.php
Normal file
102
src/conduit/driver/socket/Server.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\conduit\driver\socket;
|
||||
|
||||
use think\worker\Worker;
|
||||
use Workerman\Connection\TcpConnection;
|
||||
use Workerman\Protocols\Frame;
|
||||
|
||||
class Server
|
||||
{
|
||||
protected $data = [];
|
||||
|
||||
/** @var array<string,TcpConnection[]> */
|
||||
protected $subscribers = [];
|
||||
|
||||
public function onMessage(TcpConnection $connection, $buffer)
|
||||
{
|
||||
if (empty($buffer)) {
|
||||
return;
|
||||
}
|
||||
/** @var Command $command */
|
||||
$command = unserialize($buffer);
|
||||
|
||||
$result = Result::create($command->id);
|
||||
|
||||
switch ($command->name) {
|
||||
case 'get':
|
||||
$result->data = $this->data[$command->key] ?? null;
|
||||
break;
|
||||
case 'set':
|
||||
$this->data[$command->key] = $command->data;
|
||||
break;
|
||||
case 'inc':
|
||||
if (!isset($this->data[$command->key]) || !is_integer($this->data[$command->key])) {
|
||||
$this->data[$command->key] = 0;
|
||||
}
|
||||
$result->data = $this->data[$command->key] += $command->data ?? 1;
|
||||
break;
|
||||
case 'sAdd':
|
||||
if (!isset($this->data[$command->key]) || !is_array($this->data[$command->key])) {
|
||||
$this->data[$command->key] = [];
|
||||
}
|
||||
$this->data[$command->key] = array_merge($this->data[$command->key], $command->data);
|
||||
break;
|
||||
case 'sRem':
|
||||
if (!isset($this->data[$command->key]) || !is_array($this->data[$command->key])) {
|
||||
$this->data[$command->key] = [];
|
||||
}
|
||||
$this->data[$command->key] = array_diff($this->data[$command->key], [$command->data]);
|
||||
break;
|
||||
case 'sMembers':
|
||||
if (!isset($this->data[$command->key]) || !is_array($this->data[$command->key])) {
|
||||
$this->data[$command->key] = [];
|
||||
}
|
||||
$result->data = $this->data[$command->key];
|
||||
break;
|
||||
case 'subscribe':
|
||||
if (!isset($this->subscribers[$command->key])) {
|
||||
$this->subscribers[$command->key] = [];
|
||||
}
|
||||
$this->subscribers[$command->key][] = $connection;
|
||||
break;
|
||||
case 'publish':
|
||||
if (!empty($this->subscribers[$command->key])) {
|
||||
foreach ($this->subscribers[$command->key] as $conn) {
|
||||
$conn->send(serialize(Event::create($command->key, $command->data)));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (isset($result->id)) {
|
||||
$connection->send(serialize($result));
|
||||
}
|
||||
}
|
||||
|
||||
public function onClose(TcpConnection $connection)
|
||||
{
|
||||
if (!empty($this->subscribers)) {
|
||||
foreach ($this->subscribers as $key => $connections) {
|
||||
$this->subscribers[$key] = array_udiff($connections, [$connection], function ($a, $b) {
|
||||
return $a <=> $b;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function run($domain)
|
||||
{
|
||||
//启动服务端
|
||||
$server = new self();
|
||||
|
||||
$worker = new Worker($domain);
|
||||
|
||||
$worker->name = 'conduit';
|
||||
$worker->protocol = Frame::class;
|
||||
$worker->reloadable = false;
|
||||
|
||||
$worker->onMessage = [$server, 'onMessage'];
|
||||
$worker->onClose = [$server, 'onClose'];
|
||||
}
|
||||
}
|
||||
39
src/config/worker.php
Normal file
39
src/config/worker.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
use think\worker\websocket\Handler;
|
||||
|
||||
return [
|
||||
'http' => [
|
||||
'enable' => true,
|
||||
'host' => '0.0.0.0',
|
||||
'port' => 8080,
|
||||
'worker_num' => 4,
|
||||
'options' => [],
|
||||
],
|
||||
'websocket' => [
|
||||
'enable' => false,
|
||||
'handler' => Handler::class,
|
||||
'ping_interval' => 25000,
|
||||
'ping_timeout' => 60000,
|
||||
],
|
||||
//队列
|
||||
'queue' => [
|
||||
'enable' => false,
|
||||
'workers' => [],
|
||||
],
|
||||
'hot_update' => [
|
||||
'enable' => env('APP_DEBUG', false),
|
||||
'name' => ['*.php'],
|
||||
'include' => [app_path(), config_path(), root_path('route')],
|
||||
'exclude' => [],
|
||||
],
|
||||
];
|
||||
17
src/contract/ResetterInterface.php
Normal file
17
src/contract/ResetterInterface.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\contract;
|
||||
|
||||
use think\App;
|
||||
use think\worker\Sandbox;
|
||||
|
||||
interface ResetterInterface
|
||||
{
|
||||
/**
|
||||
* "handle" function for resetting app.
|
||||
*
|
||||
* @param \think\App $app
|
||||
* @param Sandbox $sandbox
|
||||
*/
|
||||
public function handle(App $app, Sandbox $sandbox);
|
||||
}
|
||||
31
src/contract/websocket/HandlerInterface.php
Normal file
31
src/contract/websocket/HandlerInterface.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\contract\websocket;
|
||||
|
||||
use think\Request;
|
||||
use think\worker\websocket\Frame;
|
||||
|
||||
interface HandlerInterface
|
||||
{
|
||||
/**
|
||||
* "onOpen" listener.
|
||||
*
|
||||
* @param Request $request
|
||||
*/
|
||||
public function onOpen(Request $request);
|
||||
|
||||
/**
|
||||
* "onMessage" listener.
|
||||
*
|
||||
* @param Frame $frame
|
||||
*/
|
||||
public function onMessage(Frame $frame);
|
||||
|
||||
/**
|
||||
* "onClose" listener.
|
||||
*/
|
||||
public function onClose();
|
||||
|
||||
public function encodeMessage($message);
|
||||
|
||||
}
|
||||
15
src/message/PushMessage.php
Normal file
15
src/message/PushMessage.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\message;
|
||||
|
||||
class PushMessage
|
||||
{
|
||||
public $to;
|
||||
public $data;
|
||||
|
||||
public function __construct($to, $data)
|
||||
{
|
||||
$this->to = $to;
|
||||
$this->data = $data;
|
||||
}
|
||||
}
|
||||
39
src/protocols/FlexHttp.php
Normal file
39
src/protocols/FlexHttp.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\protocols;
|
||||
|
||||
use think\worker\websocket\Frame;
|
||||
use Workerman\Connection\TcpConnection;
|
||||
use Workerman\Protocols\Http;
|
||||
use Workerman\Protocols\Websocket;
|
||||
|
||||
class FlexHttp
|
||||
{
|
||||
public static function input(string $buffer, TcpConnection $connection)
|
||||
{
|
||||
if (empty($connection->context->websocketHandshake)) {
|
||||
return Http::input($buffer, $connection);
|
||||
} else {
|
||||
return Websocket::input($buffer, $connection);
|
||||
}
|
||||
}
|
||||
|
||||
public static function decode(string $buffer, TcpConnection $connection)
|
||||
{
|
||||
if (empty($connection->context->websocketHandshake)) {
|
||||
return Http::decode($buffer, $connection);
|
||||
} else {
|
||||
$data = Websocket::decode($buffer, $connection);
|
||||
return new Frame($connection->id, $data);
|
||||
}
|
||||
}
|
||||
|
||||
public static function encode(mixed $response, TcpConnection $connection): string
|
||||
{
|
||||
if (empty($connection->context->websocketHandshake)) {
|
||||
return Http::encode($response, $connection);
|
||||
} else {
|
||||
return Websocket::encode($response, $connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/resetters/ClearInstances.php
Normal file
23
src/resetters/ClearInstances.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\resetters;
|
||||
|
||||
use think\App;
|
||||
use think\worker\contract\ResetterInterface;
|
||||
use think\worker\Sandbox;
|
||||
|
||||
class ClearInstances implements ResetterInterface
|
||||
{
|
||||
public function handle(App $app, Sandbox $sandbox)
|
||||
{
|
||||
$instances = ['log', 'session', 'view', 'response', 'cookie'];
|
||||
|
||||
$instances = array_merge($instances, $sandbox->getConfig()->get('worker.instances', []));
|
||||
|
||||
foreach ($instances as $instance) {
|
||||
$app->delete($instance);
|
||||
}
|
||||
|
||||
return $app;
|
||||
}
|
||||
}
|
||||
18
src/resetters/ResetConfig.php
Normal file
18
src/resetters/ResetConfig.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\resetters;
|
||||
|
||||
use think\App;
|
||||
use think\worker\contract\ResetterInterface;
|
||||
use think\worker\Sandbox;
|
||||
|
||||
class ResetConfig implements ResetterInterface
|
||||
{
|
||||
|
||||
public function handle(App $app, Sandbox $sandbox)
|
||||
{
|
||||
$app->instance('config', clone $sandbox->getConfig());
|
||||
|
||||
return $app;
|
||||
}
|
||||
}
|
||||
22
src/resetters/ResetEvent.php
Normal file
22
src/resetters/ResetEvent.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\resetters;
|
||||
|
||||
use think\App;
|
||||
use think\worker\concerns\ModifyProperty;
|
||||
use think\worker\contract\ResetterInterface;
|
||||
use think\worker\Sandbox;
|
||||
|
||||
class ResetEvent implements ResetterInterface
|
||||
{
|
||||
use ModifyProperty;
|
||||
|
||||
public function handle(App $app, Sandbox $sandbox)
|
||||
{
|
||||
$event = clone $sandbox->getEvent();
|
||||
$this->modifyProperty($event, $app);
|
||||
$app->instance('event', $event);
|
||||
|
||||
return $app;
|
||||
}
|
||||
}
|
||||
21
src/resetters/ResetModel.php
Normal file
21
src/resetters/ResetModel.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\resetters;
|
||||
|
||||
use think\App;
|
||||
use think\Model;
|
||||
use think\worker\contract\ResetterInterface;
|
||||
use think\worker\Sandbox;
|
||||
|
||||
class ResetModel implements ResetterInterface
|
||||
{
|
||||
|
||||
public function handle(App $app, Sandbox $sandbox)
|
||||
{
|
||||
if (class_exists(Model::class)) {
|
||||
Model::setInvoker(function (...$args) use ($sandbox) {
|
||||
return $sandbox->getSnapshot()->invoke(...$args);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/resetters/ResetPaginator.php
Normal file
30
src/resetters/ResetPaginator.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\resetters;
|
||||
|
||||
use think\App;
|
||||
use think\Paginator;
|
||||
use think\worker\contract\ResetterInterface;
|
||||
use think\worker\Sandbox;
|
||||
|
||||
class ResetPaginator implements ResetterInterface
|
||||
{
|
||||
|
||||
public function handle(App $app, Sandbox $sandbox)
|
||||
{
|
||||
Paginator::currentPathResolver(function () use ($sandbox) {
|
||||
return $sandbox->getSnapshot()->request->baseUrl();
|
||||
});
|
||||
|
||||
Paginator::currentPageResolver(function ($varPage = 'page') use ($sandbox) {
|
||||
|
||||
$page = $sandbox->getSnapshot()->request->param($varPage);
|
||||
|
||||
if (filter_var($page, FILTER_VALIDATE_INT) !== false && (int) $page >= 1) {
|
||||
return (int) $page;
|
||||
}
|
||||
|
||||
return 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
33
src/resetters/ResetService.php
Normal file
33
src/resetters/ResetService.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\resetters;
|
||||
|
||||
use think\App;
|
||||
use think\worker\concerns\ModifyProperty;
|
||||
use think\worker\contract\ResetterInterface;
|
||||
use think\worker\Sandbox;
|
||||
|
||||
class ResetService implements ResetterInterface
|
||||
{
|
||||
use ModifyProperty;
|
||||
|
||||
/**
|
||||
* "handle" function for resetting app.
|
||||
*
|
||||
* @param App $app
|
||||
* @param Sandbox $sandbox
|
||||
*/
|
||||
public function handle(App $app, Sandbox $sandbox)
|
||||
{
|
||||
foreach ($sandbox->getServices() as $service) {
|
||||
$this->modifyProperty($service, $app);
|
||||
if (method_exists($service, 'register')) {
|
||||
$service->register();
|
||||
}
|
||||
if (method_exists($service, 'boot')) {
|
||||
$app->invoke([$service, 'boot']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
112
src/response/File.php
Normal file
112
src/response/File.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\response;
|
||||
|
||||
use DateTime;
|
||||
use RuntimeException;
|
||||
use SplFileInfo;
|
||||
use think\Response;
|
||||
|
||||
class File extends Response
|
||||
{
|
||||
public const DISPOSITION_ATTACHMENT = 'attachment';
|
||||
public const DISPOSITION_INLINE = 'inline';
|
||||
|
||||
protected $header = [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Accept-Ranges' => 'bytes',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var SplFileInfo
|
||||
*/
|
||||
protected $file;
|
||||
|
||||
public function __construct($file, ?string $contentDisposition = null, bool $autoEtag = true, bool $autoLastModified = true, bool $autoContentType = true)
|
||||
{
|
||||
$this->setFile($file, $contentDisposition, $autoEtag, $autoLastModified, $autoContentType);
|
||||
}
|
||||
|
||||
public function getFile()
|
||||
{
|
||||
return $this->file;
|
||||
}
|
||||
|
||||
public function setFile($file, ?string $contentDisposition = null, bool $autoEtag = true, bool $autoLastModified = true, bool $autoContentType = true)
|
||||
{
|
||||
if (!$file instanceof SplFileInfo) {
|
||||
$file = new SplFileInfo((string) $file);
|
||||
}
|
||||
|
||||
if (!$file->isReadable()) {
|
||||
throw new RuntimeException('File must be readable.');
|
||||
}
|
||||
|
||||
$this->header['Content-Length'] = $file->getSize();
|
||||
|
||||
$this->file = $file;
|
||||
|
||||
if ($autoEtag) {
|
||||
$this->setAutoEtag();
|
||||
}
|
||||
|
||||
if ($autoLastModified) {
|
||||
$this->setAutoLastModified();
|
||||
}
|
||||
|
||||
if ($contentDisposition) {
|
||||
$this->setContentDisposition($contentDisposition);
|
||||
}
|
||||
|
||||
if ($autoContentType) {
|
||||
$this->setAutoContentType();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setAutoContentType()
|
||||
{
|
||||
if (extension_loaded('fileinfo')) {
|
||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||
|
||||
$mimeType = finfo_file($finfo, $this->file->getPathname());
|
||||
if ($mimeType) {
|
||||
$this->header['Content-Type'] = $mimeType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function setContentDisposition(string $disposition, string $filename = '')
|
||||
{
|
||||
if ('' === $filename) {
|
||||
$filename = $this->file->getFilename();
|
||||
}
|
||||
|
||||
$this->header['Content-Disposition'] = "{$disposition}; filename=\"{$filename}\"";
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setAutoLastModified()
|
||||
{
|
||||
$mTime = $this->file->getMTime();
|
||||
if ($mTime) {
|
||||
$date = DateTime::createFromFormat('U', (string) $mTime);
|
||||
$this->lastModified($date->format('D, d M Y H:i:s') . ' GMT');
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setAutoEtag()
|
||||
{
|
||||
$eTag = "W/\"" . sha1_file($this->file->getPathname()) . "\"";
|
||||
|
||||
return $this->eTag($eTag);
|
||||
}
|
||||
|
||||
protected function sendData(string $data): void
|
||||
{
|
||||
readfile($this->file->getPathname());
|
||||
}
|
||||
}
|
||||
22
src/response/Iterator.php
Normal file
22
src/response/Iterator.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\response;
|
||||
|
||||
use IteratorAggregate;
|
||||
use think\Response;
|
||||
use Traversable;
|
||||
|
||||
class Iterator extends Response implements IteratorAggregate
|
||||
{
|
||||
protected $iterator;
|
||||
|
||||
public function __construct(Traversable $iterator)
|
||||
{
|
||||
$this->iterator = $iterator;
|
||||
}
|
||||
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
return $this->iterator;
|
||||
}
|
||||
}
|
||||
66
src/response/Websocket.php
Normal file
66
src/response/Websocket.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\response;
|
||||
|
||||
use think\Event;
|
||||
use think\Response;
|
||||
|
||||
class Websocket extends Response
|
||||
{
|
||||
protected $listeners = [];
|
||||
|
||||
public function onOpen($listener)
|
||||
{
|
||||
$this->listeners['Open'] = $listener;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function onMessage($listener)
|
||||
{
|
||||
$this->listeners['Message'] = $listener;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function onEvent($listener)
|
||||
{
|
||||
$this->listeners['Event'] = $listener;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function onClose($listener)
|
||||
{
|
||||
$this->listeners['Close'] = $listener;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function onConnect($listener)
|
||||
{
|
||||
$this->listeners['Connect'] = $listener;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function onDisconnect($listener)
|
||||
{
|
||||
$this->listeners['Disconnect'] = $listener;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function onPing($listener)
|
||||
{
|
||||
$this->listeners['Ping'] = $listener;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function onPong($listener)
|
||||
{
|
||||
$this->listeners['Pong'] = $listener;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function subscribe(Event $event)
|
||||
{
|
||||
foreach ($this->listeners as $eventName => $listener) {
|
||||
$event->listen('worker.websocket.' . $eventName, $listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/watcher/Driver.php
Normal file
8
src/watcher/Driver.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\watcher;
|
||||
|
||||
interface Driver
|
||||
{
|
||||
public function watch(callable $callback);
|
||||
}
|
||||
72
src/watcher/Find.php
Normal file
72
src/watcher/Find.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\watcher;
|
||||
|
||||
use Symfony\Component\Process\Process;
|
||||
use Workerman\Timer;
|
||||
|
||||
class Find implements Driver
|
||||
{
|
||||
protected $name;
|
||||
protected $directory;
|
||||
protected $exclude;
|
||||
|
||||
public function __construct($directory, $exclude, $name)
|
||||
{
|
||||
$this->directory = $directory;
|
||||
$this->exclude = $exclude;
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function watch(callable $callback)
|
||||
{
|
||||
$ms = 2000;
|
||||
$seconds = ceil(($ms + 1000) / 1000);
|
||||
$minutes = sprintf('-%.2f', $seconds / 60);
|
||||
|
||||
$dest = implode(' ', $this->directory);
|
||||
|
||||
$name = empty($this->name) ? '' : ' \( ' . join(' -o ', array_map(fn($v) => "-name \"{$v}\"", $this->name)) . ' \)';
|
||||
$notName = '';
|
||||
$notPath = '';
|
||||
if (!empty($this->exclude)) {
|
||||
$excludeDirs = $excludeFiles = [];
|
||||
foreach ($this->exclude as $directory) {
|
||||
$directory = rtrim($directory, '/');
|
||||
if (is_dir($directory)) {
|
||||
$excludeDirs[] = $directory;
|
||||
} else {
|
||||
$excludeFiles[] = $directory;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($excludeFiles)) {
|
||||
$notPath = ' -not \( ' . join(' -and ', array_map(fn($v) => "-name \"{$v}\"", $excludeFiles)) . ' \)';
|
||||
}
|
||||
|
||||
if (!empty($excludeDirs)) {
|
||||
$notPath = ' -not \( ' . join(' -and ', array_map(fn($v) => "-path \"{$v}/*\"", $excludeDirs)) . ' \)';
|
||||
}
|
||||
}
|
||||
|
||||
$command = "find {$dest}{$name}{$notName}{$notPath} -mmin {$minutes} -type f -print";
|
||||
|
||||
Timer::add($ms / 1000, function () use ($callback, $command) {
|
||||
$stdout = $this->exec($command);
|
||||
if (!empty($stdout)) {
|
||||
call_user_func($callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function exec($command)
|
||||
{
|
||||
$process = Process::fromShellCommandline($command);
|
||||
$process->run();
|
||||
if ($process->isSuccessful()) {
|
||||
return $process->getOutput();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
52
src/watcher/Scan.php
Normal file
52
src/watcher/Scan.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\watcher;
|
||||
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Symfony\Component\Finder\SplFileInfo;
|
||||
use Workerman\Timer;
|
||||
|
||||
class Scan implements Driver
|
||||
{
|
||||
protected $finder;
|
||||
|
||||
protected $files = [];
|
||||
|
||||
public function __construct($directory, $exclude, $name)
|
||||
{
|
||||
$this->finder = new Finder();
|
||||
$this->finder
|
||||
->files()
|
||||
->name($name)
|
||||
->in($directory)
|
||||
->exclude($exclude);
|
||||
}
|
||||
|
||||
protected function findFiles()
|
||||
{
|
||||
$files = [];
|
||||
/** @var SplFileInfo $f */
|
||||
foreach ($this->finder as $f) {
|
||||
$files[$f->getRealpath()] = $f->getMTime();
|
||||
}
|
||||
return $files;
|
||||
}
|
||||
|
||||
public function watch(callable $callback)
|
||||
{
|
||||
$this->files = $this->findFiles();
|
||||
|
||||
Timer::add(2, function () use ($callback) {
|
||||
$files = $this->findFiles();
|
||||
|
||||
foreach ($files as $path => $time) {
|
||||
if (empty($this->files[$path]) || $this->files[$path] != $time) {
|
||||
call_user_func($callback);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->files = $files;
|
||||
});
|
||||
}
|
||||
}
|
||||
15
src/websocket/Event.php
Normal file
15
src/websocket/Event.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\websocket;
|
||||
|
||||
class Event
|
||||
{
|
||||
public $type;
|
||||
public $data;
|
||||
|
||||
public function __construct($type, $data = null)
|
||||
{
|
||||
$this->type = $type;
|
||||
$this->data = $data;
|
||||
}
|
||||
}
|
||||
10
src/websocket/Frame.php
Normal file
10
src/websocket/Frame.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\websocket;
|
||||
|
||||
class Frame
|
||||
{
|
||||
public function __construct(public int $fd, public string $data)
|
||||
{
|
||||
}
|
||||
}
|
||||
71
src/websocket/Handler.php
Normal file
71
src/websocket/Handler.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\websocket;
|
||||
|
||||
use think\Event;
|
||||
use think\Request;
|
||||
use think\worker\contract\websocket\HandlerInterface;
|
||||
use think\worker\websocket\Event as WsEvent;
|
||||
|
||||
class Handler implements HandlerInterface
|
||||
{
|
||||
protected $event;
|
||||
|
||||
public function __construct(Event $event)
|
||||
{
|
||||
$this->event = $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* "onOpen" listener.
|
||||
*
|
||||
* @param Request $request
|
||||
*/
|
||||
public function onOpen(Request $request)
|
||||
{
|
||||
$this->event->trigger('worker.websocket.Open', $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* "onMessage" listener.
|
||||
*
|
||||
* @param Frame $frame
|
||||
*/
|
||||
public function onMessage(Frame $frame)
|
||||
{
|
||||
$this->event->trigger('worker.websocket.Message', $frame);
|
||||
|
||||
$event = $this->decode($frame->data);
|
||||
if ($event) {
|
||||
$this->event->trigger('worker.websocket.Event', $event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* "onClose" listener.
|
||||
*/
|
||||
public function onClose()
|
||||
{
|
||||
$this->event->trigger('worker.websocket.Close');
|
||||
}
|
||||
|
||||
protected function decode($payload)
|
||||
{
|
||||
$data = json_decode($payload, true);
|
||||
if (!empty($data['type'])) {
|
||||
return new WsEvent($data['type'], $data['data'] ?? null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function encodeMessage($message)
|
||||
{
|
||||
if ($message instanceof WsEvent) {
|
||||
return json_encode([
|
||||
'type' => $message->type,
|
||||
'data' => $message->data,
|
||||
]);
|
||||
}
|
||||
return $message;
|
||||
}
|
||||
}
|
||||
67
src/websocket/Pusher.php
Normal file
67
src/websocket/Pusher.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\websocket;
|
||||
|
||||
use think\worker\Manager;
|
||||
use think\worker\message\PushMessage;
|
||||
|
||||
/**
|
||||
* Class Pusher
|
||||
*/
|
||||
class Pusher
|
||||
{
|
||||
|
||||
/** @var Room */
|
||||
protected $room;
|
||||
|
||||
/** @var Manager */
|
||||
protected $manager;
|
||||
|
||||
protected $to = [];
|
||||
|
||||
public function __construct(Manager $manager, Room $room)
|
||||
{
|
||||
$this->manager = $manager;
|
||||
$this->room = $room;
|
||||
}
|
||||
|
||||
public function to(...$values)
|
||||
{
|
||||
foreach ($values as $value) {
|
||||
if (is_array($value)) {
|
||||
$this->to(...$value);
|
||||
} elseif (!in_array($value, $this->to)) {
|
||||
$this->to[] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Push message to related descriptors
|
||||
* @param $data
|
||||
* @return void
|
||||
*/
|
||||
public function push($data): void
|
||||
{
|
||||
$fds = [];
|
||||
|
||||
foreach ($this->to as $room) {
|
||||
$clients = $this->room->getClients($room);
|
||||
if (!empty($clients)) {
|
||||
$fds = array_merge($fds, $clients);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (array_unique($fds) as $fd) {
|
||||
[$workerId, $fd] = explode('.', $fd);
|
||||
$this->manager->sendMessage((int) $workerId, new PushMessage((int) $fd, $data));
|
||||
}
|
||||
}
|
||||
|
||||
public function emit(string $event, ...$data): void
|
||||
{
|
||||
$this->push(new Event($event, $data));
|
||||
}
|
||||
}
|
||||
54
src/websocket/Room.php
Normal file
54
src/websocket/Room.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\websocket;
|
||||
|
||||
use think\worker\Conduit;
|
||||
|
||||
class Room
|
||||
{
|
||||
|
||||
public function __construct(protected Conduit $conduit)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function add($fd, ...$rooms)
|
||||
{
|
||||
$this->conduit->sAdd($this->getClientKey($fd), ...$rooms);
|
||||
|
||||
foreach ($rooms as $room) {
|
||||
$this->conduit->sAdd($this->getRoomKey($room), $fd);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete($fd, ...$rooms)
|
||||
{
|
||||
$rooms = count($rooms) ? $rooms : $this->getRooms($fd);
|
||||
|
||||
$this->conduit->sRem($this->getClientKey($fd), ...$rooms);
|
||||
|
||||
foreach ($rooms as $room) {
|
||||
$this->conduit->sRem($this->getRoomKey($room), $fd);
|
||||
}
|
||||
}
|
||||
|
||||
public function getClients(string $room)
|
||||
{
|
||||
return $this->conduit->sMembers($this->getRoomKey($room)) ?: [];
|
||||
}
|
||||
|
||||
public function getRooms(string $fd)
|
||||
{
|
||||
return $this->conduit->sMembers($this->getClientKey($fd)) ?: [];
|
||||
}
|
||||
|
||||
protected function getClientKey(string $key)
|
||||
{
|
||||
return "ws:client:{$key}";
|
||||
}
|
||||
|
||||
protected function getRoomKey($room)
|
||||
{
|
||||
return "ws:room:{$room}";
|
||||
}
|
||||
}
|
||||
81
src/websocket/socketio/EnginePacket.php
Normal file
81
src/websocket/socketio/EnginePacket.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\websocket\socketio;
|
||||
|
||||
class EnginePacket
|
||||
{
|
||||
/**
|
||||
* Engine.io packet type `open`.
|
||||
*/
|
||||
const OPEN = 0;
|
||||
|
||||
/**
|
||||
* Engine.io packet type `close`.
|
||||
*/
|
||||
const CLOSE = 1;
|
||||
|
||||
/**
|
||||
* Engine.io packet type `ping`.
|
||||
*/
|
||||
const PING = 2;
|
||||
|
||||
/**
|
||||
* Engine.io packet type `pong`.
|
||||
*/
|
||||
const PONG = 3;
|
||||
|
||||
/**
|
||||
* Engine.io packet type `message`.
|
||||
*/
|
||||
const MESSAGE = 4;
|
||||
|
||||
/**
|
||||
* Engine.io packet type 'upgrade'
|
||||
*/
|
||||
const UPGRADE = 5;
|
||||
|
||||
/**
|
||||
* Engine.io packet type `noop`.
|
||||
*/
|
||||
const NOOP = 6;
|
||||
|
||||
public $type;
|
||||
|
||||
public $data = '';
|
||||
|
||||
public function __construct($type, $data = '')
|
||||
{
|
||||
$this->type = $type;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
public static function open($payload)
|
||||
{
|
||||
return new self(self::OPEN, $payload);
|
||||
}
|
||||
|
||||
public static function pong($payload = '')
|
||||
{
|
||||
return new self(self::PONG, $payload);
|
||||
}
|
||||
|
||||
public static function ping()
|
||||
{
|
||||
return new self(self::PING);
|
||||
}
|
||||
|
||||
public static function message($payload)
|
||||
{
|
||||
return new self(self::MESSAGE, $payload);
|
||||
}
|
||||
|
||||
public static function fromString(string $packet)
|
||||
{
|
||||
return new self(substr($packet, 0, 1), substr($packet, 1) ?: '');
|
||||
}
|
||||
|
||||
public function toString()
|
||||
{
|
||||
return $this->type . $this->data;
|
||||
}
|
||||
}
|
||||
198
src/websocket/socketio/Handler.php
Normal file
198
src/websocket/socketio/Handler.php
Normal file
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\websocket\socketio;
|
||||
|
||||
use Exception;
|
||||
|
||||
use think\Config;
|
||||
use think\Event;
|
||||
use think\Request;
|
||||
use think\worker\contract\websocket\HandlerInterface;
|
||||
use think\worker\Websocket;
|
||||
use think\worker\websocket\Event as WsEvent;
|
||||
use think\worker\websocket\Frame;
|
||||
use Workerman\Timer;
|
||||
|
||||
class Handler implements HandlerInterface
|
||||
{
|
||||
/** @var Config */
|
||||
protected $config;
|
||||
|
||||
protected $event;
|
||||
|
||||
protected $websocket;
|
||||
|
||||
protected $eio;
|
||||
|
||||
protected $pingTimeoutTimer = 0;
|
||||
protected $pingIntervalTimer = 0;
|
||||
|
||||
protected $pingInterval;
|
||||
protected $pingTimeout;
|
||||
|
||||
public function __construct(Event $event, Config $config, Websocket $websocket)
|
||||
{
|
||||
$this->event = $event;
|
||||
$this->config = $config;
|
||||
$this->websocket = $websocket;
|
||||
$this->pingInterval = $this->config->get('worker.websocket.ping_interval', 25000);
|
||||
$this->pingTimeout = $this->config->get('worker.websocket.ping_timeout', 60000);
|
||||
}
|
||||
|
||||
/**
|
||||
* "onOpen" listener.
|
||||
*
|
||||
* @param Request $request
|
||||
*/
|
||||
public function onOpen(Request $request)
|
||||
{
|
||||
$this->eio = $request->param('EIO');
|
||||
|
||||
$payload = json_encode(
|
||||
[
|
||||
'sid' => base64_encode(uniqid()),
|
||||
'upgrades' => [],
|
||||
'pingInterval' => $this->pingInterval,
|
||||
'pingTimeout' => $this->pingTimeout,
|
||||
]
|
||||
);
|
||||
|
||||
$this->push(EnginePacket::open($payload));
|
||||
|
||||
$this->event->trigger('worker.websocket.Open', $request);
|
||||
|
||||
if ($this->eio < 4) {
|
||||
$this->resetPingTimeout($this->pingInterval + $this->pingTimeout);
|
||||
$this->onConnect();
|
||||
} else {
|
||||
$this->schedulePing();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* "onMessage" listener.
|
||||
*
|
||||
* @param Frame $frame
|
||||
*/
|
||||
public function onMessage(Frame $frame)
|
||||
{
|
||||
$enginePacket = EnginePacket::fromString($frame->data);
|
||||
|
||||
$this->event->trigger('worker.websocket.Message', $enginePacket);
|
||||
|
||||
$this->resetPingTimeout($this->pingInterval + $this->pingTimeout);
|
||||
|
||||
switch ($enginePacket->type) {
|
||||
case EnginePacket::MESSAGE:
|
||||
$packet = Packet::fromString($enginePacket->data);
|
||||
switch ($packet->type) {
|
||||
case Packet::CONNECT:
|
||||
$this->onConnect($packet->data);
|
||||
break;
|
||||
case Packet::EVENT:
|
||||
$type = array_shift($packet->data);
|
||||
$data = $packet->data;
|
||||
$result = $this->event->trigger('worker.websocket.Event', new WsEvent($type, $data));
|
||||
|
||||
if ($packet->id !== null) {
|
||||
$responsePacket = Packet::create(Packet::ACK, [
|
||||
'id' => $packet->id,
|
||||
'nsp' => $packet->nsp,
|
||||
'data' => $result,
|
||||
]);
|
||||
|
||||
$this->push($responsePacket);
|
||||
}
|
||||
break;
|
||||
case Packet::DISCONNECT:
|
||||
$this->event->trigger('worker.websocket.Disconnect');
|
||||
$this->websocket->close();
|
||||
break;
|
||||
default:
|
||||
$this->websocket->close();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case EnginePacket::PING:
|
||||
$this->event->trigger('worker.websocket.Ping');
|
||||
$this->push(EnginePacket::pong($enginePacket->data));
|
||||
break;
|
||||
case EnginePacket::PONG:
|
||||
$this->event->trigger('worker.websocket.Pong');
|
||||
$this->schedulePing();
|
||||
break;
|
||||
default:
|
||||
$this->websocket->close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* "onClose" listener.
|
||||
*/
|
||||
public function onClose()
|
||||
{
|
||||
Timer::del($this->pingTimeoutTimer);
|
||||
Timer::del($this->pingIntervalTimer);
|
||||
$this->event->trigger('worker.websocket.Close');
|
||||
}
|
||||
|
||||
protected function onConnect($data = null)
|
||||
{
|
||||
try {
|
||||
$this->event->trigger('worker.websocket.Connect', $data);
|
||||
$packet = Packet::create(Packet::CONNECT);
|
||||
if ($this->eio >= 4) {
|
||||
$packet->data = ['sid' => base64_encode(uniqid())];
|
||||
}
|
||||
} catch (Exception $exception) {
|
||||
$packet = Packet::create(Packet::CONNECT_ERROR, [
|
||||
'data' => ['message' => $exception->getMessage()],
|
||||
]);
|
||||
}
|
||||
|
||||
$this->push($packet);
|
||||
}
|
||||
|
||||
protected function resetPingTimeout($timeout)
|
||||
{
|
||||
Timer::del($this->pingTimeoutTimer);
|
||||
$this->pingTimeoutTimer = Timer::delay($timeout, function () {
|
||||
$this->websocket->close();
|
||||
});
|
||||
}
|
||||
|
||||
protected function schedulePing()
|
||||
{
|
||||
Timer::del($this->pingIntervalTimer);
|
||||
$this->pingIntervalTimer = Timer::delay($this->pingInterval, function () {
|
||||
$this->push(EnginePacket::ping());
|
||||
$this->resetPingTimeout($this->pingTimeout);
|
||||
});
|
||||
}
|
||||
|
||||
public function encodeMessage($message)
|
||||
{
|
||||
if ($message instanceof WsEvent) {
|
||||
$message = Packet::create(Packet::EVENT, [
|
||||
'data' => array_merge([$message->type], $message->data),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($message instanceof Packet) {
|
||||
$message = EnginePacket::message($message->toString());
|
||||
}
|
||||
|
||||
if ($message instanceof EnginePacket) {
|
||||
$message = $message->toString();
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
protected function push($data)
|
||||
{
|
||||
$this->websocket->push($data);
|
||||
}
|
||||
|
||||
}
|
||||
134
src/websocket/socketio/Packet.php
Normal file
134
src/websocket/socketio/Packet.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace think\worker\websocket\socketio;
|
||||
|
||||
/**
|
||||
* Class Packet
|
||||
*/
|
||||
class Packet
|
||||
{
|
||||
/**
|
||||
* Socket.io packet type `connect`.
|
||||
*/
|
||||
const CONNECT = 0;
|
||||
|
||||
/**
|
||||
* Socket.io packet type `disconnect`.
|
||||
*/
|
||||
const DISCONNECT = 1;
|
||||
|
||||
/**
|
||||
* Socket.io packet type `event`.
|
||||
*/
|
||||
const EVENT = 2;
|
||||
|
||||
/**
|
||||
* Socket.io packet type `ack`.
|
||||
*/
|
||||
const ACK = 3;
|
||||
|
||||
/**
|
||||
* Socket.io packet type `connect_error`.
|
||||
*/
|
||||
const CONNECT_ERROR = 4;
|
||||
|
||||
/**
|
||||
* Socket.io packet type 'binary event'
|
||||
*/
|
||||
const BINARY_EVENT = 5;
|
||||
|
||||
/**
|
||||
* Socket.io packet type `binary ack`. For acks with binary arguments.
|
||||
*/
|
||||
const BINARY_ACK = 6;
|
||||
|
||||
public $type;
|
||||
public $nsp = '/';
|
||||
public $data = null;
|
||||
public $id = null;
|
||||
|
||||
public function __construct(int $type)
|
||||
{
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
public static function create($type, array $decoded = [])
|
||||
{
|
||||
$new = new self($type);
|
||||
$new->id = $decoded['id'] ?? null;
|
||||
if (isset($decoded['nsp'])) {
|
||||
$new->nsp = $decoded['nsp'] ?: '/';
|
||||
} else {
|
||||
$new->nsp = '/';
|
||||
}
|
||||
$new->data = $decoded['data'] ?? null;
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function toString()
|
||||
{
|
||||
$str = '' . $this->type;
|
||||
if ($this->nsp && '/' !== $this->nsp) {
|
||||
$str .= $this->nsp . ',';
|
||||
}
|
||||
|
||||
if ($this->id !== null) {
|
||||
$str .= $this->id;
|
||||
}
|
||||
|
||||
if (null !== $this->data) {
|
||||
$str .= json_encode($this->data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
|
||||
public static function fromString(string $str)
|
||||
{
|
||||
$i = 0;
|
||||
|
||||
$packet = new Packet((int) substr($str, 0, 1));
|
||||
|
||||
// look up namespace (if any)
|
||||
if ('/' === substr($str, $i + 1, 1)) {
|
||||
$nsp = '';
|
||||
while (++$i) {
|
||||
$c = substr($str, $i, 1);
|
||||
if (',' === $c) {
|
||||
break;
|
||||
}
|
||||
$nsp .= $c;
|
||||
if ($i === strlen($str)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$packet->nsp = $nsp;
|
||||
} else {
|
||||
$packet->nsp = '/';
|
||||
}
|
||||
|
||||
// look up id
|
||||
$next = substr($str, $i + 1, 1);
|
||||
if ('' !== $next && is_numeric($next)) {
|
||||
$id = '';
|
||||
while (++$i) {
|
||||
$c = substr($str, $i, 1);
|
||||
if (null == $c || !is_numeric($c)) {
|
||||
--$i;
|
||||
break;
|
||||
}
|
||||
$id .= substr($str, $i, 1);
|
||||
if ($i === strlen($str)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$packet->id = intval($id);
|
||||
}
|
||||
|
||||
// look up json data
|
||||
if (substr($str, ++$i, 1)) {
|
||||
$packet->data = json_decode(substr($str, $i), true);
|
||||
}
|
||||
|
||||
return $packet;
|
||||
}
|
||||
}
|
||||
2
tests/Pest.php
Normal file
2
tests/Pest.php
Normal file
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
define('STUB_DIR', realpath(__DIR__ . '/stub'));
|
||||
153
tests/feature/HttpTest.php
Normal file
153
tests/feature/HttpTest.php
Normal file
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Cookie\CookieJar;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
$process = null;
|
||||
beforeAll(function () use (&$process) {
|
||||
$process = new Process(['php', 'think', 'worker'], STUB_DIR, [
|
||||
'PHP_WEBSOCKET_ENABLE' => 'false',
|
||||
'PHP_QUEUE_ENABLE' => 'false',
|
||||
]);
|
||||
$process->start();
|
||||
$wait = 0;
|
||||
|
||||
while (!$process->getOutput()) {
|
||||
$wait++;
|
||||
if ($wait > 30) {
|
||||
throw new Exception('server start failed');
|
||||
}
|
||||
sleep(1);
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(function () use (&$process) {
|
||||
echo $process->getOutput();
|
||||
$process->stop();
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
$this->httpClient = new Client([
|
||||
'base_uri' => 'http://127.0.0.1:8080',
|
||||
'cookies' => true,
|
||||
'http_errors' => false,
|
||||
'timeout' => 1,
|
||||
]);
|
||||
});
|
||||
|
||||
it('callback route', function () {
|
||||
$response = $this->httpClient->get('/');
|
||||
|
||||
expect($response->getStatusCode())
|
||||
->toBe(200)
|
||||
->and($response->getBody()->getContents())
|
||||
->toBe('hello world');
|
||||
});
|
||||
|
||||
it('controller route', function () {
|
||||
$jar = new CookieJar();
|
||||
|
||||
$response = $this->httpClient->get('/test', ['cookies' => $jar]);
|
||||
|
||||
expect($response->getStatusCode())
|
||||
->toBe(200)
|
||||
->and($response->getBody()->getContents())
|
||||
->toBe('test')
|
||||
->and($jar->getCookieByName('name')->getValue())
|
||||
->toBe('think');
|
||||
});
|
||||
|
||||
it('json post', function () {
|
||||
|
||||
$data = [
|
||||
'name' => 'think',
|
||||
];
|
||||
$response = $this->httpClient->post('/json', [
|
||||
'json' => $data,
|
||||
]);
|
||||
|
||||
expect($response->getStatusCode())
|
||||
->toBe(200)
|
||||
->and($response->getBody()->getContents())
|
||||
->toBe(json_encode($data));
|
||||
});
|
||||
|
||||
it('put and delete request', function () {
|
||||
$response = $this->httpClient->put('/');
|
||||
|
||||
expect($response->getStatusCode())
|
||||
->toBe(200)
|
||||
->and($response->getBody()->getContents())
|
||||
->toBe('put');
|
||||
|
||||
$response = $this->httpClient->delete('/');
|
||||
|
||||
expect($response->getStatusCode())
|
||||
->toBe(200)
|
||||
->and($response->getBody()->getContents())
|
||||
->toBe('delete');
|
||||
});
|
||||
|
||||
it('file response', function () {
|
||||
$response = $this->httpClient->get('/static/asset.txt');
|
||||
|
||||
expect($response->getStatusCode())
|
||||
->toBe(200)
|
||||
->and($response->getBody()->getContents())
|
||||
->toBe(file_get_contents(STUB_DIR . '/public/asset.txt'));
|
||||
});
|
||||
|
||||
it('sse', function () {
|
||||
$response = $this->httpClient->get('/sse', [
|
||||
'stream' => true,
|
||||
'timeout' => 3,
|
||||
]);
|
||||
|
||||
$body = $response->getBody();
|
||||
|
||||
$buffer = '';
|
||||
while (!$body->eof()) {
|
||||
$text = $body->read(1);
|
||||
if ($text == "\r") {
|
||||
continue;
|
||||
}
|
||||
$buffer .= $text;
|
||||
if ($text == "\n") {
|
||||
if ($buffer != "\n") {
|
||||
expect($buffer)->toStartWith('data: ');
|
||||
}
|
||||
$buffer = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('hot update', function () {
|
||||
$response = $this->httpClient->get('/hot');
|
||||
|
||||
expect($response->getStatusCode())
|
||||
->toBe(404);
|
||||
|
||||
$route = <<<PHP
|
||||
<?php
|
||||
|
||||
use think\\facade\\Route;
|
||||
|
||||
Route::get('/hot', function () {
|
||||
return 'hot';
|
||||
});
|
||||
PHP;
|
||||
|
||||
file_put_contents(STUB_DIR . '/route/hot.php', $route);
|
||||
|
||||
sleep(2);
|
||||
|
||||
$response = $this->httpClient->get('/hot');
|
||||
|
||||
expect($response->getStatusCode())
|
||||
->toBe(200)
|
||||
->and($response->getBody()->getContents())
|
||||
->toBe('hot');
|
||||
})->after(function () {
|
||||
@unlink(STUB_DIR . '/route/hot.php');
|
||||
})->skipOnWindows();
|
||||
5
tests/feature/QueueTest.php
Normal file
5
tests/feature/QueueTest.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
it('queue', function () {
|
||||
|
||||
});
|
||||
77
tests/feature/WebsocketTest.php
Normal file
77
tests/feature/WebsocketTest.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use React\EventLoop\Loop;
|
||||
use Symfony\Component\Process\Process;
|
||||
use function Ratchet\Client\connect;
|
||||
|
||||
$process = null;
|
||||
beforeAll(function () use (&$process) {
|
||||
$process = new Process(['php', 'think', 'worker'], STUB_DIR, [
|
||||
'PHP_WEBSOCKET_ENABLE' => 'true',
|
||||
'PHP_QUEUE_ENABLE' => 'false',
|
||||
'PHP_HOT_ENABLE' => 'false',
|
||||
]);
|
||||
$process->start();
|
||||
$wait = 0;
|
||||
|
||||
while (!$process->getOutput()) {
|
||||
$wait++;
|
||||
if ($wait > 30) {
|
||||
throw new Exception('server start failed');
|
||||
}
|
||||
sleep(1);
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(function () use (&$process) {
|
||||
echo $process->getOutput();
|
||||
$process->stop();
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
$this->httpClient = new Client([
|
||||
'base_uri' => 'http://127.0.0.1:8080',
|
||||
'cookies' => true,
|
||||
'http_errors' => false,
|
||||
'timeout' => 1,
|
||||
]);
|
||||
});
|
||||
|
||||
it('http', function () {
|
||||
$response = $this->httpClient->get('/');
|
||||
|
||||
expect($response->getStatusCode())
|
||||
->toBe(200)
|
||||
->and($response->getBody()->getContents())
|
||||
->toBe('hello world');
|
||||
});
|
||||
|
||||
it('websocket', function () {
|
||||
$connected = 0;
|
||||
$messages = [];
|
||||
connect('ws://127.0.0.1:8080/websocket')
|
||||
->then(function (\Ratchet\Client\WebSocket $conn) use (&$connected, &$messages) {
|
||||
$connected++;
|
||||
$conn->on('message', function ($msg) use ($conn, &$messages) {
|
||||
$messages[] = (string) $msg;
|
||||
$conn->close();
|
||||
});
|
||||
});
|
||||
|
||||
connect('ws://127.0.0.1:8080/websocket')
|
||||
->then(function (\Ratchet\Client\WebSocket $conn) use (&$connected, &$messages) {
|
||||
$connected++;
|
||||
$conn->on('message', function ($msg) use ($conn, &$messages) {
|
||||
$messages[] = (string) $msg;
|
||||
$conn->close();
|
||||
});
|
||||
|
||||
$conn->send('hello');
|
||||
});
|
||||
|
||||
Loop::get()->run();
|
||||
|
||||
expect($connected)->toBe(2);
|
||||
expect($messages)->toBe(['hello', 'hello']);
|
||||
});
|
||||
1
tests/stub/.env
Normal file
1
tests/stub/.env
Normal file
@@ -0,0 +1 @@
|
||||
APP_DEBUG=true
|
||||
20
tests/stub/app/controller/Index.php
Normal file
20
tests/stub/app/controller/Index.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace app\controller;
|
||||
|
||||
use think\facade\Cookie;
|
||||
use think\Request;
|
||||
|
||||
class Index
|
||||
{
|
||||
public function test()
|
||||
{
|
||||
Cookie::set('name', 'think');
|
||||
return 'test';
|
||||
}
|
||||
|
||||
public function json(Request $request)
|
||||
{
|
||||
return json($request->post());
|
||||
}
|
||||
}
|
||||
4
tests/stub/config/app.php
Normal file
4
tests/stub/config/app.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
return [
|
||||
'error_message' => 'error',
|
||||
];
|
||||
9
tests/stub/config/cache.php
Normal file
9
tests/stub/config/cache.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
return [
|
||||
'default' => 'file',
|
||||
'stores' => [
|
||||
'file' => [
|
||||
'type' => 'File',
|
||||
],
|
||||
]
|
||||
];
|
||||
40
tests/stub/config/queue.php
Normal file
40
tests/stub/config/queue.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006-2016 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: yunwuxin <448901948@qq.com>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
return [
|
||||
'default' => 'redis',
|
||||
'connections' => [
|
||||
'sync' => [
|
||||
'type' => 'sync',
|
||||
],
|
||||
'database' => [
|
||||
'type' => 'database',
|
||||
'queue' => 'default',
|
||||
'table' => 'jobs',
|
||||
'connection' => null,
|
||||
],
|
||||
'redis' => [
|
||||
'type' => 'redis',
|
||||
'queue' => 'default',
|
||||
'host' => env('REDIS_HOST', 'redis'),
|
||||
'port' => env('REDIS_PORT', 6379),
|
||||
'password' => '',
|
||||
'select' => 0,
|
||||
'timeout' => 0,
|
||||
'persistent' => true,
|
||||
'retry_after' => 600,
|
||||
],
|
||||
],
|
||||
'failed' => [
|
||||
'type' => 'none',
|
||||
'table' => 'failed_jobs',
|
||||
],
|
||||
];
|
||||
45
tests/stub/config/worker.php
Normal file
45
tests/stub/config/worker.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: liu21st <liu21st@gmail.com>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
use think\worker\websocket\Handler;
|
||||
|
||||
return [
|
||||
'http' => [
|
||||
'enable' => env('HTTP_ENABLE', true),
|
||||
'host' => '0.0.0.0',
|
||||
'port' => 8080,
|
||||
'worker_num' => 2,
|
||||
'options' => [],
|
||||
],
|
||||
'websocket' => [
|
||||
'enable' => env('WEBSOCKET_ENABLE', true),
|
||||
'handler' => Handler::class,
|
||||
'ping_interval' => 25000,
|
||||
'ping_timeout' => 60000,
|
||||
],
|
||||
//队列
|
||||
'queue' => [
|
||||
'enable' => env('QUEUE_ENABLE', true),
|
||||
'workers' => [
|
||||
'default' => [],
|
||||
],
|
||||
],
|
||||
//共享数据
|
||||
'conduit' => [
|
||||
'type' => 'socket',
|
||||
],
|
||||
'hot_update' => [
|
||||
'enable' => env('HOT_ENABLE', true),
|
||||
'name' => ['*.php'],
|
||||
'include' => [app_path(), config_path(), root_path('route')],
|
||||
'exclude' => [],
|
||||
],
|
||||
];
|
||||
1
tests/stub/public/asset.txt
Normal file
1
tests/stub/public/asset.txt
Normal file
@@ -0,0 +1 @@
|
||||
Asset
|
||||
51
tests/stub/route/app.php
Normal file
51
tests/stub/route/app.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
use think\facade\Route;
|
||||
|
||||
Route::get('/', function () {
|
||||
return 'hello world';
|
||||
});
|
||||
|
||||
Route::put('/', function () {
|
||||
return 'put';
|
||||
});
|
||||
|
||||
Route::delete('/', function () {
|
||||
return 'delete';
|
||||
});
|
||||
|
||||
Route::get('/sse', function () {
|
||||
|
||||
$generator = function () {
|
||||
foreach (range(0, 9) as $event) {
|
||||
yield 'data: ' . json_encode($event) . "\n\n";
|
||||
}
|
||||
|
||||
yield "data: [DONE]\n\n";
|
||||
};
|
||||
|
||||
$response = new \think\worker\response\Iterator($generator());
|
||||
|
||||
return $response->header([
|
||||
'Content-Type' => 'text/event-stream',
|
||||
'Cache-Control' => 'no-cache, must-revalidate',
|
||||
]);
|
||||
});
|
||||
|
||||
Route::get('/websocket', function () {
|
||||
return (new \think\worker\response\Websocket())
|
||||
->onOpen(function (\think\worker\Websocket $websocket) {
|
||||
$websocket->join('foo');
|
||||
})
|
||||
->onMessage(function (\think\worker\Websocket $websocket, \think\worker\websocket\Frame $frame) {
|
||||
$websocket->to('foo')->push($frame->data);
|
||||
});
|
||||
});
|
||||
|
||||
Route::get('test', 'index/test');
|
||||
Route::post('json', 'index/json');
|
||||
|
||||
Route::get('static/:path', function (string $path) {
|
||||
$filename = public_path() . $path;
|
||||
return new \think\worker\response\File($filename);
|
||||
})->pattern(['path' => '.*\.\w+$']);
|
||||
2
tests/stub/runtime/.gitignore
vendored
Normal file
2
tests/stub/runtime/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
12
tests/stub/think
Normal file
12
tests/stub/think
Normal file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use think\App;
|
||||
|
||||
require __DIR__ . '/../../vendor/autoload.php';
|
||||
|
||||
$app = new App(__DIR__);
|
||||
|
||||
$app->console->addCommands([\think\worker\command\Server::class]);
|
||||
|
||||
$app->console->run();
|
||||
Reference in New Issue
Block a user