본문 바로가기

.NET/MVC.NET

Long Polling with MVC.NET

http://blogs.claritycon.com/blog/2011/04/12/roll-your-own-mvc-3-long-polling-chat-site/ 위의 사이트에서 퍼온 글입니다.

12

Apr 11

Roll Your Own MVC 3 Long Polling Chat Site



I recently read a great article on WebSocket alternatives in MVC by Clay Lenhart and decided to write up a quick proof of concept Chat Server Example for .Net MVC.  Here is a quick run-down of what I did.

 
First, read up on Comet; a basic way to have real-time notifications sent to a browser (like push notifications).  One common Comet technique involves “Long Polling” an http connection.  What this means is that we call out to a URL that expects to wait around for a long time before something happens.
 
 
 

The Long Poll Chat Async Controller

The first step for the chat server was to create a new Async Controller (Async what? Check out this great introduction to Async Controllers) to handle our Long Poll requests.  Here is the ChatController I created:
 
using System.Web.MVC
 
public class ChatController : AsyncController
{
    [AsyncTimeout(ChatServer.MaxWaitSeconds * 1002)]
    public void IndexAsync()
    {
        AsyncManager.OutstandingOperations.Increment();
 
        ChatServer.CheckForMessagesAsync(msgs =>
        {
            AsyncManager.Parameters["response"] = new ChatResponse
            {
                messages = msgs
            };
            AsyncManager.OutstandingOperations.Decrement();
        });
    }
 
    public ActionResult IndexCompleted(ChatResponse response)
    {
        return Json(response);
    }
 
    [HttpPost]
    public ActionResult New(string user, string msg)
    {
        ChatServer.AddMessage(user, msg);
        return Json(new
            {
                d = 1
            });
    }
}

 
You can see that we’ve created our Index action as an Async that calls our to our ChatServer and checks for a message to arrive.  By default, I’ve specified a 60 second time limit before returning an empty list of messages, but you could theoretically let this go on for hours waiting for a response.  Once a response comes back from the ChatServer, we tell the AsyncManager we are done and return our messages in JSON form.
 

The ChatServer

I chose to use Observables and the Reactive (Rx) Framework to have a synchronized way of notifying all interested parties when a new message comes in.  To do this, I took advantage of the Subject<T> class that wraps a lot of the Observable and Observer implementation details.  To handle persisting my chat history, I use a simple Queue to keep the last 100 or so by default.  Whenever a new message comes in, I check the history length and pop off anything over the limit, then push in my new messages.  Yes, eventually I’d have to figure out a strategy for pushing this back to a database or Redis cache server thingy but this is just a demo, so relax.
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
 
public class ChatResponse
{
    public List<MessageInfo> messages { get; set; }
}
 
public class MessageInfo
{
    public long id { get; set; }
    public long timestamp { get; set; }
    public string message { get; set; }
    public UserInfo user { get; set; }
}
 
public class UserInfo
{
    public long id { get; set; }
    public string name { get; set; }
}
 
public class ChatServer
{
    public const int MaxHistoryCount = 100;
    public const int MaxWaitSeconds = 60;
 
    private static object _msgLock = new object();
    private static Subject<MessageInfo> _messages = new Subject<MessageInfo>();
 
    private static object _historyLock = new object();
    private static Queue<MessageInfo> _history = new Queue<MessageInfo>(MaxHistoryCount + 5);       
 
    static ChatServer()
    {
        _messages
            .Subscribe(msg =>
            {
                lock (_historyLock)
                {
                    while (_history.Count > MaxHistoryCount)
                        _history.Dequeue();  
 
                    _history.Enqueue(msg);
                }
            });
    }
 
    public static void CheckForMessagesAsync(Action<List<MessageInfo>> onMessages)
    {
        var queued = ThreadPool.QueueUserWorkItem(new WaitCallback(parm =>
        {
            var msgs = new List<MessageInfo>();
            var wait = new AutoResetEvent(false);
            using (var subscriber = _messages.Subscribe(msg =>
                                            {
                                                msgs.Add(msg);
                                                wait.Set();
                                            }))
            {
                // Wait for the max seconds for a new msg
                wait.WaitOne(TimeSpan.FromSeconds(MaxWaitSeconds));
            }
 
            ((Action<List<MessageInfo>>)parm)(msgs);
        }), onMessages);
 
        if (!queued)
            onMessages(new List<MessageInfo>());
    }
 
