天天看點

Asp.NET MVC 使用 SignalR 實作推送功能一(Hubs 線上聊天室)

簡介

      ASP .NET SignalR 是一個ASP .NET 下的類庫,可以在ASP .NET 的Web項目中實作實時通信。什麼是實時通信的Web呢?就是讓用戶端(Web頁面)和伺服器端可以互相通知消息及調用方法,當然這是實時操作的。

WebSockets是HTML5提供的新的API,可以在Web網頁與伺服器端間建立Socket連接配接,當WebSockets可用時(即浏覽器支援Html5)SignalR使用WebSockets,當不支援時SignalR将使用其它技術來保證達到相同效果。

SignalR當然也提供了非常簡單易用的高階API,使伺服器端可以單個或批量調用用戶端上的JavaScript函數,并且非常 友善地進行連接配接管理,例如用戶端連接配接到伺服器端,或斷開連接配接,用戶端分組,以及用戶端授權,使用SignalR都非常 容易實作。

SignalR 将與用戶端進行實時通信帶給了ASP .NET 。當然這樣既好用,而且也有足夠的擴充性。以前使用者需要重新整理頁面或使用Ajax輪詢才能實作的實時顯示資料,現在隻要使用SignalR,就可以簡單實作了。

最重要的是您無需重建立立項目,使用現有ASP .NET項目即可無縫使用SignalR。

  以上是來自百度百科的解釋,個人覺得通俗來講就是WebSockets是一種握手協定,當使用者于伺服器建立連接配接(握手成功)時,雙方就建立了一個連接配接通道,互相傳遞時時資料。在這之前,我們一般來說實作這個功能的方式就是Ajax輪詢,通過Ajax循環擷取資料,這當然是非常消耗資源的,并且給伺服器帶來一定的壓力。而WebSockets雖然可達到全雙工通信,但依然需要送出請求,不過這種請求的Header是很小的-大概隻有 2 Bytes。這裡我們重點要知道的一點就是,WebSockets這個通道是雙工通道,簡單了解就是用戶端(javascript、jquery)的方法不但可以調取伺服器的功能程式方法,并且伺服器的功能程式也可以調取全部(廣播)或某個(單點傳播)或某一類(多點傳播)用戶端(javascript、jquery)的方法。而SignalR則是微軟給我們內建的一個WebSockets API,原理跟WebSockets是一緻的,隻是當WebSockets可用時(即浏覽器支援Html5)SignalR使用WebSockets,當不支援時SignalR将使用其它技術來保證達到相同效果。

使用

很多新的技術沒有必要非得了解,隻需要知道你的應用環境可以用這項技術很簡便的去實作,用的多了,用的久了,自然而然就會慢慢的了解這項技術的原理了。

今天我們一步一步來介紹一下如何使用SignalR,這一篇文章介紹的是使用Hubs SignalR集線器去實作,下一篇我們将介紹使用SignalR永久連接配接類去實作,我們做個簡單的聊天室。

先給大家貼一下Demo的效果圖:

Asp.NET MVC 使用 SignalR 實作推送功能一(Hubs 線上聊天室)
Asp.NET MVC 使用 SignalR 實作推送功能一(Hubs 線上聊天室)

一、準備:

SignalR 運作在 .NET 4.5 平台上,是以需要安裝 .NET 4.5。為了友善示範,本示例使用 ASP.NET MVC 在 Win 7 系統來實作。這需要安裝 ASP.NET MVC 3 或 ASP.NET MVC 4。

二:Hubs代碼示例:

1、首先我們建立一個MVC項目工程名字叫做SignalR_Chat

2、然後打開 工具 - NuGet程式包管理器 - 程式包管理器控制台

Asp.NET MVC 使用 SignalR 實作推送功能一(Hubs 線上聊天室)

3、安裝SignalR

輸出NuGet指令:Install-Package Microsoft.AspNet.SignalR

Asp.NET MVC 使用 SignalR 實作推送功能一(Hubs 線上聊天室)

安裝成功後我們發現我們的bin裡已經添加了我們需要的元件,并且在Scripts檔案夾下添加了SignalR的Jquery引用

Asp.NET MVC 使用 SignalR 實作推送功能一(Hubs 線上聊天室)
Asp.NET MVC 使用 SignalR 實作推送功能一(Hubs 線上聊天室)

4、我們建立一個檔案夾叫Hubs,然後添加SignalR集線器類ChatHub.cs

