在Laravel WebSockets中实现连接生命周期管理与业务逻辑绑定

在Laravel WebSockets中实现连接生命周期管理与业务逻辑绑定

本教程将指导您如何在laravel websockets中定制连接的生命周期事件,包括连接的打开与关闭。通过扩展默认的websocket处理器,我们将演示如何捕获并关联客户端的业务上下文(如用户id、订单id),从而在连接断开时执行特定的业务逻辑,例如自动解锁正在处理的订单,实现对应用资源的精确状态管理。

引言:定制WebSocket连接的必要性

在实时应用开发中,WebSocket连接不仅仅是数据传输的通道,它更代表了客户端与服务器之间的一种持续性会话。很多业务场景需要我们能够感知并响应这些会话的生命周期事件,例如:

资源锁定与解锁:当用户打开一个订单页面进行编辑时,锁定该订单以防止其他用户同时修改;当用户关闭页面或断开连接时,自动解锁订单。用户在线状态:实时显示用户的在线或离线状态。协作编辑:跟踪文档编辑者的连接状态,实现实时协作。

Laravel WebSockets 包(Beyondcode 的 Pusher 替代方案)提供了强大的功能,但其默认处理器可能无法满足所有复杂的业务需求。为了实现上述场景,我们需要扩展其核心处理器,介入连接的打开、关闭及消息处理过程,并注入自定义的业务逻辑。

理解WebSocket处理器

Laravel WebSockets 的核心是 BeyondCodeLaravelWebSocketsWebSocketsWebSocketHandler 接口,它定义了处理WebSocket连接生命周期的方法:

onOpen(ConnectionInterface $connection, RequestInterface $request, $appId): 当新的WebSocket连接建立时调用。onClose(ConnectionInterface $connection): 当WebSocket连接关闭时调用。onMessage(ConnectionInterface $connection, MessageInterface $msg): 当收到来自客户端的消息时调用。onError(ConnectionInterface $connection, Exception $e): 当连接发生错误时调用。onPong(ConnectionInterface $connection, MessageInterface $msg): 当收到客户端的 Pong 消息时调用。

通常,我们不是直接实现 WebSocketHandler 接口,而是继承 BeyondCodeLaravelWebSocketsWebSocketsPusherHandler。PusherHandler 已经实现了 Pusher 协议的诸多细节,我们可以在此基础上重写或增强特定方法,以集成我们的业务逻辑。

创建自定义WebSocket处理器

为了定制连接行为,我们首先需要创建一个自定义的处理器类。我们将使用 SplObjectStorage 来存储与每个连接关联的业务上下文数据,因为 ConnectionInterface 对象是唯一的且可以作为 SplObjectStorage 的键。

首先,在 app/WebSockets 目录下创建 CustomWebSocketHandler.php 文件:

// app/WebSockets/CustomWebSocketHandler.phpconnections = new SplObjectStorage();    }    /**     * 当新的WebSocket连接建立时调用。     *     * @param ConnectionInterface $connection     * @param PsrHttpMessageRequestInterface $request     * @param string $appId     * @return void     */    public function onOpen(ConnectionInterface $connection, PsrHttpMessageRequestInterface $request, $appId)    {        // 调用父类的onOpen方法,确保Pusher协议的正常初始化        parent::onOpen($connection, $request, $appId);        Log::info("Connection opened: {$connection->resourceId}");        // 尝试从请求中获取业务上下文,例如用户ID或订单ID        // 客户端可以通过WebSocket URL的查询参数传递这些信息        $queryParams = $request->getQueryParams();        $userId = $queryParams['user_id'] ?? null;        $orderId = $queryParams['order_id'] ?? null;        // 存储连接与业务上下文        $this->connections->attach($connection, [            'resource_id' => $connection->resourceId,            'user_id' => $userId,            'order_id' => $orderId,            'connected_at' => now(),            'channels' => [], // 用于存储该连接订阅的频道        ]);        if ($orderId) {            Log::info("Order {$orderId} is now being processed by user {$userId} via connection {$connection->resourceId}");            // 触发事件以锁定订单            event(new AppEventsOrderLocked($orderId, $userId, $connection->resourceId));        }    }    /**     * 当收到客户端消息时调用。     *     * @param ConnectionInterface $connection     * @param RatchetMessageComponentMessageInterface $msg     * @return void     */    public function onMessage(ConnectionInterface $connection, RatchetMessageComponentMessageInterface $msg)    {        parent::onMessage($connection, $msg);        $payload = json_decode($msg->getPayload());        // 进一步处理消息,例如当客户端订阅特定频道时更新上下文        if (isset($payload->event) && $payload->event === 'pusher:subscribe' && isset($payload->data->channel)) {            $channelName = $payload->data->channel;            $context = $this->connections->offsetGet($connection);            $context['channels'][] = $channelName;            $this->connections->offsetSet($connection, $context); // 更新存储的上下文            Log::info("Connection {$connection->resourceId} subscribed to channel: {$channelName}");            // 如果频道名包含订单ID,可以进一步提取并更新            if (preg_match('/^private-order.(d+)$/', $channelName, $matches)) {                $orderId = $matches[1];                if ($context['order_id'] !== $orderId) {                    Log::warning("Connection {$connection->resourceId} subscribed to order {$orderId}, but initial order was {$context['order_id']}");                    // 可以在这里更新或处理冲突                }            }        }    }    /**     * 当WebSocket连接关闭时调用。     *     * @param ConnectionInterface $connection     * @return void     */    public function onClose(ConnectionInterface $connection)    {        Log::info("Connection closed: {$connection->resourceId}");        // 确保该连接存在于我们的存储中        if ($this->connections->contains($connection)) {            $context = $this->connections->offsetGet($connection);            $userId = $context['user_id'];            $orderId = $context['order_id'];            if ($orderId) {                Log::info("Order {$orderId} is no longer processed by user {$userId} via connection {$connection->resourceId}");                // 触发事件以解锁订单                event(new AppEventsOrderUnlocked($orderId, $userId, $connection->resourceId));            }            // 清理连接上下文            $this->connections->detach($connection);        }        // 调用父类的onClose方法        parent::onClose($connection);    }    /**     * 当连接发生错误时调用。     *     * @param ConnectionInterface $connection     * @param Exception $e     * @return void     */    public function onError(ConnectionInterface $connection, Exception $e)    {        Log::error("Connection error for {$connection->resourceId}: " . $e->getMessage());        parent::onError($connection, $e);    }}

