背景
以前我们使用Web页面和服务端交互时多采用Ajax轮询(不停间隔的请求服务器)服务端,客户端数量少可以选择轮询,一旦客户端数量比较多,并发多时,服务端很容易崩溃,同时页面长期在轮询状态下很容易使页面假死,卡顿,体验极差;为了解决web和服务端的消息通信,H5采用的WebSocket可以解决这个难题,但是对浏览器要求必须满足H5,因此要兼容当前的大多浏览器需要考虑其他的推送技术来实现。
一、什么是SignalR
是一个ASP.NET下的类库,可以在ASP.NET的的Web项目中实现实时通信,支持长轮询(Long Polling)、WebSocket、serverSentEvents、foeverFrame四中主要的传输协议。
二、应用范围及作用
SignalR应用环境在ASP.NET4.5的Web项目中,可以实现Web客户端和ASP.NET 的服务端的实时通信;
由于SignalR是一个类库,使用起来比较方便,引入即可完成项目的无缝使用。
三、传输方式选择
1、客户端web浏览器支持WebSocket,服务端支持WebSocket,同时支持跨域,SignalR选择WebSocket模式进行消息通信;
2、无法满足H5环境则使用长轮询方式,当然如果客户端数量比较大时长轮询会导致严重的资源浪费和性能成本。
3、指定传输方式,根据客户端和服务端的数据实时性和数量的要求,我们可以指定传输方式,以便使用更合适的方式进行前后通信。
可以通过使用$.connection.start({transport:['webSockets','foeverFrame',……]})方式进行选择指定
- webSockets:一个基于TCP协议的的双工连接,客户端浏览器首先通过Http向服务器发送一个请求,一个包含附加头信息(Upgrade: WebSocket)的http请求,这表明他是一个申请协议升级的http请求,服务器收到请求以后进行解析,解析完成以后将应答信息返回给客户端,此时服务端可客户端的websocket就建立起来了,双方可以进行自由通信,直到服务端可客户端任何一方终止通信,连接关闭。
- foeverFrame:适用IE浏览器,在页面中插入一个隐藏的Iframe,利用其src属性在服务器和客户端创建一条长连接,服务器向Iframe传输数据来实时更新页面。
- serverSentEvents:类似于长轮询的机制,但是在每一次的连接中,不只是等待一次的数据更新,客户端发送请求以后,服务端一直会保持连接这个请求直到服务端有新的数据更新,将数据返回到客户端,此时服务端不关闭连接。仍然保持连接,供其他消息使用,他的特征就是发起一次连接可以供所有消息进行处理。
- longPolling:和SSE的区别是客户端发送一次请求以后,如果服务端没有数据更新该连接将保持直到服务端有数据更新,将数据返回到客户端,并将连接关闭。
四、案例分析
使用SignalR长轮询方式实现消息通讯
1、服务端
开发前需要在nget找到SignalR组件,引入项目。
创建Hub类,创建[HubName("messageHub")]标识的类,用于获取客户端的消息
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;
namespace WebApplication2
{
[HubName("messageHub")]
public class MyHub1 : Hub
{
public void Hello()
{
Clients.All.hello();
}
}
}
创建Startup类,定义服务端和web应用端的标准接口,客户端通过SignalR框架实现与服务端Owin适配,实现服务端和客户端的请求。
using Owin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace WebApplication2
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.MapSignalR();
}
}
}
创建webapi controller,通过调用Hub类实现webapi控制层的消息交互,本案例中通过获取(string nick, string id)两个参数实现前后端的消息通讯,其中id为消息内容,nick为昵称。
using Microsoft.AspNet.SignalR;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
namespace WebApplication2.Controllers
{
[RoutePrefix("api/msg")]
public class MessageInfoController : ApiController
{
// GET api/<controller>
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/<controller>/5
[HttpGet]
[Route("getmsg/{nick}/{id}")]
public string Get(string nick, string id)
{
var hub = GlobalHost.ConnectionManager.GetHubContext<MyHub1>();
hub.Clients.All.showMessage(nick + "," + id+","+ DateTime.Now.ToUniversalTime().ToString());
return "value";
}
// PUT api/<controller>/5
public void Put(int id, [FromBody]string value)
{
}
// DELETE api/<controller>/5
public void Delete(int id)
{
}
}
}
2、web客户端
引入相关的js,可以从nget下载
其中 <script src="http://192.168.31.112:8009/signalr/hubs"></script>的配置方式为IP+端口+/signalr/hubs模式配置即可。
html界面编码开始,分为两部分,一个是登录模块为了获取注册人的昵称,另一个是登录成功以后的聊天主界面。
<body>
<div class="mypop" id="liaotian" style="display: block">
<div class="graybox"></div>
<div class="popbox">
<div class="poptop">
<h3>匿名聊天室</h3>
<a href="#" class="close"></a>
</div>
<div class="popcon">
<div>
<div class="group">
<input type="text" id="nickname" value="请输入昵称" onfocus="if (value == '请输入昵称') { value = '' }" onblur=" if (value == '') { value = '请输入昵称' }">
<i class="fa fa-user"></i>
</div>
</div>
</div>
<div class="divbtn clearfix">
<a class="btn" onclick="quxiao()">取消</a>
<a href="#" class="btn" onclick="GoHome()">进入聊天室</a>
</div>
</div>
</div>
<div id="msg" style="margin:10px;">
<h3>欢迎<font color='blue' style="font-family: Glyphicons Halflings" size="3" id="nick"></font>进入聊天室</h3>
<div id="msgcontent">
</div>
<div>
</div>
<div style="text-align: left; width: 98%; margin: 10px auto 0;zoom:1;">
<textarea id="message" rows=5 style="width:90%;border:1px solid #271818;height:98px;float:left;"></textarea>
<input class="button" type="button" value="发送消息" id="btnSend" />
</div>
</div>
</body>
CSS样式部分
<style type="text/css">
#msgcontent {
width: 96%;
height: 400px;
border: 2px solid blue;
padding: 5px;
margin: 5px 0px;
overflow-x: hidden;
overflow-y: auto;
}
.button {
height: 100px;
padding: 15px 12px;
display: inline-block;
color: white;
margin-left: 10px;
background-color: #4CAF50;
border-radius: 8px;
text-align: center;
}
</style>
js逻辑实现
<script type="text/javascript">
var clientName ="";
var eUsers = $("#users");
var hubUrl = 'http://192.168.31.112:8009/signalr'; //服务端ip
$(function(){
//绑定输入框,这里只能 是ID
$("#btnSend").keydown(function(event){
event=document.all?window.event:event;
if((event.keyCode || event.which)==13){
var msg = $("#message").val();
$.get('http://192.168.31.112:8009/api/msg/getmsg/' + clientName + '/' + msg + '', {
}, function (res) {
// alert("suc" + msg);
$("#message").val("");
$("#message").focus();
}, 'text');
}
});
});
//初始化服务端和客户端的连接
function initTalk(){
document.getElementById('nick').innerHTML = clientName;
$.connection.hub.url = hubUrl;
var hub = $.connection.messageHub;
hub.client.showMessage = function (msg) {
var html = "<div id='conheight'><font size='8' color='blue'>" + msg.split(',')[0] + " " + msg.split(',')[2] + "</font></br>" + msg.split(',')[1] + "</div>";
$("#msgcontent").append(html);
var conheight = document.getElementById("conheight").clientHeight;
console.log(conheight);
if (conheight > 400) {
document.getElementById("msgcontent").scrollTop = document.getElementById("conheight").scrollHeight;
}
}
$.connection.hub.start().done(function () {
$("#btnSend").click(function () {
var msg = $("#message").val();
$.get('http://192.168.31.112:8009/api/msg/getmsg/' + clientName + '/' + msg + '', {
}, function (res) {
// alert("suc" + msg);
$("#message").val("");
$("#message").focus();
}, 'text');
});
});
}
//登录方法,登录成功以后调用 initTalk()进行$.connection服务端的连接
function GoHome() {
clientName = $("#nickname").val();
if (clientName.length == 0 || clientName == "请输入昵称") {
alert("随便起个名字吧,我们不会记录");
return;
}
document.getElementById("liaotian").style.display = "none";
initTalk();
}
function randomString() {
var $chars = 'ycz123'; /****默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1****/
var maxPos = $chars.length;
var pwd = '';
for (i = 0; i < 3; i++) {
pwd += $chars.charAt(Math.floor(Math.random() * maxPos));
}
return pwd;
}
function quxiao() {
window.open(location, '_self').close();
}
</script>
完整的前端源码,搭建好服务端,可以直接运行
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title></title>
<link href="../Content/style.css" rel="stylesheet" />
<script src="../Scripts/jquery-1.10.2.min.js"></script>
<script src="../Scripts/jquery.signalR-2.4.0.js"></script>
<script src="../Scripts/jquery.signalR-2.4.0.min.js"></script>
<script src="http://192.168.31.112:8009/signalr/hubs"></script>
<style type="text/css">
#msgcontent {
width: 96%;
height: 400px;
border: 2px solid blue;
padding: 5px;
margin: 5px 0px;
overflow-x: hidden;
overflow-y: auto;
}
.button {
height: 100px;
padding: 15px 12px;
display: inline-block;
color: white;
margin-left: 10px;
background-color: #4CAF50;
border-radius: 8px;
text-align: center;
}
</style>
<script type="text/javascript">
var clientName ="";
var eUsers = $("#users");
var hubUrl = 'http://192.168.31.112:8009/signalr';
$(function(){
//绑定输入框,这里只能 是ID
$("#btnSend").keydown(function(event){
event=document.all?window.event:event;
if((event.keyCode || event.which)==13){
var msg = $("#message").val();
$.get('http://192.168.31.112:8009/api/msg/getmsg/' + clientName + '/' + msg + '', {
}, function (res) {
// alert("suc" + msg);
$("#message").val("");
$("#message").focus();
}, 'text');
}
});
});
function initTalk(){
document.getElementById('nick').innerHTML = clientName;
$.connection.hub.url = hubUrl;
var hub = $.connection.messageHub;
hub.client.showMessage = function (msg) {
var html = "<div id='conheight'><font size='8' color='blue'>" + msg.split(',')[0] + " " + msg.split(',')[2] + "</font></br>" + msg.split(',')[1] + "</div>";
$("#msgcontent").append(html);
var conheight = document.getElementById("conheight").clientHeight;
console.log(conheight);
if (conheight > 400) {
document.getElementById("msgcontent").scrollTop = document.getElementById("conheight").scrollHeight;
}
}
$.connection.hub.start().done(function () {
$("#btnSend").click(function () {
var msg = $("#message").val();
$.get('http://192.168.31.112:8009/api/msg/getmsg/' + clientName + '/' + msg + '', {
}, function (res) {
// alert("suc" + msg);
$("#message").val("");
$("#message").focus();
}, 'text');
});
});
}
function GoHome() {
clientName = $("#nickname").val();
if (clientName.length == 0 || clientName == "请输入昵称") {
alert("随便起个名字吧,我们不会记录");
return;
}
document.getElementById("liaotian").style.display = "none";
initTalk();
}
function randomString() {
var $chars = 'ycz123'; /****默认去掉了容易混淆的字符oOLl,9gq,Vv,Uu,I1****/
var maxPos = $chars.length;
var pwd = '';
for (i = 0; i < 3; i++) {
pwd += $chars.charAt(Math.floor(Math.random() * maxPos));
}
return pwd;
}
function quxiao() {
window.open(location, '_self').close();
}
</script>
</head>
<body>
<div class="mypop" id="liaotian" style="display: block">
<div class="graybox"></div>
<div class="popbox">
<div class="poptop">
<h3>匿名聊天室</h3>
<a href="#" class="close"></a>
</div>
<div class="popcon">
<div>
<div class="group">
<input type="text" id="nickname" value="请输入昵称" onfocus="if (value == '请输入昵称') { value = '' }" onblur=" if (value == '') { value = '请输入昵称' }">
<i class="fa fa-user"></i>
</div>
</div>
</div>
<div class="divbtn clearfix">
<a class="btn" onclick="quxiao()">取消</a>
<a href="#" class="btn" onclick="GoHome()">进入聊天室</a>
</div>
</div>
</div>
<div id="msg" style="margin:10px;">
<h3>欢迎<font color='blue' style="font-family: Glyphicons Halflings" size="3" id="nick"></font>进入聊天室</h3>
<div id="msgcontent">
</div>
<div>
</div>
<div style="text-align: left; width: 98%; margin: 10px auto 0;zoom:1;">
<textarea id="message" rows=5 style="width:90%;border:1px solid #271818;height:98px;float:left;"></textarea>
<input class="button" type="button" value="发送消息" id="btnSend" />
</div>
</div>
</body>
</html>
最终效果
聊天界面
演示地址:http://47.96.86.35/TalkTest/Views/GetMessage.html