正文
webman是一款基于workerman开发的高性能HTTP服务框架。 webman用于替代传统的php-fpm架构,提供超高性能可扩展的HTTP服务。 可以用webman开发网站,也可以开发HTTP接口或者微服务。
除此之外,webman还支持自定义进程,可以做workerman能做的任何事情, 例如websocket服务、物联网、游戏、TCP服务、UDP服务、unix socket服务等等。
webman 简单易用,学习成本极低,代码书写与传统框架没有区别。 可以 参考手册 webman手册 进行开发。
数据库
webman数据库默认采用的是 illuminate/database,也就是laravel的数据库,用法与laravel相同。
用户身份验证
webman 中用户身份验证可以使用自带中间件实现。
日志
webman使用 monolog/monolog 处理日志。
提供的方法
Log::log($level, $message, array $context = [])
Log::debug($message, array $context = [])
Log::info($message, array $context = [])
Log::notice($message, array $context = [])
Log::warning($message, array $context = [])
Log::error($message, array $context = [])
Log::critical($message, array $context = [])
Log::alert($message, array $context = [])
Log::emergency($message, array $context = [])
等价于:
$log = Log::channel('default');
$log->log($level, $message, array $context = [])
$log->debug($message, array $context = [])
$log->info($message, array $context = [])
$log->notice($message, array $context = [])
$log->warning($message, array $context = [])
$log->error($message, array $context = [])
$log->critical($message, array $context = [])
$log->alert($message, array $context = [])
$log->emergency($message, array $context = [])
配置
项目config目录下 log.php
文件:
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
return [
'default' => [
'handlers' => [
[
'class' => Monolog\Handler\RotatingFileHandler::class,
'constructor' => [
runtime_path() . '/logs/webman.log',
7, //$maxFiles
Monolog\Logger::DEBUG,
],
'formatter' => [
'class' => Monolog\Formatter\LineFormatter::class,
'constructor' => [ null, 'Y-m-d H:i:s', true],
],
]
],
],
'test' => [
'handlers' => [
[
'class' => Monolog\Handler\RotatingFileHandler::class,
'constructor' => [
runtime_path() . '/logs/test/test.log',
7, //$maxFiles
Monolog\Logger::DEBUG,
],
'formatter' => [
'class' => Monolog\Formatter\LineFormatter::class,
'constructor' => [ null, 'Y-m-d H:i:s', true],
],
]
],
],
];
使用
<?php
namespace app\controller;
use support\Request;
use support\Log;
class Foo
{
public function testUrl(Request $request)
{
$log = Log::channel('test');
$log->info('testUrl', $request->all());
return response('hello test');
}
}
队列
队列可以用Redis实现,webman 中可以使用 Redis消息队列插件 redis-queue 。
具体分析,查看 webman中Redis队列实现分析 。
Socket服务
webman 中可以使用 webman/push 。 webman/push 是一个推送插件,客户端基于订阅模式,兼容 pusher,拥有众多客户端如JS、安卓(java)、IOS(swift)、IOS(Obj-C)、uniapp。 后端推送SDK支持PHP、Node、Ruby、Asp、Java、Python、Go等。使用起来非常简单稳定。适用于消息推送、聊天等诸多即时通讯场景。
安装:
composer require webman/push
在项目 config/plugin/webman/push
目录下自动生成相关配置文件。
app.php
文件:
<?php
return [
'enable' => true,
'websocket' => 'websocket://0.0.0.0:2121',
'api' => 'http://0.0.0.0:2222',
'app_key' => 'dfa7def75912345f88e02526691ee055',
'app_secret' => 'cd88c5033d8e591dd248412345be633f',
'channel_hook' => 'http://127.0.0.1:8787/plugin/webman/push/hook',
'auth' => '/plugin/webman/push/auth'
];
process.php
文件:
<?php
use Webman\Push\Server;
return [
'server' => [
'handler' => Server::class,
'listen' => config('plugin.webman.push.app.websocket'),
'count' => 1, // 必须是1
'reloadable' => false, // 执行reload不重启
'constructor' => [
'api_listen' => config('plugin.webman.push.app.api'),
'app_info' => [
config('plugin.webman.push.app.app_key') => [
'channel_hook' => config('plugin.webman.push.app.channel_hook'),
'app_secret' => config('plugin.webman.push.app.app_secret'),
],
]
]
]
];
route.php
文件:
<?php
/**
* This file is part of webman.
*
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
use support\Request;
use Webman\Route;
use Webman\Push\Api;
/**
* 推送js客户端文件
*/
Route::any('/plugin/webman/push/push.js', function (Request $request) {
return response()->file(base_path().'/vendor/webman/push/src/push.js');
});
/**
* 私有频道鉴权,这里应该使用session辨别当前用户身份,然后确定该用户是否有权限监听channel_name
*/
Route::any(config('plugin.webman.push.app.auth'), function (Request $request) {
$pusher = new Api(config('plugin.webman.push.app.api'), config('plugin.webman.push.app.app_key'), config('plugin.webman.push.app.app_secret'));
$channel_name = $request->post('channel_name');
$session = $request->session();
// 这里应该通过session和channel_name判断当前用户是否有权限监听channel_name
$has_authority = true;
if ($has_authority) {
return response($pusher->socketAuth($channel_name, $request->post('socket_id')));
} else {
return response('Forbidden', 403);
}
});
/**
* 当频道上线以及下线时触发的回调
* 频道上线:是指某个频道从没有连接在线到有连接在线的事件
* 频道下线:是指某个频道的所有连接都断开触发的事件
*/
Route::any(parse_url(config('plugin.webman.push.app.channel_hook'), PHP_URL_PATH), function (Request $request) {
// 没有x-pusher-signature头视为伪造请求
if (!$webhook_signature = $request->header('x-pusher-signature')) {
return response('401 Not authenticated', 401);
}
$body = $request->rawBody();
// 计算签名,$app_secret 是双方使用的密钥,是保密的,外部无从得知
$expected_signature = hash_hmac('sha256', $body, config('plugin.webman.push.app.app_secret'), false);
// 安全校验,如果签名不一致可能是伪造的请求,返回401状态码
if ($webhook_signature !== $expected_signature) {
return response('401 Not authenticated', 401);
}
// 这里存储这上线 下线的channel数据
$payload = json_decode($body, true);
$channels_online = $channels_offline = [];
foreach ($payload['events'] as $event) {
if ($event['name'] === 'channel_added') {
$channels_online[] = $event['channel'];
} else if ($event['name'] === 'channel_removed') {
$channels_offline[] = $event['channel'];
}
}
// 业务根据需要处理上下线的channel,例如将在线状态写入数据库,通知其它channel等
// 上线的所有channel
echo 'online channels: ' . implode(',', $channels_online) . "\n";
// 下线的所有channel
echo 'offline channels: ' . implode(',', $channels_offline) . "\n";
return 'OK';
});
服务端推送
客户端订阅某个频道,服务端调用API接口推送。
调用 changed/user
Url地址时 socket 通知 订阅客户 事件信息 举例:
<?php
namespace app\controller;
use support\Request;
use Webman\Push\Api;
class Changed
{
/**
* 用户
* @param Request $request
* @return \support\Response
*/
public function user(Request $request)
{
$api = new Api(
// webman下可以直接使用config获取配置,非webman环境需要手动写入相应配置
config('plugin.webman.push.app.api'),
config('plugin.webman.push.app.app_key'),
config('plugin.webman.push.app.app_secret')
);
// 给订阅 user-1 的所有客户端推送 message 事件的消息
$api->trigger('user-1', 'message', [
'content' => $request->input("content")
]);
return json(["code" => 200, "msg" => "ok", "data" => []]);
}
}
自定义进程
webman 中 自定义进程 可以创建定制化进程。
自定义非监听进程例子
新建 process/FindUserAuthInfo.php
:
<?php
namespace process;
use Workerman\Timer;
use support\Db;
class FindUserAuthInfo
{
public function onWorkerStart()
{
// 每隔10秒检查一次数据库是否有新用户注册
Timer::add(10, function(){
Db::table('users')->where('regist_timestamp', '>', time()-10)->get();
});
}
}
在config/process.php
中添加如下配置
return [
// ... 其它进程配置省略
'find_user_auth_info' => [
'handler' => process\FindUserAuthInfo::class
],
];
注意:listen省略则不监听任何端口,count省略则进程数默认为1。
自定义监听例子
新建 process/Pusher.php
:
<?php
namespace process;
use Workerman\Connection\TcpConnection;
class Pusher
{
public function onConnect(TcpConnection $connection)
{
echo "onConnect\n";
}
public function onWebSocketConnect(TcpConnection $connection, $http_buffer)
{
echo "onWebSocketConnect\n";
}
public function onMessage(TcpConnection $connection, $data)
{
$connection->send($data); // 把连接收到的数据发回去。这里应该对$data做相应事件处理
}
public function onClose(TcpConnection $connection)
{
echo "onClose\n";
}
}
注意:所有onXXX属性均为public
在config/process.php
中添加如下配置:
return [
// ... 其它进程配置省略
// websocket_test 为进程名称
'websocket_test' => [
// 这里指定进程类,就是上面定义的Pusher类
'handler' => process\Pusher::class,
'listen' => 'websocket://0.0.0.0:8888',
'count' => 1,
],
];
配置文件说明
一个进程完整的配置定义如下:
return [
// ...
// websocket_test 为进程名称
'websocket_test' => [
// 这里指定进程类
'handler' => process\Pusher::class,
// 监听的协议 ip 及端口 (可选)
'listen' => 'websocket://0.0.0.0:8888',
// 进程数 (可选,默认1)
'count' => 2,
// 进程运行用户 (可选,默认当前用户)
'user' => '',
// 进程运行用户组 (可选,默认当前用户组)
'group' => '',
// 当前进程是否支持reload (可选,默认true)
'reloadable' => true,
// 是否开启reusePort (可选,此选项需要php>=7.0,默认为true)
'reusePort' => true,
// transport (可选,当需要开启ssl时设置为ssl,默认为tcp)
'transport' => 'tcp',
// context (可选,当transport为是ssl时,需要传递证书路径)
'context' => [],
// 进程类构造函数参数,这里为 process\Pusher::class 类的构造函数参数 (可选)
'constructor' => [],
],
];
总结
webman的自定义进程实际上就是workerman的一个简单封装,它将配置与业务分离, 并且将workerman的onXXX回调通过类的方法来实现,其它用法与workerman完全相同。
问题汇总
A facade root has not been set
文档中说 webman数据库默认采用的是 illuminate/database,但使用 \Illuminate\Support\Facades\DB
进行查询时却总是报错:
RuntimeException: A facade root has not been set. /vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php:258
最后查下来webman文档中使用的是自带的 support\Db
,挺坑的。
使用小结
参考资料
webman手册 https://www.workerman.net/doc/webman/
webman源码 https://github.com/walkor/webman
webman官网 https://www.workerman.net/webman
Laravel 8 中文文档 https://learnku.com/docs/laravel/8.x
workerman手册 https://www.workerman.net/doc/workerman/
webman中Redis队列实现分析 https://ibaiyang.github.io/blog/php/2022/05/10/webman中Redis队列实现分析.html