代码说明:

SplObjectStorage $connections: 这是关键,用于存储每个 ConnectionInterface 对象及其关联的业务数据。onOpen 方法:在调用 parent::onOpen 之后,我们从 RequestInterface $request 的查询参数中尝试提取 user_id 和 order_id。将这些信息与 connection 对象一起存储到 $this->connections 中。如果成功获取到 orderId,则触发一个 OrderLocked 事件,通知应用层锁定该订单。onMessage 方法 (可选但推荐):此方法用于处理客户端发送的所有消息。在这里,我们特别关注 pusher:subscribe 事件。当客户端订阅一个频道时,我们可以解析频道名称(例如 private-order.123),从中提取更具体的业务ID,并更新 SplObjectStorage 中该连接的上下文信息。这在初始 onOpen 无法获得所有上下文时非常有用。onClose 方法:在连接关闭时,我们通过 ConnectionInterface $connection 从 $this->connections 中检索之前存储的业务上下文。根据上下文中的 order_id,触发一个 OrderUnlocked 事件,通知应用层解锁订单。最后,从 $this->connections 中移除该连接的上下文,防止内存泄漏。onError 方法: 记录错误信息,以便调试。

定义业务事件

为了解耦 WebSocket 处理器与具体的业务逻辑,我们推荐使用 Laravel 事件。

OrderLocked 事件:

// app/Events/OrderLocked.phporderId = $orderId;        $this->userId = $userId;        $this->connectionId = $connectionId;    }}

OrderUnlocked 事件:

// app/Events/OrderUnlocked.phporderId = $orderId;        $this->userId = $userId;        $this->connectionId = $connectionId;    }}

然后,您可以在 app/Listeners 中创建相应的监听器来处理这些事件,例如更新数据库中的订单状态。

注册自定义处理器

最后一步是告诉 Laravel WebSockets 使用您的自定义处理器。修改 config/websockets.php 文件:

// config/websockets.phpreturn [    // ... 其他配置    'handler' => AppWebSocketsCustomWebSocketHandler::class,    // ... 其他配置];

客户端实现

为了让 onOpen 方法能够获取到 user_id 和 order_id,客户端在建立 WebSocket 连接时需要将这些信息作为查询参数传递。

使用 Laravel Echo 和 JavaScript:

import Echo from 'laravel-echo';window.Pusher = require('pusher-js');// 假设您在后端视图中将这些ID传递给前端const currentUserId = @json(auth()->id());const currentOrderId = @json($order->id ?? null); // 如果在订单页面window.Echo = new Echo({    broadcaster: 'pusher',    key: process.env.MIX_PUSHER_APP_KEY,    wsHost: window.location.hostname,    wsPort: 6001

