通过 MVC SignalR Hub 实现实时聊天 Web 解决方案






4.85/5 (126投票s)
本文通过聊天实现和客户端与服务器之间的交互,并涉及 Hub 类,来解释 SignalR 的概念。
引言
如今,由于信息量的增加以及在短时间内获取数据的必要性,我们需要技术来满足这方面的需求。假设在股市价格每时每刻都在变化,您认为用户应该每时每刻刷新页面以获取最新价格吗?显然,对于这样的问题,这不是一个合理的解决方案。或者随着产品和服务的生产增加,我们需要客户服务来帮助用户和买家,最好且最便宜的沟通方式是通过聊天程序进行对话。同样,我们不能强迫用户按按钮来接收我们的最新消息。
SignalR 是一种实时技术,它使用一组异步库在客户端和服务器之间建立持久连接。用户无需像刷新页面或按按钮这样的传统方式,即可从服务器接收最新更新的数据。
背景
您需要了解 MVC 4.0 技术和 EntityFramework > 4.0 才能更好地理解本文。
另一方面,SignalR 使用以下方法来建立实时 Web 连接:
1. WebSocket
WebSocket 是一种全双工协议,内部使用 HTTP 握手,允许消息流在 TCP 之上进行。它支持:Google Chrome (> 16)、Firefox (> 11)、IE (> 10)、Win IIS (>8.0)。由于消息加密和全双工特性,WebSocket 是最佳解决方案,SignalR 首先会检查 Web 服务器和客户端服务器是否支持 WebSocket。
单工通信
它只在一个方向上传播,一个点广播,而另一个点只能收听而不能发送消息,例如电视和广播。
半双工
一个点发送消息,此时另一个点不能发送消息,需要等到第一个点完成传输后再发送消息,一次只能进行一种通信,例如老式无线设备对讲机和 HTTP 协议。
全双工
两个点可以同时发送和接收消息,无需等待对方完成传输,例如电话和 WebSocket 协议。
2. 服务器发送事件 (SSE)
SignalR 的下一个选择是服务器发送事件,因为它在服务器和客户端之间实现了持久通信。在这种方法中,通信不会断开,服务器的最新数据将自动更新并通过 HTTP 连接传输到客户端。EventSource
是 HTML5 技术的一部分。
var evsrc = new EventSource("url");
// Load and Register Event Handler for Messages in this section
evsrc.addEventListener("message", function (event) {
//processing data in this section
});
3. 永久帧
当客户端向服务器发送请求时,服务器将一个隐藏的 iframe 作为分块数据发送到客户端,这个 iframe 负责永久保持客户端和服务器之间的连接。每当服务器更改数据时,它会以脚本标签的形式发送数据到客户端(隐藏的 iframe),这些脚本将按顺序接收。
4. 轮询
客户端向服务器发送请求,服务器立即响应,但之后服务器会断开连接,所以要重新建立服务器和客户端之间的通信,我们需要等待客户端的下一个请求。为了解决这个问题,我们必须手动设置超时,并且每隔 10 秒客户端都会向服务器发送请求以检查服务器端的任何新修改并获取最新更新数据。轮询会消耗资源,不是一种经济的解决方案。
5. 长轮询
客户端向服务器发送请求,服务器立即响应,并且此连接会一直保持直到特定时间,在此期间客户端无需向服务器发送显式请求,而在轮询中,客户端必须在超时期间向服务器发送显式请求。Comet 编程涵盖了此概念。
简而言之,SignalR 库会在客户端和服务器之间选择一种数据传输类型,其优先级顺序为:WebSocket、服务器发送事件、长轮询和永久帧。该库中有两个类,如下所示:
1. PersistentConnection
它级别较低,因此很复杂,需要更多的配置,但作为回报,它提供了更多处理类的便利。
2. Hub
它级别较高,使用起来更受欢迎。
如何借助 SignalR 和 Hub 类实现简单的聊天场景?
我的目标只是提供一个用于集成 SignalR 的随机场景。您可以将其用于您自己的场景,我将遵循以下步骤来挑战服务器(hub
类)和客户端,并说明客户端如何发送请求以及服务器如何响应?它们如何相互交互?
场景描述
我想建立一个面向客户服务部门的应用程序。有一些管理员负责帮助客户,另一方面,有一些客户提出问题并需要帮助。
假设有两名管理员在线并连接到聊天服务,第一个客户进来提问,系统会将第一个客户连接到第一个空闲的管理员,对于第二个客户,这个过程会重复,但第三个客户会收到系统的警报,告知没有管理员可以提供帮助。每当第一个客户断开连接时,第一个管理员就会变得空闲。
我的场景合同是使用标志来提醒哪个连接的用户是普通用户还是管理员,以及哪个是空闲或忙碌。在我的数据库中,如果 admincode 等于零,则表示用户,否则表示管理员,并且我定义了一个标志“tpflag
”(在应用程序中)等于零表示用户,等于一表示管理员。每当他们连接到聊天标志时,“freeflag
”变为零,表示忙碌的用户,并且一旦客户离开对话,它就变为一,表示空闲状态。
if freeflag==0 ==> Busy
if freeflag==1 ==> Free
if tpflag==0 ==> User
if tpflag==1 ==> Admin
必备组件
- Visual Studio 2012
- SQL Server 2008
- 通过包管理器控制台安装必要的依赖项
步骤 1:创建项目
文件 --> 新建项目 --> ASP.NET MVC 4 Web 应用程序 {输入名称和目录} --> {模板=基本 & 视图引擎=Razor}
步骤 2:打开 PM 以安装依赖文件
菜单(工具)--> 程序集包管理器 --> 程序包管理器控制台
步骤 3:卸载旧依赖项的说明
首先,卸载所有旧依赖项,以便安装新版本的 SignalR 2.x.x。在行中:
PM> Uninstall-Package Microsoft.AspNet.SignalR –RemoveDependencies
步骤 4:安装必需依赖文件的说明
对于新版本,请使用:
PM> Install-Package Microsoft.AspNet.SignalR
我已在此实践中使用 signalr 版本 2.0.1。
PM> Install-Package Microsoft.AspNet.SignalR -Version 2.0.1
PM> Install-Package Microsoft.Owin
通过输入此指令,NuGet 将完成运行 SignalR 所需的所有依赖项注入。如果您查看解决方案中的“引用”部分,您会找到 Microsoft.ASPNet.SignalR.x、Microsoft.Owin.x.x..等,或者如果您查看解决方案中的“脚本”部分,您会找到 jquery-1.x、jquery.signalR 2.x.x 等,因此您可以放心地处理所有依赖项。
解决方案 --> 打开引用 -->
解决方案 --> 脚本 -->
另一方面,在成功安装 SignalR 依赖项后,您将在包控制台上方找到完整的帮助,如 readme.txt。其中包含开始使用 SignalR 的所有必要说明。我将在接下来的步骤中解释这些说明。
提示 (1):NuGet
如果您遇到错误“无法解析远程名称:'www.nuget.org'”,那么您应该更改包管理器设置,该设置位于包源的右侧。
您应该将源从 https 协议更改为 http 协议来解决此问题。
提示 (2):Owin
检查您的引用部分,确保有 Owin,否则请按照此说明操作:右键单击引用 --> 管理 NuGet 程序包 --> 在左侧选择“联机” --> 搜索 Owin --> 选择 Owin (Owin IAppBuilder
启动接口) --> 安装。
然后,您应该在引用部分看到 Owin。
步骤 5:启动类
要启用项目中的 SignalR,您应该创建一个名为 Startup 的类。(如果在 SignalR 的早期版本,即第一个版本中,您过去在 global.asax 的 Application Start 中编写 RouteTable.Routes.MapHubs();
,现在请忘记它,只使用 startup
类。右键单击:在项目名称 {SignalR} 上 --> 添加类 --> 名称:Startup.cs
using Microsoft.Owin;
using Owin;
[assembly: OwinStartup(typeof(MvcSignal.Startup))]
namespace MvcSignal
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.MapSignalR();
}
}
}
步骤 6:根据我们的场景组织数据库
步骤 6.1:创建“tbl_User”
“tbl_user
”将收集用户和管理员信息,如果“AdminCode
”是从前一个表中填写的数字,则表示该管理员属于某个 department
,否则如果填写为(零),则表示普通用户。(“UserID
” int
+ identity=yes
和“AdminCode
”默认值为 0
)
步骤 6.2:创建“tbl_Conversation”
“tbl_Conversation
”将收集用户和管理员之间的对话数据。此表将在对话结束后填充。 { “ConID” int + identity=yes }
步骤 7:创建 Hub 类
步骤 7.1:模型(文件夹)--> 创建类“UserInfo.cs”
public class UserInfo
{
public string ConnectionId { get; set; }
public string UserName { get; set; }
public string UserGroup { get; set; }
//if freeflag==0 ==> Busy
//if freeflag==1 ==> Free
public string freeflag { get; set; }
//if tpflag==2 ==> User Admin
//if tpflag==0 ==> User Member
//if tpflag==1 ==> Admin
public string tpflag { get; set; }
public int UserID { get; set; }
public int AdminID { get; set; }
}
步骤 7.2:模型(文件夹)--> 创建类“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; }
}
步骤 7.3:控制器(文件夹)?创建“HomeController.cs”
public ActionResult Chat()
{
ViewBag.Message = "Your contact page.";
return View();
}
右键单击 Chat()
--> 选择 **添加视图** -->
步骤 7.4:创建 Chat.cshtml {客户端}
@{
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 () { //This section will run whenever we call Chat.cshtml page
$("#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) {
// <<<<<-- ***** Return to Server [ Connect ] *****
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();
// <<<<<-- ***** Return to Server [ SendMessageToGroup ] *****
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>
}
步骤 7.5:创建 Model1.edmx
为了方便从数据库获取和插入数据,我创建了模型如下:右键单击项目名称 --> 添加新项 --> 选择“ADO.NET 实体数据模型” --> 选择“从数据库生成” --> 建立与您的数据库的连接 --> 选择您的表。
步骤 8:创建文件夹并命名为 Hubs,然后创建一个简单类并命名为“MyHub.cs”
{如果您有最新版本的 Visual Studio,您可以添加新项并选择“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>();
//-->>>>> ***** Receive Request From Client [ Connect ] *****
public void Connect(string userName, string password)
{
var id = Context.ConnectionId;
string userGroup="";
//Manage Hub Class
//if freeflag==0 ==> Busy
//if freeflag==1 ==> Free
//if tpflag==0 ==> User
//if tpflag==1 ==> Admin
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
{
//You can check if user or admin did not login before
//by below line which is an if condition
//if (UsersList.Count(x => x.ConnectionId == id) == 0)
//Here you check if there is no userGroup which is
//same DepID --> this is User otherwise this is Admin
//userGroup = DepID
if ((int)userInfo.AdminCode == 0)
{
//now we encounter ordinary user which needs userGroup and at this step,
//system assigns the first of free Admin among UsersList
var strg = (from s in UsersList where (s.tpflag == "1")
&& (s.freeflag == "1") select s).First();
userGroup = strg.UserGroup;
//Admin becomes busy so we assign zero to freeflag
//which is shown admin is busy
strg.freeflag = "0";
//now add USER to UsersList
UsersList.Add(new UserInfo { ConnectionId = id,
UserID = userInfo.UserID,
UserName = userName,
UserGroup = userGroup,
freeflag = "0",
tpflag = "0", });
//whether it is Admin or User now both of them has userGroup
//and I Join this user or admin to specific group
Groups.Add(Context.ConnectionId, userGroup);
Clients.Caller.onConnected(id, userName, userInfo.UserID, userGroup);
}
else
{
//If user has admin code so admin code is same userGroup
//now add ADMIN to UsersList
UsersList.Add(new UserInfo { ConnectionId = id,
AdminID = userInfo.UserID,
UserName = userName,
UserGroup = userInfo.AdminCode.ToString(),
freeflag = "1",
tpflag = "1" });
//whether it is Admin or User now both of them has userGroup and
//I Join this user or admin to specific group
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";
//***** Return to Client *****
Clients.Caller.NoExistAdmin();
}
}
// <<<<<-- ***** Return to Client [ NoExist ] *****
//--group ***** Receive Request From Client [ SendMessageToGroup ] *****
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;
// If you want to Broadcast message to all UsersList use below line
// Clients.All.getMessages(userName, message);
//If you want to establish peer to peer connection use below line
//so message will be send just for user and admin who are in same group
//***** Return to Client *****
Clients.Group(strgroup).getMessages(userName, message);
}
}
// <<<<<-- ***** Return to Client [ getMessages ] *****
//--group ***** Receive Request From Client *****
//{ Whenever User close session then OnDisconneced will be occurs }
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")
{
//user logged off == user
try
{
var stradmin = (from s in UsersList where
(s.UserGroup == item.UserGroup) && (s.tpflag == "1") select s).First();
//become free
stradmin.freeflag = "1";
}
catch
{
//***** Return to Client *****
Clients.Caller.NoExistAdmin();
}
}
//save conversation to dat abase
}
return base.OnDisconnected();
}
}
}
规则和约定
客户端调用服务器端方法时的前缀
( 客户端 --> 服务器 ) // 客户端向服务器发送请求
1. objHub.server.methodname() { 服务器端的方法名}
并且服务器端(myHub.cs 类)中存在完全相同的方法名。
服务器调用客户端方法时的前缀
( 服务器 --> 客户端 ) // 服务器调用客户端方法 & {客户端的方法名}
Clients.caller.methodname()
// caller 表示仅发送请求的用户Clients.all.methodname()
// all 表示所有连接的用户Clients.Group(groupName).methodname()
// Group 表示仅属于同一组的用户
当“MyHub.cs”类中有 ***** 返回客户端 ***** 时,表示您需要在客户端编写同名的 jQuery 函数。
事实上,它们的交互如下:
提示 (3):调用服务器类
有一些小提示:每当您想调用您的服务器类时,在客户端,您应该始终使用特定的命名约定,即驼峰式,例如,如果您的 hub 类名为“MyHub
”,您应该从“myHub
”实例化您的对象,或者如果您有一个“SendMessageToGroup
”,您应该从“sendMessageToGroup
”调用它,因此应该是这样的:
测试用例
要获得相同的结果,您应该拥有与第七步中所述相同的数据库。
案例 1
测试计划:如果客户端尝试登录但没有管理员,则系统会显示警报。
测试步骤
- 运行项目
- 用户名:mahsa
- 密码:123
- 预期输出:系统显示警报
案例 2
测试计划:至少有一名空闲管理员,然后有一个客户登录,然后第一名管理员将被分配给第一个需要帮助的空闲客户。
测试步骤
- 运行项目
- 用户名:admin1
- 密码:123
- 登录 {admin1 作为第一位管理员}
- 将 URL 复制到另一个浏览器
- 用户名:mahsa
- 密码:123
- 登录 {mahsa 作为第一位客户}
- 如果“mahsa”发送消息,“admin1”会看到它,因为他们在同一个组。当第一个客户成功登录时,会被分配给第一个空闲管理员。
- 将 URL 复制到另一个浏览器
- 用户名:kashi
- 密码:123
- 登录 {kashi 作为第二位客户}
- 系统显示警报并说“没有管理员,然后系统显示警报”
- 将 URL 复制到另一个浏览器
- 用户名:admin2
- 密码:123
- “kashi”和“admin2”看不到“admin1”和“mahsa”之间的对话。
技巧
提示 1. 管理员和用户的不同 UI,并为管理员显示等待用户
如果您需要管理员和用户的不同 UI,您应该为用户和管理员创建不同的 div
,并使用不同的 CSS,并通过 class 属性为它们分配特定的 CSS。
当您想从 Hub 类将管理员消息发送给管理员时,请发送到不同的客户端方法,例如 objHub.client.getMessagesAdmin
,对于用户则是 objHub.client.getMessagesUser
。
在 Chat.cshtml 中,使用不同的 div
“divMessageAdmin
”和“divMessageUser
”实现这些具有不同 UI 的方法,您应该用适当的消息填充这些 div
。
所以请遵循:
- 创建不同的
div
<div class="Admin" id="divMessageAdmin"></div>
<div Class="User" id="divMessageUser"></div>
- 在
Hub
类 ->SendMessageToGroup
->检查用户是否为管理员 ->
Clients.Group(strgroup).getMessagesAdmin(userName, message);
检查用户是否为普通用户 ->
Clients.Group(strgroup).getMessagesUser(userName, message);
- 在 Chat.cshtml 中
如果用户是管理员
objHub.client.getMessagesAdmin = function (userName, message) { $("#txtMessage").val(''); $('#divMessageAdmin').append('<div><p>' + userName + ': ' + message + '</p></div>'); var height = $('#divMessageAdmin')[0].scrollHeight; $('#divMessageAdmin').scrollTop(height); }
如果用户是普通用户
objHub.client.getMessagesUser = function (userName, message) {
$("#txtMessage").val(''); $('#getMessagesUser').append('<div><p>' + userName + ': ' + message + '</p></div>'); var height = $('#getMessagesUser')[0].scrollHeight; $('#getMessagesUser').scrollTop(height); }
在管理员的 UI 中查看等待用户
- 在
Hub
类 ->Connect
-> 中,您有以下代码行:catch { string msg = "All Administrators are busy, please be patient and try again"; // Return to Client Clients.Caller.NoExistAdmin(); }
请发送用户名,给这位需要等待空闲管理员的用户
Clients.Caller.NoExistAdmin(username);
- 在 Chat.cshtml 中
objHub.client.NoExistAdmin = function (username) { var divNoExist = $('
没有管理员可以回复……请稍后再试
'); $("#divChat").hide(); $("#divLogin").show(); $("#divWaitingUser").append(' ' + userName + ' '); $(divNoExist).hide(); $('#divalarm').prepend(divNoExist); $(divNoExist).fadeIn(900).delay(9000).fadeOut(900); }