อธิบายโปรโตคอล 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 ได้ เซิร์ฟเวอร์ของคุณต้องลงลายเซ็นคำขอนั้นด้วยซีเคร็ตของแอป และซีเคร็ตจะไม่ออกจากเซิร์ฟเวอร์ของคุณเลย

Browser / apppusher-js · EchoHibiraws.hibira.ioYour serverpusher-http-*WebSocketprotocol 7HTTP APIwebhookschannel authorizationPOST socket_id + channel_name → {"auth": "key:signature"}

การเชื่อมต่อ

ไคลเอนต์เชื่อมต่อกับ URL WebSocket ของแอป พาธมีคีย์ของแอป (เป็นข้อมูลสาธารณะ ใส่ไว้ในเบราว์เซอร์ได้อย่างปลอดภัย) และสตริงคิวรีบอกเวอร์ชันโปรโตคอลกับไลบรารีที่ใช้:

wss://ws-hkg.hibira.io/app/APP_KEY?protocol=7&client=js&version=8.4.0&flash=false

เมื่อ 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 ของทุกอีเวนต์ที่ส่งแบบนี้ และไลบรารีไคลเอนต์จะถอดรหัสให้คุณ

ClientHibiraGET wss://ws.hibira.io/app/APP_KEY?protocol=7Upgrade: websocketpusher:connection_established{"socket_id":"123.456","activity_timeout":120}pusher:subscribe{"channel":"orders"}pusher_internal:subscription_succeeded"channel":"orders"order-shippedyour event, from your server120 s without a messagepusher:pingpusher:pong

รูปแบบข้อความ

ทุกข้อความทั้งสองทิศทางเป็นออบเจกต์ JSON หนึ่งตัวที่มีชื่อ event ส่วนใหญ่มี channel และมี data อีเวนต์ที่ชื่อขึ้นต้นด้วย pusher: หรือ pusher_internal: เป็นของโปรโตคอล ชื่ออื่นทั้งหมดเป็นอีเวนต์ของแอปพลิเคชัน

การรักษาการเชื่อมต่อ

การเชื่อมต่อใช้ฮาร์ตบีตระดับโปรโตคอล เมื่อไคลเอนต์ไม่ได้รับอะไรเลยเป็นเวลา 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 ใช้ข้อความเดียว และได้รับ 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 ที่ใช้ซีเคร็ตของแอปเป็นคีย์ นำหน้าด้วยคีย์ของแอป

ClientYour serverHibiraconnectsocket_id "123.456"POST /pusher/authsocket_id, channel_name{"auth":"APP_KEY:HMAC…"}pusher:subscribe{"channel":"private-orders","auth":…}HMAC-SHA256(secret, "123.456:private-orders") matchespusher_internal:subscription_succeeded

ไลบรารีเซิร์ฟเวอร์ทำได้ในการเรียกครั้งเดียว (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:error 4009
  • ผู้ส่งจะไม่ได้รับอีเวนต์ของตัวเอง บนช่อง presence ผู้รับจะได้ user_id ของผู้ส่ง
  • สูงสุด 10 ครั้งต่อวินาทีต่อการเชื่อมต่อ ส่วนที่เกินจะถูกทิ้งพร้อม pusher:error 4301
{"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 โดยการเชื่อมต่อยังเปิดอยู่

HTTP API

เซิร์ฟเวอร์ของคุณเผยแพร่และสอบถามด้วยคำขอ HTTP ไปยังโฮสต์เดียวกับ WebSocket ภายใต้ /apps/APP_ID/:

เนื้อหาของอีเวนต์ระบุชื่ออีเวนต์ ช่อง และข้อมูลในรูปสตริง 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=529798e83b8523846d8314a6b2b6c517
import 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"}]}

ตรวจลายเซ็นก่อนเชื่อถือเว็บฮุก โดยเปรียบเทียบแบบเวลาคงที่:

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 ใช้โฮสต์เดียวกัน

ไลบรารีไคลเอนต์รับโฮสต์เป็นตัวเลือก และเมื่อกำหนดโฮสต์แล้วตัวเลือก 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 ซึ่งแสดงทุกเฟรมของการเชื่อมต่อกับแอปของคุณ