    private static long currMsgId = 0;
    public static void AddMessage(string userName, string message)
    {
        _messages
            .OnNext(new MessageInfo
            {
                message = message,
                timestamp = UnixTicks(DateTime.UtcNow),
                user = new UserInfo
                {
                    id = currMsgId++,
                    name = userName
                }
            });
    }
 
    public static List<MessageInfo> GetHistory()
    {
        var msgs = new List<MessageInfo>();
        lock (_historyLock)
            msgs = _history.ToList();
 
        return msgs;
    }
 
    private static long UnixTicks(DateTime dt)
    {
        DateTime d1 = new DateTime(1970, 1, 1);
        DateTime d2 = dt.ToUniversalTime();
        TimeSpan ts = new TimeSpan(d2.Ticks - d1.Ticks);
        return (long)ts.TotalMilliseconds;
    }
}
 
The Client Side (HTML and JS)

On the client side, we need to take advantage of some of jQuery’s helper methods for POSTing to an MVC Action (the Index Action on the Chat Controller in our case).  We are going to make a POST call to /Chat that will sit around until a new message arrives, or our 60 second timeout is reached.  If a new message comes in, our POST call will return immediately with the new message(s).  From there, we just insert them into our current chat history.
 
@model MotherEffinChatSite.Models.HomeVM
 
@{
    Page.Title = "MVC Chat Example";
}
 
@section ScriptSection {
    <!-- The new message template... TODO: make it sexy -->
    <script id="msgTmpl" type="text/x-jquery-tmpl">
        <li><p style="margin-bottom:-2px; color: black;"><strong>${user.name}</strong></p><p>${message}</p></li>
    </script>
 
    <script type="text/javascript">
        var initUserName = "user" + Math.floor(Math.random() * 101);
 
        function getMessages() {
            $.post("/Chat", null, function (data, s) {
                if (data.messages) {
                    $('#msgTmpl').tmpl(data.messages).appendTo('#chatList');
                }
 
                setTimeout(function () {
                    getMessages();
                }, 500)
            });
        }
 
        $(document).ready(function () {
            // Post to /Chat/New when submitting a message.
            $('#msgBtn').bind('click', function () {
                var msgVal = $('#msgBox').val();
                $('#msgBox').val('');
                $.post("/Chat/New", { user: $('#userBox').val(), msg: msgVal }, function (data, s) {
                    if (data.d) {
                        log('added message');
                    }
                    else {
                        log('error adding message');
                    }
                    log(data);
                    log(s);
                });
            });
 
            // Submit a message on enter key.
            $('#msgBox').keydown(function (e) {
                if (e.keyCode == 13) {
                    $('#msgBtn').click();
                }
            });
 
            // initialize the user box name.
            $('#userBox').val(initUserName);
 
            // fire off the long polling after timeout so we don't get constant page loading
            setTimeout(function () {
                getMessages();
            }, 100)
        });
    </script>
}
 
<h1>MVC Long Poll Chat</h1>
<br />
<div id="chatRoot">
    <div>
        <label for="userName">User: </label><input type="text" name="userName" id="userBox" />
    </div>
    <!-- yeah yeah yeah, TODO: put styles in css... it's just a demo, relax -->
    <ul id="chatList" style="list-style-type: none; margin: 20px 0px 10px 0px; max-height: 500px; min-height: 500px; overflow: auto;">
        @foreach (var msg in Model.Messages) {
            <li>
                <p style="margin-bottom:-2px; color: black;">
                <strong>@msg.user.name</strong>
                </p>
                <p>@msg.message</p>
            </li>
        }
    </ul>
    <div>
        <input type="text" id="msgBox" /><input id="msgBtn" type="submit" value="speak on it" style="margin-left: 10px;" /><br />
    </div>
</div>
 

See It In Action



I’ve uploaded the MVC Chat Example to AppHarbor for people to play around with.  You can also download the sample project from my dropbox account, or view the source at the MVCChatSite Project page on BitBucket.
 
 
 

 

 

 

Comments Off , permalink


Tagged , , ,

Comments are closed.