Asp.NET MVC 使用 SignalR 實作推送功能一(Hubs 線上聊天室)
Asp.NET MVC 使用 SignalR 實作推送功能一(Hubs 線上聊天室)

在上面的代碼中:

(1)HubName 這個特性是為了讓用戶端知道如何建立與伺服器端對應服務的代理對象,如果沒有設定該屬性,則以伺服器端的服務類名字作為 HubName 的預設值;

(2)Chat 繼承自 Hub,從下面 Hub 的接口圖可以看出:Hub 支援向發起請求者(Caller),所有用戶端(Clients),特定組(Group) 推送消息。

5、添加OWIN Startup Class

Asp.NET MVC 使用 SignalR 實作推送功能一(Hubs 線上聊天室)

修改 Configuration方法

using Microsoft.Owin;
using Owin;
using SignalR_Chat.Connections;

[assembly: OwinStartupAttribute(typeof(SignalR_Chat.Startup))]
namespace SignalR_Chat
{
    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.MapSignalR();
       //這個是下一篇永久連接配接類的 我們先不用
            //app.MapSignalR<MyConnection>("/echo");
        }
    }
}      

6、我們實作一個聊天室,代碼我是分步貼出來的,後面我會附上完整的代碼和Demo。

首先,我們建立一個類 标記使用者和線上狀态

public class OnlineUserInfo
        {
            //使用者ID
            public string UserId { get; set; }
            //使用者連接配接ID
            public string ConnectionId { get; set; }
            //使用者昵稱
            public string UserNickName { get; set; }
            //使用者頭像
            public string UserFaceImg { get; set; }
            //使用者狀态
            public string UserStates { get; set; }
        }      

然後,我們在ChatHub中執行個體化一下這個類

/// <summary>
        /// 這個是通過Hub集線器 大家可以參考 Connections下的 MyConnection 永久連接配接
        /// </summary>
        [HubName("chat")]
        public class ChatHub : Hub
        {
            static List<OnlineUserInfo> UserList = new List<OnlineUserInfo>();
        }      

我們寫一個使用者連接配接時注冊一個群組,使用者後面的多點傳播

/// <summary>
            /// 注冊群組 注冊使用者資訊
            /// </summary>
            /// <param name="groupid">群組ID</param>
            /// <param name="usernickname">使用者昵稱</param>
            /// <param name="userfaceimg">使用者頭像</param>
            /// <param name="userid">使用者在網站中的唯一辨別ID</param>
            public void Group(string groupid, string usernickname, string userfaceimg, string userid)
            {
                //添加使用者到群組 Groups.Add(使用者連接配接ID,群組)
                Groups.Add(Context.ConnectionId, groupid);

                //如果說是一個簡單的聊天室 下面這段代碼是沒有什麼作用的 因為Context.ConnectionId是唯一的使用者于伺服器之間的連接配接
                //這裡我傳遞進來了 使用者的昵稱和頭像 還有網站中使用者的ID 是以我要把使用者的資訊添加到我們上面建立的那個清單類中

                //如果使用者不存在線上清單中
                if (UserList.Where(p => p.UserId == userid).FirstOrDefault() == null)
                {
                    //我們在清單中 添加這個使用者 并且标記使用者線上 UserStates = "True"
                    UserList.Add(new OnlineUserInfo() { UserId = userid, ConnectionId = Context.ConnectionId, UserNickName = usernickname, UserFaceImg = userfaceimg, UserStates = "True" });
                }
                    //如果使用者已經存在于線上清單中
                else
                {
                    //我們更新使用者清單中使用者的資訊 (這裡更新的資訊主要是使用者的連接配接ID  ConnectionId = Context.ConnectionId)
                    var UserInfo = UserList.Where(p => p.UserId == userid).FirstOrDefault();
                    UserList.Remove(UserInfo);
                    UserList.Add(new OnlineUserInfo() { UserId = userid, ConnectionId = Context.ConnectionId, UserNickName = usernickname, UserFaceImg = userfaceimg, UserStates = "True" });
                }
                //這個方法是調用用戶端LoginUser方法 并且傳遞目前使用者清單 用戶端會重新整理目前使用者清單 調用的是全部的已連接配接的使用者 Clients.All
                Clients.All.LoginUser(Common.JsonConverter.Serialize(UserList));
                //這個方法是調用用戶端的 addNewMessageToPage方法 目的是實作 當一個使用者上線是 提醒所有的使用者 某個使用者上線了 提醒的是所有的已連接配接使用者 是以也是Clients.All
                Clients.All.addNewMessageToPage("<dl  class=\"messageTip clearfix\"><dt></dt><dd>系統消息:" + DateTime.Now.ToString("HH:mm:ss") + "&nbsp;" + usernickname + "&nbsp;上線了<dd></dl>");
            }      

