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的效果圖:

一、准備:

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程序包管理器 - 程序包管理器控制台

3、安裝SignalR

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

安裝成功后我們發現我們的bin里已經添加了我們需要的組件,並且在Scripts文件夾下添加了SignalR的Jquery引用

4、我們新建一個文件夾叫Hubs,然后添加SignalR集線器類ChatHub.cs

在上面的代碼中:

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

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

5、添加OWIN Startup Class

修改 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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM