しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

ASP.NET SignalR で頻繁に通信が行われるアプリを効率よく実装する方法

ASP.NET SignalR のデモでよく見るような、ドラッグドロップでオブジェクトを動かして、それを全クライアントに配信するようなアプリケーションだと、そのまま実装するとサーバとクライアント間で数 ms 間隔で通信が行われてしまいます。

それは非常に効率が良くないということで、ASP.NET SignalR の公式ページには高頻度で通信が行われるアプリケーションの実装方法がまとめられています。

Tutorial: High-Frequency Realtime with SignalR 2 | The ASP.NET Site

この例ではクライアント側とサーバ側にメッセージループを実装して、最大でも 100ms 間隔でしか通信を行わないようにしています。

<script>
    $(function () {
        var moveShapeHub = $.connection.moveShapeHub,
            $shape = $("#shape"),
            // Send a maximum of 10 messages per second 
            // (mouse movements trigger a lot of messages)
            messageFrequency = 10, 
            // Determine how often to send messages in
            // time to abide by the messageFrequency
            updateRate = 1000 / messageFrequency, 
            shapeModel = {
                left: 0,
                top: 0
            },
            moved = false;

        moveShapeHub.client.updateShape = function (model) {
            shapeModel = model;
            $shape.css({ left: model.left, top: model.top });
        };

        $.connection.hub.start().done(function () {
            $shape.draggable({
                drag: function () {
                    shapeModel = $shape.offset();
                    moved = true;
                }
            });

            // Start the client side server update interval
            setInterval(updateServerModel, updateRate);
        });

        function updateServerModel() {
            // Only update server if we have a new movement
            if (moved) {
                moveShapeHub.server.updateModel(shapeModel);
                moved = false;
            }
        }
    });
</script>

しかし、どうしても実装がごちゃごちゃしてしまって分かりにくいですね。そんな時に backburner.js というライブラリの紹介記事を見つけたので、これを使えば SignalR でのクライアント側は簡単に実装できると思いました。

クライアントサイド JavaScript のパフォーマンス改善には backburner.js が便利! - tricknotesのぼうけんのしょ

という訳で backburner.js について調べてみたんですが、NuGet にはまだ登録されていないようでした。

ところがはてなブックマークのコメントで同じことは underscore.js で出来ると書かれていたのと NuGet にも登録されていたので、今回は underscore.js を使ってみることにしました。underscore.js については以下の記事がわかりやすかったです。

便利機能満載のライブラリUnderscore.js - にのせき日記

今回は throttle というメソッドを使えば、同じことは出来そうですね。という訳で公式ページのサンプルを underscore.js を使って書き換えてみました。

<script src="Scripts/jquery-2.0.0.min.js"></script>
<script src="Scripts/jquery-ui-1.10.3.min.js"></script>
<script src="Scripts/jquery.signalR-1.1.1.min.js"></script>
<script src="Scripts/underscore.min.js"></script>
<script>
    $(function() {
        var connection = $.hubConnection();
        var move = connection.createHubProxy("move");

        var shape = $("#shape");

        move.on("Receive", function(x, y) {
            shape.animate({ left: x, top: y }, { duration: 100, queue: false });
        });

        connection.start(function() {
            shape.draggable({
                drag: _.throttle(function() {
                    var offset = shape.offset();

                    move.invoke("Send", offset.left, offset.top);
                }, 100)
            });
        });
    });
</script>

今回は 100ms 毎に 1 回だけサーバと通信するようにしています。余計なコードが綺麗になくなって、本質的な部分だけ残りましたね。

drag した時の関数は高頻度に呼び出されるので、drag に underscore.js の throttle メソッドで作成した関数を渡しています。通信が間引かれた分、動きはカクカクしますがアニメーションを使って補完しました。

初めて underscore.js を使ってみたんですが、いろんな機能が用意されていて今まで損してたなーと思いました。まずは、バーチャル緑タイツマンのアップデートに期待しましょうか。