Pusher プロトコル解説:接続、チャンネル、認証、Webhook

Pusher Channels プロトコル(バージョン 7)の仕組み:WebSocket のハンドシェイク、メッセージ形式、チャンネルの種類と署名、クライアントイベント、エラーコード、HTTP API と Webhook、そして Hibira での実装。

Pusher プロトコルとは

Pusher Channels プロトコルは、pusher-js、Laravel Echo、公式の Pusher サーバーライブラリがリアルタイムサービスとやり取りするための WebSocket メッセージと HTTP 呼び出しの取り決めです。公開ドキュメントがあり安定していて、現在のバージョンは 7 です。Hibira はこのプロトコルを実装した Pusher 互換の独立したサービスなので、ホストを変えるだけでこれらのライブラリがそのまま動きます。

プロトコルは 3 つの部分からなります。

  • WebSocket 接続。 ブラウザやアプリが WebSocket を 1 本開き、その上でチャンネルを購読してイベントを受け取ります。
  • HTTP API。 あなたのサーバーが署名付きの HTTP リクエストでイベントを配信し、チャンネルの状態を問い合わせます。クライアントがこれを使って配信することはありません。
  • Webhook。 チャンネルに購読者が付いたときや空になったとき、presence のメンバーが参加・退出したとき、クライアントがイベントを送ったときに、サービスがあなたのサーバーへ知らせます。

もう 1 つ、あなたのクライアントとあなたのサーバーの間でも呼び出しがあります。クライアントが private または presence チャンネルに参加する前に、あなたのサーバーがアプリのシークレットでそのリクエストに署名します。シークレットがサーバーの外に出ることはありません。

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

接続

クライアントはアプリの WebSocket URL に接続します。パスにはアプリキー(公開情報で、ブラウザに含めても安全)が入り、クエリ文字列でプロトコルのバージョンとライブラリを伝えます。

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

WebSocket が開くと、サーバーは pusher:connection_established を送ります。data には、署名や HTTP API でこの接続を識別するソケット ID と、秒単位のアクティビティタイムアウトが入っています。

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

メッセージの形式

どちら向きのメッセージも、event 名、多くの場合は channel、そして data を持つ 1 つの JSON オブジェクトです。名前が pusher: か pusher_internal: で始まるイベントはプロトコルのもので、それ以外の名前はアプリケーションのイベントです。

接続を保つ

接続はプロトコルレベルのハートビートを使います。クライアントは activity_timeout 秒(Hibira では 120 秒)何も受け取らないと pusher:ping を送り、サーバーが pusher:pong を返します。サーバー側も同じで、120 秒沈黙したクライアントに pusher:ping を送り、30 秒以内に何も届かなければ 4201 で接続を閉じます。その後クライアントは再接続します。ユーザーと Hibira の間にプロキシやロードバランサーがある場合、そのアイドルタイムアウトは 150 秒より長くしてください。

チャンネル

チャンネル名の接頭辞が種類を決めます。名前は最大 164 文字で、A-Z a-z 0-9 _ - = @ , . ; が使えます。

public チャンネルの購読はメッセージ 1 つで、pusher_internal:subscription_succeeded が返ってきます。

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

あとはあなたのサーバーが HTTP API で orders に配信すれば、購読者全員がイベントを受け取ります。

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

private と presence チャンネルの認可

private チャンネルには、この接続の参加をあなたのサーバーが認めたという証明が必要です。クライアントライブラリはソケット ID とチャンネル名を送って、あなたの認証エンドポイントに問い合わせます。サーバーはユーザーのセッションを確認し、署名を返します。署名は、アプリのシークレットをキーとした socket_id:channel_name の HMAC-SHA256(16 進数)に、アプリキーを前置したものです。

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

サーバーライブラリなら 1 回の呼び出しで済みます(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 が届きます。複数のタブを開いたユーザーも 1 人として数えます。Hibira では Pusher と同じく、presence チャンネルあたり 100 メンバー、user_info は 1 KB までです。もっと大きな視聴者には、Hibira 独自の拡張である presence-large- チャンネルが人数と 100 人分のサンプルを送り、参加と退出は定期的にまとめて届けます。

暗号化チャンネル

private-encrypted- チャンネルでは、サーバーライブラリがチャンネル名と暗号化マスターキーから導いたキー(NaCl secretbox)で各イベントのデータを暗号化します。認証レスポンスはそのチャンネルのキーも shared_secret として返すので、クライアントは復号できます。Hibira は private チャンネルと同じように購読を認可し、暗号文を中身を読めないまま中継します。暗号化チャンネルではクライアントイベントは使えません。

ユーザー認証

チャンネルとは別に、接続はユーザーとしてサインインすることもできます。クライアントは user_data(少なくとも id を持つ JSON 文字列)と、socket_id::user::user_data に対して署名した auth を付けて pusher:signin を送ります。pusher:signin_success のあとは、あなたのサーバーがそのユーザーのすべての接続にイベントを送ったり(sendToUser、#server-to-user-USER_ID で届く)、すべての接続を閉じたり(terminate_connections)できます。

クライアントイベント

クライアントは、あなたのサーバーを経由せずに、同じチャンネルのほかの購読者へ直接イベントを送れます。入力中の表示、カーソル位置、ゲームの操作などに向いています。ルールは次のとおりです。

  • イベント名は client- で始まる。
  • 参加済みの private と presence チャンネルでのみ使える。public と暗号化チャンネルでは使えない。
  • アプリの設定でクライアントイベントを有効にする必要がある。無効なら pusher:error 4009 が返る。
  • 送信者は自分のイベントを受け取らない。presence チャンネルでは受信者に送信者の user_id が届く。
  • 1 接続あたり毎秒 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 を持ちます。署名は、メソッド、パス、名前順に並べたその他のパラメーターを改行でつないだものの HMAC-SHA256(アプリのシークレットをキーとする 16 進数)です。

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 など)が、ホストのオプションを設定するだけで署名してくれます。

Webhook

Webhook は、アプリのページで設定した URL へ Hibira が送る POST リクエストです。各リクエストには署名があり、X-Pusher-Key にアプリキー、X-Pusher-Signature に生の本文の HMAC-SHA256(アプリのシークレットをキーとする 16 進数)が入ります。イベントは最大 1 秒まとめられ、1 リクエストに最大 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 でのプロトコルの対応

Pusher のクラスターと同じように、各アプリは 1 つのクラスターに属し、そのクライアントとサーバーはクラスターのホストに接続します。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 テスターを開くと、あなたのアプリへの接続のフレームがすべて表示されます。