หน้าเว็บที่ต้องเห็นของใหม่เองโดยไม่ต้องกด refresh มีให้ทำบ่อยกว่าที่คิด หน้ารายการออเดอร์ที่ร้านเปิดทิ้งไว้ หน้ารอผลตรวจสลิป หรือหน้าที่สั่ง export Excel ใน queue แล้วรอให้ไฟล์เสร็จ ทางที่ง่ายที่สุดคือ polling ให้หน้าเว็บถาม server ทุกกี่วินาที ส่วน broadcasting เป็นอีกทางที่ server บอกหน้าเว็บเองเมื่อมีอะไรเปลี่ยน
polling กับ WebSocket ต่างกันที่ใครเริ่มคุย
polling คือ browser เป็นฝ่ายถามทุกครั้ง ถามทุก 5 วินาทีก็คือชั่วโมงละ 720 request ต่อหนึ่งแท็บ แม้คำตอบเกือบทั้งหมดจะเป็น "ยังเหมือนเดิม" และของใหม่ก็มาช้าได้เกือบหนึ่งรอบ
WebSocket คือสายที่ browser เปิดค้างไว้กับ server แล้วส่งข้อความได้ทั้งสองทาง server จึงส่งของใหม่มาได้ทันทีที่มี โดย browser ไม่ต้องถามก่อน เอกสาร Broadcasting ของ Laravel เรียกวิธีนี้ว่าทางเลือกที่ประหยัดกว่าการ poll server ไปเรื่อย ๆ
broadcasting มีสี่ชิ้นทำงานต่อกัน
หลักของ broadcasting คือหน้าเว็บเข้าไปฟัง channel ที่มีชื่อ ส่วน Laravel ส่ง event เข้า channel นั้น
| ชิ้น | หน้าที่ | ใน Laravel คือ |
|---|---|---|
| event | บอกว่ามีอะไรเกิดขึ้น และส่งไป channel ไหน | class ที่ implements ShouldBroadcast |
| queue worker | ส่ง event ออกไปนอก request เว็บจึงไม่ช้าลง | php artisan queue:work |
| WebSocket server | ถือสายของทุก browser ไว้ แล้วกระจาย event | Laravel Reverb, Pusher Channels, Ably หรือ Mercure |
| Echo | ฝั่งหน้าเว็บ เข้าไปฟัง channel แล้วรับ event | laravel-echo หรือ @laravel/echo-react |
ตอนพัฒนายังมี driver log ที่เขียน event ลง log ให้ดู และ null สำหรับปิด broadcasting ตอนรัน test
event บอกว่าจะส่งไป channel ไหน
event ที่จะ broadcast คือ event ปกติของ Laravel ที่ implements ShouldBroadcast แล้วมีเมธอด broadcastOn() บอกชื่อ channel
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class OrderStatusUpdated implements ShouldBroadcast
{
public function __construct(public Order $order) {}
public function broadcastOn(): PrivateChannel
{
return new PrivateChannel('orders.'.$this->order->id);
}
}
ยิงด้วย OrderStatusUpdated::dispatch($order) เหมือน event ทั่วไป property ที่เป็น public จะกลายเป็นข้อมูลที่ส่งไปทั้งหมด ถ้าอยากเลือกเองว่าจะส่งอะไร ให้เขียน broadcastWith() คืนเป็น array ส่วนชื่อ event ที่หน้าเว็บเห็นคือชื่อ class เปลี่ยนได้ด้วย broadcastAs()
Laravel ส่ง event ออกไปผ่าน queue เสมอ ถ้าไม่มี queue worker รันอยู่ event จะค้างใน queue และหน้าเว็บไม่เห็นอะไรเลย ถ้าอยากให้ส่งทันทีใน request นั้น ให้ implements ShouldBroadcastNow แทน
channel มีสามแบบตามว่าใครฟังได้
- public (
Channel) ใครก็ฟังได้ เหมาะกับข้อมูลที่เปิดอยู่แล้ว เช่นจำนวนสินค้าคงเหลือหน้าร้าน - private (
PrivateChannel) ต้องผ่านการตรวจสิทธิ์ก่อน เช่นออเดอร์ของลูกค้าแต่ละคน ถ้าอยากให้ข้อมูลเข้ารหัสระหว่างทางด้วย ใช้EncryptedPrivateChannel - presence (
PresenceChannel) เป็น private ที่รู้ด้วยว่าตอนนี้ใครอยู่ใน channel บ้าง เช่นหน้าที่บอกว่าพนักงานคนไหนกำลังเปิดออเดอร์นี้อยู่
สิทธิ์ของ private และ presence channel เขียนไว้ใน routes/channels.php คืน true ถ้าให้ฟังได้
Broadcast::channel('orders.{orderId}', function (User $user, int $orderId) {
return $user->id === Order::findOrNew($orderId)->user_id;
});
Echo ส่ง request ไปที่ /broadcasting/auth ให้เองตอนเข้า channel ถ้ายังไม่ได้ล็อกอิน Laravel จะปฏิเสธทันทีโดยไม่เรียก callback นี้เลย
Reverb เป็น WebSocket server ที่รันบนเครื่องเราเอง
Laravel Reverb เป็น WebSocket server ของทีม Laravel เอง ติดตั้งผ่าน Composer แล้วรันบนเซิร์ฟเวอร์ของเรา จึงไม่มีค่าบริการรายเดือนแยก และคุย protocol เดียวกับ Pusher ส่วน Pusher Channels กับ Ably เป็นบริการที่ host ให้ เหมาะกับเว็บที่รัน process ของตัวเองไม่ได้
broadcasting ปิดไว้ในโปรเจกต์ใหม่ เปิดด้วยคำสั่งเดียว
php artisan install:broadcasting --reverb
คำสั่งนี้สร้าง config/broadcasting.php กับ routes/channels.php ติดตั้ง package ของ Reverb และ Echo ทั้งฝั่ง Composer และ npm แล้วเขียนค่า REVERB_APP_ID, REVERB_APP_KEY, REVERB_APP_SECRET ลง .env ให้ จากนั้นรัน php artisan reverb:start ซึ่งเปิดรอที่ 0.0.0.0:8080
ฝั่ง React ตั้ง Echo ครั้งเดียวด้วย configureEcho({ broadcaster: 'reverb' }) แล้วใช้ hook useEcho('orders.' + orderId, 'OrderStatusUpdated', (e) => ...) ใน component ที่ต้องฟัง hook จะออกจาก channel ให้เองตอน component หายไป ถ้าใช้ Inertia 3.11 ขึ้นไป live props ทำให้ไม่ต้องเขียน hook เองด้วยซ้ำ บอก controller ว่า prop ไหนฟัง event ไหน แล้ว Inertia โหลดค่าใหม่ให้
บน production ต้องรัน Reverb ค้างไว้ตลอด
Reverb ไม่ได้ทำงานเป็น request ละครั้งแบบ PHP-FPM แต่เป็น process ที่เปิดค้างไว้ถือสายของทุก browser เว็บบน shared hosting ที่ใช้ cPanel หรือ DirectAdmin จึงมักรัน Reverb ไม่ได้ ทางที่เหลือคือใช้ Pusher หรือ Ably หรือกลับไปใช้ polling ส่วนบน VPS มีเรื่องที่ต้องตั้งดังนี้
-
ให้ Supervisor ดูแล process ทั้ง
reverb:startและqueue:workถ้าล่มจะได้เปิดใหม่เอง และสั่งphp artisan reverb:restartทุกครั้งที่ deploy เพราะ process ที่ค้างอยู่ยังใช้โค้ดเก่า (ลำดับขั้นตอนทั้งหมดอยู่ในโพสต์เรื่องลำดับการ deploy) -
วาง Reverb ไว้หลัง nginx ตั้งโดเมนย่อยอย่าง
ws.example.comให้ nginx รับwss://ที่พอร์ต 443 แล้วส่งต่อทั้งหมดไปที่ Reverb ทั้ง/appที่ browser ต่อ WebSocket เข้ามา และ/appsที่ Laravel ส่ง event เข้าไป จุดที่ต้องมีคือ header ที่บอกให้เปลี่ยนสายเป็น WebSocketlocation / { proxy_http_version 1.1; proxy_set_header Host $http_host; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; proxy_pass http://127.0.0.1:8080; } -
แยกค่าสองชุดใน
.envให้ถูกREVERB_SERVER_HOSTกับREVERB_SERVER_PORTคือที่ที่ Reverb เปิดรอ ค่าเริ่มต้นคือ0.0.0.0:8080ซึ่งรับจากทุก network ถ้ามี nginx อยู่ข้างหน้าแล้ว ตั้งเป็น127.0.0.1ได้ พอร์ต 8080 จะได้ไม่เปิดออกนอกเครื่อง ส่วนREVERB_HOSTกับREVERB_PORTคือที่อยู่ที่ Laravel และ browser ใช้ติดต่อ เช่นws.example.comที่พอร์ต 443 -
เผื่อจำนวนสายที่เปิดพร้อมกัน ทุกสายนับเป็นไฟล์ที่เปิดอยู่หนึ่งไฟล์ ดูเพดานได้ด้วย
ulimit -nและถ้าจะรับเกิน 1,000 สายพร้อมกัน เอกสารแนะนำให้ลง extensionuvเพราะ event loop ตัวเริ่มต้นเปิดไฟล์ได้ราว 1,024 ไฟล์
กรณีที่อาจเจอเมื่อใช้งานจริง
- คนที่กดบันทึกเห็นรายการซ้ำ ได้จาก response ของตัวเองหนึ่งครั้ง และจาก event อีกครั้ง ใช้
broadcast(new OrderStatusUpdated($order))->toOthers()ให้ส่งถึงทุกคนยกเว้นคนที่กด event ต้องใช้ traitInteractsWithSocketsด้วย - event มาถึงก่อนข้อมูลลงฐานข้อมูล ถ้ายิง event ใน transaction หน้าเว็บอาจโหลดข้อมูลใหม่ตอนที่ transaction ยังไม่ commit แล้วได้ค่าเดิมกลับไป ให้ event implements
ShouldDispatchAfterCommitแบบเดียวกับการรอ commit ก่อนรัน job - ไม่มี event มาถึงเลย เช็กก่อนว่า queue worker รันอยู่ และ
BROADCAST_CONNECTIONใน.envไม่ได้เป็นlogหรือnullระหว่างพัฒนารันphp artisan reverb:start --debugดูข้อความที่วิ่งผ่านได้ - ส่งข้อมูลมากเกินกว่าที่คนในช่องควรเห็น Laravel ส่ง property ที่เป็น
publicออกไปทั้งหมด ถ้า event ถือ model ไว้ทั้งก้อน ให้เขียนbroadcastWith()เลือกเฉพาะค่าที่ต้องใช้






