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 チャンネルに参加する前に、あなたのサーバーがアプリのシークレットでそのリクエストに署名します。シークレットがサーバーの外に出ることはありません。
接続
クライアントはアプリの WebSocket URL に接続します。パスにはアプリキー(公開情報で、ブラウザに含めても安全)が入り、クエリ文字列でプロトコルのバージョンとライブラリを伝えます。
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 には、署名や HTTP API でこの接続を識別するソケット ID と、秒単位のアクティビティタイムアウトが入っています。
{"event":"pusher:connection_established","data":"{\"socket_id\":\"123.456\",\"activity_timeout\":120}"}data は JSON オブジェクトではなく、JSON を入れた文字列である点に注意してください。サーバーが送るすべてのイベントの data はこの形でエンコードされ、クライアントライブラリがデコードしてくれます。
メッセージの形式
どちら向きのメッセージも、event 名、多くの場合は channel、そして data を持つ 1 つの JSON オブジェクトです。名前が pusher: か pusher_internal: で始まるイベントはプロトコルのもので、それ以外の名前はアプリケーションのイベントです。
| イベント | 向き | 用途 |
|---|---|---|
pusher:connection_established | サーバー → クライアント | 接続の準備ができた。ソケット 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 秒(Hibira では 120 秒)何も受け取らないと pusher:ping を送り、サーバーが pusher:pong を返します。サーバー側も同じで、120 秒沈黙したクライアントに pusher:ping を送り、30 秒以内に何も届かなければ 4201 で接続を閉じます。その後クライアントは再接続します。ユーザーと Hibira の間にプロキシやロードバランサーがある場合、そのアイドルタイムアウトは 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 チャンネルの購読はメッセージ 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 進数)に、アプリキーを前置したものです。
サーバーライブラリなら 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:error4009 が返る。 - 送信者は自分のイベントを受け取らない。presence チャンネルでは受信者に送信者の
user_idが届く。 - 1 接続あたり毎秒 10 件まで。超えた分は破棄され、
pusher:error4301 が返る。
{"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 として届き、接続は開いたままです。
| 範囲 | クライアントの動作 |
|---|---|
| 4000–4099 | 同じ設定で再接続しない。 |
| 4100–4199 | 間隔を空けて再接続する。 |
| 4200–4299 | すぐに再接続する。 |
| 4300–4399 | エラーのみ。接続は開いたまま。 |
| コード | Hibira が送るとき |
|---|---|
| 4001 | アプリキーが存在しない。 |
| 4003 | アプリが無効になっている。 |
| 4004 | アカウントがプランの接続数上限を超えている。 |
| 4007 | 対応していないプロトコルバージョン。 |
| 4008 | プロトコルバージョンの指定がない。 |
| 4009 | 許可されていない:オリジンがアプリの許可リストにない、接続が切断された、またはクライアントイベントが許可されていない。 |
| 4100 | 容量超過、またはクライアントの読み込みが遅すぎる。間隔を空けて再試行。 |
| 4200 | サーバーの再起動中。すぐに再接続。 |
| 4201 | pong が時間内に届かなかった。 |
| 4301 | クライアントイベントのレート上限、または 1 日のメッセージ上限に達した。 |
HTTP API
あなたのサーバーは、WebSocket と同じホストの /apps/APP_ID/ 以下に HTTP リクエストを送って配信と問い合わせを行います。
| リクエスト | 用途 |
|---|---|
POST /apps/APP_ID/events | 1 つのイベントを最大 100 チャンネルに配信する。 |
POST /apps/APP_ID/batch_events | 1 回のリクエストで最大 10 イベントを配信する。 |
GET /apps/APP_ID/channels | 購読者のいるチャンネルを一覧する。接頭辞での絞り込みと購読数にも対応。 |
GET /apps/APP_ID/channels/CHANNEL | 1 つのチャンネルの状態。 |
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(アプリのシークレットをキーとする 16 進数)です。
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,
});実際には、公式サーバーライブラリ(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"}]}| イベント | 送られるとき |
|---|---|
channel_occupied / channel_vacated | チャンネルに最初の購読者が付いた、または最後の購読者がいなくなった。 |
member_added / member_removed | ユーザーの最初のソケットが presence チャンネルに参加した、または最後のソケットが退出した。 |
client_event | クライアントが client- イベントを送った。 |
cache_miss | クライアントがキャッシュの空の cache チャンネルを購読した。 |
subscription_count | カウントが有効なとき、チャンネルの購読数が変わった。 |
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 は同じホストを使います。
| クラスター | リージョン | ホスト |
|---|---|---|
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 テスターを開くと、あなたのアプリへの接続のフレームがすべて表示されます。