Giải thích giao thức Pusher: kết nối, kênh, xác thực và webhook

Cách giao thức Pusher Channels (phiên bản 7) hoạt động: bắt tay WebSocket, định dạng thông điệp, các loại kênh và chữ ký, sự kiện client, mã lỗi, HTTP API và webhook, cùng cách Hibira triển khai nó.

Giao thức Pusher là gì

Giao thức Pusher Channels là tập hợp các thông điệp WebSocket và lệnh gọi HTTP mà pusher-js, Laravel Echo và các thư viện máy chủ chính thức của Pusher dùng để trao đổi với một dịch vụ thời gian thực. Giao thức có tài liệu công khai và ổn định; phiên bản hiện tại là 7. Hibira là một dịch vụ độc lập, tương thích Pusher, triển khai giao thức này, nên các thư viện đó chạy được ngay khi bạn đổi host.

Giao thức có ba phần:

  • Kết nối WebSocket. Trình duyệt và ứng dụng mở một WebSocket, đăng ký kênh và nhận sự kiện trên đó.
  • HTTP API. Máy chủ của bạn phát sự kiện và truy vấn kênh bằng các yêu cầu HTTP có chữ ký. Client không bao giờ phát sự kiện qua đây.
  • Webhook. Dịch vụ báo cho máy chủ của bạn khi kênh có người đăng ký hoặc trống, khi thành viên presence vào hoặc rời, và khi client gửi sự kiện.

Còn một lệnh gọi nữa diễn ra giữa client và máy chủ của chính bạn: trước khi client được vào kênh private hoặc presence, máy chủ của bạn ký yêu cầu đó bằng secret của ứng dụng, và secret không bao giờ rời khỏi máy chủ của bạn.

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

Kết nối

Client kết nối tới URL WebSocket của ứng dụng. Đường dẫn chứa app key (thông tin công khai, đưa vào trình duyệt vẫn an toàn), còn chuỗi truy vấn cho biết phiên bản giao thức và thư viện đang dùng:

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

Khi WebSocket mở, máy chủ gửi pusher:connection_established. data của nó chứa socket ID (dùng để xác định kết nối này trong chữ ký và trong HTTP API) và activity timeout tính bằng giây:

{"event":"pusher:connection_established","data":"{\"socket_id\":\"123.456\",\"activity_timeout\":120}"}

Lưu ý data là một chuỗi chứa JSON, không phải đối tượng JSON. Máy chủ mã hóa data của mọi sự kiện nó gửi theo cách này, và thư viện client sẽ giải mã giúp bạn.

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

Định dạng thông điệp

Mọi thông điệp theo cả hai chiều là một đối tượng JSON có tên event, thường có channel, và data. Sự kiện có tên bắt đầu bằng pusher: hoặc pusher_internal: thuộc về giao thức; mọi tên khác là sự kiện của ứng dụng.

Giữ kết nối

Kết nối dùng nhịp tim ở mức giao thức. Khi client không nhận được gì trong activity_timeout giây (120 giây trên Hibira), nó gửi pusher:ping và máy chủ trả lời pusher:pong. Máy chủ cũng làm vậy với client im lặng: gửi pusher:ping sau 120 giây im lặng, và nếu không có thông điệp nào trong 30 giây thì đóng kết nối với 4201, sau đó client kết nối lại. Nếu có proxy hoặc bộ cân bằng tải giữa người dùng và Hibira, thời gian chờ nhàn rỗi của nó phải dài hơn 150 giây.

Kênh

Tiền tố của tên kênh quyết định loại kênh. Tên dài tối đa 164 ký tự và dùng được A-Z a-z 0-9 _ - = @ , . ;.

Đăng ký kênh public chỉ cần một thông điệp, được trả lời bằng pusher_internal:subscription_succeeded:

{"event":"pusher:subscribe","data":{"channel":"orders"}}

Sau đó máy chủ của bạn phát tới orders bằng HTTP API, và mọi người đăng ký đều nhận được sự kiện:

{"event":"order-shipped","channel":"orders","data":"{\"id\":1042}"}

