Azure Web PubSub は WebSocket ベースでリアルタイム配信を実現できる便利なサービスですが、JSON では動くのに protobuf に切り替えた途端「クライアント側で受信できない」「Invalid event name で切断される」といったトラブルが起きがちです。本記事では原因の本質、正しい protobuf スキーマ、C# での実装ポイントを具体例つきで整理します。
発生している症状
今回の状況を、よくある再現パターンとして整理すると次のとおりです。
System.Net.WebSockets.ClientWebSocketで Azure Web PubSub に接続しているjson.webpubsub.azure.v1では双方向通信できるprotobuf.webpubsub.azure.v1に切り替えると、クライアント側でデータを受信できない- 受信データを
DownstreamMessageとしてパースしようとすると、次のような理由で切断される
{
"disconnectedMessage": {
"reason": "Invalid payload. Invalid value : '': Invalid event name. Valid event name should be word in between 1 and 128 characters long."
}
}
この「Invalid event name」は、Azure Web PubSub の protobuf サブプロトコルが要求するメッセージ形式に対して、送信側の protobuf が噛み合っていないときに典型的に出ます。
最重要ポイント:protobuf サブプロトコルは「自由に protobuf を流せる」仕組みではない
ここが最初の落とし穴です。
protobuf.webpubsub.azure.v1 を指定した瞬間、送受信フレームは “Azure Web PubSub が定義する protobuf プロトコル” に従う必要があります。つまり、クライアントが送るバイナリは「自分が定義した任意の protobuf」ではなく、Azure Web PubSub が理解できる UpstreamMessage でなければなりません(受信も DownstreamMessage になります)。
混乱しやすいので、やりたいことと最適な方式を表にします。
| やりたいこと | おすすめの方式 | 送るデータの形 | ハマりどころ |
|---|---|---|---|
| サーバー(Upstream)に自前の protobuf を送りたい | シンプル WebSocket(サブプロトコルなし or 自前) | 任意のバイナリ(自分の protobuf) | PubSub プロトコルを選ぶと、任意バイナリは解釈されずエラーになりがち |
| クライアント同士でグループ送受信したい(Pub/Sub) | PubSub WebSocket(json.webpubsub.azure.v1 / protobuf.webpubsub.azure.v1) | UpstreamMessage で join / sendToGroup を送る | 「自分の protobuf をそのまま送ってしまう」→ Invalid payload |
| 固定グループに送るだけで OK(接続時に group を決め打ち) | シンプル WebSocket の sendToGroup モード | 任意のバイナリ(自分の protobuf) | 複数グループ/動的 join が不要ならこれが最短 |
つまり、今回の「JSON は通るが protobuf で受信できない」は、ほぼ確実に“protobuf サブプロトコルが期待する構造で送っていない”が原因です。
エラーを正しく読む:「Invalid event name」で切断される本当の意味
このエラー文が示しているのは、次のいずれかです。
EventMessageを送っているのに、イベント名(event)が空- そもそも送っているバイナリが
UpstreamMessageとして解釈できず、内部的に「event が空」扱いになっている
ここで重要なのは、PubSub プロトコルにおける EventMessage は「グループに配信するメッセージ」ではなく「Upstream のイベントハンドラに送るイベント」という点です。
- 他のクライアントに配信したい →
SendToGroupMessage - サーバーのイベントハンドラに届けたい →
EventMessage(このときeventが必須)
「protobuf にしたら受信できない」の背景には、JSON では sendToGroup を送っていたのに、protobuf 側で EventMessage を組み立ててしまっている、というズレがよくあります。
公式仕様:Web PubSub の protobuf スキーマ(要点抜粋)
Azure Web PubSub の protobuf サブプロトコルは、送信用に UpstreamMessage、受信用に DownstreamMessage を定義しています。公式ドキュメントのスキーマ(抜粋)は概ね次の形です(読みやすさのために必要部分だけ抜粋しています)。
syntax = "proto3";
import "google/protobuf/any.proto";
message UpstreamMessage {
oneof message {
SendToGroupMessage send_to_group_message = 1;
EventMessage event_message = 5;
JoinGroupMessage join_group_message = 6;
LeaveGroupMessage leave_group_message = 7;
PingMessage ping_message = 9;
}
message SendToGroupMessage {
string group = 1;
optional uint64 ack_id = 2;
MessageData data = 3;
}
message EventMessage {
string event = 1; // ← これが空だと Invalid event name
MessageData data = 2;
optional uint64 ack_id = 3;
}
message JoinGroupMessage {
string group = 1;
optional uint64 ack_id = 2;
}
message LeaveGroupMessage {
string group = 1;
optional uint64 ack_id = 2;
}
message PingMessage {}
}
message MessageData {
oneof data {
string text_data = 1;
bytes binary_data = 2;
google.protobuf.Any protobuf_data = 3;
}
}
message DownstreamMessage {
oneof message {
AckMessage ack_message = 1;
DataMessage data_message = 2;
SystemMessage system_message = 3;
PongMessage pong_message = 4;
}
message AckMessage {
uint64 ack_id = 1;
bool success = 2;
optional ErrorMessage error = 3;
message ErrorMessage {
string name = 1;
string message = 2;
}
}
message DataMessage {
string from = 1; // "group" or "server"
optional string group = 2;
MessageData data = 3;
}
message SystemMessage {
oneof message {
ConnectedMessage connected_message = 1;
DisconnectedMessage disconnected_message = 2;
}
message ConnectedMessage {
string connection_id = 1;
string user_id = 2;
}
message DisconnectedMessage {
string reason = 2;
}
}
message PongMessage {}
}
ポイントは次の 3 つです。
- グループ配信は
SendToGroupMessage(EventMessage ではない) EventMessage.eventは空にできない(1〜128 文字の “word” が必要)- ペイロードは
MessageDataの oneof(text / binary / Any(protobuf))で表現する
おすすめの解決策:まずは公式クライアント SDK で動作確認する
結論から言うと、protobuf のサブプロトコルは「生 WebSocket での実装」が可能ではあるものの、デバッグが難しく、落とし穴も多いです。
最短で安定させるなら、WebPubSubClient にプロトコル処理(ack、再接続、Downstream の分岐など)を任せるのが近道です。まず SDK で “確実に動く状態” を作り、その後に必要なら ClientWebSocket 実装へ落とす、という順番がトラブルシュートに強いです。
必要なパッケージ(C#)
最低限、次の構成が実務では扱いやすいです。
| 用途 | パッケージ例 | 補足 |
|---|---|---|
| Web PubSub クライアント SDK | Azure.Messaging.WebPubSub.Client | 接続、join/leave、sendToGroup、ack 処理を提供 |
| protobuf サブプロトコル実装 | WebPubSub.Protobuf.Protocols | .NET 側で protobuf サブプロトコルを利用するための実装(NuGet の README に例あり) |
| protobuf 基盤 | Google.Protobuf | .proto から生成した型を扱う |
インストール例:
dotnet add package Azure.Messaging.WebPubSub.Client
dotnet add package WebPubSub.Protobuf.Protocols
dotnet add package Google.Protobuf
SDK での最小サンプル:接続 → グループ参加 → 送受信
ポイントは Protocol に protobuf 用の実装を指定することです。
using Azure.Messaging.WebPubSub.Clients;
using WebPubSub.Client.Protobuf; // WebPubSub.Protobuf.Protocols で提供される名前空間
public class Program
{
private const string Group = "protobuf-client-group";
public static async Task Main()
{
var clientAccessUri = "wss://<your-service>.webpubsub.azure.com/client/hubs/<hubName>?access_token=<token>";
var client = new WebPubSubClient(
new Uri(clientAccessUri),
new WebPubSubClientOptions
{
Protocol = new WebPubSubProtobufProtocol()
});
client.Connected += args =>
{
Console.WriteLine($"Connected: {args.ConnectionId}");
return Task.CompletedTask;
};
client.Disconnected += args =>
{
Console.WriteLine("Disconnected");
return Task.CompletedTask;
};
client.GroupMessageReceived += args =>
{
// args.Message.Data は BinaryData
Console.WriteLine($"Group: {args.Message.Group} / DataType: {args.Message.DataType}");
Console.WriteLine($"Payload: {args.Message.Data}");
return Task.CompletedTask;
};
await client.StartAsync();
// 参加(roles が不足していると Forbidden の ack になる)
await client.JoinGroupAsync(Group);
// 送信(テキスト)
await client.SendToGroupAsync(
Group,
BinaryData.FromString("hello from protobuf subprotocol"),
WebPubSubDataType.Text);
Console.WriteLine("Press Enter to exit...");
Console.ReadLine();
await client.LeaveGroupAsync(Group);
await client.StopAsync();
}
}
ここで “protobuf” という言葉が出てきても、アプリの payload は Text / Binary / Protobuf のどれでも良い点が重要です。サブプロトコル(制御メッセージ)が protobuf であるだけで、実データは用途に応じて選べます。
「自分の protobuf を送りたい」なら、まずは Binary として運ぶのが簡単
クライアント間で独自メッセージ(たとえば MyAppMessage)を protobuf でやり取りしたい場合、最も事故が少ないのは次の運び方です。
- Web PubSub の制御プロトコル:protobuf サブプロトコルに任せる(joinGroup / sendToGroup など)
- アプリのデータ:自分の protobuf を binary(バイト列)として送る(
WebPubSubDataType.Binary)
たとえば、myapp.proto から生成した MyAppMessage を送るなら:
// MyAppMessage をバイト列にして送る
var payloadBytes = myAppMessage.ToByteArray();
await client.SendToGroupAsync(
Group,
BinaryData.FromBytes(payloadBytes),
WebPubSubDataType.Binary);
受信側では args.Message.Data.ToArray() を取り出して MyAppMessage.Parser.ParseFrom(...) で復元できます。
「まず動かす」「型情報や互換性はアプリ側で管理する」という方針なら、これが最短です。
ClientWebSocket で実装する場合の実装指針
生の ClientWebSocket で実装する場合でも、原理は同じです。送信するバイト列は常に UpstreamMessage でなければならない、そして受信は DownstreamMessage を分岐して処理する、この 2 点が絶対条件です。
手順の全体像
protobuf.webpubsub.azure.v1をサブプロトコルとして接続UpstreamMessage.join_group_messageを送る(必要ならack_idを付ける)- 受信ループで
DownstreamMessageをパースし、system_message/ack_message/data_messageを処理 - グループに配信したい場合は
UpstreamMessage.send_to_group_messageを送る - サーバーイベントに送る場合は
UpstreamMessage.event_messageを送る(eventは必ず非空)
受信実装の注意:ReceiveAsync は 1 回で全部返ってくるとは限らない
protobuf に限らず WebSocket はフレームが分割されることがあります。result.EndOfMessage が true になるまで結合してからパースしないと、「たまにパースエラーになる」タイプの不具合になります。
// 受信:EndOfMessage まで結合してから ParseFrom する
static async Task<byte[]> ReceiveFullMessageAsync(ClientWebSocket ws, CancellationToken ct)
{
using var ms = new MemoryStream();
var buffer = new byte[8192];
while (true)
{
var result = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), ct);
if (result.MessageType == WebSocketMessageType.Close)
{
return Array.Empty<byte>();
}
ms.Write(buffer, 0, result.Count);
if (result.EndOfMessage)
{
return ms.ToArray();
}
}
}
送信例:JoinGroup(ack_id を付けるとトラブルシュートが楽)
// ackId を単純にインクリメントする
ulong ackId = 0;
var join = new UpstreamMessage
{
JoinGroupMessage = new UpstreamMessage.Types.JoinGroupMessage
{
Group = "group1",
AckId = ++ackId
}
};
await webSocket.SendAsync(
join.ToByteArray(),
WebSocketMessageType.Binary,
endOfMessage: true,
cancellationToken: CancellationToken.None);
送信例:グループ配信は SendToGroupMessage
ここが最も重要です。グループ配信に EventMessage を使うと、期待した「他クライアントへの配信」にならないか、event が空で切断されます。
var send = new UpstreamMessage
{
SendToGroupMessage = new UpstreamMessage.Types.SendToGroupMessage
{
Group = "group1",
AckId = ++ackId,
Data = new MessageData
{
TextData = "hello group"
}
}
};
await webSocket.SendAsync(
send.ToByteArray(),
WebSocketMessageType.Binary,
endOfMessage: true,
cancellationToken: CancellationToken.None);
送信例:サーバーのイベントハンドラに投げるなら EventMessage(event は必須)
var evt = new UpstreamMessage
{
EventMessage = new UpstreamMessage.Types.EventMessage
{
Event = "myEvent", // ← 空文字は不可
AckId = ++ackId,
Data = new MessageData
{
TextData = "send to upstream event handler"
}
}
};
await webSocket.SendAsync(
evt.ToByteArray(),
WebSocketMessageType.Binary,
endOfMessage: true,
cancellationToken: CancellationToken.None);
受信例:DownstreamMessage を分岐して処理する
var bytes = await ReceiveFullMessageAsync(webSocket, CancellationToken.None);
if (bytes.Length == 0) return;
var msg = DownstreamMessage.Parser.ParseFrom(bytes);
switch (msg.MessageCase)
{
case DownstreamMessage.MessageOneofCase.SystemMessage:
if (msg.SystemMessage.MessageCase ==
DownstreamMessage.Types.SystemMessage.MessageOneofCase.DisconnectedMessage)
{
Console.WriteLine("Disconnected: " + msg.SystemMessage.DisconnectedMessage.Reason);
}
break;
case DownstreamMessage.MessageOneofCase.AckMessage:
Console.WriteLine($"Ack: {msg.AckMessage.AckId} / Success: {msg.AckMessage.Success}");
if (!msg.AckMessage.Success && msg.AckMessage.Error != null)
{
Console.WriteLine($"Error: {msg.AckMessage.Error.Name} - {msg.AckMessage.Error.Message}");
}
break;
case DownstreamMessage.MessageOneofCase.DataMessage:
var data = msg.DataMessage.Data;
if (data.DataCase == MessageData.DataOneofCase.TextData)
{
Console.WriteLine($"Text: {data.TextData}");
}
else if (data.DataCase == MessageData.DataOneofCase.BinaryData)
{
Console.WriteLine($"Binary length: {data.BinaryData.Length}");
}
else if (data.DataCase == MessageData.DataOneofCase.ProtobufData)
{
Console.WriteLine($"Any type: {data.ProtobufData.TypeUrl}");
}
break;
}
権限(roles)が不足していると join / send が成功しない
protobuf に限らず、PubSub プロトコルで「クライアントがグループへ join / send」するには、クライアントに roles を付与する必要があります。典型例として、次のどちらか(または両方)が必要です。
| role | できること | よくある症状 |
|---|---|---|
webpubsub.joinLeaveGroup(または webpubsub.joinLeaveGroup.<group>) | グループ参加/離脱 | join の ack が Forbidden |
webpubsub.sendToGroup(または webpubsub.sendToGroup.<group>) | グループ送信 | send の ack が Forbidden |
「JSON では動くのに protobuf で動かない」ケースでも、実は roles が揃っていない/接続 URL の生成条件が違う、ということがあります。ack を有効にして AckMessage を必ず見ていくと切り分けが早くなります。
よくある落とし穴チェックリスト
| 症状 | 原因 | 対処 |
|---|---|---|
| 接続直後に切断される(Invalid payload) | protobuf サブプロトコルなのに UpstreamMessage 以外を送っている | 送信バイト列が UpstreamMessage の wire format になっているか確認 |
| Invalid event name で切断される | EventMessage.event が空、または EventMessage を誤用 | グループ配信は SendToGroupMessage にする。Event を使うなら非空の event 名を付ける |
| join したはずなのに受信しない | join が失敗している(Forbidden など)/join 完了前に send している | ackId を付けて AckMessage を確認。成功後に送信する |
| たまに ParseFrom で例外 | WebSocket の分割受信を考慮していない | EndOfMessage まで結合してからパース |
| 「protobuf で送りたい」のに受信側が解釈できない | サービスプロトコルの protobuf と、アプリ独自 protobuf を混同 | アプリ payload は Binary で運ぶか、Any を使うか方針を決める |
まとめ:何を直せば「受信できない」を抜けられるか
protobuf.webpubsub.azure.v1を使うなら、送信フレームは常にUpstreamMessage- グループ配信は
SendToGroupMessage(EventMessageは Upstream イベント用途) EventMessage.eventは空にできない(空だと Invalid event name で切断)- まずは
WebPubSubClient+ Protobuf Protocol で動作確認し、必要になってから生 WebSocket に落とすと安全 - 「独自 protobuf を送りたい」だけなら、payload は Binary として運ぶのが最もシンプル
protobuf は速くて軽い一方、“どの層の protobuf なのか(サービス制御か、アプリ payload か)”を取り違えると一気に難易度が上がります。今回の症状はその典型なので、まずは「SendToGroupMessage を正しく送れているか」「EventMessage を誤用していないか」「event が空になっていないか」を起点に見直すと、最短で解決できます。

コメント