swoole初体验

49 阅读6分钟

通过项目来接触swoole

我学习swoole已经有段时间了。但是一直没有去实际应用。今天有一个项目。我打算用swoole来实际应用一下。

项目其实很简单,就是用户进入小程序后显示系统通知。用户点击通知弹窗的 "关闭" 或者 "知道了" 按钮。弹窗关闭,记录用户行为。这个弹窗显示与否,需要和后端交互才能知道。所以这里我决定用swoole的实现。

为什么不用api来获取呢?这里考虑到两个方面

  1. 考虑到后面的功能需要用到swoole。所以先进行尝试一下。积累经验
  2. 我们知道api接口,是会发起一次http请求。会消耗一定的资源(当然现在还不明显)。利用swoole的长连接这样,既可以节省资源。又可以对资源的复用。

好! 废话不多说。上代码。

代码

$webSocketServer = new Swoole\WebSocket\Server(
    '0.0.0.0',
    xxxx,
    SWOOLE_PROCESS,
SWOOLE_SOCK_TCP | SWOOLE_SSL  // ⚠️ 必须开启 SSL
);

$webSocketServer->set([
    'worker_num' => 1,
    'max_wait_time' => 10, // 默认 3 秒,这里延长
    'ssl_cert_file' => '/usr/share/nginx/www/lift_memories/compose/conf/nginx/ssl/fullchain.cer',
    'ssl_key_file'  => '/usr/share/nginx/www/lift_memories/compose/conf/nginx/ssl/tonyfeng.xyz.key',
    'ssl_protocols' => SWOOLE_SSL_TLSv1_2 | SWOOLE_SSL_TLSv1_3,
    'ssl_ciphers' => 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256'
]);

echo "✅ WebSocket Server listening at wss://0.0.0.0:xxxx\n";

$webSocketServer->on('workerStart', function ($ser, $workerId){
    if($ser->taskworker) return;

    require __DIR__ . "/vendor/autoload.php";

    $app = new \think\App(getcwd());
    $app->setEnvName(getenv('APP_ENV'))->initialize();
    $ser->app = $app;

    $redis = new Redis();
    $redis->connect('redis', 6379); // 修改为你的 Redis 地址
// $redis->auth('your_password'); // 如果有密码,记得加上
    $prefix = "flow:";

    $keys = $redis->keys("{$prefix}ws_online:*");
    if (!empty($keys)) {
        foreach ($keys as $key) {
            $redis->del($key);
        }
        echo "🧹 已清除旧的 Redis WebSocket 映射,共 " . count($keys) . " 项\n";
    } else {
        echo "✅ 没有旧的 WebSocket 映射,无需清理\n";
    }
    $ser->redis = $redis;
    $ser->prefix = $prefix;
    if($workerId === 0){
        Swoole\Timer::tick(1000, function () use($ser){
            try{
                $redis = $ser->redis;
                $prefix = $ser->prefix;

                while ($data = $redis->lpop("{$prefix}ws_push_queue")) {
                    $msg = json_decode($data, true);
                    $openid = $msg['openid'] ?? '';
                    $activityId = $msg['activity_id'] ?? '';
                    $hexiaoTime = $msg['hexiao_time'] ?? '';
                    $status = $msg['status'] ?? '';

                    $key = "{$prefix}ws_online:{$openid}:{$activityId}";
                    $fd = $redis->get($key);

                    if ($fd && $ser->isEstablished((int)$fd)) {
                        $ser->push((int)$fd, json_encode(['data' => [
                            'msg_type' => 'hexiao',
                            'status' => $status,
                            'hexiao_time' => $hexiaoTime
                        ]]));
                        echo "✅ 推送成功:fd={$fd}, openid={$openid}, activity_id={$activityId}\n";
                    } else {
                        echo "❌ 无法推送,fd 不存在或连接失效:{$key}\n";
                    }
                }
            }catch (\Throwable $e){
                echo "[定时器异常] " . $e->getMessage() . "\n";
            }
        });
    }

    $dbConfig = [
        'host' => env('database.hostname'),
        'port' => env('database.hostport'),
        'username' => env('database.username'),
        'password' => env('database.password'),
        'database' => env('database.database'),
        'charset' => env('database.charset'),
    ];

    $ser->mysqlPool = new \app\flow\common\lib\MySQLPool($dbConfig, 10); // 连接池大小 10


    echo "Worker {$workerId} MySQL 连接池初始化完成\n";
});