Cấp quyền kênh private và presence

Kênh private cần bằng chứng rằng máy chủ của bạn cho phép kết nối này tham gia. Thư viện client hỏi endpoint xác thực của bạn, gửi kèm socket ID và tên kênh. Máy chủ kiểm tra phiên của người dùng rồi trả về chữ ký: giá trị HMAC-SHA256 dạng hex của socket_id:channel_name, dùng secret của ứng dụng làm khóa, đặt app key ở phía trước.

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

Thư viện máy chủ làm việc này trong một lệnh gọi (pusher.authorizeChannel(socketId, channel) trong Node, $pusher->authorizeChannel() trong PHP, Broadcast::channel() trong Laravel). Tự viết thì như sau:

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}` };
}

Client gửi kết quả cùng với yêu cầu đăng ký:

{"event":"pusher:subscribe","data":{"channel":"private-orders","auth":"APP_KEY:81065a8a0a6e7e1f501edc52ef55314aa125075065593e6e9bed5e538ec3d12f"}}

Chữ ký sai bị từ chối bằng pusher:subscription_error loại AuthError; kết nối vẫn mở.

Kênh presence

Kênh presence còn cho biết người dùng là ai. Phản hồi xác thực có thêm channel_data, một chuỗi JSON chứa user_id và user_info (tùy chọn), và chữ ký bao gồm cả phần này: chuỗi cần ký là socket_id:channel_name:channel_data.

{"auth":"APP_KEY:SIGNATURE","channel_data":"{\"user_id\":\"42\",\"user_info\":{\"name\":\"Mei\"}}"}

Khi đó pusher_internal:subscription_succeeded mang theo danh sách thành viên (ids, hash và count), rồi member_added và member_removed đến khi người dùng vào ra. Người dùng mở nhiều tab vẫn chỉ tính là một. Giống Pusher, Hibira cho phép 100 thành viên mỗi kênh presence và user_info tối đa 1 KB. Với lượng khán giả lớn hơn, kênh presence-large- (phần mở rộng của Hibira) gửi số người và một mẫu 100 thành viên, còn lượt vào ra được gửi theo lô định kỳ.

Kênh mã hóa

Trên kênh private-encrypted-, thư viện máy chủ mã hóa dữ liệu của từng sự kiện bằng một khóa sinh ra từ tên kênh và khóa chính mã hóa của bạn (NaCl secretbox). Phản hồi xác thực cũng trả về khóa của kênh đó dưới dạng shared_secret để client giải mã. Hibira cấp quyền đăng ký như kênh private và chuyển tiếp bản mã mà không đọc được nội dung. Kênh mã hóa không cho phép sự kiện client.

Xác thực người dùng

Ngoài kênh, một kết nối còn có thể đăng nhập với tư cách người dùng. Client gửi pusher:signin kèm user_data (chuỗi JSON có ít nhất id) và auth ký trên socket_id::user::user_data. Sau pusher:signin_success, máy chủ của bạn có thể gửi sự kiện tới người dùng đó ở mọi nơi họ đang kết nối (sendToUser, gửi qua #server-to-user-USER_ID) và đóng mọi kết nối của họ (terminate_connections).

Sự kiện client

Client có thể gửi sự kiện thẳng tới những người đăng ký khác trên cùng kênh mà không phải đi vòng qua máy chủ của bạn: báo đang gõ, vị trí con trỏ, nước đi trong game. Quy tắc:

  • Tên sự kiện bắt đầu bằng client-.
  • Chỉ trên kênh private và presence mà client đã tham gia; không bao giờ trên kênh public hay kênh mã hóa.
  • Phải bật sự kiện client trong cài đặt ứng dụng; nếu không, máy chủ trả lời bằng pusher:error 4009.
  • Người gửi không nhận lại sự kiện của chính mình. Trên kênh presence, người nhận có user_id của người gửi.
  • Tối đa 10 sự kiện mỗi giây trên mỗi kết nối; vượt quá sẽ bị bỏ kèm pusher:error 4301.
{"event":"client-typing","channel":"private-chat-7","data":{"user":"Mei"}}

Máy chủ của bạn có thể theo dõi sự kiện client bằng webhook client_event.

Kênh cache

Kênh cache ghi nhớ sự kiện cuối cùng trong 30 phút và gửi nó cho mỗi người đăng ký mới ngay sau subscription_succeeded, nhờ vậy trang hiển thị được trạng thái hiện tại mà không cần hỏi máy chủ của bạn. Khi chưa có gì được lưu, client nhận pusher:cache_miss và máy chủ của bạn nhận webhook cache_miss để phát giá trị hiện tại.

Mã lỗi và mã đóng kết nối

Các mã từ 4000 đến 4299 đóng kết nối, và dải mã cho client biết phải làm gì tiếp theo. Các mã từ 4300 trở lên đến dưới dạng pusher:error và kết nối vẫn mở.

HTTP API

Máy chủ của bạn phát và truy vấn bằng các yêu cầu HTTP tới cùng host với WebSocket, dưới /apps/APP_ID/:

Nội dung sự kiện gồm tên sự kiện, các kênh và dữ liệu dạng chuỗi; socket_id loại trừ kết nối gây ra sự kiện, để người gửi không thấy thay đổi của mình hai lần. Dữ liệu sự kiện tối đa 10 KB.

{"name":"order-shipped","channels":["orders"],"data":"{\"id\":1042}","socket_id":"123.456"}

Ký yêu cầu

Mỗi yêu cầu mang auth_key, auth_timestamp (giây Unix, lệch không quá 600 giây so với đồng hồ máy chủ), auth_version=1.0, body_md5 với yêu cầu có nội dung, và auth_signature. Chữ ký là giá trị HMAC-SHA256 dạng hex, dùng secret của ứng dụng làm khóa, của phương thức, đường dẫn và các tham số còn lại sắp xếp theo tên, nối với nhau bằng dấu xuống dòng:

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,
});

Trên thực tế, các thư viện máy chủ chính thức (pusher cho Node, pusher/pusher-php-server, pusher cho Python, pusher-http-go và các thư viện khác) sẽ ký giúp bạn khi bạn đặt tùy chọn host.

Webhook

Webhook là các yêu cầu POST từ Hibira tới URL bạn đặt trong trang ứng dụng. Mỗi yêu cầu đều có chữ ký: X-Pusher-Key chứa app key và X-Pusher-Signature là giá trị HMAC-SHA256 dạng hex của nội dung gốc, dùng secret của ứng dụng làm khóa. Sự kiện được gom theo lô tối đa một giây, tối đa 100 sự kiện mỗi yêu cầu.

{"time_ms":1792458481958,"events":[{"name":"channel_occupied","channel":"orders"}]}

Kiểm tra chữ ký trước khi tin một webhook, so sánh trong thời gian không đổi:

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 ánh xạ giao thức thế nào

Mỗi ứng dụng nằm trong một cụm, giống các cụm của Pusher, và client cùng máy chủ của nó kết nối tới host của cụm đó. WebSocket và HTTP API dùng chung host.

Thư viện client nhận host qua tùy chọn; khi đó tùy chọn cluster bị bỏ qua:

const pusher = new Pusher('APP_KEY', {
  cluster: 'hkg',
  wsHost: 'ws-hkg.hibira.io',
  forceTLS: true,
  enabledTransports: ['ws', 'wss'],
});

Những gì chạy được mà không cần sửa: pusher-js, Laravel Echo và Laravel Broadcasting, các thư viện máy chủ chính thức, thư viện iOS và Android chính thức, các client cộng đồng cho Flutter và Unity cho phép đặt host, và thông báo đẩy tương thích Beams. Chuyển một ứng dụng sẵn có chỉ cần đổi host và dùng thông tin xác thực mới; xem Chuyển từ Pusher và hướng dẫn tích hợp. Muốn xem giao thức hoạt động trực tiếp, hãy mở công cụ kiểm thử WebSocket, nơi hiển thị mọi khung của kết nối tới ứng dụng của bạn.