Azure Web PubSubでprotobufが受信できない原因と解決策|protobuf.webpubsub.azure.v1の正しい送受信(C#)

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 クライアント SDKAzure.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 点が絶対条件です。

手順の全体像

  1. protobuf.webpubsub.azure.v1 をサブプロトコルとして接続
  2. UpstreamMessage.join_group_message を送る(必要なら ack_id を付ける)
  3. 受信ループで DownstreamMessage をパースし、system_message / ack_message / data_message を処理
  4. グループに配信したい場合は UpstreamMessage.send_to_group_message を送る
  5. サーバーイベントに送る場合は 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 が空になっていないか」を起点に見直すと、最短で解決できます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次