初始化
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/concerns/ModifyProperty.php
Normal file
18
src/concerns/ModifyProperty.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?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);
|
||||||
|
$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