Saturday, January 21, 2017

Real Time Streaming with WebSocket

Explaining WebSocket's meaning, behavior and use case with sample code.
WebSocket is protocol used to communicate between client and server in real time. It is useful when you want client update content on website without having to make request interval.

Table of Contents

1 Different between WebSocket vs HTTP vs AJAX
2 Example Use Case
3 WebSocket Behavior
4 ASP.NET SignalR Method
5 WebSocket in ASP.NET Requirements
6 Setup WebSocket in ASP.NET
7 Sample WebSocket Chat App in ASP.NET
8 Connect to WebSocket Server using C# Code

Different between WebSocket vs HTTP vs AJAX

HTTP Protocol is standard way to use for making request on webpage. It is stateless protocol. HTTP connection must be re-estrablised in each request.

However, WebSocket is stateful protocol. WebSocket connection will be reused throughout its lifecycle. So, WebSocket have significantly less overhead than HTTP. You can see overhead consumption between HTTP and WebSocket here.

AJAX is umbella term that refer to mechanism of updating web content without having to refresh webpage. Theoretically, AJAX can use any request, i.e. HTTP, HTTPS, WebSocket etc. to request data as long as it does not force the website to refresh.

Example Use Case

Here is some sample use case of using WebSocket:
  • Server use WebSocket to notify user immediately when status is change.
  • Using WebSocket to feed newest data to client in real time.
  • Chat Applications which frequently send/receive message from server.

WebSocket Behavior

WebSocket will be closed automatically when
  • Sever rebuild/recompiled code.
  • Client close webpage on browser.

ASP.NET SignalR Method

WebSocket in ASP.NET have official library called SignalR. However, This requires client to install JQuery. This is not good when your website is designed to use other Javascript framework. So, in this tutorial, I will give how to use pure WebSocket in ASP.NET.
If you still want to use SingalR, beware that many website have code sample in version 1 which is deprecated. You should go to version 2 now, Official docs is here

WebSocket in ASP.NET Requirements

  • Client must have IE 10+, or Firefox 6+, or Chrome 14+, or Safari 5+
  • Development machine must be Windows 8+ with Visual Studio 2012+ installed and IIS 8+ installed (IIS Express is not supported)
  • Server OS must be Windows Server 2012+

Setup WebSocket in ASP.NET

1WebSocket Protocol features must be installed on IIS. Go to control panel, and click 'Program and features'. Then, click on 'Turn Windows features on or off' on right pane. Check on item 'WebSocket Protocol'.

2Open IIS and click your computer name in IIS -> Open Configuration Editor

3Under section "system.webServer/webSocket", set enabled=true and set ping timeout as you want. If you set ping timeout=0, it will not check connection status and server will don't know when connection loss.

(Optional) Another way is to set in your project's web.config
<system.webSocket>
    <webSocket enabled="true" pingInterval="00:00:05" />
<system.webSocket />

Sample WebSocket Chat App in ASP.NET

1Open Package Manager Console

and install WebSocket dll by typing command below
Install-Package Microsoft.WebSockets

2Create new MyWebSocket.cs class containing code for handling WebSocket. This class will have 4 methods: onOpen, onClose, onMessage, and onError.
using Microsoft.Web.WebSockets;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Script.Serialization;
using System.Web.SessionState;

public class MyWebSocket : WebSocketHandler
{
    private JavaScriptSerializer serializer = new JavaScriptSerializer();
    public static WebSocketCollection clients = new WebSocketCollection();
    string userName;

    public MyWebSocket(string userName)
    {
        this.userName = userName;
    }

    public override void OnOpen()
    {
        clients.Add(this);

        string jsonResponse = serializer.Serialize(new
        {
            type = "login",
            userName = userName
        });

        clients.Broadcast(jsonResponse);
        //this.Send(string.Format("Welcome URL {0}, userName {1}", this.WebSocketContext.UserHostAddress, userName));
    }

    public override void OnMessage(string message)
    {
        string jsonResponse = serializer.Serialize(new
        {
            type = "chitchat",
            userName = userName,
            message = message
        });

        clients.Broadcast(jsonResponse);

        //you can also filter user!. 
        //But best practice is to send all notification and let client filter it.
        //clients.Where(websocket => (websocket as MyWebSocket).userName != this.userName)
    }

    public override void OnClose()
    {
        base.OnClose();
        clients.Remove(this);

        string jsonResponse = serializer.Serialize(new
        {
            type = "logout",
            userName = userName
        });

        clients.Broadcast(jsonResponse);
    }

    public override void OnError()
    {
        base.OnError();

        string jsonResponse = serializer.Serialize(new
        {
            type = "error",
            userName = userName
        });

        clients.Broadcast(jsonResponse);
    }
}

3Create Generic Handler (ashx) named MyWebSocketHandler.ashx used to accept connection from client.

<%@ WebHandler Language="C#" CodeBehind="MyWebSocketHandler.cs" Class="MyWebSocketHandler" %>

And create separate class name MyWebSocketHandler.cs which contains code for MyWebSocketHandler.ashx
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Threading.Tasks;
using System.Net.WebSockets;
using System.Web.WebSockets;
using System.Threading;
using System.Text;
using System.Web.SessionState;