為了友善大家了解,我這裡就先把用戶端的LoginUser和addNewMessageToPage方法貼出來,讓大家好了解伺服器是怎樣調用用戶端的js方法的

//接收伺服器資訊
    chat.client.addNewMessageToPage = function (message) {
        //#chatContent就是一個div層 我們把伺服器傳回的資訊追加到這個層上 跟QQ聊天相反,新的資訊我們追加頂部
        $('#chatContent').prepend(message);
    };
    //伺服器端調用的LoginUser方法,根據傳回的使用者清單 輸出使用者清單到頁面上
    chat.client.LoginUser = function (UserList) {
        //在下一篇介紹的持久連接配接類中 是可以直接傳回Json的 這裡不知道怎麼回事 接收的Json總是被接收成字元串 是以這裡我們解析一下
        var data = eval("(" + UserList + ")");
        var html = "";
        for(var i=0;i<data.length;i++)
        {
            //這裡我們做了一個判斷 就是 解析使用者清單Json時 如果使用者的ID 就是目前使用者的ID 那麼就不添加 這跟QQ不一樣啊 QQ中好友清單中是有自己的
            if (data[i].UserId != $("#Juser-userid").val()) {
                //如果使用者的線上狀态是線上呢 我們就添加onclick方法 實作 點選使用者的使用者 可以私聊 如果不線上 就不添加了 因為我們這個是沒有存資料庫的 是以沒有做離線消息
                if (data[i].UserStates == "True") {
                    html += "<dl onclick=\"javascript:sendPerMessage('" + data[i].ConnectionId + "','" + data[i].UserNickName + "')\" class=\"clearfix tab-item-1\"><dt><img src=\"" + data[i].UserFaceImg + "\"></dt><dd>" + data[i].UserNickName + "</dd></dl>";
                }
                else
                {
                    html += "<dl onclick=\"javascript:void(0)\" class=\"clearfix tab-item-1 liveout\"><dt><img src=\"" + data[i].UserFaceImg + "\"></dt><dd>" + data[i].UserNickName + "</dd></dl>";
                }
            }
        }
        //更新頁面使用者清單
        $("#OnlineUsers").html(html);
    };      

這裡注意的是伺服器調用的用戶端方法跟用戶端寫的JS方法大小寫是一樣的,後面我們介紹用戶端調用伺服器方法的時候會将 用戶端調用伺服器方法的時候 伺服器方法的首字母是小寫的,這裡提醒一下。

下面是我們的發送消息的方法,當我發送消息的時候 傳遞我的頭像和昵稱給伺服器 讓别人顯示消息的時候能顯示出誰發送的(這個跟QQ消息類似啊 友善大家了解)

一個是群組消息當然也可以是全部消息,另一個是私聊,就是指定發送個某一個使用者

