空談録

世界で5人くらいに役立ちたい

UWPのMessageWebSocketでSlackのReal Time Messagingにつなぐとき

おーーーっとふぁぼツバメ選手助走をつけてからの渾身の腹パンをMessageWebSocketに打ち込んだぁぁぁ!
はい、どうでもいいですね

というわけでタイトル通りです。UWPでReal Time Messagingにつなぎたいんだけど!っていう話ですが、MessageWebSocketでつなぐのがたいへんクソだったという話です
マジでわけわからなすぎる。そもそも情報がなさすぎる。助けてUWPのプロ

Real Time Messagingにつなぐのには

rtm.start method | Slack
Slackのrtm.startメソッドたたいて、戻ってきたデータのurlを用いてWebSocketで接続するだけです。

ちなみにWPFのときはwebsocket-sharpを使っています。
github.com
めっちゃ素直でいい子です。残念ながらUWPには対応していませんが…

で、UWPのときはMessageWebSocketとかいうのを使います
MessageWebSocket Class (Windows)
つなぎ方は最高に不可解です。もう嫌だ

MessageWebSocket.ConnectAsyncのやり方について

いつも通りダメパターンから書いていきましょうね

MessageWebSocketに指定したWebSocket Uriで接続して、MessageReceivedの結果をIObservable<string>で返すメソッドConnectAsyncを考えます。
とりあえず次のような感じで書いてみましょう

using System.Reactive.Linq;
using Windows.Networking.Sockets;
using System.IO;
using Windows.Storage.Streams;
using System.Threading.Tasks;
using System.Reactive.Subjects;

// ~~~~~~~

private MessageWebSocket webSockets { get; set; }
private DataWriter dataWriter { get; set; }

public async Task<IObservable<string>> ConnectAsync(string url)
{
    webSockets = new MessageWebSocket();
    webSockets.Control.MessageType = SocketMessageType.Utf8;

    webSockets.Closed += WebSockets_Closed;

    var observable = Observable.FromEventPattern(webSockets, "MessageReceived").Select(x => {
        var e = ((MessageWebSocketMessageReceivedEventArgs)x.EventArgs);
        using (var reader = new StreamReader(e.GetDataStream().AsStreamForRead()))
        {
            return reader.ReadToEnd();
        }
    });

    await webSockets.ConnectAsync(new Uri(url));
    dataWriter = new DataWriter(webSockets.OutputStream);

    return observable;
}

まあ普通のFromEventPatternです。FromEventは使い方がわからず…
とりあえず上のコードは返したobservableをSubscribeしてもイベント登録が行えていないらしく何も飛んできません。これはUWPの仕様?
FromEventなら動くぜ!って方はご連絡をください…

というわけでSubject<T>を使って実現してみましょう
このときも次のように書くことはできません。(以下のコードはConnectAsyncのみ記述)

public async Task<IObservable<string>> ConnectAsync(string url)
{
    webSockets = new MessageWebSocket();
    webSockets.Control.MessageType = SocketMessageType.Utf8;

    webSockets.Closed += WebSockets_Closed;
    
    var messageSubject = new Subject<string>();
    
    await webSockets.ConnectAsync(new Uri(url));
    dataWriter = new DataWriter(webSockets.OutputStream);

    webSockets.MessageReceived += (sender, e) =>
    {
        using (var reader = new StreamReader(e.GetDataStream().AsStreamForRead()))
        {
            messageSubject.OnNext(reader.ReadToEnd());
        }
    };

    return messageSubject;
}

このコードは、「MessageReceivedのAddHandlerの部分でInvalidOperationException」を起こします
何を言ってるのかよくわからないって? 私も知らんし…むしろどういう記述してたら+=のところで落ちんの…
推測だけだとConnectAsyncの時点でMessageReceivedのイベントハンドラを囲い込んで実行する、とかそんな感じでしょうか。どっちにしてもどこに書いてあるんだ

というわけで最終的には以下のコードが動きます。

public async Task<IObservable<string>> ConnectAsync(string url)
{
    webSockets = new MessageWebSocket();
    webSockets.Control.MessageType = SocketMessageType.Utf8;

    webSockets.Closed += WebSockets_Closed;

    var messageSubject = new Subject<string>();
    webSockets.MessageReceived += (sender, e) =>
    {
        using (var reader = new StreamReader(e.GetDataStream().AsStreamForRead()))
        {
            messageSubject.OnNext(reader.ReadToEnd());
        }
    };
    
    await webSockets.ConnectAsync(new Uri(url));
    dataWriter = new DataWriter(webSockets.OutputStream);

    return messageSubject;
}

というわけでコードの順序を間違えると落ちることを覚えておきましょう

Pingを送ろう

さて、Real Time Messagingでは一定時間通信できていなかったらpingを送って通信が持続しているか確認しろと書かれています。
MessageWebSocketではちゃんと送信もできますので(当然だ)そちらも実装していきましょう
こちらも落ちるコードの実例がいくつもあります。見ていきましょう
…いうほどなかった

送るときのメソッドはstringを受け取ったらWebSocketで送るSendAsync(string)を実装します
usingは上のusingで足りるはず(System抜く人はいないだろう…)

public async Task SendAsync(string text)
{
    using (var dataWriter = new DataWriter(webSockets.OutputStream))
    {
        dataWriter.WriteString(text);
        await dataWriter.StoreAsync();
    }
}

まずは落ちるパターン、usingで囲むです
これをするとOutputStreamが閉じます。2回目以降は送れません。閉じてるって言われて落ちます
とはいえこの記事のコードだと接続時にDataWriterを作成して退避しているのでこれをする人はいないでしょう…

というわけでこうなります

public async Task SendAsync(string text)
{
    dataWriter.WriteString(text);
    await dataWriter.StoreAsync();
}

このときStoreAsyncではなくFlushAsyncだと動きません。
なんだすんなりいくじゃん?ってなるでしょう?
このとき「不正なデータを転送するとその時点でWebSocketが切断」されます
おそらくなんですが通信エラーを引くと何も言わずに死んでるのでは…

まじめにデータが正しいかどうかは確認した方がいいです。できるならWPFのほうで同じping文字列送信して動くことを確認しましょう。
私はそれでようやく気付きました。変な動きのときはそちらに気を付けましょう。

ちなみに私はこんなんを毎回送りつけています

private const string pingText = "{ \"id\": 1234, \"type\": \"ping\"}";

確認用にでもどうぞ


というわけでまあはい、わけがわからないよ…という感想です。
もうかかわりたくない。というかMessageWebSocketで調べても簡単なサンプルみたいなのしか出てこないのが本当に困った

ラボ畜ワークは今週はさほど多くないので自分のプログラミングを進めたいなぁというところ

この辺で