public class MyWebSocketHandler : IHttpHandler, IRequiresSessionState
{
    private System.Web.Script.Serialization.JavaScriptSerializer serializer = new System.Web.Script.Serialization.JavaScriptSerializer();

    public void ProcessRequest(HttpContext context)
    {
        if (context.IsWebSocketRequest)
        {
            HttpSessionState session = context.Session;
            string userName = session["userName"].ToString();

            //check authenication here...
            bool authenValid = true;

            if (authenValid)
                context.AcceptWebSocketRequest(webSocketContext => ProcessWebsocketSession(webSocketContext, userName));
        }
        else
        {
            string jsonResponse = serializer.Serialize(new
            {
                type = "warning",
                userName = "",
                message = "someone try to hack our website! I have to close all connection!"
            });

            //close all connections.
            MyWebSocket.clients.Broadcast(jsonResponse);
            List<MyWebSocket> websocketListTmp = new List<MyWebSocket>(MyWebSocket.clients.Select(tmp => (MyWebSocket)tmp));
            foreach (MyWebSocket websocket in websocketListTmp)
            {
                websocket.Close();
            }
        }
    }

    public bool IsReusable
    {
        get
        {
            return false;
        }
    }

    private Task ProcessWebsocketSession(AspNetWebSocketContext context, string userName)
    {
        MyWebSocket handler = new MyWebSocket(userName);
        var processTask = handler.ProcessWebSocketRequestAsync(context);
        return processTask;
    }
}

4Create chat.aspx file

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="chat.aspx.cs" Inherits="chat" %>

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>WebSocket Chat</title>
    <script type="text/javascript" src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
    <script type="text/javascript">
        var ws;
        $().ready(function ()
        {
            var userName = $("#userID").html();
            console.log("userName is " + userName);
            $("#btnHack").click(function ()
            {
                var url = "http://" + window.location.hostname + "//WebSocketSample/MyWebSocketHandler.ashx";
                var request = new XMLHttpRequest();
                request.open("GET", url, true);
                request.send();
            });

            $("#btnConnect").click(function () {
                $("#spanStatus").text("connecting");
                ws = new WebSocket("ws://" + window.location.hostname + "/WebSocketSample/MyWebSocketHandler.ashx");
                ws.onopen = function () {
                    $("#spanStatus").text("connected");
                };
                ws.onmessage = function (evt)
                {
                    var jsonString = evt.data;
                    var response = JSON.parse(jsonString);
                    var responseUserName = response.userName;
                    var outputText = "";

                    if (response.type == "login")
                    {
                        if (responseUserName == userName)
                            outputText = "Welcome!"
                        else
                            outputText = responseUserName + " joined chat room";
                    }
                    else if (response.type == "chitchat")
                    {
                        var msg = response.message;
                        outputText = responseUserName + " chit-chat: " + msg;
                    }
                    else if(response.type == "logout")
                    {
                        outputText = responseUserName + " leaved chat room.";
                    }
                    else if (response.type == "warning")
                    {
                        outputText = "WARN: " + response.message;
                    }
                    else if (response.type == "error")
                    {
                        outputText = responseUserName + "got some error";
                    }

                    $("#chatHistory").append(outputText);
                    $("#chatHistory").append("<br />");
                };
                ws.onerror = function (evt) {
                    $("#spanStatus").text(evt.message);
                };
                ws.onclose = function () {
                    $("#spanStatus").text("disconnected");
                };
            });
            $("#btnSend").click(function () {
                if (ws.readyState == WebSocket.OPEN) {
                    ws.send($("#textInput").val());
                }
                else {
                    $("#spanStatus").text("Connection is closed");
                }
            });
            $("#btnDisconnect").click(function () {
                ws.close();
            });
        });
    </script>
</head>
<body>
    userName: <asp:label runat="server" ID="userID"></asp:label><br />
    <input type="button" value="Connect" id="btnConnect" />
    <input type="button" value="Disconnect" id="btnDisconnect" />
    <input type="button" value="Hack WebSite" id="btnHack" /><br />
    <input type="text" id="textInput" />
    <input type="button" value="Send" id="btnSend" /><br />
    <span id="spanStatus">(display)</span><br />
    <span id="chatHistory"></span>
</body>
</html>

And chat.aspx.cs will contain code for generate user ID.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

public partial class chat : System.Web.UI.Page
{
    private static object lockObj = new object();
    private static int userIDRunningNumber = 1;

    public int getNewUserID()
    {
        int userID = userIDRunningNumber;
        lock(lockObj)
        {
            userIDRunningNumber++;
        }
        return userID;
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        string userName = "user" + getNewUserID();
        this.userID.Text = userName;

        Session["userName"] = userName;
    }
}

5In web.config, add this code to open connection for MyWebSocketHandler.ashx
<system.webServer>
    <handlers>
      <add path="/MyWebSocketHandler.ashx" verb="*" name="MyWebSocketHandler"
         type="MyWebSocketHandler"/>
    </handlers>
</system.webServer>

6Try run app and enjoy playing chat apps!

Connect to WebSocket Server using C# Code

If your client is C# WinForms App or Console App, there are 2 libraries to help connecting to WebSocket Server:


Happy Coding!

No comments:

Post a Comment