/// <summary>
            /// 發送消息 自定義判斷是發送給全部使用者還是某一個組(類似于群聊啦)
            /// </summary>
            /// <param name="groupid">接收的組</param>
            /// <param name="userfaceimg">發送使用者的頭像</param>
            /// <param name="usernickname">發送使用者的昵稱</param>
            /// <param name="message">發送的消息</param>
            public void Send(string groupid, string userfaceimg, string usernickname, string message)
            {
                if (groupid == "All")//全部使用者(廣播)
                {
                    //調用所有用戶端的addNewMessageToPage方法 推送一條消息
                    Clients.All.addNewMessageToPage("<dl class=\"clearfix\"><dt><img src=\"" + userfaceimg + "\" /></dt><dd><i></i><div class=\"J_Users\">" + usernickname + "</div><div class=\"J_Content\">" + message + "</div></dd></dl>");
                }
                else//指定組(多點傳播)
                {
                    //調用指定用戶端的addNewMessageToPage方法 推送一條消息(所有屬于組groupid的已連接配接使用者)
                    Clients.Group(groupid).addNewMessageToPage("<dl class=\"clearfix\"><dt><img src=\"" + userfaceimg + "\" /></dt><dd><i></i><div class=\"J_Users\">" + usernickname + "</div><div class=\"J_Content\">" + message + "</div></dd></dl>");
                }
            }

            /// <summary>
            /// 發送給指定使用者(單點傳播)
            /// </summary>
            /// <param name="clientId">接收使用者的連接配接ID</param>
            /// <param name="userfaceimg">發送使用者的頭像</param>
            /// <param name="usernickname">發送使用者的昵稱</param>
            /// <param name="message">發送的消息</param>
            public void SendSingle(string clientId, string userfaceimg, string usernickname, string message)
            {
                //首先我們擷取一下接收使用者的資訊
                var UserInfo = UserList.Where(p => p.ConnectionId == clientId).FirstOrDefault();
                //如果使用者不存在或使用者的線上狀态為False 那麼提醒一下 發送使用者 對方不線上
                if (UserInfo == null || UserInfo.UserStates == "False")
                {
                    Clients.Client(Context.ConnectionId).addNewMessageToPage("<dl  class=\"messageTip clearfix\"><dt></dt><dd>系統消息:目前使用者不線上<dd></dl>");
                }
                else
                {
                    //如果使用者存在并且線上呢 就把消息推送給接收的使用者 并且加上目前使用者資訊 以及添加一個onclick事件 讓接收的使用者 可以直接點選消息的使用者 回複 私聊資訊 (不然還要在使用者清單中找到誰給我發的消息 點選回複 這不科學...)
                    Clients.Client(clientId).addNewMessageToPage("<dl class=\"clearfix\"><dt onclick=\"javascript:sendPerMessage('" + Context.ConnectionId + "','" + usernickname + "')\"><img src=\"" + userfaceimg + "\" /></dt><dd class=\"per\"><s></s><div onclick=\"javascript:sendPerMessage('" + Context.ConnectionId + "','" + usernickname + "')\" class=\"J_Users\">" + usernickname + "<span>私聊</span></div><div class=\"J_Content\">" + message + "</div></dd></dl>");
                    //這句是發送給發送使用者的 總不能我發送個私聊 對方收到了資訊 我這裡什麼都不顯示是吧 我也顯示我發送的私聊資訊 因為發送發就是我自己 是以不加onclick事件了 不允許自己跟自己聊天哦
                    Clients.Client(Context.ConnectionId).addNewMessageToPage("<dl class=\"clearfix\"><dt><img src=\"" + userfaceimg + "\" /></dt><dd class=\"per\"><s></s><div class=\"J_Users\">" + usernickname + "<span>私聊</span></div><div class=\"J_Content\">" + message + "</div></dd></dl>");
                }
            }      

這裡我貼一下前台代碼

$.connection.hub.start().done(function () {
        //使用者連接配接時 注冊一下群組和個人資訊哦 這個的伺服器代碼 我們上面貼出來了
        //這個Demo是為了讓大家了解SigalR是以沒有做多複雜的流程 個人資訊 我是直接傳遞的 
        //$("#groupid").val()這個是要注冊的群組,可以自己定義 多點傳播的時候 隻要是在這一個組裡的都會收到 
        //$("#Juser-login").val()這個是發送方也就是我的昵稱
        //$("#Juser-faceimg").val()這個是我的頭像
        //$("#Juser-userid").val()這個是我在網站中的唯一辨別ID,使用者連接配接的ID(Context.ConnectionId)也是唯一的,那麼為什麼還要我在這個網站中的ID呢?
        //解釋一下子:單頁面的聊天室是沒有多大必要的,但是比如我們這個功能是放到公用裡的,就像網站的線上客服一樣,你總不能每個頁面都寫一套吧 既然是引用的這一個頁面 
        //那麼使用者打開其他頁面的時候 這個Context.ConnectionId是會變的,那我怎麼知道這又是誰呢 我們就用使用者在網站中的唯一辨別ID作為參照,當新的連接配接進來時 我們看下是不是ID一樣 
        //一樣的話我們就更新使用者清單中這個唯一辨別ID使用者的Context.ConnectionId和線上狀态 不一樣的話就添加新使用者
        chat.server.group($("#groupid").val(), $("#Juser-login").val(), $("#Juser-faceimg").val(), $("#Juser-userid").val());
        $('.sendBtn').click(function () {
            //這裡做一下判斷 如果沒有輸入消息就發送 那麼提示一下
            if ($('#MessageBox').val().length <= 0)
            {
                $('#chatContent').prepend("<dl  class=\"messageTip clearfix\"><dt></dt><dd>系統消息:請輸入資訊<dd></dl>");
            }
            else
            {
                //sendToConnectId 是我們自定義的一個字段 如果你點選了某一個使用者 那麼就把他的ConnectionId賦給sendToConnectId 我們知道是私聊
                //當然,使用者點選退出私聊的時候 這個字段會被賦為空 表示是群聊 這個大家在Demo中一看就明白了
                if (sendToConnectId != "" && sendToConnectId.length > 0) {
                    //調用伺服器私聊方法 !!!注意啊!!!伺服器的私聊方法是 public void SendSingle(string clientId, string userfaceimg, string usernickname, string message)
                    //這裡是chat.server.sendSingle 首字母小寫啊 用戶端調用的伺服器方法首字母要小寫  伺服器調用的用戶端方法 大小寫一緻 
                    chat.server.sendSingle(sendToConnectId, $("#Juser-faceimg").val(), $("#Juser-login").val(), $('#MessageBox').val());
                    $('#MessageBox').val("").focus();
                }
                else {
                    //這裡是群聊 我們示範的沒有做群組聊天 是以這裡傳遞的是"All"表示 全部,會發送給全部使用者
                    //說明一下友善了解:比如我們有這麼一個情景,這個聊天是一個讨論,對某一篇文章或産品的讨論,那麼是不是應該隻在這篇文章或這個産品頁面的使用者才能收發屬于這篇文章或産品消息呢,在其他頁面
                    //的使用者不應該能收發這裡的消息呀 那麼我們上面的代碼chat.server.group中傳遞的groupid就應該是某篇文章或産品的辨別,把他們劃分到一個組裡比如chat.server.group("A123")一個自定義字元串加上文章或産品ID,
                    //或直接用文章或産品的IDchat.server.group("123") 這裡就chat.server.send("123", ...);就實作了 隻有在這個頁面中的使用者才能收到此消息 就跟QQ的群是一樣的
                    chat.server.send("All", $("#Juser-faceimg").val(), $("#Juser-login").val(), $('#MessageBox').val());
                    $('#MessageBox').val("").focus();
                }
            }
        });
    });      