$webSocketServer->on('WorkerExit', function ($ser, $workerId){
    // 停掉定时器
    Swoole\Timer::clearAll();
    // 关闭数据库连接
    if(isset($ser->mysqlPool)){
        $ser->mysqlPool->closeAll();
    }

    if(isset($ser->redis)){
        $ser->redis->close();
    }
});

$webSocketServer->on('open', function ($ser, $req){
   echo "客户端IP:" . $req->server['remote_addr'] . PHP_EOL;
   $fd = $req->fd;
    echo "{$fd}连接成功" . PHP_EOL;
});

$webSocketServer->on('message', function ($ser, $frame){
    echo "收到消息:{$frame->data}" . PHP_EOL;
    if($frame->data === 'ping'){
        $ser->push($frame->fd, 'pong');
        return;
    }

    $tasks = json_decode($frame->data, true);

    switch ($tasks['type']){
        case 'sys_messages':
            if (empty($tasks['token'])) {
                $ser->push($frame->fd, json_encode(['code' => 401, 'msg' => '未登录']));
                return;
            }
            try{
                $chatService = new \app\flow\servers\ChatService($tasks['token']);
                $mysql = $ser->mysqlPool->get();
                if(!$mysql){
                    $ser->push($frame->fd, '数据库连接失败,请稍后再试');
                    return;
                }
                $result = $chatService->getSystemMessages($mysql);
                $ser->push($frame->fd, $result);
            } finally {
                if(isset($mysql)){
                    $ser->mysqlPool->put($mysql);
                }
            }

            break;
    }
});

$webSocketServer->on("close", function ($ser, $fd){
    echo "用户{$fd}断开了连接\n";
    $keys = $ser->redis->keys('flow:ws_online:*');
    if (!empty($keys)) {
        foreach ($keys as $key) {
            $fdd = intval($ser->redis->get($key));
            if($fdd === $fd){
                $ser->redis->del($key);
            }
        }
        echo "🧹 已清除旧的 Redis WebSocket 映射" . PHP_EOL;
    }
});

$webSocketServer->start();

代码解释

这个是开启一个websocket 服务
【在这里说一下。swoole 可以作为 tcp udp http  WebSocket  MQTT  Process(进程)协程  服务器】
里面的参数分别为监听的 IP 地址 0.0.0.0 代表 任何人都可以访问,是公开的
然后就是端口号。这个根据自己的业务进行设置即可
 SWOOLE_PROCESS:多进程模式,Swoole 会以多进程方式运行服务器,主进程加多个工作进程,适合高并发。
 SWOOLE_SOCK_TCP 表示使用 TCP 协议的套接字。TCP 是面向连接、可靠的传输协议,WebSocket 就是基于 TCP 连接的
 SWOOLE_SSL 表示启用 SSL/TLS 加密,服务器会启用加密传输,支持 wss://(安全的 WebSocket 连接)。
 这里说一下什么是套接字?
 【套接字就是两个端点直接建立的可信的信道。那端点是什么呢?其实端点就是ip+端口组成的。端点之间进行通信,就必须建立一个可信的通道。这个通道称之为套接字。】

$webSocketServer = new Swoole\WebSocket\Server(
    '0.0.0.0',
    xxxx,
    SWOOLE_PROCESS,
SWOOLE_SOCK_TCP | SWOOLE_SSL  // ⚠️ 必须开启 SSL
);
worker_num:工作进程数,决定了服务器同时能有多少个进程处理请求。更多进程通常意味着更高的并发处理能力
max_wait_time:工作进程最大等待时间,单位秒。Swoole 会优先关闭空闲时间超过该值的工作进程
ssl_cert_file:SSL 证书文件路径
ssl_key_file:SSL 证书私钥文件路径
ssl_protocols:启用的 SSL/TLS 协议版本(这个不要设置的过低)
ssl_ciphers:加密套件,指定可用的加密算法,提升安全性。(这个可以根据客户端支持的加密套件进行补充)
$webSocketServer->set([
    'worker_num' => 1,
    'max_wait_time' => 10, // 默认 3 秒,这里延长
    'ssl_cert_file' => '/usr/share/nginx/www/lift_memories/compose/conf/nginx/ssl/fullchain.cer',
    'ssl_key_file'  => '/usr/share/nginx/www/lift_memories/compose/conf/nginx/ssl/tonyfeng.xyz.key',
    'ssl_protocols' => SWOOLE_SSL_TLSv1_2 | SWOOLE_SSL_TLSv1_3,
    'ssl_ciphers' => 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256'
]);
这里可以理解为Worker 进程启动时的初始化(我说明一下:这里框架我用的是thinkphp6)(每个 Worker 启动时都会触发一次)。
这里解释一下什么是worker进程:
一个swoole server 启动的时候 会分为好几个进程
Master 进程 : 启动时第一个运行的进程。负责监听端口、管理其他进程,不直接处理业务逻辑。
Manager 进程:管理和监控 Worker 进程、Task Worker 进程,如果某个 Worker 挂了,会负责拉起来一个新的
Worker 进程:真正处理客户端发来请求的进程,也就是真正干活的
Task Worker 进程:专门处理耗时任务的进程,比如写文件、发邮件、视频处理等。
首先判断 如果是任务进程 直接返回 不执行后续代码(因为 Task Worker 主要负责异步任务,不需要加载 TP6 核心、Redis、MySQL 连接池等业务资源。)
后面是集成tp6框架核心,连接redis.开启了一个定时任务,开启任务时 我这里做了一个判断
当进程等于 0 时 开启任务。当然我这里就一个进程。这里的进程的多少 是根据上一步配置进程数的数量决定的。
如果 worker_num > 1,每个 Worker 都会执行 workerStart,那么定时任务也会执行多次。
为了避免重复执行,这里限定只有 Worker 0 负责跑定时任务

