Broadcasting ใน Laravel คืออะไร

Broadcasting คือการส่ง event จาก Laravel ไปถึงหน้าเว็บที่เปิดอยู่ผ่าน WebSocket ทันทีที่มีอะไรเปลี่ยน แทนให้หน้าเว็บคอยถาม server ซ้ำ ๆ ทำงานด้วยสี่ชิ้น คือ event, queue worker, WebSocket server อย่าง Reverb และ Echo ในหน้าเว็บ

เผยแพร่
เวลาอ่าน
5 นาที
แท็ก
#Laravel#Queue
ในโพสต์นี้ 7 หัวข้อ
  1. polling กับ WebSocket ต่างกันที่ใครเริ่มคุย
  2. broadcasting มีสี่ชิ้นทำงานต่อกัน
  3. event บอกว่าจะส่งไป channel ไหน
  4. channel มีสามแบบตามว่าใครฟังได้
  5. Reverb เป็น WebSocket server ที่รันบนเครื่องเราเอง
  6. บน production ต้องรัน Reverb ค้างไว้ตลอด
  7. กรณีที่อาจเจอเมื่อใช้งานจริง

หน้าเว็บที่ต้องเห็นของใหม่เองโดยไม่ต้องกด 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 ที่บอกให้เปลี่ยนสายเป็น WebSocket

    location / {
        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 สายพร้อมกัน เอกสารแนะนำให้ลง extension uv เพราะ event loop ตัวเริ่มต้นเปิดไฟล์ได้ราว 1,024 ไฟล์

กรณีที่อาจเจอเมื่อใช้งานจริง

  • คนที่กดบันทึกเห็นรายการซ้ำ ได้จาก response ของตัวเองหนึ่งครั้ง และจาก event อีกครั้ง ใช้ broadcast(new OrderStatusUpdated($order))->toOthers() ให้ส่งถึงทุกคนยกเว้นคนที่กด event ต้องใช้ trait InteractsWithSockets ด้วย
  • 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() เลือกเฉพาะค่าที่ต้องใช้

ตอนถัดไปของซีรีส์ Broadcasting และ Reverb ใน Laravel

อ่านต่อ

ถูกอ้างถึงใน

ล่าสุด

ดูทั้งหมด →