يحتاج تطبيق الدردشة، ولوحة متابعة الطلبات، والإشعارات الفورية، ومؤشرات لوحة التحكم الحية إلى قناة تدفع التغيير إلى المتصفح فور حدوثه. هنا يأتي Laravel Reverb: خادم WebSocket رسمي عالي الأداء، يندمج مباشرة مع منظومة بث الأحداث في Laravel ويتيح تشغيل البنية اللحظية داخل بيئتك بدل الاعتماد الإلزامي على مزوّد خارجي.
يعتمد هذا الدليل على توثيق Laravel 13.x الرسمي المتاح وقت الكتابة. راجع متطلبات إصدار مشروعك قبل تنفيذ الأوامر في بيئة قائمة.
ما Laravel Reverb؟
Laravel Reverb هو خادم WebSocket رسمي مفتوح المصدر لتطبيقات Laravel. يحافظ على اتصالات طويلة العمر بين العملاء والخادم، ويستقبل الرسائل ويوزّع الأحداث على المشتركين بسرعة. وهو متوافق مع بروتوكول Pusher، لذلك يعمل بسلاسة مع طبقة Broadcasting في Laravel ومع مكتبة Laravel Echo في الواجهة الأمامية، ويمكن لأي عميل يدعم بروتوكول Pusher الاتصال به.
في HTTP التقليدي يرسل المتصفح طلبًا ثم ينتظر الاستجابة؛ ولا يعرف بحدوث تغيير جديد إلا بطلب آخر أو عبر polling دوري. أما WebSocket فيبدأ بترقية اتصال HTTP إلى قناة ثنائية الاتجاه تبقى مفتوحة. بعد ذلك يستطيع الخادم دفع الحدث فورًا دون انتظار طلب جديد من العميل، كما يستطيع العميل إرسال رسائل وفق الحدود التي يسمح بها التطبيق.
يصلح Reverb لحالات مثل غرف الدردشة، تتبع حالة الشحن، الإشعارات داخل التطبيق، المزادات الحية، مؤشرات التداول، لوحات العمليات، التعاون بين المستخدمين، وعرض الأشخاص الموجودين داخل صفحة أو غرفة. لكنه ليس بديلًا عن قاعدة البيانات أو الطوابير؛ بل طبقة نقل لحظية تعمل معهما.
متى لا تحتاج Reverb؟
- إذا كانت البيانات تتغير كل بضع دقائق ويقبل المستخدم تأخيرًا بسيطًا، فقد يكون polling بسيط (مثل
wire:pollفي Livewire) أقل كلفة تشغيلية. - إذا كان التدفق من الخادم إلى العميل فقط وبحجم صغير، فقد تكفي Server-Sent Events أو مزوّد مُدار.
- إذا لم يكن لدى فريقك قدرة على تشغيل عملية طويلة العمر ومراقبتها، فخدمة مُدارة (Laravel Cloud أو Pusher أو Ably) أنسب في البداية، ويمكن الانتقال لاحقًا لأن الكود في Laravel لا يتغير تقريبًا.
كيف تعمل Reverb داخل Laravel؟
تتكون الدورة المعتادة من خمسة أجزاء مترابطة:
- حدث Laravel: يحدث تغيير في الخادم، مثل تحديث حالة طلب، ثم يُطلق التطبيق Event يطبّق
ShouldBroadcast. - الطابور: يحوّل Laravel بث الحدث افتراضيًا إلى مهمة queued job حتى لا يطيل زمن استجابة طلب HTTP.
- Broadcasting driver: يرسل اتصال
reverbحمولة الحدث إلى خادم Reverb عبر HTTP API (المسار/apps) باستخدام بيانات التطبيق الموقّعة بالسر. - خادم Reverb: يعرف الاتصالات والقنوات النشطة، ثم يمرّر الرسالة إلى العملاء المشتركين في القناة المقصودة عبر WebSocket (المسار
/app). - Laravel Echo: يشترك المتصفح في القناة ويستمع إلى اسم الحدث، ثم يحدّث الواجهة دون إعادة تحميل الصفحة.
[Controller] → Event::dispatch → [Queue] → [Worker] → HTTP POST /apps/... → [Reverb]
↓ WebSocket /app/...
[Browser + Echo] ← ─────────────────────────────────────────────────────── ┘
↑ للقنوات الخاصة: POST /broadcasting/auth → callback في routes/channels.phpهذه الحدود مهمة عند التشخيص: نجاح إنشاء السجل في قاعدة البيانات لا يعني أن عامل الطابور يعمل، واتصال Echo لا يعني أن المستخدم مخوّل لدخول قناة خاصة. افحص كل طبقة مستقلة.
المكوّنات الأساسية
laravel/reverb: خادم WebSocket وتكوين تطبيقاته واتصالاته.- Laravel Broadcasting: تعريف الأحداث والقنوات وأسماء الأحداث وحمولاتها وشروط بثها.
- Laravel Echo: واجهة JavaScript للاشتراك والاستماع وإدارة الاتصال، مع حزم جاهزة لـReact وVue وSvelte.
pusher-js: عميل البروتوكول الذي يستخدمه إعداد Echo الخاص بـReverb.- عامل Queue: ينفّذ مهام البث غير المتزامنة.
- Redis: مطلوب عند التوسع الأفقي لمزامنة الرسائل بين عقد Reverb، وليس شرطًا لتشغيل عقدة واحدة.
الفرق بين Reverb وEcho وPusher وAbly وMercure
يأتي Laravel بأربعة drivers بث رسمية: Reverb وPusher Channels وAbly وMercure، إضافة إلى driver log للتطوير وnull لتعطيل البث في الاختبارات.
| الأداة | الدور | مكان التشغيل | متى تختارها؟ |
|---|---|---|---|
| Laravel Reverb | خادم WebSocket وBroadcasting driver رسمي | خوادمك، أو مُدار عبر Laravel Cloud | عند الرغبة في تكامل Laravel أصلي وتحكم بالبنية والتكلفة |
| Laravel Echo | عميل JavaScript للاشتراك والاستماع | المتصفح أو الواجهة الأمامية | تستخدمه مع أي driver مما سبق؛ ليس خادمًا منافسًا لها |
| Pusher Channels | خدمة بث WebSocket مُدارة | بنية Pusher | عندما تفضّل عدم إدارة خوادم WebSocket |
| Ably | منصة Real-Time مُدارة | بنية Ably | عندما تحتاج خدمة مُدارة وخصائص منصتها |
| Mercure | Hub يعتمد Server-Sent Events | Hub تديره أنت أو مُدار | عندما يكفيك تدفق أحادي من الخادم إلى العميل أو لديك Hub قائم |
الاختيار ليس مقارنة سرعة مجردة. Reverb يمنحك سيطرة تشغيلية أكبر، لكنه ينقل إليك مسؤوليات TLS، وإدارة العملية، والحدود، والمراقبة، والتوسع. الخدمات المُدارة تقلل عبء التشغيل مقابل تكلفة واعتماد على طرف خارجي. وتقدّم Laravel Cloud خيارًا وسطًا: بنية WebSocket مُدارة بالكامل تعمل بعناقيد Reverb.
تثبيت Laravel Reverb وإعداده
المسار السريع الموصى به
البث غير مفعّل افتراضيًا في تطبيقات Laravel الجديدة. فعّله واختر Reverb بالأمر الرسمي:
php artisan install:broadcasting --reverb
npm install
npm run devيثبّت الأمر حزم Composer وNPM المطلوبة، ويشغّل reverb:install خلف الكواليس، وينشئ أو يحدّث ملفات الإعداد ومنها config/broadcasting.php وconfig/reverb.php وroutes/channels.php، ويضيف متغيرات البيئة. ويمكن تشغيل php artisan install:broadcasting دون الخيار ثم اختيار Reverb من الأسئلة التفاعلية.
التثبيت اليدوي
composer require laravel/reverb
php artisan reverb:installبعد التثبيت تأكد من أن BROADCAST_CONNECTION=reverb، ثم راجع بيانات التطبيق. يجب أن تتطابق القيم التي يستخدمها Laravel لإرسال الأحداث مع القيم التي يتحقق منها Reverb:
BROADCAST_CONNECTION=reverb
REVERB_APP_ID=my-app-id
REVERB_APP_KEY=my-app-key
REVERB_APP_SECRET=my-app-secret
REVERB_HOST=localhost
REVERB_PORT=8080
REVERB_SCHEME=http
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"تنبيه أمني: القيمة REVERB_APP_KEY تصل إلى الواجهة عبر VITE_REVERB_APP_KEY، وهي معرّف عام وليست سرًا. أما REVERB_APP_SECRET فسر خاص بالخادم يوقّع طلبات البث، ولا يجوز أبدًا تضمينه في متغير يبدأ بـVITE_ أو في JavaScript أو في مستودع Git. ولا تخلط بينهما وبين APP_KEY الخاص بـLaravel؛ فهو مفتاح التشفير الرئيسي للتطبيق وسرّي تمامًا.
إعداد Laravel Echo يدويًا
إن لم تستخدم أمر التثبيت، ثبّت الحزم أولًا (يتطلب broadcaster الخاص بـReverb إصدار laravel-echo 1.16.0 أو أحدث):
npm install --save-dev laravel-echo pusher-jsimport Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
});ضع الإعداد عادة في resources/js/echo.js واستورده من ملف JavaScript الرئيسي. تذكّر أن متغيرات VITE_* تُدمج داخل ملفات JavaScript وقت البناء، لذلك يجب إعادة البناء (npm run build) كلما تغيّرت.
تشغيل الخادم والعامل
php artisan reverb:start
php artisan queue:workيستمع Reverb افتراضيًا على 0.0.0.0:8080. للتطوير تستطيع استخدام منفذ مخصص أو وضع التشخيص:
php artisan reverb:start --host=127.0.0.1 --port=9000
php artisan reverb:start --debugفرّق بين REVERB_SERVER_HOST وREVERB_SERVER_PORT اللذين يحددان عنوان الاستماع الداخلي للعملية، وبين REVERB_HOST وREVERB_PORT اللذين يحددان الوجهة التي يستخدمها Laravel والعملاء. في الإنتاج قد يستمع Reverb داخليًا على 8080 بينما يظهر للعالم عبر wss://ws.example.com:443.
WSS محليًا مع Herd أو Valet
إذا كان موقعك المحلي يعمل بـHTTPS عبر Laravel Herd أو أمر valet secure، يستطيع Reverb استخدام الشهادة نفسها مباشرة بتمرير اسم الموقع:
php artisan reverb:start --host="0.0.0.0" --port=8080 --hostname="laravel.test"يصبح الخادم متاحًا عبر wss://laravel.test:8080، ويتجنب ذلك أخطاء Mixed Content أثناء التطوير. ويمكن بدلًا من ذلك تحديد شهادة يدويًا عبر خيارات tls في config/reverb.php.
أنواع القنوات والأذونات
القناة العامة Public Channel
تسمح لأي عميل يعرف اسمها بالاشتراك دون مصادقة. استخدمها فقط لبيانات عامة فعلًا، مثل إعلان منشور عام. يمثلها Channel.
القناة الخاصة Private Channel
تتطلب مصادقة وتفويضًا عبر تطبيق Laravel. يمثلها PrivateChannel، وتُعرّف قاعدة التفويض في routes/channels.php. عند الاشتراك يرسل Echo تلقائيًا طلبًا إلى /broadcasting/auth، فينفّذ Laravel الـcallback المطابق. لا تعتمد على صعوبة تخمين اسم القناة؛ القرار الأمني الحقيقي هو callback التفويض.
تدعم callbacks القنوات ربط النماذج (route model binding) كما في المسارات العادية، ما يجعل القاعدة أوضح:
use App\Models\Order;
use App\Models\User;
use Illuminate\Support\Facades\Broadcast;
Broadcast::channel('orders.{order}', function (User $user, Order $order) {
return $user->id === $order->user_id;
});لاحظ أن ربط النماذج هنا لا يدعم التقييد التلقائي (scoping) كما في مسارات HTTP، لذلك اجعل الشرط داخل الـcallback صريحًا. وإذا كان المستخدم غير مسجّل الدخول يُرفض التفويض تلقائيًا دون تنفيذ الـcallback. ولاستخدام حارس غير الافتراضي (مثل حارس إداري أو API):
Broadcast::channel('admin.alerts', function ($admin) {
return $admin->is_active;
}, ['guards' => ['web', 'admin']]);فئات القنوات Channel Classes
عندما يكبر routes/channels.php، انقل منطق التفويض إلى فئة مستقلة قابلة للاختبار وحقن الاعتماديات:
php artisan make:channel OrderChannel// routes/channels.php
use App\Broadcasting\OrderChannel;
Broadcast::channel('orders.{order}', OrderChannel::class);
// app/Broadcasting/OrderChannel.php
public function join(User $user, Order $order): array|bool
{
return $user->can('view', $order);
}استخدام Policy داخل join() كما في المثال يوحّد قواعد الوصول بين صفحات HTTP والقنوات اللحظية. ولعرض كل قواعد التفويض المسجلة استخدم php artisan channel:list.
قناة الحضور Presence Channel
تبني على القناة الخاصة وتضيف معرفة المشتركين الموجودين. بدل إرجاع true تعيد callback مصفوفة صغيرة من بيانات المستخدم المسموح كشفها لبقية الأعضاء، أو false/null للرفض:
Broadcast::channel('chat.{roomId}', function (User $user, int $roomId) {
if (! $user->rooms()->whereKey($roomId)->exists()) {
return false;
}
return ['id' => $user->id, 'name' => $user->name];
});كل ما تعيده هذه المصفوفة يراه جميع أعضاء القناة، فلا تضع فيها البريد الإلكتروني أو رقم الهاتف أو أي حقل داخلي.
مثال عملي: تحديث حالة طلب لحظيًا
سنرسل الحالة الجديدة إلى صاحب الطلب عبر قناة خاصة، مع حمولة محدودة بدل تسلسل نموذج Eloquent كامل.
1. إنشاء الحدث
php artisan make:event OrderStatusUpdated<?php
namespace App\Events;
use App\Models\Order;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Contracts\Broadcasting\ShouldRescue;
use Illuminate\Contracts\Events\ShouldDispatchAfterCommit;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderStatusUpdated implements ShouldBroadcast, ShouldDispatchAfterCommit, ShouldRescue
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(public Order $order) {}
public function broadcastOn(): array
{
return [new PrivateChannel('orders.'.$this->order->id)];
}
public function broadcastAs(): string
{
return 'order.status.updated';
}
public function broadcastWith(): array
{
return [
'id' => $this->order->id,
'status' => $this->order->status,
'updated_at' => $this->order->updated_at?->toISOString(),
];
}
}ثلاثة قرارات في هذا الحدث تستحق التوضيح:
ShouldDispatchAfterCommit: لأن الحدث يعتمد على بيانات قد تتغير داخل transaction. من دونه قد تُنفَّذ مهمة البث قبل تثبيت المعاملة فتقرأ بيانات قديمة. وإذا كان خيارafter_commitمفعّلًا في اتصال الطابور، يعالج Laravel ذلك لجميع المهام.ShouldRescue: البث غالبًا ميزة مكمّلة، فإذا تعذّر الوصول إلى الطابور أو حدث خطأ أثناء البث، يُسجَّل الاستثناء في معالج الأخطاء ويكمل الطلب بدل أن يرى المستخدم صفحة خطأ.InteractsWithSockets: مطلوب لاستخدامtoOthers()لاحقًا.
2. إطلاق الحدث
$order->update(['status' => 'shipped']);
OrderStatusUpdated::dispatch($order->fresh());إذا حدّثت واجهة المستخدم محليًا من استجابة HTTP ولا تريد أن تستقبل الجلسة نفسها نسخة مكررة، استخدم:
broadcast(new OrderStatusUpdated($order->fresh()))->toOthers();تعمل toOthers() بقراءة الترويسة X-Socket-ID من الطلب. يضيفها Echo تلقائيًا إذا كنت تستخدم نسخة Axios العامة؛ أما مع fetch أو عميل HTTP آخر فأضفها يدويًا:
fetch('/orders/42/ship', {
method: 'POST',
headers: { 'X-Socket-ID': window.Echo.socketId() },
});ولبث سريع دون إنشاء فئة حدث كاملة، تتيح واجهة Broadcast الأحداث المجهولة (anonymous events):
Broadcast::private('orders.'.$order->id)
->as('order.status.updated')
->with(['id' => $order->id, 'status' => $order->status])
->send();3. الاستماع في الواجهة
window.Echo.private(`orders.${orderId}`)
.listen('.order.status.updated', (event) => {
document.querySelector('[data-order-status]').textContent = event.status;
});النقطة قبل اسم الحدث مطلوبة عند استخدام broadcastAs() باسم مخصص، لأنها تمنع Echo من إضافة البادئة App\Events. وعند مغادرة الصفحة، ألغِ الاشتراك لتجنب الاتصالات أو المستمعين غير الضروريين:
window.Echo.leave(`orders.${orderId}`);4. لا تثق بأن كل حدث سيصل
WebSocket ليس طابور رسائل مضمون التسليم: إذا انقطع اتصال المستخدم دقيقة، فالأحداث التي بُثّت خلالها لن تُعاد. لذلك عامل البث كإشعار بالتغيير، وأعد جلب الحالة الحالية من API عند إعادة الاتصال أو عند عودة التبويب إلى الواجهة:
window.Echo.connector.pusher.connection.bind('connected', () => {
refreshOrderStatus(orderId); // طلب HTTP يجلب الحالة الحالية
});مثال سريع لقناة حضور
window.Echo.join(`chat.${roomId}`)
.here((users) => renderUsers(users))
.joining((user) => addUser(user))
.leaving((user) => removeUser(user))
.listen('NewMessage', (event) => appendMessage(event.message))
.error((error) => console.error(error));تُستدعى here فور الانضمام بقائمة الأعضاء الحاليين، وerror عندما يعيد endpoint التفويض حالة غير 200.
الاستخدام مع React وVue وSvelte
توفّر Echo حزمًا رسمية بخطافات (hooks) جاهزة: @laravel/echo-react و@laravel/echo-vue و@laravel/echo-svelte، وتستخدمها حزم البداية (starter kits) الرسمية. أهم ميزة فيها أنها تغادر القناة تلقائيًا عند إزالة المكوّن، فتختفي مشكلة المستمعين المنسيين.
// إعداد مرة واحدة عند تشغيل التطبيق
import { configureEcho } from '@laravel/echo-react';
configureEcho({ broadcaster: 'reverb' });import { useEcho, useConnectionStatus } from '@laravel/echo-react';
type OrderEvent = { id: number; status: string; updated_at: string };
function OrderStatus({ orderId }: { orderId: number }) {
const [status, setStatus] = useState<string>();
const connection = useConnectionStatus();
useEcho<OrderEvent>(`orders.${orderId}`, '.order.status.updated', (e) => {
setStatus(e.status);
});
return (
<div>
{connection !== 'connected' && <small>جارٍ إعادة الاتصال…</small>}
<span>{status}</span>
</div>
);
}useEcho: للقنوات الخاصة، ويقبل مصفوفة أحداث ونوع TypeScript للحمولة.useEchoPublicوuseEchoPresence: للقنوات العامة وقنوات الحضور.useEchoModel: للاستماع إلى بث نماذج Eloquent.useConnectionStatus: يعيد إحدى الحالاتconnectedأوconnectingأوreconnectingأوdisconnectedأوfailed، وهو مفيد لإظهار مؤشر اتصال للمستخدم.
الواجهة نفسها متاحة في Vue وSvelte مع اختلاف الاستيراد فقط.
ميزات إضافية تستحق المعرفة
أحداث العميل Whisper
لمؤشرات مثل "فلان يكتب الآن" لا داعي لإرسال طلب إلى Laravel؛ يرسل العميل حدثًا مباشرة إلى بقية المشتركين في قناة خاصة أو قناة حضور:
window.Echo.private(`chat.${roomId}`)
.whisper('typing', { name: currentUser.name });
window.Echo.private(`chat.${roomId}`)
.listenForWhisper('typing', (e) => showTyping(e.name));بما أن هذه الرسائل لا تمر بالخادم، فلا تستخدمها لأي شيء يحتاج تحققًا أو حفظًا، وطبّق throttle على الإرسال في الواجهة حتى لا تُرسل رسالة مع كل ضغطة مفتاح.
بث الإشعارات Notifications
إذا كان لديك Notification يستخدم قناة broadcast، فاستقبلها عبر قناة المستخدم الخاصة التي يضيف أمر التثبيت قاعدة تفويضها افتراضيًا:
window.Echo.private(`App.Models.User.${userId}`)
.notification((notification) => showToast(notification.type));بث تغييرات النماذج تلقائيًا
بإضافة الـtrait BroadcastsEvents إلى نموذج Eloquent وتعريف broadcastOn(string $event)، يبث النموذج تلقائيًا أحداث الإنشاء والتحديث والحذف باسم مثل .PostUpdated. هذه الطريقة سريعة، لكنها تبث النموذج كاملًا افتراضيًا، لذلك عرّف broadcastWith() لتقليص الحمولة قبل استخدامها مع نماذج تحتوي بيانات حساسة.
أحداث Reverb الداخلية
يطلق Reverb أحداث Laravel عادية خلال دورة حياة الاتصال، ويمكنك الاستماع إليها للتسجيل أو الإحصاءات: ChannelCreated وChannelRemoved وConnectionPruned وMessageReceived وMessageSent ضمن المساحة Laravel\Reverb\Events. اجعل المستمعين عليها خفيفين جدًا لأنها تعمل داخل عملية Reverb نفسها.
اختبار البث
اختبر ثلاثة أشياء منفصلة: أن الحدث يُطلق، وأن حمولته والقناة صحيحة، وأن قاعدة التفويض ترفض المستخدم الخطأ.
use App\Events\OrderStatusUpdated;
use Illuminate\Support\Facades\Event;
it('broadcasts the new status to the order owner', function () {
Event::fake([OrderStatusUpdated::class]);
$order = Order::factory()->create();
$this->actingAs($order->user)
->post("/orders/{$order->id}/ship")
->assertOk();
Event::assertDispatched(OrderStatusUpdated::class, function ($event) use ($order) {
return $event->broadcastOn()[0]->name === "private-orders.{$order->id}"
&& $event->broadcastWith()['status'] === 'shipped';
});
});في بيئة الاختبار اضبط BROADCAST_CONNECTION=null في phpunit.xml حتى لا تحاول الاختبارات الاتصال بخادم Reverb حقيقي. ولاختبار التفويض، استخرج المنطق إلى Channel Class أو Policy واختبره كوحدة مستقلة.
إعداد Laravel Reverb في الإنتاج
الفصل بين المنفذ الداخلي والعنوان العام
REVERB_SERVER_HOST=0.0.0.0
REVERB_SERVER_PORT=8080
REVERB_HOST=ws.example.com
REVERB_PORT=443
REVERB_SCHEME=httpsإذا كان Reverb يعمل على الخادم نفسه مع Nginx، فاجعل REVERB_SERVER_HOST=127.0.0.1 بدل 0.0.0.0 حتى لا يكون المنفذ الداخلي متاحًا من الشبكة أصلًا.
اجعل Nginx ينهي TLS ويوجّه اتصالات WebSocket إلى Reverb. يجب أن يمرر مساري /app لاتصالات WebSocket و/apps لطلبات API، وأن يحافظ على ترويسات الترقية:
server {
listen 443 ssl;
http2 on;
server_name ws.example.com;
ssl_certificate /etc/letsencrypt/live/ws.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ws.example.com/privkey.pem;
location / {
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header Scheme $scheme;
proxy_set_header SERVER_PORT $server_port;
proxy_set_header REMOTE_ADDR $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_read_timeout 300s;
proxy_pass http://127.0.0.1:8080;
}
}المهلة proxy_read_timeout الافتراضية في Nginx (60 ثانية) قد تغلق الاتصالات الخاملة؛ يرسل بروتوكول Pusher رسائل ping دورية تحافظ عليها عادة، لكن رفع المهلة يمنح هامشًا آمنًا. وإذا كان أمامك load balancer سحابي فاضبط مهلة الخمول فيه أيضًا. ولا تعرض منفذ 8080 للعامة، واضبط firewall وفق ذلك. إذا كنت تستخدم Laravel Forge فإنه يضبط هذا الإعداد تلقائيًا.
إدارة العملية
Reverb عملية طويلة العمر، لذلك استخدم Supervisor أو مدير عمليات مماثل لإعادة تشغيلها عند الفشل أو إقلاع الخادم:
[program:reverb]
command=php /var/www/example.com/artisan reverb:start
directory=/var/www/example.com
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/supervisor/reverb.log
stopasgroup=true
killasgroup=trueوارفع حد الملفات الذي يفتحه Supervisor نفسه في قسم [supervisord] من ملف supervisord.conf، وإلا ورثت عملية Reverb حدًا منخفضًا مهما رفعته في النظام:
[supervisord]
minfds=10000ولا تنسَ برنامجًا مماثلًا لعامل الطابور (queue:work) أو Horizon؛ فمن دونه لن يُبث أي حدث.
النشر
العملية طويلة العمر لا ترى تغييرات الكود أو الإعداد دون إعادة تشغيل. أضف إلى سكربت النشر:
php artisan config:cache
php artisan reverb:restart
php artisan queue:restartينهي reverb:restart الاتصالات بسلاسة ثم يعيد مدير العمليات تشغيل الخادم، فيعيد Echo الاتصال تلقائيًا من جهة العميل.
أمان Laravel Reverb
- WSS في الإنتاج: استخدم TLS حتى لا تمر الجلسات والرسائل بنص واضح، وأنهِ TLS عبر Nginx غالبًا.
- حصر Origins: حدّد
allowed_originsلكل تطبيق فيconfig/reverb.php. الطلب من Origin غير موجود يُرفض. تجنب*في الإنتاج ما لم تكن الحالة عامة ومقصودة. - تفويض كل قناة خاصة: اربط القرار بالمستخدم والكيان المطلوب، وليس بمجرد تسجيل الدخول. ويُفضَّل إعادة استخدام Policies الموجودة.
- تقليل بيانات الحدث: استخدم
broadcastWith()ولا تبث النموذج كاملًا إذا كان يحتوي حقولًا شخصية أو داخلية؛ فكل الخصائص العامة في الحدث تُبث افتراضيًا. - حماية الأسرار: احتفظ بـ
REVERB_APP_SECRETعلى الخادم فقط، ودوّر القيم عند الاشتباه بتسريبها. - عزل منفذ الخدمة: اسمح للـreverse proxy والخدمات المطلوبة فقط بالوصول إلى المنفذ الداخلي.
- مصادقة مناسبة: تأكد من أن endpoint التفويض
/broadcasting/authيعمل بالحارس المناسب لتطبيق الويب أو الـAPI (مثل Sanctum)، وبسياسة CORS وCSRF صحيحة. - الحذر مع Whisper: أحداث العميل لا تمر بالخادم، فلا تعتمد عليها لأي قرار أو بيانات موثوقة.
- عدم الثقة بالعميل: الرسالة اللحظية لتحسين الواجهة، وليست دليلًا نهائيًا لصلاحية عملية مالية أو إدارية؛ تحقّق على الخادم دائمًا.
يمكن لخادم Reverb واحد خدمة عدة تطبيقات عبر مصفوفة apps في config/reverb.php، ولكل تطبيق بيانات اعتماد وOrigins خاصة. لا تخلط صلاحيات التطبيقات، وافصلها منطقيًا بوضوح.
'apps' => [
[
'app_id' => env('REVERB_APP_ID'),
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'allowed_origins' => ['app.example.com'],
// ...
],
],الأداء والتوسع
كل اتصال WebSocket يبقى في الذاكرة ويمثل ملفًا مفتوحًا في أنظمة Unix. لذلك لا يكفي حساب معدل طلبات HTTP؛ راقب الاتصالات المتزامنة، وحجم الرسائل، وعدد الرسائل في الثانية، واستهلاك الذاكرة والملفات والمنافذ.
حد الملفات المفتوحة
ulimit -nلرفع الحد لمستخدم العملية، عدّل /etc/security/limits.conf:
www-data soft nofile 10000
www-data hard nofile 10000Event Loop
يعتمد Reverb داخليًا على ReactPHP event loop. يستخدم افتراضيًا stream_select الذي لا يحتاج امتدادًا إضافيًا، لكنه محدود عادة بنحو 1024 ملفًا مفتوحًا. إذا كنت تستهدف أكثر من ألف اتصال متزامن، تحتاج loop غير مقيد بهذا الحد؛ ويتحول Reverb تلقائيًا إلى ext-uv عندما يكون متاحًا:
pecl install uvNginx والمنافذ
اضبط worker_rlimit_nofile وworker_connections في nginx.conf وفق اختبار حمل واقعي:
worker_rlimit_nofile 10000;
events {
worker_connections 10000;
multi_accept on;
}راقب أيضًا نطاق المنافذ المحلية في Linux، لأن كل اتصال يمر عبر الـproxy يستهلك منفذًا:
cat /proc/sys/net/ipv4/ip_local_port_range
# 32768 60999النطاق الافتراضي أعلاه يعني سقفًا يقارب 28 ألف اتصال لكل خادم. يمكن توسيعه عبر /etc/sysctl.conf، لكن التوسع الأفقي هو الحل الموصى به بعد هذا الحد.
التوسع الأفقي عبر Redis
REVERB_SCALING_ENABLED=trueعند تشغيل عدة عقد Reverb، تستخدم الحزمة إمكانات publish/subscribe في اتصال Redis الافتراضي لنشر الرسالة إلى بقية العقد. ضع العقد خلف load balancer يوزع الاتصالات، ووصلها جميعًا بخادم Redis مركزي مخصص وموثوق. اختبر سلوك موازن الحمل والمهل الزمنية مع الاتصالات طويلة العمر.
قِس قبل أن تقرر
لا يوجد رقم سحري لسعة الخادم؛ فالاتصال الخامل لا يساوي غرفة دردشة عالية النشاط، والحمولات الصغيرة لا تساوي بث نماذج ضخمة. نفّذ load test يحاكي عدد الاتصالات الحقيقي وتواتر الرسائل وحجمها وموجات إعادة الاتصال الجماعية (مثلًا بعد نشر جديد)، ثم ضع هامشًا للأعطال والذروة. أدوات مثل k6 أو Artillery تدعم سيناريوهات WebSocket.
المراقبة والتشخيص عبر Laravel Pulse
يتكامل Reverb رسميًا مع Laravel Pulse لعرض عدد الاتصالات والرسائل. بعد تثبيت Pulse، أضف recorders التالية إلى config/pulse.php:
use Laravel\Reverb\Pulse\Recorders\ReverbConnections;
use Laravel\Reverb\Pulse\Recorders\ReverbMessages;
'recorders' => [
ReverbConnections::class => ['sample_rate' => 1],
ReverbMessages::class => ['sample_rate' => 1],
],ثم أضف البطاقتين إلى لوحة Pulse:
<x-pulse>
<livewire:reverb.connections cols="full" />
<livewire:reverb.messages cols="full" />
</x-pulse>شغّل daemon الآتي على خادم Reverb كي تظهر البيانات بصورة صحيحة، ويفضَّل تحت Supervisor أيضًا:
php artisan pulse:checkفي بنية Reverb المتوسعة أفقيًا شغّل pulse:check على خادم واحد فقط.
تشخيص الاتصال خطوة بخطوة
- المتصفح: افتح أدوات المطور ← Network ← فلتر WS. يجب أن ترى اتصالًا إلى
/app/{key}بحالة 101، ورسائل الاشتراك والأحداث في تبويب Messages. - التفويض: ابحث عن طلب
/broadcasting/auth؛ إن أعاد 403 فالمشكلة في القاعدة أو الحارس، وإن أعاد 419 فالمشكلة في CSRF. - الطابور: تأكد أن العامل يعمل وأن
php artisan queue:failedلا يحتوي مهام بث فاشلة. - الخادم: شغّل مؤقتًا
reverb:start --debugلرؤية تدفق البيانات، ثم أطفئه؛ لا تتركه نشطًا في الإنتاج بسبب الضوضاء والكلفة واحتمال ظهور بيانات حساسة في السجلات. - عزل المشكلة: غيّر مؤقتًا
BROADCAST_CONNECTION=logلترى فيstorage/logsإن كان Laravel يبث الحدث أصلًا.
الأخطاء الشائعة وحلولها
| العَرَض | السبب المرجح | الفحص أو الحل |
|---|---|---|
| الاتصال يعمل لكن الحدث لا يصل | عامل queue متوقف | شغّل php artisan queue:work وافحص failed jobs |
| خطأ 403 عند قناة خاصة | فشل التفويض أو guard غير صحيح | راجع routes/channels.php وchannel:list والجلسة وendpoint التفويض |
خطأ 419 عند /broadcasting/auth | رمز CSRF مفقود أو منتهٍ | تأكد من وجود meta الـCSRF أو من إعداد Sanctum للـSPA |
| فشل WSS أو Mixed Content | الصفحة HTTPS والاتصال WS أو شهادة/Proxy خاطئ | استخدم https و443 واضبط TLS وترويسات Upgrade |
| الاتصال يُرفض مباشرة | Origin غير مدرج أو مفتاح خاطئ | راجع allowed_origins وتطابق REVERB_APP_KEY مع VITE_REVERB_APP_KEY |
| الواجهة لا تزال تتصل بعنوان قديم | أصول Vite لم تُبن بعد تغيير البيئة | أعد تشغيل build وانشر الملفات الجديدة |
| الحدث يصل باسم غير متوقع | استخدام broadcastAs() دون نقطة في Echo | استمع إلى .custom.event |
| العميل يرى الحدث مرتين | تحديث محلي ثم استقبال البث نفسه | استخدم toOthers() وتأكد من إرسال X-Socket-ID |
toOthers() لا تؤثر | الطلب لا يحمل ترويسة X-Socket-ID | استخدم Axios العام أو أضف Echo.socketId() يدويًا |
| بيانات قديمة أو ModelNotFound | البث سبق commit قاعدة البيانات | استخدم ShouldDispatchAfterCommit أو after_commit |
| المستخدم يفقد تحديثات بعد انقطاع الشبكة | WebSocket لا يعيد الأحداث الفائتة | أعد جلب الحالة عند حدث connected |
| يتوقف الاتصال بعد النشر | Reverb لم يُعد تشغيله | نفّذ php artisan reverb:restart مع مدير عمليات |
| السعة تتوقف قرب ألف اتصال | حد stream_select أو الملفات | ثبّت ext-uv واضبط nofile وminfds وNginx |
أفضل الممارسات
- صمّم أسماء القنوات حول الموارد والصلاحيات مثل
orders.{order}، واجعل التفويض قابلًا للاختبار عبر Channel Classes وPolicies. - اعتبر القنوات العامة عامة بالكامل، ولا تضع فيها أي بيانات لا تريد كشفها.
- استخدم
broadcastWith()لعقد بيانات صغير ومستقر، وأضف version عند الحاجة لتطوير payload. - اترك البث على queue افتراضيًا، وخصّص طابورًا مستقلًا عبر
broadcastQueue()أو السمتين#[Connection]و#[Queue]إذا احتجت عزل الحمل. استخدمShouldBroadcastNowفقط عندما تفهم أثر التنفيذ المتزامن على زمن الطلب. - استخدم
broadcastWhen()لمنع أحداث لا يحتاجها العميل بدل بثها ثم تجاهلها. - أضف
ShouldRescueللأحداث المكمّلة حتى لا يُفشل تعطّل البث طلب المستخدم. - عامل البث كإشعار بالتغيير لا كمصدر الحقيقة، وأعد المزامنة من API بعد إعادة الاتصال.
- افصل خادم Reverb عن web workers منطقيًا، وراقب كل عملية بمدير خدمات.
- اختبر إعادة الاتصال وفقد الشبكة وتغيير تبويب المتصفح، وليس المسار المثالي فقط.
- نفّذ إعادة تشغيل سلسة عند كل نشر يمس الكود أو الإعداد.
- راقب الاتصالات والرسائل والطوابير والذاكرة وRedis وموازن الحمل معًا.
- ابدأ بعقدة واحدة، ثم توسع أفقيًا بناءً على قياسات فعلية وخطة تحمل فشل العقدة.
قائمة تحقق قبل الإطلاق
- ☐
BROADCAST_CONNECTION=reverbوقيمREVERB_*صحيحة، وREVERB_APP_SECRETغير موجود في أي متغيرVITE_*. - ☐ أصول Vite مبنية بالقيم العامة الصحيحة (
REVERB_HOSTو443 وhttps). - ☐ Nginx ينهي TLS ويمرر
/appو/appsمع ترويسات Upgrade، والمنفذ الداخلي مغلق أمام العامة. - ☐
allowed_originsمحددة بنطاقاتك فقط. - ☐ Reverb وعامل الطابور و
pulse:checkتحت Supervisor، معminfdsمرتفع. - ☐
ext-uvمثبت إن كنت تتوقع أكثر من ألف اتصال، وحدودnofileوNginx مرفوعة. - ☐ كل قناة خاصة لها اختبار يثبت رفض المستخدم غير المخوّل.
- ☐ الحمولات محدودة عبر
broadcastWith()، وبيانات أعضاء الحضور لا تكشف حقولًا حساسة. - ☐ سكربت النشر يشغّل
reverb:restartوqueue:restart. - ☐ الواجهة تعيد مزامنة الحالة بعد إعادة الاتصال وتعرض مؤشر حالة الاتصال.
- ☐ اختبار حمل نُفّذ بأرقام قريبة من الذروة المتوقعة.
الأسئلة الشائعة
هل Reverb بديل عن Laravel Echo؟
لا. Reverb خادم WebSocket، بينما Echo مكتبة عميل في الواجهة. غالبًا تستخدمهما معًا.
هل Redis مطلوب دائمًا؟
لا تحتاجه عقدة Reverb واحدة لمجرد تشغيل WebSocket. يصبح Redis جزءًا أساسيًا من إعداد Reverb الرسمي عند تفعيل التوسع الأفقي بين عدة خوادم.
هل أحتاج Queue Worker؟
نعم في المسار المعتاد، لأن الأحداث التي تطبق ShouldBroadcast تُبث عبر queued jobs. يمكن استخدام ShouldBroadcastNow للبث عبر sync queue، لكن ذلك ينقل الكلفة إلى التنفيذ الحالي.
هل يمكن تشغيل عدة تطبيقات على خادم Reverb واحد؟
نعم، تدعم الحزمة تعريف عدة عناصر داخل apps في config/reverb.php، ولكل تطبيق بيانات اعتماد وإعدادات Origins.
هل يجب تشغيل Reverb على المنفذ 443؟
ليس داخليًا. النمط الشائع أن يستمع على 8080 خلف Nginx، بينما يتصل العميل علنًا عبر WSS على 443.
ما الفرق بين Private وPresence؟
كلتاهما تتطلبان التفويض. تضيف Presence معلومات عن الأعضاء الموجودين وأحداث الانضمام والمغادرة، ولذلك يجب تقليل بيانات العضو المعادة.
هل يكفي إخفاء اسم القناة لحمايتها؟
لا. يجب استخدام قناة خاصة أو حضور وتعريف قاعدة Authorization تتحقق من حق المستخدم في المورد المطلوب.
هل يضمن Reverb وصول كل رسالة؟
لا. يسلّم الرسالة للمتصلين لحظة البث فقط. إذا كان الاستلام المضمون مهمًا، احفظ البيانات في قاعدة البيانات واجعل العميل يعيد المزامنة بعد إعادة الاتصال.
هل يعمل Reverb مع تطبيقات الجوال؟
نعم. بما أنه متوافق مع بروتوكول Pusher، يمكن لمكتبات عميل Pusher الرسمية على iOS وAndroid وFlutter الاتصال به بتوجيهها إلى مضيفك ومفتاحك، مع ضبط endpoint التفويض للعمل بالتوكن.
كيف أعرف قدرة الخادم؟
بالاختبار تحت حمل يمثل منتجك، مع مراقبة عدد الملفات والمنافذ والذاكرة والرسائل والطوابير. لا تعتمد على عدد اتصالات نظري منفرد.
الخلاصة
يمنح Laravel Reverb تطبيقات Laravel مسارًا رسميًا لبناء خصائص Real-Time من دون مغادرة نموذج الأحداث والقنوات والتفويض المألوف في الإطار. بداية صحيحة تعني تثبيت Broadcasting وEcho، تعريف أحداث صغيرة وآمنة، تشغيل queue worker وReverb، ثم نقل TLS إلى reverse proxy وإدارة العمليات بصورة دائمة. وعندما يرتفع الحمل، تصبح حدود الملفات وevent loop وRedis والموازن والمراقبة عناصر من تصميم المنتج لا تفاصيل مؤجلة.
قوة Reverb ليست في فتح اتصال WebSocket فقط، بل في دمج النقل اللحظي مع Authorization والطوابير والأحداث وPulse ضمن تجربة Laravel موحدة. وإذا طبّقت القياس والأمان والنشر السلس منذ البداية، تستطيع الانتقال من إشعار بسيط إلى بنية متعددة العقد دون إعادة اختراع طبقة البث.
التعليقات (0)
لا توجد تعليقات بعد — كن أول من يشارك رأيه.
أضف تعليقك