之后就是启动了一个mysql 连接池。(连接池我放到下一篇文章来说)
$webSocketServer->on('workerStart', function ($ser, $workerId){
    if($ser->taskworker) return;

    require __DIR__ . "/vendor/autoload.php";

    $app = new \think\App(getcwd());
    $app->setEnvName(getenv('APP_ENV'))->initialize();
    $ser->app = $app;

    $redis = new Redis();
    $redis->connect('redis', 6379); // 修改为你的 Redis 地址
// $redis->auth('your_password'); // 如果有密码,记得加上
    $prefix = "flow:";

    $keys = $redis->keys("{$prefix}ws_online:*");
    if (!empty($keys)) {
        foreach ($keys as $key) {
            $redis->del($key);
        }
        echo "🧹 已清除旧的 Redis WebSocket 映射,共 " . count($keys) . " 项\n";
    } else {
        echo "✅ 没有旧的 WebSocket 映射,无需清理\n";
    }
    $ser->redis = $redis;
    $ser->prefix = $prefix;
    if($workerId === 0){
        Swoole\Timer::tick(1000, function () use($ser){
            try{
                $redis = $ser->redis;
                $prefix = $ser->prefix;

                while ($data = $redis->lpop("{$prefix}ws_push_queue")) {
                    $msg = json_decode($data, true);
                    $openid = $msg['openid'] ?? '';
                    $activityId = $msg['activity_id'] ?? '';
                    $hexiaoTime = $msg['hexiao_time'] ?? '';
                    $status = $msg['status'] ?? '';

                    $key = "{$prefix}ws_online:{$openid}:{$activityId}";
                    $fd = $redis->get($key);

                    if ($fd && $ser->isEstablished((int)$fd)) {
                        $ser->push((int)$fd, json_encode(['data' => [
                            'msg_type' => 'hexiao',
                            'status' => $status,
                            'hexiao_time' => $hexiaoTime
                        ]]));
                        echo "✅ 推送成功:fd={$fd}, openid={$openid}, activity_id={$activityId}\n";
                    } else {
                        echo "❌ 无法推送,fd 不存在或连接失效:{$key}\n";
                    }
                }
            }catch (\Throwable $e){
                echo "[定时器异常] " . $e->getMessage() . "\n";
            }
        });
    }

    $dbConfig = [
        'host' => env('database.hostname'),
        'port' => env('database.hostport'),
        'username' => env('database.username'),
        'password' => env('database.password'),
        'database' => env('database.database'),
        'charset' => env('database.charset'),
    ];

    $ser->mysqlPool = new \app\flow\common\lib\MySQLPool($dbConfig, 10); // 连接池大小 10


    echo "Worker {$workerId} MySQL 连接池初始化完成\n";
});
这个主要是当服务停止时所做的一些操作。
关掉定时器
关闭数据库连接
关闭redis
$webSocketServer->on('WorkerExit', function ($ser, $workerId){
    // 停掉定时器
    Swoole\Timer::clearAll();
    // 关闭数据库连接
    if(isset($ser->mysqlPool)){
        $ser->mysqlPool->closeAll();
    }

    if(isset($ser->redis)){
        $ser->redis->close();
    }
});
这里是客户端成功连接服务端时,可以在这里面写一些相应的逻辑
$webSocketServer->on('open', function ($ser, $req){

   echo "客户端IP:" . $req->server['remote_addr'] . PHP_EOL;
   $fd = $req->fd;
    echo "{$fd}连接成功" . PHP_EOL;
});
这里的是当服务端收到来自客户端的消息时,可根据消息的不同来处理相应的逻辑。并返回消息给客户端。
这里做了心跳的机制。什么是心跳机制?为什么要做?
心跳机制,是用来检测连接是否存活。这里通过客户端发来的ping ,服务端收到后给客户端返回pong 以此来判断 连接没有中断
我这里是根据客户端发来的消息,来对用户是否显示通知进行判断操作。这里连接的数据库。查询数据,并做出相应的判断。
$webSocketServer->on('message', function ($ser, $frame){
    echo "收到消息:{$frame->data}" . PHP_EOL;
    if($frame->data === 'ping'){
        $ser->push($frame->fd, 'pong');
        return;
    }

    $tasks = json_decode($frame->data, true);

    switch ($tasks['type']){
        case 'sys_messages':
            if (empty($tasks['token'])) {
                $ser->push($frame->fd, json_encode(['code' => 401, 'msg' => '未登录']));
                return;
            }
            try{
                $chatService = new \app\flow\servers\ChatService($tasks['token']);
                $mysql = $ser->mysqlPool->get();
                if(!$mysql){
                    $ser->push($frame->fd, '数据库连接失败,请稍后再试');
                    return;
                }
                $result = $chatService->getSystemMessages($mysql);
                $ser->push($frame->fd, $result);
            } finally {
                if(isset($mysql)){
                    $ser->mysqlPool->put($mysql);
                }
            }

            break;
    }
});
这里是当客户端  断开连接的时候触发的。
这里可以看到断开连接后。对redis中的数据做了删除的操作
$webSocketServer->on("close", function ($ser, $fd){
    echo "用户{$fd}断开了连接\n";
    $keys = $ser->redis->keys('flow:ws_online:*');
    if (!empty($keys)) {
        foreach ($keys as $key) {
            $fdd = intval($ser->redis->get($key));
            if($fdd === $fd){
                $ser->redis->del($key);
            }
        }
        echo "🧹 已清除旧的 Redis WebSocket 映射" . PHP_EOL;
    }
});
启动服务
$webSocketServer->start();

优化以及改进

我知道这段代码还有一些问题。和需要改进的地方。比如 客户端断开连接时 mysql 连接 归还。 而且 这里的redis 也需要 连接池等等。也希望 看到的同仁 。多多提意见。刚刚接触swoole的伙伴也可以发表一下意见和建议大家共同学习进步。 ok 今天先到这里 后面 我会继续发表我个人学习开发所得的经验和成果。以及小项目。谢谢!