项目中需要向网页上主动推送信息并弹层提示用户,目前有两种实现方法
- ajax轮询
- http 1.1 长连接
- websocket主动推送
网页主要用在手机端。
- 第一种不断发请求浪费流量且不稳定
- 第二种对服务器资源占用较大,针对每一次请求都要异步阻塞维持
- 第三种websocket,客户端与服务器只需第一次通过带有websocket标识的Http请求建立连接,不需要每次解析http请求及用户信息,连接维持交由web服务器,handler也无需每次解析请求状态。
三者的优缺点及原理讲解主要是看的这两篇文章"看完让你彻底搞懂Websocket原理",WebSocket原理及技术简介
最终选择了websocket,以下是asp.net core 测试实现方式。
- .net core 提供了websocketserver的组件,需要在nuget上引用Microsoft.AspNetCore.WebSockets.Server
在project.json 文件中添加引用,然后点击restore引用,如图
- 在项目中添加一个websocktHandler的处理类,处理接收到请求后的逻辑
- 在startup.cs 中注册 websocket 服务,并将处理回调注册到流程中,当请求为
"/ws"
走websockt处理handler websocket 制定了标识位来标识当前请求,根据该标识处理不同的逻辑
opcode的值: 0x1代表此帧为文本数据帧, 0x2代表此帧为二进制数据帧, 0x8为控制帧中的连接关闭帧(close frame), 0x9为控制帧中的Ping帧, 0xA(十进制的10)为控制帧中的Pong帧。
- 接收请求后将当前请求的socket以键值对方式存入内存中,以便给指定用户发送信息
- 根据接收到的数据内容,按分隔符分割处理,获取到【发送人】【数据】【接收人】
分隔符在配置文件中配置
.net core 添加自定义配置,需要引用
Microsoft.Extensions.Options.ConfigurationExtensions
,引用方式同上。
8.startup.cs 中注册,新建AppSettings类,并映射
services.AddOptions();
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
以上便是websocket服务端的处理流程,最终效果如下
下面是代码
WebSocketHandler.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Options;
namespace WebApplication
{
public class SocketHandler
{
static Dictionary<string, WebSocket> userDic = new Dictionary<string, WebSocket>();
SocketHandler(WebSocket socket)
{
this.socket = socket;
this.BufferSize = AppSettingsServices.AppSettings.WebSocketConfig.MaxLenth;
}
int BufferSize;
WebSocket socket;
async Task EchoLoop()
{
var buffer = new byte[BufferSize];
var seg = new ArraySegment<byte>(buffer);
while (this.socket.State == WebSocketState.Open)
{
var incoming = await this.socket.ReceiveAsync(seg, CancellationToken.None);
if (incoming.MessageType == WebSocketMessageType.Close)
{
if(userDic.Values.Contains(this.socket))
{
userDic.Remove(userDic.FirstOrDefault(m=>m.Value==this.socket).Key);
}
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure,
String.Empty, CancellationToken.None);
}
else
{
// string receviceString = System.Text.Encoding.UTF8.GetString(buffer);
string receviceString = System.Text.Encoding.UTF8.GetString(buffer.Where(b => b != 0).ToArray());
if (string.IsNullOrEmpty(receviceString)) { return; }
string separator = AppSettingsServices.AppSettings.WebSocketConfig.Separator;
var msg = receviceString.Split(new[] { separator }, StringSplitOptions.None);
string toname = string.Empty;
string data = string.Empty;
string fromname = string.Empty;
if (msg.Length > 2)
{
toname = msg[0];
data = msg[1];
fromname = msg[2];
}
string tomsg = data;
List<WebSocket> toSocketList = new List<WebSocket>();
if (!string.IsNullOrEmpty(toname))
{
if (!userDic.ContainsKey(fromname))
{
userDic.Add(fromname, this.socket);
}
if (userDic.ContainsKey(toname))
{
toSocketList.Add(userDic[toname]);
}
else
{
toSocketList.Add(userDic[fromname]);
tomsg = "用户不在线";
}
}
else
{
toSocketList.AddRange(userDic.Values.ToList());
}
byte[] toBuffer = System.Text.Encoding.UTF8.GetBytes(tomsg);
var outgoing = new ArraySegment<byte>(toBuffer);
foreach(WebSocket s in toSocketList){
await s.SendAsync(outgoing, WebSocketMessageType.Text, true, CancellationToken.None);
}
// await this.socket.SendAsync(outgoing, WebSocketMessageType.Text, true, CancellationToken.None);
}
}
}
static async Task Acceptor(HttpContext hc, Func<Task> n)
{
if (!hc.WebSockets.IsWebSocketRequest)
return;
var socket = await hc.WebSockets.AcceptWebSocketAsync();
var h = new SocketHandler(socket);
await h.EchoLoop();
}
/// <summary>
/// branches the request pipeline for this SocketHandler usage
/// </summary>
/// <param name="app"></param>
public static void Map(IApplicationBuilder app)
{
app.UseWebSockets();
app.Use(SocketHandler.Acceptor);
}
}
}
AppSettings.cs
namespace WebApplication
{
public class AppSettings
{
public WebSocketConfig WebSocketConfig { get; set; }
}
public class WebSocketConfig
{
//单次发送最大值
public int MaxLenth { get; set; }
//to和data的分割符号
public string Separator { get; set; }
}
}
AppSettingsServices.cs
using Microsoft.Extensions.Options;
namespace WebApplication
{
public class AppSettingsServices
{
public static AppSettings AppSettings;
public static void SetAppSettings(IOptions<AppSettings> v)
{
AppSettings= v.Value;
}
}
}
Startup.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using WebApplication;
using WebApplication.Data;
using WebApplication.Models;
using WebApplication.Services;
namespace WebApplication
{
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);
if (env.IsDevelopment())
{
// For more details on using the user secret store see https://go.microsoft.com/fwlink/?LinkID=532709
builder.AddUserSecrets();
}
builder.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddMvc();
// Add application services.
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
services.AddOptions();
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IOptions<AppSettings> appsettings)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseIdentity();
// Add external authentication middleware below. To configure them please see https://go.microsoft.com/fwlink/?LinkID=532715
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
AppSettingsServices.SetAppSettings(appsettings);
app.Map("/ws", SocketHandler.Map);
}
// public async void a (IApplicationBuilder b){
// b.UseWebSockets();
// b.Use(new SocketHandler.Acceptor);
// }
}
}
客户端JS代码
//w:向谁发,d 发送内容,f 谁发的
function sendData(w, d, f) {
initWebSocket();
if (webSocket.OPEN && webSocket.readyState == 1) {
var s = "<custom_separator>";
webSocket.send(w + s + d + s + f);
}
if (webSocket.readyState == 2 || webSocket.readyState == 3) {
$("#div_receive").append("WebSocket closed");
}
}
function initWebSocket() {
var url = "ws://localhost:5000/ws";
if (!webSocket) {
webSocket = new WebSocket(url);
//Open connection handler.
webSocket.onopen = function () {
$("#div_receive").append("WebSocket opened" + "<br>");
};
//Message data handler.
webSocket.onmessage = function (e) {
$("#div_receive").append(e.data + "<br>");
};
//Close event handler.
webSocket.onclose = function () {
$("#div_receive").append("WebSocket closed." + "<br>");
};
//Error event handler.
webSocket.onerror = function (e) {
$("#div_receive").append(e.message + "<br>");
}
}
}