Pusher 協定詳解:連線、頻道、驗證與 Webhook

Pusher Channels 協定(第 7 版)如何運作:WebSocket 握手、訊息格式、各種頻道與其簽章、客戶端事件、錯誤代碼、HTTP API 與 Webhook,以及 Hibira 如何實作。

Pusher 協定是什麼

Pusher Channels 協定是 pusher-js、Laravel Echo 與官方 Pusher 伺服器函式庫和即時服務溝通時使用的一組 WebSocket 訊息與 HTTP 呼叫。它有公開文件且相當穩定,目前版本是 7。Hibira 是實作這套協定、相容 Pusher 的獨立服務,所以只要換掉主機位址,這些函式庫就能直接使用。

協定分成三個部分:

  • WebSocket 連線。 瀏覽器與 App 開一條 WebSocket,在上面訂閱頻道、接收事件。
  • HTTP API。 你的伺服器用簽章過的 HTTP 請求發布事件、查詢頻道。客戶端永遠不透過它發布。
  • Webhook。 服務在頻道有人或清空、presence 成員加入或離開、客戶端送出事件時通知你的伺服器。

另外還有一個呼叫發生在你自己的客戶端和你自己的伺服器之間:客戶端加入 private 或 presence 頻道之前,要由你的伺服器用 App secret 替這個請求簽章,而 secret 永遠不會離開你的伺服器。

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

建立連線

客戶端連到 App 的 WebSocket 網址。路徑帶著 App key(公開資訊,可以放在瀏覽器裡),查詢字串則說明使用的協定版本和函式庫:

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 秒(Hibira 為 120 秒)沒收到任何訊息時,會送出 pusher:ping,伺服器回 pusher:pong。伺服器對沉默的客戶端也一樣:沉默 120 秒後送出 pusher:ping,若 30 秒內沒有任何訊息,就以 4201 關閉連線,之後客戶端會重新連線。如果使用者和 Hibira 之間有 proxy 或負載平衡器,它的閒置逾時必須超過 150 秒。

頻道

頻道名稱的前綴決定類型。名稱最長 164 個字元,可使用 A-Z a-z 0-9 _ - = @ , . ;。

訂閱 public 頻道只要一則訊息,伺服器以 pusher_internal:subscription_succeeded 回應:

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

接著你的伺服器用 HTTP API 發布到 orders,每個訂閱者都會收到事件:

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

授權 private 與 presence 頻道

private 頻道需要證明你的伺服器允許這條連線加入。客戶端函式庫會帶著 socket ID 和頻道名稱詢問你的驗證端點。你的伺服器檢查使用者的 session 後回傳簽章:以 App secret 為金鑰,對 socket_id:channel_name 計算 HMAC-SHA256 的十六進位值,前面加上 App key。

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

伺服器函式庫一行就能完成(Node 的 pusher.authorizeChannel(socketId, channel)、PHP 的 $pusher->authorizeChannel()、Laravel 的 Broadcast::channel())。自己動手寫則是這樣:

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"}}

簽章錯誤時會收到類型為 AuthError 的 pusher:subscription_error,連線本身維持開啟。

Presence 頻道

presence 頻道還要說明使用者是誰。驗證回應多了 channel_data,這是一個含 user_id 和選填 user_info 的 JSON 字串,簽章也涵蓋它:要簽的字串是 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 和 Pusher 一樣,每個 presence 頻道最多 100 位成員、user_info 最多 1 KB。更大的觀眾群可以用 presence-large- 頻道(Hibira 的擴充):它送出人數與 100 位成員的樣本,加入與離開則定期批次送達。

加密頻道

在 private-encrypted- 頻道上,伺服器函式庫會用由頻道名稱和你的加密主金鑰衍生出的金鑰(NaCl secretbox)加密每個事件的資料。驗證回應也會以 shared_secret 回傳該頻道的金鑰,讓客戶端能解密。Hibira 以 private 頻道的方式授權訂閱,並原樣轉送密文,無法讀取內容。加密頻道不允許客戶端事件。

使用者驗證

除了頻道之外,連線也可以以使用者身分登入。客戶端送出 pusher:signin,附上 user_data(至少含 id 的 JSON 字串)以及對 socket_id::user::user_data 簽章的 auth。收到 pusher:signin_success 後,你的伺服器就能把事件送給這位使用者的所有連線(sendToUser,經由 #server-to-user-USER_ID 送達),也能關閉他的全部連線(terminate_connections)。

客戶端事件

客戶端可以把事件直接送給同一頻道的其他訂閱者,不必繞經你的伺服器:輸入中提示、游標位置、遊戲操作都適用。規則如下:

  • 事件名稱以 client- 開頭。
  • 只能在客戶端已加入的 private 和 presence 頻道上使用,public 和加密頻道都不行。
  • 必須在 App 設定中開啟客戶端事件,否則伺服器會回 pusher:error 4009。
  • 送出者不會收到自己的事件。在 presence 頻道上,接收者會拿到送出者的 user_id。
  • 每條連線每秒最多 10 則,超過的會被丟棄並回 pusher:error 4301。
{"event":"client-typing","channel":"private-chat-7","data":{"user":"Mei"}}

你的伺服器可以透過 client_event webhook 追蹤客戶端事件。

Cache 頻道

cache 頻道會記住最後一個事件 30 分鐘,並在每位新訂閱者的 subscription_succeeded 之後立刻送出,讓頁面不必詢問你的伺服器就能顯示目前狀態。沒有快取內容時,客戶端會收到 pusher:cache_miss,你的伺服器則收到 cache_miss webhook,可以趁機發布目前的值。

錯誤與關閉代碼

4000 到 4299 的代碼會關閉連線,範圍告訴客戶端下一步該怎麼做。4300 以上的代碼以 pusher:error 送達,連線維持開啟。

HTTP API

你的伺服器向與 WebSocket 相同的主機、在 /apps/APP_ID/ 底下發出 HTTP 請求來發布和查詢:

事件內容包含事件名稱、頻道,以及字串形式的資料;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。簽章是以 App secret 為金鑰,對「方法、路徑、依名稱排序的其他參數」以換行連接後計算的 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,
});

實務上,官方伺服器函式庫(Node 的 pusher、pusher/pusher-php-server、Python 的 pusher、pusher-http-go 等)只要設定 host 選項,就會替你簽章。

Webhook

Webhook 是 Hibira 向你在 App 頁面設定的網址發出的 POST 請求。每個請求都有簽章:X-Pusher-Key 是 App key,X-Pusher-Signature 是以 App secret 為金鑰、對原始內容計算的 HMAC-SHA256 十六進位值。事件最多累積一秒批次送出,每次最多 100 個。

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

信任 webhook 之前先檢查簽章,並以固定時間比較:

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 如何對應這套協定

每個 App 位於一個叢集,就像 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 的推播通知。搬移既有 App 只需要換主機和新的憑證,請參考從 Pusher 遷移和整合指南。想即時觀察協定,可以打開 WebSocket 測試工具,它會顯示連到你的 App 的每一個訊框。