webman使用引导


webman使用引导


正文

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


返回