以上就是在Laravel WebSockets中实现连接生命周期管理与业务逻辑绑定的详细内容,更多请关注php中文网其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1339205.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
在Dockerfile中高效安装PHPUnit:避免常见陷阱与最佳实践
上一篇 2025年12月13日 02:44:12
Laravel多语言路由实践:优雅地为所有路由添加语言参数
下一篇 2025年12月13日 02:44:31

相关推荐

  • composer require-dev和require有什么不同_Composer Require与Require-Dev区别解析

    require用于声明项目运行必需的依赖,如框架、数据库组件和第三方SDK,这些包会随项目部署到生产环境;2. require-dev用于声明仅在开发和测试阶段需要的工具,如PHPUnit、PHPStan、Faker等,不会默认部署到生产环境;3. 安装时composer install根据环境决定…

    2026年5月10日
    1000
  • 修复Django电商项目中AJAX过滤产品列表图片不显示问题

    在Django电商项目中,当使用AJAX动态加载过滤后的产品列表时,常遇到图片无法正常显示的问题。这通常是由于前端模板中图片加载方式(如data-setbg属性结合JavaScript库)与AJAX动态内容更新机制不兼容所致。解决方案是直接在AJAX返回的HTML中使用标准的标签来渲染图片,确保浏览…

    2026年5月10日
    000
  • 开源免费PHP工具 PHP开发效率提升利器

    推荐开源免费PHP开发工具以提升效率:VS Code、Sublime Text轻量高效,PhpStorm专业强大;调试用Xdebug、Kint、Ray;依赖管理选Composer;代码质量工具包括PHPStan、Psalm、PHP_CodeSniffer;数据库管理可用%ignore_a_1%MyA…

    2026年5月10日
    000
  • Matplotlib 地图中多类型图例的创建与优化

    Matplotlib 地图中多类型图例的创建与优化Matplotlib 地图中多类型图例的创建与优化Matplotlib 地图中多类型图例的创建与优化Matplotlib 地图中多类型图例的创建与优化

    本教程旨在解决matplotlib地图可视化中,如何在一个图例中同时展示颜色块(如区域分类)和自定义标记(如特定兴趣点)的问题。文章详细介绍了当传统`patch`对象无法正确显示标记时,如何利用`matplotlib.lines.line2d`创建标记图例句柄,并将其与颜色块图例句柄合并,从而生成一…

    2026年5月10日 用户投稿
    100
  • Golang JSON序列化:控制敏感字段暴露的最佳实践

    本教程探讨golang中如何高效控制结构体字段在json序列化时的可见性。当需要将包含敏感信息的结构体数组转换为json响应时,通过利用`encoding/json`包提供的结构体标签,特别是`json:”-“`,可以轻松实现对特定字段的忽略,从而避免敏感数据泄露,确保api…

    2026年5月10日
    000
  • 怎么在PHP代码中实现图片上传功能_PHP图片上传功能实现与安全处理教程

    首先创建含enctype的HTML表单,再用PHP接收文件,检查目录、移动临时文件,验证类型与大小,生成唯一文件名,并调整php.ini限制以确保上传成功。 如果您尝试在PHP项目中添加图片上传功能,但服务器无法正确接收或保存文件,则可能是由于表单配置、文件处理逻辑或安全限制的问题。以下是实现该功能…

    2026年5月10日
    100
  • 获取日期中的周数:CodeIgniter 教程

    本教程旨在帮助开发者在 CodeIgniter 框架中,从日期字符串中准确提取周数。我们将使用 PHP 内置的 DateTime 类,并提供详细的代码示例和注意事项,确保您能够轻松地在项目中实现此功能。 使用 DateTime 类获取周数 PHP 的 DateTime 类提供了一种便捷的方式来处理日…

    2026年5月10日
    000
  • 比特币新手教程 比特币交易平台有哪些

    比特币是一种去中心化的数字货币,基于区块链技术实现点对点交易,具有匿名性、有限发行和不可篡改等特点;新手可通过交易所购买,P2P交易获得比特币,常用平台包括Binance、OKX和Huobi;交易流程包括注册账户、实名认证、绑定支付方式、充值法币并下单购买,可选择市价单或限价单;比特币存储方式有交易…

    2026年5月10日
    000
  • 如何让动态追加元素的类事件生效?

    如何在追加元素后使其绑定类事件生效 在页面中引入三方 JavaScript 类并通过添加相应 class 来调用事件方法是一种常见的做法。然而,如果通过 JavaScript 追加标签元素,即使添加了对应的 class,事件也可能无法生效。 为了解决这个问题,可以尝试以下步骤: 检查追加的标签是否为…

    2026年5月10日
    000
  • RichHandler与Rich Progress集成:解决显示冲突的教程

    在使用rich库的`richhandler`进行日志输出并同时使用`progress`组件时,可能会遇到显示错乱或溢出问题。这通常是由于为`richhandler`和`progress`分别创建了独立的`console`实例导致的。解决方案是确保日志处理器和进度条组件共享同一个`console`实例…

    2026年5月10日
    000
  • 修复点击时按钮抖动:CSS垂直对齐实践

    本文探讨了在Web开发中,交互式按钮(如播放/暂停按钮)在点击时发生意外垂直位移的问题。通过分析CSS样式变化对元素布局的影响,我们发现这是由于按钮不同状态下的边框样式和内边距改变,以及默认的垂直对齐行为共同作用所致。核心解决方案是利用CSS的vertical-align属性,将其设置为middle…

    2026年5月10日
    000
  • 使用 Jupyter Notebook 进行探索性数据分析

    Jupyter Notebook通过单元格实现代码与Markdown结合,支持数据导入(pandas)、清洗(fillna)、探索(matplotlib/seaborn可视化)、统计分析(describe/corr)和特征工程,便于记录与分享分析过程。 Jupyter Notebook 是进行探索性…

    2026年5月10日
    000
  • php常量怎么用_PHP常量(define/const)定义与使用方法

    PHP中可通过define函数和const关键字定义常量,用于存储不可变值。define适用于全局作用域,支持动态名称和条件定义,如define(‘SITE_NAME’, ‘MyWebsite’);const在编译时生效,语法简洁但限制多,只能在类或全…

    2026年5月10日
    000
  • 如何在HTML中插入表单元素_HTML表单控件与输入类型使用指南

    HTML表单通过标签构建,包含action和method属性定义数据提交目标与方式,常用input类型如text、password、email等适配不同输入需求,配合label、required、placeholder提升可用性,结合textarea、select、button等控件实现完整交互,是…

    2026年5月10日
    000
  • 前端缓存策略与JavaScript存储管理

    根据数据特性选择合适的存储方式并制定清晰的读写与清理逻辑,能显著提升前端性能;合理运用Cookie、localStorage、sessionStorage、IndexedDB及Cache API,结合缓存策略与定期清理机制,可在保证用户体验的同时避免安全与性能隐患。 前端缓存和JavaScript存…

    2026年5月10日
    100
  • HTML5网页如何实现手势操作 HTML5网页移动端交互的处理技巧

    首先利用原生touch事件实现滑动判断,再通过preventDefault解决滚动冲突,接着引入Hammer.js处理复杂手势,最后通过优化点击区域、避免事件冲突和增加视觉反馈提升体验。 在移动端浏览器中,HTML5网页可以通过触摸事件实现手势操作,提升用户体验。虽然原生JavaScript提供了基…

    2026年5月10日
    000
  • 深入理解 Express.js 中 next() 参数的作用与中间件机制

    本文深入探讨 express.js 中间件函数中的 `next()` 参数。它负责将控制权传递给请求-响应周期中的下一个中间件或路由处理程序。文章将详细解释 `next()` 的工作原理、中间件的注册与执行顺序,以及不正确使用 `next()` 可能导致请求挂起的风险,并通过代码示例和实际应用场景,…

    2026年5月10日
    000
  • PHP动态生成表单输入与POST数据获取实践指南

    本教程详细阐述了如何在php中根据动态数据源(如数据库值)生成多个表单输入框,并演示了如何通过post方法准确无误地获取这些动态生成的输入值。文章强调了正确的输入框命名策略,避免了常见的命名误区,并提供了完整的代码示例,确保开发者能够高效处理动态表单数据。 动态生成表单输入 在Web开发中,我们经常…

    2026年5月10日
    000
  • JavaScript 闭包:理解闭包原理与内存泄漏问题

    闭包是函数访问其外部作用域变量的能力,即使外部函数已执行完毕。如 inner 函数引用 outer 中的 count,形成闭包,使变量持久存在。闭包本身无害,但可能因延长变量生命周期导致内存泄漏,例如事件监听器引用大对象时。若未及时清理 DOM 事件或定时器,闭包会阻止垃圾回收,造成内存占用过高。解…

    2026年5月10日
    000
  • JavaScript 动态菜单点击高亮效果实现教程

    本教程详细介绍了如何使用 JavaScript 实现动态菜单的点击高亮功能。通过事件委托和状态管理,当用户点击菜单项时,被点击项会高亮显示(绿色),同时其他菜单项恢复默认样式(白色)。这种方法避免了不必要的DOM操作,提高了性能和代码可维护性,确保了无论点击方向如何,功能都能稳定运行。 动态菜单高亮…

    2026年5月10日
    200

发表回复

登录后才能评论
关注微信