Introduction
Nowadays, due to increase in the amount of information and necessity of achieving data in short time, we need technologies to cover our requirement in this issue. Assume when in stock market prices are changing each moment, do you think that user should refresh page every moment to inform the last price? Obviously, it is not a reasonable solution for such a problem. Or with increase in producing products and services, we need customer service to help user and buyer, the best and cheaper communication is conversation by chat program. By the same token, we cannot force user to press button for receiving our last message.
SignalR is a real time technology which is using the set of asynchrony library to make a persistence connection between client and server. User can receive last update data from server without the traditional way such as refresh page or press button.
Background
You need to know MVC 4.0 Technology and EntityFramework > 4.0 to get this article better.
In the other hand, SignalR uses the below approaches to establish real time web:
1. WebSocket
Websocket is a full duplex protocol and uses http handshaking internally and allow stream of messages flow on top of TCP. It supports: Google Chrome (> 16) Fire Fox (> 11) IE (> 10) Win IIS (>8.0). Due to encrypt message and full duplex, websocket is the best solution and at first signalR checks both web server and client server whether they support websocket or not.
Simplex Communication
It just spreads in one way when one point just broadcasts while another point just can listen without sending message, such as television and radio.
Half Duplex
One point sends message and at this moment, another point cannot send message and should wait until the first point finishes its transmission and then send its message, it is just a one communication line at a time, such as old wireless device walkie-talkie and HTTP protocol.
Full Duplex
Both points can send and receive message at a time simultaneously, there is no need to wait until the other point finishes its transmission such as telephones and websocket protocol.
Full Duplex
2. Server Sent Events (SSE)
The next choice for signalr is server sent event, because of persistence communication between server and client. In this approach, communication does not disconnect and last data from server will update automatically and transmit to client via HTTP connection. EventSource
is part of HTML5 technology.
var evsrc = new EventSource("url");
evsrc.addEventListener("message", function (event) {
});
3. Forever Frame
When client sends request to server, then server sends a hidden iframe as chunked block to client so this iframe is responsible to keep connection between client and server forever. Whenever server changes data, then send data as script tag to client (hidden iframe) and these scripts will be received sequentially.
4. Polling
Client sends request to server and server responses immediately but after that, server disconnects connection so again for establishing communication between server and client, we should wait for next request from client. To solve this problem, we have to set timeout manually and for each 10 seconds client sends request to server to check new modification in server side and gets last update data. Polling uses resources and it is not an economic solution.
5. Long Polling
Client sends request to server and server responds immediately and this connection remains until a specific time and during this period clients do not have to send explicit request to server while in polling client has to send explicit request to server during timeout. Comet programming covers this concept.
Briefly, SignalR library chooses one type of transmit data between client and server, its priority is websocket, server sent event, long polling and forever iframe. There are two classes inside this library as follows:
1. Persistentconnection
It is low level so it is complex and needs more configuration but in return gives more facility to handle class personally.
2. Hub
It is high level and more popular to use it.
How to Implement Simple Chat Scenario With the Aid of Signalr and Hub Class?
My aim is just to issue a random scenario for involving signalr. You can use it for your personal scenario and I just follow the below steps to make challenge with server (hub
class) and client side and illustrate how client send request and server respond? How they interact with each other?
Scenario Description
I want to establish an application for customer service department. There are some administrations that are responsible for helping the client and on the other side, there are clients who ask question and need help.
Assume two admins are online and connect to chat service and the first client comes to ask a question, so system connects the first client to first free admin and for the second client, this story will repeat, but the third client gives alarm from system that there is no admin to help. Whenever the first client disconnects, the first admin becomes free.
My contract for this scenario is to use flag for reminding which user who is connected is user or admin and which one is free or busy. In my database, if admincode is equal to zero so it is user otherwise it is admin, and I define flag “tpflag
” (in application) is equal to zero for user and equal to one for admin. Whenever they connect to chat flag, “freeflag
” becomes zero which shows busy user and as soon as client leaves conversation becomes one which shows free status.
if freeflag==0 ==> Busy
if freeflag==1 ==> Free
if tpflag==0 ==> User
if tpflag==1 ==> Admin
Prerequisites
- Visual Studio 2012
- SQL Server 2008
- Install necessary dependency from package manager console
Step 1: Create Project
File --> New Project --> ASP.NET MVC 4 Web Application { Give Name and Directory} --> { Template=Basic & View Engine=Razor }
Step 2: Open PM for Installing Dependency Files
Menu (Tools) --> Library Package Manager --> Package Manager Console
Step 3: Instruction for Removing Old Dependency
At first, remove all of the old dependencies for installing new version of SignalR 2.x.x In Line:
PM> Uninstall-Package Microsoft.AspNet.SignalR –RemoveDependencies
Step 4: Instruction for Installing Necessary Dependency Files
For new version, use:
PM> Install-Package Microsoft.AspNet.SignalR
I have used signalr version 2.0.1 for this practice:
PM> Install-Package Microsoft.AspNet.SignalR -Version 2.0.1
PM> Install-Package Microsoft.Owin
By writing this instruction, nuget does all of the dependency injection that you need to run signalr. If you look at the Reference part in the solution, you will find Microsoft.ASPNet.SignalR.x, Microsoft.Owin.x.x.., etc., or if you look at the Scripts part in solution jquery-1.x , jquery.signalR 2.x.x, etc. so feel comfortable about all of the dependencies.
Solution --> Open Reference -->
Solution --> Scripts -->
On the other hand, after installing signalR dependencies successfully, you will find complete help as readme.txt above the package console. It contains all the necessary instructions to get started with signalr. I explain these instructions in the next steps.
Tips (1): NuGet
If you encounter this error “The remote name could not be resolved: 'www.nuget.org'” So you should change Package Manager Settings which is located in front of the Package Source.
You should change the source from https to http protocol to solve this problem.
Tips (2): Owin
Check your references part to be sure there is Owin, otherwise follow this instruction: Right Click on References --> Manage NuGet Packages --> Select Online in left side --> search Owin --> Select Owin (Owin IAppBuilder
startup interface) --> Install.
Then, you should see Owin in your reference part.
Step 5: Startup Class
For enabling signalr in project, you should create class as startup. (If in the previous version of signalr, I mean the first version, you used to write RouteTable.Routes.MapHubs();
in Application Start in global.asax, now forget about it and just use startup
class. Right Click: On Project Name {SignalR} --> Add Class --> Name: Startup.cs
using Microsoft.Owin;
using Owin;
[assembly: OwinStartup(typeof(MvcSignal.Startup))]
namespace MvcSignal
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.MapSignalR();
}
}
}
Step 6: Organize Database According To Our Scenario
Step 6.1: Create “tbl_User”
“tbl_user
” will collect user and admin, if “AdminCode
” was filled by number from previous table so it is admin who belongs to department
otherwise if it was filled by (zero) illustrate to ordinary user.{ “UserID
” int
+ identity=yes
and “AdminCode
” default value = 0
}
Step 6.2: Create “tbl_Conversation”
“tbl_Conversation
” which will collect data from conversation between user and admin. This table will be filled after finishing conversation. { “ConID” int + identity=yes }
Step 7: Create Hub Class
Step 7.1: Model (folder) --> Create class “UserInfo.cs”
public class UserInfo
{
public string ConnectionId { get; set; }
public string UserName { get; set; }
public string UserGroup { get; set; }
public string freeflag { get; set; }
public string tpflag { get; set; }
public int UserID { get; set; }
public int AdminID { get; set; }
}
Step 7.2: Model (folder) --> Create class “MessageInfo.cs”
public class MessageInfo
{
public string UserName { get; set; }
public string Message { get; set; }
public string UserGroup { get; set; }
public string StartTime { get; set; }
public string EndTime { get; set; }
public string MsgDate { get; set; }
}
Step 7.3: Controller (folder) ? Create “HomeController.cs”
public ActionResult Chat()
{
ViewBag.Message = "Your contact page.";
return View();
}
Right click on Chat()
--> Select Add View -->
Step 7.4: Create Chat.cshtml { Client Side }
@{
ViewBag.Title = "Chat";
}
<div id="divLogin" class="mylogin">
User Name:<input id="txtUserName" type="text" /><br />
Password : <input id="txtPassword" type="password" /><br />
<input id="btnLogin" type="button" value="Login" />
<div id="divalarm"></div>
</div>
<div id="divChat" class="mylogin">
<div id="welcome"></div><br />
<input id="txtMessage" type="text" />
<input id="btnSendMessage" type="button" value="Send" />
<div id="divMessage"></div>
</div>
<input id="hUserId" type="hidden" />
<input id="hId" type="hidden" />
<input id="hUserName" type="hidden" />
<input id="hGroup" type="hidden" />
@section scripts {
<script src="~/Scripts/jquery-1.8.2.min.js"></script>
<script src="~/Scripts/jquery.signalR-2.0.1.min.js" type="text/javascript"></script>
<script src="~/signalr/hubs" type="text/javascript"></script>
@*<script type="text/javascript" src="@Url.Content("~/signalr/hubs")"></script>*@
@* <script type="text/javascript"
src='<%= ResolveClientUrl("~/signalr/hubs") %>'></script>*@
<script>
$(function () {
$("#divChat").hide();
$("#divLogin").show();
var objHub = $.connection.myHub;
loadClientMethods(objHub);
$.connection.hub.start().done(function () {
loadEvents(objHub);
});
});
function loadEvents(objHub) {
$("#btnLogin").click(function () {
var name = $("#txtUserName").val();
var pass = $("#txtPassword").val();
if (name.length > 0 && pass.length > 0) {
objHub.server.connect(name, pass);
}
else {
alert("Please Insert UserName and Password");
}
});
$('#btnSendMessage').click(function () {
var msg = $("#txtMessage").val();
if (msg.length > 0) {
var userName = $('#hUserName').val();
objHub.server.sendMessageToGroup(userName, msg);
}
});
$("#txtPassword").keypress(function (e) {
if (e.which == 13) {
$("#btnLogin").click();
}
});
$("#txtMessage").keypress(function (e) {
if (e.which == 13) {
$('#btnSendMessage').click();
}
});
}
function loadClientMethods(objHub) {
objHub.client.NoExistAdmin = function () {
var divNoExist = $('<div><p>There is no Admin
to response you try again later</P></div>');
$("#divChat").hide();
$("#divLogin").show();
$(divNoExist).hide();
$('#divalarm').prepend(divNoExist);
$(divNoExist).fadeIn(900).delay(9000).fadeOut(900);
}
objHub.client.getMessages = function (userName, message) {
$("#txtMessage").val('');
$('#divMessage').append('<div><p>' +
userName + ': ' + message + '</p></div>');
var height = $('#divMessage')[0].scrollHeight;
$('#divMessage').scrollTop(height);
}
objHub.client.onConnected = function (id, userName, UserID, userGroup) {
var strWelcome = 'Welcome' + +userName;
$('#welcome').append('<div><p>Welcome:' +
userName + '</p></div>');
$('#hId').val(id);
$('#hUserId').val(UserID);
$('#hUserName').val(userName);
$('#hGroup').val(userGroup);
$("#divChat").show();
$("#divLogin").hide();
}
}
</script>
}
Step 7.5: Create Model1.edmx
To have a simple way to fetch and insert data from and to database, I create model as follows: Right click on project name --> Add New Item --> Select “ADO.NET Entity Data Model” --> Select “Generate From Data Base” --> Make Connection to your data base --> Select your tables.
Step 8: Create Folder and Name It Hubs Then Create Simple Class and Name It “MyHub.cs”
{If you have the last update version of Visual Studio, you can add new item and select “SignalR Hub Class”}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.AspNet.SignalR;
using MvcSignal.Models;
using Microsoft.AspNet.SignalR.Hubs;
namespace MvcSignal
{
public class MyHub : Hub
{
static List UsersList = new List();
static List<messageinfo> MessageList = new List<messageinfo>();
public void Connect(string userName, string password)
{
var id = Context.ConnectionId;
string userGroup="";
var ctx = new TestEntities();
var userInfo =
(from m in ctx.tbl_User
where m.UserName == userName && m.Password == password
select new { m.UserID, m.UserName, m.AdminCode }).FirstOrDefault();
try
{
if ((int)userInfo.AdminCode == 0)
{
var strg = (from s in UsersList where (s.tpflag == "1")
&& (s.freeflag == "1") select s).First();
userGroup = strg.UserGroup;
strg.freeflag = "0";
UsersList.Add(new UserInfo { ConnectionId = id,
UserID = userInfo.UserID,
UserName = userName,
UserGroup = userGroup,
freeflag = "0",
tpflag = "0", });
Groups.Add(Context.ConnectionId, userGroup);
Clients.Caller.onConnected(id, userName, userInfo.UserID, userGroup);
}
else
{
UsersList.Add(new UserInfo { ConnectionId = id,
AdminID = userInfo.UserID,
UserName = userName,
UserGroup = userInfo.AdminCode.ToString(),
freeflag = "1",
tpflag = "1" });
Groups.Add(Context.ConnectionId, userInfo.AdminCode.ToString());
Clients.Caller.onConnected(id, userName, userInfo.UserID,
userInfo.AdminCode.ToString());
}
}
catch
{
string msg = "All Administrators are busy, please be patient and try again";
Clients.Caller.NoExistAdmin();
}
}
public void SendMessageToGroup(string userName, string message)
{
if (UsersList.Count != 0)
{
var strg = (from s in UsersList
where (s.UserName == userName) select s).First();
MessageList.Add(new MessageInfo
{ UserName = userName, Message = message, UserGroup = strg.UserGroup });
string strgroup = strg.UserGroup;
Clients.Group(strgroup).getMessages(userName, message);
}
}
public override System.Threading.Tasks.Task OnDisconnected()
{
var item = UsersList.FirstOrDefault(x => x.ConnectionId == Context.ConnectionId);
if (item != null)
{
UsersList.Remove(item);
var id = Context.ConnectionId;
if (item.tpflag == "0")
{
try
{
var stradmin = (from s in UsersList where
(s.UserGroup == item.UserGroup) && (s.tpflag == "1") select s).First();
stradmin.freeflag = "1";
}
catch
{
Clients.Caller.NoExistAdmin();
}
}
}
return base.OnDisconnected();
}
}
}
Rules and Contracts
The prefix when client wants to call server method in server side:
( Client --> Server ) // Client send request to server
1. objHub.server.methodname() { methodname of server side }
and there is exactly the same methodname in server side (myHub.cs class) .
The prefix when server wants to call client method in client side:
( Server --> Client ) // Server calls client method & { methodname of client side }
Clients.caller.methodname()
// caller means only user who sends request Clients.all.methodname()
// all means all of connected user Clients.Group(groupName).methodname()
// Group means just users who are in same group
When there is ***** Return to Client ***** in “MyHub.cs” class, it means you have to write jquery function with the same name on client side.
Indeed, their interaction is as follows:
Tips (3): Call Server Class
There are tiny tips whenever you want to call your server class; always in client side you should use specific naming convention which is camel type, for instance if your hub class name is “MyHub
”, you should instantiate your object from “myHub
” or if you have “SendMessageToGroup
”, you should call it from “sendMessageToGroup
” so it should be like:
Test Case
To have the same result, you should have database as same as I have explained in the seventh step.
Case 1
Test Plan: If Client tries to login and there is no admin, then system shows an alarm.
Testing Steps
- Run project
- UserName: mahsa
- Password: 123
- Expected Output: System shows alarm
Case 2
Test Plan: There is at least on free admin and then one client login and then the first admin will be assigned to the first free client who needs help.
Testing Steps
- Run project
- UserName: admin1
- Password: 123
- Login {admin1 as first admin}
- Copy URL to another web browser
- UserName: mahsa
- Password: 123
- Login {mahsa as first client}
- If “mahsa” send message, so “admin1” will see it, because they are in the same group. When the first client true to login, then add to the first free admin.
- Copy URL to another web browser
- UserName: kashi
- Password: 123
- Login { kashi as second client}
- System shows alarm and says “there is no admin then system shows an alarm”
- Copy URL to another web browser
- UserName: admin2
- Password: 123
- “kashi” and “admin2” cannot see conversation between “admin1” and “mahsa”
Tips
Tip 1. Different UI for Admin and User and Show Waiting User for Admin
If you need a different UI for Admin from User, you should create different div
for user and admin with different CSS and assign particular CSS to them by class attribute.
When you want to send Admin Message to Admin from hub class, send to different client method such as objHub.client.getMessagesAdmin
and for user objHub.client.getMessagesUser
.
In Chat.cshtml, implement these methods with different UI by different div
"divMessageAdmin
" and "divMessageUser
", you should fill these div
s with the proper message.
So please follow:
- Create different
div
s:
<div class="Admin" id="divMessageAdmin"></div>
<div Class="User" id="divMessageUser"></div>
- In
Hub
class -> SendMessageToGroup
->
Check if user is Admin ->:
Clients.Group(strgroup).getMessagesAdmin(userName, message);
Check if user is Ordinary User->:
Clients.Group(strgroup).getMessagesUser(userName, message);
- In Chat.cshtml:
If User is Admin:
objHub.client.getMessagesAdmin = function (userName, message) {
$("#txtMessage").val('');
$('#divMessageAdmin').append('<div><p>' +
userName + ': ' + message + '</p></div>');
var height = $('#divMessageAdmin')[0].scrollHeight;
$('#divMessageAdmin').scrollTop(height);
}
If User is Ordinary User:
objHub.client.getMessagesUser = function (userName, message) {
$("#txtMessage").val('');
$('#getMessagesUser').append('<div><p>' +
userName + ': ' + message + '</p></div>');
var height = $('#getMessagesUser')[0].scrollHeight;
$('#getMessagesUser').scrollTop(height);
}
To see waiting users in Admin's UI:
- Inside
Hub
class -> Connect
-> you have these lines of code:
catch
{
string msg = "All Administrators are busy, please be patient and try again";
Clients.Caller.NoExistAdmin();
}
Please send username
for this user who has to wait for free admin:
Clients.Caller.NoExistAdmin(username);
- In Chat.cshtml:
objHub.client.NoExistAdmin = function (username) {
var divNoExist = $('
There is no Admin to respond... you try again later:
'); $("#divChat").hide(); $("#divLogin").show(); $("#divWaitingUser").append('
' + userName + '
'); $(divNoExist).hide(); $('#divalarm').prepend(divNoExist);
$(divNoExist).fadeIn(900).delay(9000).fadeOut(900); }