使用者離線或重新連接配接 重寫Hub的方法

//使用者離線
            public override Task OnDisconnected(bool stopCalled)
            {
                var UserInfo = UserList.Where(p => p.ConnectionId == Context.ConnectionId).FirstOrDefault();
                var userid = UserInfo.UserId;
                var usernickname = UserInfo.UserNickName;
                var userfaceimg = UserInfo.UserFaceImg;
                UserList.Remove(UserInfo);
                UserList.Add(new OnlineUserInfo() { UserId = userid, ConnectionId = Context.ConnectionId, UserNickName = usernickname, UserFaceImg = userfaceimg, UserStates = "False" });

                Clients.All.LoginUser(Common.JsonConverter.Serialize(UserList));
                Clients.All.addNewMessageToPage("<dl  class=\"messageTip clearfix\"><dt></dt><dd>系統消息:" + DateTime.Now.ToString("HH:mm:ss") + "&nbsp;" + usernickname + "&nbsp;離線了<dd></dl>");

                return base.OnDisconnected(true);
            }

            //使用者重新連接配接
            public override Task OnReconnected()
            {
                var UserInfo = UserList.Where(p => p.ConnectionId == Context.ConnectionId).FirstOrDefault();
                if (UserInfo != null)
                {
                    var userid = UserInfo.UserId;
                    var usernickname = UserInfo.UserNickName;
                    var userfaceimg = UserInfo.UserFaceImg;
                    UserList.Remove(UserInfo);
                    UserList.Add(new OnlineUserInfo() { UserId = userid, ConnectionId = Context.ConnectionId, UserNickName = usernickname, UserFaceImg = userfaceimg, UserStates = "True" });
                    Clients.All.LoginUser(Common.JsonConverter.Serialize(UserList));
                }
                return base.OnReconnected();
            }      

還有一些輔助的JS方法,在這裡我就不一一貼出來了,我把demo位址留給大家 ,大家可以搭建起來研究一下。

這篇文章僅僅是個人的一些了解和實作,可能中間會出現一些不合理的地方或是錯誤,請大家指正,我們共同學習研究。

Demo是用VS 2013寫的 

下載下傳:百度網盤 

補充:Demo是我寫這個部落格之前寫的 沒有用到HubName 這個特性 是以Demo跑起來會有錯誤 大家删除這個特性就沒有錯誤了 在Hubs檔案夾下的ChatHub.cs

Asp.NET MVC 使用 SignalR 實作推送功能一(Hubs 線上聊天室)

原創文章 轉載請尊重勞動成果 http://yuangang.cnblogs.com