อธิบายโปรโตคอล Pusher: การเชื่อมต่อ ช่อง การยืนยันตัวตน และเว็บฮุก
โปรโตคอล Pusher Channels (เวอร์ชัน 7) ทำงานอย่างไร: การแฮนด์เชก WebSocket รูปแบบข้อความ ประเภทช่องและลายเซ็น อีเวนต์ไคลเอนต์ โค้ดข้อผิดพลาด HTTP API และเว็บฮุก รวมถึงวิธีที่ Hibira นำไปใช้
โปรโตคอล Pusher คืออะไร
โปรโตคอล Pusher Channels คือชุดข้อความ WebSocket และการเรียก HTTP ที่ pusher-js, Laravel Echo และไลบรารีเซิร์ฟเวอร์ทางการของ Pusher ใช้สื่อสารกับบริการเรียลไทม์ มีเอกสารสาธารณะและค่อนข้างคงที่ เวอร์ชันปัจจุบันคือ 7 Hibira เป็นบริการอิสระที่เข้ากันได้กับ Pusher และใช้โปรโตคอลนี้ ไลบรารีเหล่านั้นจึงใช้งานได้ทันทีเมื่อเปลี่ยนโฮสต์
โปรโตคอลมีสามส่วน:
- การเชื่อมต่อ WebSocket เบราว์เซอร์และแอปเปิด WebSocket หนึ่งเส้น แล้วสมัครรับช่องและรับอีเวนต์ผ่านเส้นนั้น
- HTTP API เซิร์ฟเวอร์ของคุณเผยแพร่อีเวนต์และสอบถามสถานะช่องด้วยคำขอ HTTP ที่ลงลายเซ็น ไคลเอนต์ไม่เผยแพร่ผ่านช่องทางนี้
- เว็บฮุก บริการแจ้งเซิร์ฟเวอร์ของคุณเมื่อช่องมีผู้สมัครรับหรือว่างลง เมื่อสมาชิก presence เข้าหรือออก และเมื่อไคลเอนต์ส่งอีเวนต์
ยังมีการเรียกอีกหนึ่งอย่างระหว่างไคลเอนต์ของคุณกับเซิร์ฟเวอร์ของคุณเอง: ก่อนไคลเอนต์จะเข้าช่อง private หรือ presence ได้ เซิร์ฟเวอร์ของคุณต้องลงลายเซ็นคำขอนั้นด้วยซีเคร็ตของแอป และซีเคร็ตจะไม่ออกจากเซิร์ฟเวอร์ของคุณเลย
การเชื่อมต่อ
ไคลเอนต์เชื่อมต่อกับ URL WebSocket ของแอป พาธมีคีย์ของแอป (เป็นข้อมูลสาธารณะ ใส่ไว้ในเบราว์เซอร์ได้อย่างปลอดภัย) และสตริงคิวรีบอกเวอร์ชันโปรโตคอลกับไลบรารีที่ใช้:
wss://ws-hkg.hibira.io/app/APP_KEY?protocol=7&client=js&version=8.4.0&flash=false| พารามิเตอร์ | ความหมาย |
|---|---|
APP_KEY | คีย์ของแอปจากแดชบอร์ด คีย์ที่ไม่มีอยู่จะทำให้การเชื่อมต่อปิดด้วยโค้ด 4001 |
protocol | เวอร์ชันโปรโตคอล Hibira รับ 5, 6 และ 7 ถ้าไม่ระบุจะปิดด้วย 4008 ถ้าไม่รองรับจะปิดด้วย 4007 |
client, version | ชื่อและเวอร์ชันของไลบรารี ใช้สำหรับบันทึกและสถิติ |
เมื่อ WebSocket เปิด เซิร์ฟเวอร์จะส่ง pusher:connection_established ซึ่ง data มี socket ID (ใช้ระบุการเชื่อมต่อนี้ในลายเซ็นและใน HTTP API) และค่า activity timeout เป็นวินาที:
{"event":"pusher:connection_established","data":"{\"socket_id\":\"123.456\",\"activity_timeout\":120}"}สังเกตว่า data เป็นสตริงที่บรรจุ JSON ไม่ใช่ออบเจกต์ JSON เซิร์ฟเวอร์เข้ารหัส data ของทุกอีเวนต์ที่ส่งแบบนี้ และไลบรารีไคลเอนต์จะถอดรหัสให้คุณ
รูปแบบข้อความ
ทุกข้อความทั้งสองทิศทางเป็นออบเจกต์ JSON หนึ่งตัวที่มีชื่อ event ส่วนใหญ่มี channel และมี data อีเวนต์ที่ชื่อขึ้นต้นด้วย pusher: หรือ pusher_internal: เป็นของโปรโตคอล ชื่ออื่นทั้งหมดเป็นอีเวนต์ของแอปพลิเคชัน
| อีเวนต์ | ทิศทาง | หน้าที่ |
|---|---|---|
pusher:connection_established | เซิร์ฟเวอร์ → ไคลเอนต์ | การเชื่อมต่อพร้อมแล้ว มี socket ID |
pusher:error | เซิร์ฟเวอร์ → ไคลเอนต์ | ข้อผิดพลาดที่มี code และ message ดูโค้ดข้อผิดพลาดด้านล่าง |
pusher:ping / pusher:pong | ทั้งสองทาง | ฮาร์ตบีต |
pusher:subscribe | ไคลเอนต์ → เซิร์ฟเวอร์ | เข้าช่อง ช่อง private และ presence ต้องแนบลายเซ็น |
pusher:unsubscribe | ไคลเอนต์ → เซิร์ฟเวอร์ | ออกจากช่อง |
pusher_internal:subscription_succeeded | เซิร์ฟเวอร์ → ไคลเอนต์ | สมัครรับสำเร็จ ช่อง presence จะแนบรายชื่อสมาชิกมาด้วย |
pusher:subscription_error | เซิร์ฟเวอร์ → ไคลเอนต์ | การสมัครรับถูกปฏิเสธ เช่น AuthError |
pusher_internal:member_added / pusher_internal:member_removed | เซิร์ฟเวอร์ → ไคลเอนต์ | ผู้ใช้เข้าหรือออกจากช่อง presence |
pusher:signin / pusher:signin_success | ทั้งสองทาง | การยืนยันตัวตนผู้ใช้ (ลงชื่อเข้าใช้) ดูด้านล่าง |
pusher:cache_miss | เซิร์ฟเวอร์ → ไคลเอนต์ | ช่อง cache ยังไม่มีข้อมูลแคช |
pusher_internal:subscription_count | เซิร์ฟเวอร์ → ไคลเอนต์ | จำนวนซ็อกเก็ตที่สมัครรับช่อง เมื่อเปิดการนับจำนวนผู้สมัครรับ |
การรักษาการเชื่อมต่อ
การเชื่อมต่อใช้ฮาร์ตบีตระดับโปรโตคอล เมื่อไคลเอนต์ไม่ได้รับอะไรเลยเป็นเวลา activity_timeout วินาที (120 วินาทีบน Hibira) จะส่ง pusher:ping และเซิร์ฟเวอร์ตอบ pusher:pong เซิร์ฟเวอร์ก็ทำเช่นเดียวกันกับไคลเอนต์ที่เงียบ: ส่ง pusher:ping หลังเงียบ 120 วินาที และถ้าไม่มีข้อความใดมาภายใน 30 วินาทีจะปิดการเชื่อมต่อด้วย 4201 จากนั้นไคลเอนต์จะเชื่อมต่อใหม่ ถ้ามีพร็อกซีหรือโหลดบาลานเซอร์อยู่ระหว่างผู้ใช้กับ Hibira ค่า idle timeout ของมันต้องนานกว่า 150 วินาที
ช่อง
คำนำหน้าชื่อช่องเป็นตัวกำหนดประเภท ชื่อยาวได้สูงสุด 164 ตัวอักษร และใช้ A-Z a-z 0-9 _ - = @ , . ; ได้
| ช่อง | คำนำหน้า | ต้องมีลายเซ็น | อีเวนต์ไคลเอนต์ |
|---|---|---|---|
| Public | ไม่มี เช่น orders | ไม่ | ไม่ได้ |
| Private | private- | ใช่ | ได้เมื่อเปิดใช้ |
| Presence | presence- | ใช่ พร้อมข้อมูลผู้ใช้ | ได้เมื่อเปิดใช้ |
| เข้ารหัส | private-encrypted- | ใช่ | ไม่ได้ |
| Cache | cache-, private-cache-, presence-cache-, private-encrypted-cache- | ตามประเภทพื้นฐาน | ตามประเภทพื้นฐาน |
| Server-to-user | #server-to-user-USER_ID | เฉพาะผู้ใช้ที่ลงชื่อเข้าใช้คนนั้น | ไม่ได้ |
| Presence ขนาดใหญ่ (Hibira) | presence-large- | ใช่ พร้อมข้อมูลผู้ใช้ | ได้เมื่อเปิดใช้ |
การสมัครรับช่อง public ใช้ข้อความเดียว และได้รับ pusher_internal:subscription_succeeded ตอบกลับ:
{"event":"pusher:subscribe","data":{"channel":"orders"}}จากนั้นเซิร์ฟเวอร์ของคุณเผยแพร่ไปยัง orders ด้วย HTTP API และผู้สมัครรับทุกคนจะได้รับอีเวนต์:
{"event":"order-shipped","channel":"orders","data":"{\"id\":1042}"}การอนุญาตช่อง private และ presence
ช่อง private ต้องมีหลักฐานว่าเซิร์ฟเวอร์ของคุณอนุญาตให้การเชื่อมต่อนี้เข้าร่วม ไลบรารีไคลเอนต์จะถาม endpoint ยืนยันตัวตนของคุณ โดยส่ง socket ID และชื่อช่องไป เซิร์ฟเวอร์ตรวจสอบเซสชันของผู้ใช้แล้วส่งลายเซ็นกลับ: ค่า HMAC-SHA256 แบบเลขฐานสิบหกของ socket_id:channel_name ที่ใช้ซีเคร็ตของแอปเป็นคีย์ นำหน้าด้วยคีย์ของแอป
ไลบรารีเซิร์ฟเวอร์ทำได้ในการเรียกครั้งเดียว (pusher.authorizeChannel(socketId, channel) ใน Node, $pusher->authorizeChannel() ใน PHP, Broadcast::channel() ใน Laravel) ถ้าเขียนเองจะเป็นแบบนี้:
import crypto from 'node:crypto';
// POST /pusher/auth with socket_id and channel_name, after checking the session
function authorize(socketId, channel) {
const signature = crypto
.createHmac('sha256', process.env.PUSHER_APP_SECRET)
.update(`${socketId}:${channel}`)
.digest('hex');
return { auth: `${process.env.PUSHER_APP_KEY}:${signature}` };
}ไคลเอนต์ส่งผลลัพธ์ไปพร้อมกับการสมัครรับ:
{"event":"pusher:subscribe","data":{"channel":"private-orders","auth":"APP_KEY:81065a8a0a6e7e1f501edc52ef55314aa125075065593e6e9bed5e538ec3d12f"}}ลายเซ็นที่ผิดจะถูกปฏิเสธด้วย pusher:subscription_error ประเภท AuthError โดยการเชื่อมต่อยังเปิดอยู่
ช่อง Presence
ช่อง presence ต้องบอกด้วยว่าผู้ใช้คือใคร คำตอบการยืนยันตัวตนจะมี channel_data เพิ่ม ซึ่งเป็นสตริง JSON ที่มี user_id และ user_info (ไม่บังคับ) และลายเซ็นครอบคลุมส่วนนี้ด้วย สตริงที่ต้องลงลายเซ็นคือ socket_id:channel_name:channel_data
{"auth":"APP_KEY:SIGNATURE","channel_data":"{\"user_id\":\"42\",\"user_info\":{\"name\":\"Mei\"}}"}จากนั้น pusher_internal:subscription_succeeded จะนำรายชื่อสมาชิก (ids, hash และ count) มา และ member_added กับ member_removed จะตามมาเมื่อผู้ใช้เข้าออก ผู้ใช้ที่เปิดหลายแท็บนับเป็นหนึ่งคน Hibira อนุญาตสมาชิก 100 คนต่อช่อง presence และ user_info ได้ 1 KB เหมือน Pusher สำหรับผู้ชมจำนวนมากกว่านั้น ช่อง presence-large- (ส่วนขยายของ Hibira) จะส่งจำนวนคนพร้อมตัวอย่างสมาชิก 100 คน และส่งการเข้าออกเป็นชุดเป็นระยะ
ช่องเข้ารหัส
บนช่อง private-encrypted- ไลบรารีเซิร์ฟเวอร์จะเข้ารหัสข้อมูลของแต่ละอีเวนต์ด้วยคีย์ที่สร้างจากชื่อช่องและมาสเตอร์คีย์การเข้ารหัสของคุณ (NaCl secretbox) คำตอบการยืนยันตัวตนจะส่งคีย์ของช่องนั้นกลับมาเป็น shared_secret ด้วย เพื่อให้ไคลเอนต์ถอดรหัสได้ Hibira อนุญาตการสมัครรับแบบเดียวกับช่อง private และส่งต่อข้อความที่เข้ารหัสโดยอ่านเนื้อหาไม่ได้ ช่องเข้ารหัสไม่อนุญาตอีเวนต์ไคลเอนต์
การยืนยันตัวตนผู้ใช้
นอกจากช่องแล้ว การเชื่อมต่อยังลงชื่อเข้าใช้ในฐานะผู้ใช้ได้ ไคลเอนต์ส่ง pusher:signin พร้อม user_data (สตริง JSON ที่มีอย่างน้อย id) และ auth ที่ลงลายเซ็นบน socket_id::user::user_data หลังได้รับ pusher:signin_success เซิร์ฟเวอร์ของคุณจะส่งอีเวนต์ถึงผู้ใช้คนนั้นได้ทุกที่ที่เขาเชื่อมต่ออยู่ (sendToUser ส่งผ่าน #server-to-user-USER_ID) และปิดการเชื่อมต่อทั้งหมดของเขาได้ (terminate_connections)
อีเวนต์ไคลเอนต์
ไคลเอนต์ส่งอีเวนต์ตรงถึงผู้สมัครรับคนอื่นในช่องเดียวกันได้โดยไม่ต้องผ่านเซิร์ฟเวอร์ของคุณ เหมาะกับการแสดงว่ากำลังพิมพ์ ตำแหน่งเคอร์เซอร์ หรือการเดินเกม กฎมีดังนี้:
- ชื่ออีเวนต์ขึ้นต้นด้วย
client- - ใช้ได้เฉพาะบนช่อง private และ presence ที่ไคลเอนต์เข้าร่วมแล้ว ใช้บนช่อง public หรือช่องเข้ารหัสไม่ได้
- ต้องเปิดอีเวนต์ไคลเอนต์ในการตั้งค่าแอป มิฉะนั้นเซิร์ฟเวอร์จะตอบด้วย
pusher:error4009 - ผู้ส่งจะไม่ได้รับอีเวนต์ของตัวเอง บนช่อง presence ผู้รับจะได้
user_idของผู้ส่ง - สูงสุด 10 ครั้งต่อวินาทีต่อการเชื่อมต่อ ส่วนที่เกินจะถูกทิ้งพร้อม
pusher:error4301
{"event":"client-typing","channel":"private-chat-7","data":{"user":"Mei"}}เซิร์ฟเวอร์ของคุณติดตามอีเวนต์ไคลเอนต์ได้ด้วยเว็บฮุก client_event
ช่อง Cache
ช่อง cache จำอีเวนต์ล่าสุดไว้ 30 นาที และส่งให้ผู้สมัครรับใหม่ทุกคนทันทีหลัง subscription_succeeded หน้าเว็บจึงแสดงสถานะปัจจุบันได้โดยไม่ต้องถามเซิร์ฟเวอร์ของคุณ เมื่อไม่มีข้อมูลแคช ไคลเอนต์จะได้รับ pusher:cache_miss และเซิร์ฟเวอร์ของคุณจะได้รับเว็บฮุก cache_miss เพื่อเผยแพร่ค่าปัจจุบัน
โค้ดข้อผิดพลาดและโค้ดปิดการเชื่อมต่อ
โค้ด 4000 ถึง 4299 จะปิดการเชื่อมต่อ และช่วงของโค้ดบอกไคลเอนต์ว่าต้องทำอะไรต่อ โค้ดตั้งแต่ 4300 มาในรูป pusher:error โดยการเชื่อมต่อยังเปิดอยู่
| ช่วง | สิ่งที่ไคลเอนต์ทำ |
|---|---|
| 4000–4099 | ไม่เชื่อมต่อใหม่ด้วยการตั้งค่าเดิม |
| 4100–4199 | รอสักพักแล้วเชื่อมต่อใหม่ |
| 4200–4299 | เชื่อมต่อใหม่ทันที |
| 4300–4399 | แจ้งข้อผิดพลาดเท่านั้น การเชื่อมต่อยังเปิดอยู่ |
| โค้ด | Hibira ส่งเมื่อ |
|---|---|
| 4001 | ไม่มีคีย์ของแอปนี้ |
| 4003 | แอปถูกปิดใช้งาน |
| 4004 | บัญชีเกินขีดจำกัดการเชื่อมต่อของแพ็กเกจ |
| 4007 | ไม่รองรับเวอร์ชันโปรโตคอลนี้ |
| 4008 | ไม่ได้ระบุเวอร์ชันโปรโตคอล |
| 4009 | ไม่ได้รับอนุญาต: origin ไม่อยู่ในรายการของแอป การเชื่อมต่อถูกยุติ หรืออีเวนต์ไคลเอนต์ไม่ได้รับอนุญาต |
| 4100 | เกินความจุ หรือไคลเอนต์อ่านช้าเกินไป รอสักพักแล้วลองใหม่ |
| 4200 | เซิร์ฟเวอร์กำลังรีสตาร์ต เชื่อมต่อใหม่ทันที |
| 4201 | ไม่ได้รับ pong ทันเวลา |
| 4301 | ถึงขีดจำกัดอัตราอีเวนต์ไคลเอนต์หรือโควตาข้อความรายวัน |
HTTP API
เซิร์ฟเวอร์ของคุณเผยแพร่และสอบถามด้วยคำขอ HTTP ไปยังโฮสต์เดียวกับ WebSocket ภายใต้ /apps/APP_ID/:
| คำขอ | หน้าที่ |
|---|---|
POST /apps/APP_ID/events | เผยแพร่อีเวนต์หนึ่งไปยังช่องได้สูงสุด 100 ช่อง |
POST /apps/APP_ID/batch_events | เผยแพร่ได้สูงสุด 10 อีเวนต์ในคำขอเดียว |
GET /apps/APP_ID/channels | แสดงช่องที่มีผู้สมัครรับ กรองตามคำนำหน้าและดูจำนวนผู้สมัครรับได้ |
GET /apps/APP_ID/channels/CHANNEL | สถานะของช่องหนึ่ง |
GET /apps/APP_ID/channels/CHANNEL/users | ผู้ใช้ในช่อง presence |
POST /apps/APP_ID/users/USER_ID/terminate_connections | ปิดการเชื่อมต่อทั้งหมดของผู้ใช้ที่ลงชื่อเข้าใช้ |
เนื้อหาของอีเวนต์ระบุชื่ออีเวนต์ ช่อง และข้อมูลในรูปสตริง socket_id จะยกเว้นการเชื่อมต่อที่ทำให้เกิดอีเวนต์ ผู้ส่งจึงไม่เห็นการเปลี่ยนแปลงของตัวเองซ้ำสองครั้ง ข้อมูลอีเวนต์มีขนาดได้ถึง 10 KB
{"name":"order-shipped","channels":["orders"],"data":"{\"id\":1042}","socket_id":"123.456"}การลงลายเซ็นคำขอ
ทุกคำขอมี auth_key, auth_timestamp (วินาที Unix ต่างจากนาฬิกาเซิร์ฟเวอร์ไม่เกิน 600 วินาที), auth_version=1.0, body_md5 สำหรับคำขอที่มีเนื้อหา และ auth_signature ลายเซ็นคือค่า HMAC-SHA256 แบบเลขฐานสิบหกที่ใช้ซีเคร็ตของแอปเป็นคีย์ ของเมธอด พาธ และพารามิเตอร์อื่นที่เรียงตามชื่อ ต่อกันด้วยการขึ้นบรรทัดใหม่:
POST
/apps/123/events
auth_key=APP_KEY&auth_timestamp=1792458481&auth_version=1.0&body_md5=529798e83b8523846d8314a6b2b6c517import crypto from 'node:crypto';
const body = JSON.stringify({ name: 'order-shipped', channels: ['orders'], data: '{"id":1042}' });
const params = {
auth_key: process.env.PUSHER_APP_KEY,
auth_timestamp: Math.floor(Date.now() / 1000),
auth_version: '1.0',
body_md5: crypto.createHash('md5').update(body).digest('hex'),
};
const query = Object.keys(params).sort().map((k) => `${k}=${params[k]}`).join('&');
const path = `/apps/${process.env.PUSHER_APP_ID}/events`;
const signature = crypto
.createHmac('sha256', process.env.PUSHER_APP_SECRET)
.update(`POST\n${path}\n${query}`)
.digest('hex');
await fetch(`https://${process.env.PUSHER_HOST}${path}?${query}&auth_signature=${signature}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body,
});ในทางปฏิบัติ ไลบรารีเซิร์ฟเวอร์ทางการ (pusher สำหรับ Node, pusher/pusher-php-server, pusher สำหรับ Python, pusher-http-go และอื่นๆ) จะลงลายเซ็นให้เมื่อคุณตั้งค่าตัวเลือกโฮสต์
เว็บฮุก
เว็บฮุกคือคำขอ POST จาก Hibira ไปยัง URL ที่คุณตั้งไว้ในหน้าแอป ทุกคำขอมีลายเซ็น: X-Pusher-Key คือคีย์ของแอป และ X-Pusher-Signature คือค่า HMAC-SHA256 แบบเลขฐานสิบหกของเนื้อหาดิบ โดยใช้ซีเคร็ตของแอปเป็นคีย์ อีเวนต์จะถูกรวมเป็นชุดนานสูงสุดหนึ่งวินาที ไม่เกิน 100 รายการต่อคำขอ
{"time_ms":1792458481958,"events":[{"name":"channel_occupied","channel":"orders"}]}| อีเวนต์ | ส่งเมื่อ |
|---|---|
channel_occupied / channel_vacated | ช่องได้ผู้สมัครรับคนแรก หรือเสียคนสุดท้ายไป |
member_added / member_removed | ซ็อกเก็ตแรกของผู้ใช้เข้าช่อง presence หรือซ็อกเก็ตสุดท้ายออก |
client_event | ไคลเอนต์ส่งอีเวนต์ client- |
cache_miss | ไคลเอนต์สมัครรับช่อง cache ที่ยังไม่มีข้อมูลแคช |
subscription_count | จำนวนผู้สมัครรับของช่องเปลี่ยน เมื่อเปิดการนับ |
ตรวจลายเซ็นก่อนเชื่อถือเว็บฮุก โดยเปรียบเทียบแบบเวลาคงที่:
import crypto from 'node:crypto';
function verify(rawBody, signature) {
const expected = crypto.createHmac('sha256', process.env.PUSHER_APP_SECRET).update(rawBody).digest('hex');
return signature?.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}Hibira ใช้โปรโตคอลนี้อย่างไร
แต่ละแอปอยู่ในคลัสเตอร์เดียวเหมือนคลัสเตอร์ของ Pusher และไคลเอนต์กับเซิร์ฟเวอร์ของแอปเชื่อมต่อกับโฮสต์ของคลัสเตอร์นั้น WebSocket และ HTTP API ใช้โฮสต์เดียวกัน
| คลัสเตอร์ | ภูมิภาค | โฮสต์ |
|---|---|---|
hkg | ฮ่องกง | ws-hkg.hibira.io (หรือ ws.hibira.io) |
tyo | โตเกียว | ws-tyo.hibira.io (วางแผนไว้) |
sin | สิงคโปร์ | ws-sin.hibira.io (วางแผนไว้) |
ไลบรารีไคลเอนต์รับโฮสต์เป็นตัวเลือก และเมื่อกำหนดโฮสต์แล้วตัวเลือก cluster จะถูกละเว้น:
const pusher = new Pusher('APP_KEY', {
cluster: 'hkg',
wsHost: 'ws-hkg.hibira.io',
forceTLS: true,
enabledTransports: ['ws', 'wss'],
});สิ่งที่ใช้ได้โดยไม่ต้องแก้: pusher-js, Laravel Echo และ Laravel Broadcasting, ไลบรารีเซิร์ฟเวอร์ทางการ, ไลบรารี iOS และ Android ทางการ, ไคลเอนต์จากชุมชนสำหรับ Flutter และ Unity ที่ตั้งโฮสต์ได้ และการแจ้งเตือนแบบพุชที่เข้ากันได้กับ Beams การย้ายแอปที่มีอยู่ใช้แค่การเปลี่ยนโฮสต์และข้อมูลรับรองใหม่ ดูการย้ายจาก Pusherและคู่มือการเชื่อมต่อ ถ้าอยากดูโปรโตคอลทำงานจริง เปิดเครื่องมือทดสอบ WebSocket ซึ่งแสดงทุกเฟรมของการเชื่อมต่อกับแอปของคุณ