Laravel Reverb đưa WebSocket server vào hệ sinh thái Laravel, giúp xây thông báo, trạng thái đơn hàng, dashboard và cộng tác realtime mà không phải polling liên tục. Demo local khá nhanh, nhưng production cần thêm channel authorization, queue worker, TLS, reverse proxy, process manager, giới hạn kết nối và giám sát.
Luồng dữ liệu cần hiểu
- Trình duyệt dùng Laravel Echo mở WebSocket tới Reverb.
- Với private/presence channel, ứng dụng Laravel xác thực yêu cầu subscription.
- Backend dispatch event triển khai
ShouldBroadcast. - Queue worker xử lý broadcast job và gửi message tới Reverb.
- Reverb chuyển message tới các connection đã subscribe đúng channel.
Reverb giữ kết nối và phân phối message; quyền được nghe dữ liệu vẫn phải do ứng dụng quyết định.
1. Cài broadcasting với Reverb
php artisan install:broadcasting --reverb
npm install
npm run build
Lệnh cài tạo cấu hình broadcasting, Reverb, routes/channels.php, thông tin môi trường và scaffolding Echo. Không commit REVERB_APP_SECRET. Phân biệt hai nhóm biến: REVERB_SERVER_HOST/PORT là nơi process lắng nghe; REVERB_HOST/PORT/SCHEME là địa chỉ ứng dụng và browser sử dụng.
BROADCAST_CONNECTION=reverb
REVERB_SERVER_HOST=127.0.0.1
REVERB_SERVER_PORT=8080
REVERB_HOST=ws.example.com
REVERB_PORT=443
REVERB_SCHEME=https
2. Tạo một broadcast event
<?php
namespace App\Events;
use App\Models\Order;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Queue\SerializesModels;
class OrderStatusUpdated implements ShouldBroadcast
{
use SerializesModels;
public function __construct(public Order $order) {}
public function broadcastOn(): array
{
return [new PrivateChannel('orders.'.$this->order->id)];
}
public function broadcastWith(): array
{
return [
'id' => $this->order->id,
'status' => $this->order->status,
'updated_at' => $this->order->updated_at?->toISOString(),
];
}
}
Chỉ gửi field frontend thực sự cần. Không serialize cả model có quan hệ, email nội bộ hoặc dữ liệu nhạy cảm. Broadcast mặc định được đưa vào queue; hãy chạy worker để event không nằm chờ vô thời hạn.
3. Authorization cho private channel
Trong routes/channels.php:
use App\Models\Order;
use App\Models\User;
use Illuminate\Support\Facades\Broadcast;
Broadcast::channel('orders.{orderId}', function (User $user, int $orderId) {
return Order::query()
->whereKey($orderId)
->where('user_id', $user->id)
->exists();
});
Public channel cho phép bất kỳ client nào subscribe nên chỉ dùng cho dữ liệu thực sự công khai. Private channel yêu cầu xác thực và callback phải kiểm tra quyền trên đúng resource. Presence channel còn trả thông tin thành viên; chỉ trả dữ liệu tối thiểu như ID và tên hiển thị.
php artisan channel:list
4. Lắng nghe bằng Laravel Echo
window.Echo.private(`orders.${orderId}`)
.listen('OrderStatusUpdated', (event) => {
updateOrderStatus(event.status);
});
Nếu định nghĩa broadcastAs(), listener phải dùng tên tùy chỉnh với dấu chấm đầu, ví dụ .listen('.order.updated', ...). Khi component bị hủy, rời channel để tránh listener trùng và rò bộ nhớ.
5. Dispatch đúng thời điểm giao dịch
$order->update(['status' => 'shipped']);
event(new OrderStatusUpdated($order->fresh()));
Nếu event được dispatch trong database transaction, worker có thể chạy trước khi commit và đọc trạng thái cũ. Dùng after-commit của queue hoặc triển khai ShouldDispatchAfterCommit cho event khi tính nhất quán quan trọng. Thiết kế payload có ID, version hoặc timestamp để client bỏ qua event đến trễ.
6. Chạy local và kiểm tra
php artisan reverb:start --debug
php artisan queue:work
npm run dev
--debug hữu ích ở local nhưng tạo log lớn, không nên bật thường xuyên trong production. Kiểm tra tab Network/WebSocket của trình duyệt, response của /broadcasting/auth, log queue và failed_jobs. Nếu WebSocket kết nối nhưng không có event, queue worker là điểm cần kiểm tra đầu tiên.
7. Đặt Reverb sau Nginx và TLS
Trong production, để Reverb nghe ở localhost và Nginx kết thúc TLS trên cổng 443:
server {
listen 443 ssl http2;
server_name ws.example.com;
location / {
proxy_http_version 1.1;
proxy_set_header Host $http_host;
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 60s;
proxy_pass http://127.0.0.1:8080;
}
}
Reverb dùng URI /app cho WebSocket và /apps cho API, nên proxy phải phục vụ cả hai. Chỉ mở 80/443 ra Internet; cổng 8080 nên bị giới hạn nội bộ. Cài chứng chỉ hợp lệ cho hostname WebSocket và dùng wss:// khi website chạy HTTPS.
8. Giới hạn origin và bảo vệ channel
Trong config/reverb.php, đặt allowed_origins thành các domain frontend cụ thể, không dùng * trên production nếu không thực sự cần. Origin check không thay thế authentication; attacker vẫn có thể gọi backend bằng công cụ riêng nếu authorization sai.
- Dùng private channel cho dữ liệu người dùng hoặc tenant.
- Kiểm tra tenant/resource trong callback, không chỉ kiểm tra đã đăng nhập.
- Rate-limit endpoint tạo event và các client event.
- Không đưa secret, token hoặc model đầy đủ vào payload.
- Giới hạn kích thước message và số subscription hợp lý.
9. Chạy Reverb và queue bằng process manager
Reverb là process sống lâu. Dùng Supervisor hoặc systemd để tự khởi động lại. Ví dụ Supervisor:
[program:app-reverb]
command=php /var/www/app/artisan reverb:start
directory=/var/www/app
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/supervisor/app-reverb.log
stopasgroup=true
killasgroup=true
Queue worker là process riêng và cũng phải được giám sát. Sau deploy, chạy php artisan reverb:restart để kết thúc connection nhẹ nhàng rồi process manager khởi động phiên bản mới. Đồng thời restart queue worker để nạp code mới.
10. Capacity, monitoring và scale ngang
Mỗi WebSocket giữ memory và một file descriptor. Kiểm tra ulimit -n, giới hạn Nginx và Supervisor. Event loop mặc định dựa trên stream_select thường bị giới hạn khoảng 1.024 file; tài liệu Laravel khuyến nghị event loop ext-uv khi vượt khoảng 1.000 connection đồng thời.
Tích hợp Laravel Pulse để theo dõi connection và message; chỉ chạy daemon pulse:check trên một node khi scale ngang. Cảnh báo thêm memory, CPU, reconnect rate, queue lag, failed jobs và lỗi 4xx/5xx của endpoint authorization.
Khi một node không đủ, bật REVERB_SCALING_ENABLED=true, dùng Redis tập trung cho pub/sub và đặt nhiều Reverb node sau load balancer. Kiểm thử tải bằng mô hình connection thực tế, không chỉ số message/giây.
Những lỗi production thường gặp
- WebSocket 404/502: Nginx chưa proxy đúng
/app, process Reverb dừng hoặc sai port. - 403 subscription: session/cookie, CSRF, guard hoặc callback channel không khớp.
- Event không đến: queue worker chưa chạy, broadcast job thất bại hoặc sai channel/event name.
- Chỉ lỗi trên HTTPS: Echo vẫn dùng
ws://, certificate sai hoặc proxy thiếu header Upgrade. - Deploy xong vẫn chạy code cũ: chưa restart Reverb và queue worker.
- Rớt connection khi tải tăng: chạm giới hạn file descriptor, event loop, Nginx, memory hoặc port.
Checklist trước khi ra mắt
- Private/presence channel có test authorization cho phép và từ chối.
- Payload tối thiểu, không chứa secret hoặc dữ liệu tenant khác.
- Queue worker, Reverb và process manager đều hoạt động.
- WSS qua certificate hợp lệ; port nội bộ không public.
allowed_originschỉ chứa frontend hợp lệ.- Deploy script restart Reverb và queue worker.
- Đã đo connection đồng thời, queue lag, memory và reconnect.
- Có dashboard, cảnh báo và kế hoạch scale Redis/load balancer.
Kết luận
Laravel Reverb làm phần realtime hòa vào event và broadcasting của Laravel, nhưng độ tin cậy đến từ toàn bộ đường truyền: authorization đúng, queue không nghẽn, reverse proxy hỗ trợ upgrade, process được giám sát và hạ tầng có giới hạn phù hợp. Bắt đầu bằng một use case nhỏ, đo tải thật rồi scale theo connection thay vì dự đoán.
Nguồn tham khảo
Bài 29 TaskFlow: thông báo task thay đổi trên private channel
Phần tổng quan phía trên dùng đơn hàng làm ví dụ. Trong dự án xuyên suốt, mục tiêu là mở cùng project ở hai tab: khi tab thứ nhất tạo hoặc hoàn tất task, tab thứ hai nhận thông báo và hiện liên kết tải lại. Không tự reload để tránh xóa nội dung form đang nhập. Trang vẫn dùng được khi WebSocket mất kết nối; dữ liệu chính thức luôn đến từ request HTTP có policy.
Thiết kế quyền và payload
Channel projects.{id} là private và chỉ owner của project được đăng ký. Biết project ID hoặc public app key không đủ để nghe dữ liệu. Callback kiểm tra owner trên database; guest và owner của project khác bị từ chối. Event chỉ gửi task_id, không gửi tiêu đề, mô tả, email hay file. Đây là tín hiệu dữ liệu có thể đã thay đổi, không phải bản sao trạng thái đầy đủ.
Event dưới đây đặt trong App\Events, import PrivateChannel từ Illuminate\Broadcasting và hai interface từ Illuminate\Contracts\Broadcasting, Illuminate\Contracts\Events. Callback import Project, User của ứng dụng và facade Broadcast. Đoạn JavaScript là phần listener sau khi đã tạo instance Echo.
final class TaskBoardChanged implements ShouldBroadcast, ShouldDispatchAfterCommit
{
public string $queue = 'broadcasts';
public function __construct(public int $projectId, public int $taskId) {}
public function broadcastWhen(): bool
{
return config('broadcasting.default') === 'reverb';
}
public function broadcastOn(): array
{
return [new PrivateChannel('projects.'.$this->projectId)];
}
public function broadcastAs(): string { return 'task.changed'; }
public function broadcastWith(): array { return ['task_id' => $this->taskId]; }
}
// routes/channels.php; bootstrap/app.php loads it via withRouting(channels: ...).
Broadcast::channel('projects.{projectId}', function (User $user, int $projectId): bool {
return Project::query()->whereKey($projectId)->where('owner_id', $user->id)->exists();
});
// After creating a task or successfully changing its status:
event(new TaskBoardChanged($task->project_id, $task->id));
// Echo uses a leading dot because broadcastAs() supplies a custom name.
echo.private(`projects.${projectId}`).listen('.task.changed', () => {
document.querySelector('[data-realtime-refresh]').hidden = false;
});CreateTask phát sự kiện sau insert; CompleteTask phát sau conditional update thành công và refresh model. ShouldDispatchAfterCommit trì hoãn dispatch khi đang trong transaction; rollback không tạo broadcast job. Không có transaction ngoài thì dispatch diễn ra ngay. broadcastWhen chỉ bật khi connection là reverb; log/null không tạo job realtime. Worker phải nghe queue broadcasts, không chỉ default.
Cấu hình local và frontend
TaskFlow đã cài laravel/reverb, laravel-echo và pusher-js; không chạy lại installer để ghi đè cấu hình. Điền biến dưới đây trong .env phát triển, không dùng nguyên placeholder. Chỉ REVERB_APP_KEY công khai đi vào bundle Vite; tuyệt đối không tạo VITE_REVERB_APP_SECRET. config/reverb.php kế thừa cấu hình package, mặc định bind loopback và lấy danh sách domain từ REVERB_ALLOWED_ORIGINS. Biến danh sách origin này do TaskFlow bổ sung, không phải mặc định chung cho mọi dự án.
BROADCAST_CONNECTION=reverb
QUEUE_CONNECTION=database
REVERB_APP_ID=taskflow-local
REVERB_APP_KEY=<unique-public-key>
REVERB_APP_SECRET=<random-secret-only-on-server>
REVERB_HOST=localhost
REVERB_PORT=8080
REVERB_SCHEME=http
REVERB_SERVER_HOST=127.0.0.1
REVERB_SERVER_PORT=8080
REVERB_ALLOWED_ORIGINS=localhost,127.0.0.1
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
VITE_REVERB_HOST="${REVERB_HOST}"
VITE_REVERB_PORT="${REVERB_PORT}"
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
# Separate terminals; migrate only the confirmed development database first.
php artisan serve
php artisan reverb:start
php artisan queue:work database --queue=broadcasts,maintenance,notifications --tries=3
npm run build
php artisan test --filter=PrivateChannelTest
node tests/reverb-smoke.mjsTrong ví dụ môi trường, REVERB_APP_KEY là public key của Reverb, khác APP_KEY mã hóa của Laravel. Build lại assets sau khi đổi biến VITE. Echo cấu hình wsHost/wsPort/wssPort/forceTLS và header X-CSRF-TOKEN lấy từ meta csrf-token của layout; session cùng origin dùng để xác thực /broadcasting/auth. Đây không phải luồng bearer Sanctum ở bài 25. Production cần WSS/TLS, origin đúng domain và worker/process manager như phần trên; cấu hình HTTP local không được bê nguyên lên Internet.
Kiểm chứng và giới hạn
PrivateChannelTest kiểm tra endpoint authorization với owner, người khác và guest, payload tối thiểu, job sau commit và không enqueue sau rollback. Toàn suite đạt 87 tests/409 assertions. Script reverb-smoke.mjs chạy Reverb thật trên loopback cổng 18089, tự tạo credential tạm, ký subscription private trong harness, rồi chạy Laravel event với sync queue và xác nhận message qua WebSocket. Script dừng process của chính nó, không sửa .env và không ghi database.
Smoke test này xác nhận transport, không thay thế test authorization HTTP hoặc chứng minh worker nhiều process. Nếu PHP 8.3+ không ở PATH, đặt TASKFLOW_PHP thành đường dẫn runtime trước khi chạy Node; cổng kiểm thử có thể đổi bằng TASKFLOW_REVERB_TEST_PORT. Browser hai tab, TLS/reverse proxy, reconnect, Redis scale và tải đồng thời vẫn cần kiểm thử trên môi trường triển khai; chưa tuyên bố đã kiểm chứng chúng ở đây.
Không có outbox hoặc replay: crash giữa commit và enqueue, job lỗi, hay client ngắt kết nối có thể làm mất tín hiệu. Sau reconnect nên tải lại để đối chiếu trạng thái; message đến trùng chỉ làm hiện cùng liên kết. Quyền subscribe được xét tại lúc đăng ký, không tự thu hồi connection đang mở nếu owner đổi; muốn thu hồi tức thời cần cơ chế ngắt/re-authorize và kiểm thử riêng. TaskFlow hiện không cung cấp chuyển owner hoặc chia sẻ project.
Bài tập: mở hai tab cùng owner để quan sát liên kết refresh; thử tài khoản khác để xác nhận không subscribe được; dừng worker để thấy job chờ; khởi động lại worker và đối chiếu dữ liệu qua HTTP. Không đánh giá thành công chỉ từ trạng thái WebSocket 101.
Điều hướng: Bài 28: Mail, Notification và upload · Lộ trình TaskFlow.




Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.