介绍

最近在做一个基于netcore的实时消息服务。最初选用的是ASP.NET Core SignalR,但是后来发现目前它并没有支持IOS的客户端,所以自己只好又基于websocket重新搭建了一套服务。

因为前期已经使用了SignalR,所以我直接在原本的项目里面重新扩展了一套自定义websocket服务。

在网上有一篇博文介绍了如何在Asp.net Core中使用中间件来管理websocket,我的大部分代码也是参考这篇文章。在这儿贴个链接

在Asp.net Core中使用中间件来管理websocket

自定义WebSocket 中间件

要阅读ASP.NET Core中的WebSockets支持,可以在此处查看。如果你的项目跟我一样,已经使用了Signalr,那么你不需要在安装Microsoft.AspNetCore.WebSockets包,否则在项目开始前,

需要安装此Nuget包。现在你可以自定义你自己的中间件了。

/// <summary>
/// websocket 协议扩展中间件
/// </summary>
public class CustomWebSocketMiddlewarr
{
private readonly RequestDelegate _next; public CustomWebSocketMiddlewarr(RequestDelegate next)
{
_next = next;
} public async Task Invoke(HttpContext context, ICustomWebSocketFactory wsFactory, ICustomWebSocketMessageHandler wsmHandler)
{
if (context.WebSockets.IsWebSocketRequest)
{
string ConId = context.Request.Query["sign"];
if (!string.IsNullOrEmpty(ConId))
{
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
CustomWebSocket userWebSocket = new CustomWebSocket()
{
WebSocket = webSocket,
ConId = ConId
};
wsFactory.Add(userWebSocket);
//await wsmHandler.SendInitialMessages(userWebSocket);
await Listen(context, userWebSocket, wsFactory, wsmHandler); }
}
else
{
context.Response.StatusCode = ;
} await _next(context);
}
     //监听客户端发送过来的消息
private async Task Listen(HttpContext context, CustomWebSocket userWebSocket, ICustomWebSocketFactory wsFactory, ICustomWebSocketMessageHandler wsmHandler)
{
WebSocket webSocket = userWebSocket.WebSocket;
var buffer = new byte[ * ];
WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
while (!result.CloseStatus.HasValue)
{
await wsmHandler.HandleMessage(result, buffer, userWebSocket, wsFactory);
buffer = new byte[ * ];
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
}
wsFactory.Remove(userWebSocket.ConId);
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
}
}

在自定义的中间件中,首先判断是否是websocket请求,如果是的话,在查看是否有对应的sign标识,满足条件后进入后续的处理环节。

简单讲解一下这里面的处理逻辑。因为我的项目中同时存在Signalr,而Signalr也会使用到websocket协议。但是Signalr的websocket请求传入的参数是id,所以我在这儿自定义了一个参数sign为了和Signalr

做区分。那么这个sign是做什么用的呢? 其实sign是前端传过来的唯一标识,和此次连接对应,也可以理解为Signalr里面的connectionId。然后会把标识和对应websocket类到存入到一个list集合中。即代码

中的  wsFactory.Add(userWebSocket)。

CustomWebSocket是一个包含WebSocket和标识的类:

public  class CustomWebSocket
{ public string ConId { get; set; } public WebSocket WebSocket { get; set; }
}

然后定义了一个Websocket工厂类,用来存取连接到服务的Websocket实例。

//接口
public interface ICustomWebSocketFactory
{
void Add(CustomWebSocket uws);
void Remove(string conId);
List<CustomWebSocket> All();
List<CustomWebSocket> Others(CustomWebSocket client);
CustomWebSocket Client(string conId);
}
  

具体实现

public class CustomWebSocketFactory: ICustomWebSocketFactory
{
List<CustomWebSocket> List;
public CustomWebSocketFactory()
{
List = new List<CustomWebSocket>();
}
public void Add(CustomWebSocket uws)
{
List.Add(uws);
}
public void Remove(string conId)
{
List.Remove(Client(conId)); }
public List<CustomWebSocket> All()
{
return List;
} public List<CustomWebSocket> Others(CustomWebSocket client)
{
return List.Where(c => c.ConId != client.ConId).ToList();
}
public CustomWebSocket Client(string conId)
{
var uws= List.FirstOrDefault(c => c.ConId == conId);
return uws; }
}

可以看到最终我们存取websocket都是通过list来进行,所以在注入的时候一定要注意。注入成单例模式。

services.AddSingleton<ICustomWebSocketFactory, CustomWebSocketFactory>();

CustomWebSocketMessageHandle包含有关消息处理的逻辑(发送,接收)
public interface ICustomWebSocketMessageHandler
{
Task SendInitialMessages(CustomWebSocket userWebSocket);
Task HandleMessage(WebSocketReceiveResult result, byte[] buffer, CustomWebSocket userWebSocket, ICustomWebSocketFactory wsFactory);
Task SendMessageInfo(string conId, object data, ICustomWebSocketFactory wsFactory); } public class CustomWebSocketMessageHandler:ICustomWebSocketMessageHandler
{
public async Task SendInitialMessages(CustomWebSocket userWebSocket)
{
WebSocket webSocket = userWebSocket.WebSocket;
var msg = new CustomWebSocketMessage
{
MessagDateTime = DateTime.Now,
Type = WSMessageType.连接响应
}; string serialisedMessage = JsonConvert.SerializeObject(msg);
byte[] bytes = Encoding.ASCII.GetBytes(serialisedMessage);
await webSocket.SendAsync(new ArraySegment<byte>(bytes, , bytes.Length), WebSocketMessageType.Text, true, CancellationToken.None);
}
/// <summary>
/// 推送消息到客户端
/// </summary>
/// <returns></returns>
public async Task SendMessageInfo(string conId,object data, ICustomWebSocketFactory wsFactory)
{
var uws = wsFactory.Client(conId);
CustomWebSocketMessage message = new CustomWebSocketMessage();
message.DataInfo = data;
message.Type = WSMessageType.任务数量;
message.MessagDateTime = DateTime.Now;
if (uws == null)
{
//广播到其他集群节点
var listpush = new List<PushMsg>(); var push = new PushMsg()
{
sendjsonMsg = new WebSocketFanoutDto()
{
conId = conId,
data = message
},
exchangeName = "saas.reltimewsmes.exchange",
sendEnum = SendEnum.订阅模式
};
listpush.Add(push);
BTRabbitMQManage.PushMessageAsync(listpush);
return;
} var mesbuffer = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message));
var mescount = Encoding.UTF8.GetByteCount(JsonConvert.SerializeObject(message));
await uws.WebSocket.SendAsync(new ArraySegment<byte>(mesbuffer, , mescount), WebSocketMessageType.Text, true, CancellationToken.None);
} /// <summary>
/// 处理接收到的客户端信息
/// </summary>
/// <param name="result"></param>
/// <param name="buffer"></param>
/// <param name="userWebSocket"></param>
/// <param name="wsFactory"></param>
/// <returns></returns>
public async Task HandleMessage(WebSocketReceiveResult result, byte[] buffer, CustomWebSocket userWebSocket, ICustomWebSocketFactory wsFactory)
{
string msg = Encoding.UTF8.GetString(buffer);
try
{
var message = JsonConvert.DeserializeObject<CustomWebSocketMessage>(msg);
if (message.Type == WSMessageType.用户信息)
{
var logdto = JsonConvert.DeserializeObject<LoginInfoDto>(message.DataInfo.ToJsonString());
await InitUserInfo(logdto, userWebSocket, wsFactory);
} }
catch (Exception e)
{
var exbuffer = Encoding.UTF8.GetBytes(e.Message);
var excount = Encoding.UTF8.GetByteCount(e.Message);
await userWebSocket.WebSocket.SendAsync(new ArraySegment<byte>(exbuffer, , excount), result.MessageType, result.EndOfMessage, CancellationToken.None);
}
}
/// <summary>
/// 初始化用户连接关系
/// </summary>
/// <param name="dto"></param>
/// <param name="userWebSocket"></param>
/// <param name="wsFactory"></param>
/// <returns></returns>
private async Task InitUserInfo(LoginInfoDto dto, CustomWebSocket userWebSocket, ICustomWebSocketFactory wsFactory)
{
if (dto.userId == )
return;
var contectid = userWebSocket.ConId;
var key = "";
if (dto.tenantId.HasValue)
key += "T_" + dto.userId + "_" + dto.tenantId + "_" + "tenant_";
if (dto.bankId.HasValue)
key += "B_" + dto.userId + "_" + dto.bankId + "_" + "bank_";
key += dto.fromeType;
//添加缓存
CacheInstace<string>.GetRedisInstanceDefaultMemery().AddOrUpdate(key, contectid, r =>
{
r = contectid;
return r;
});
CacheInstace<string>.GetRedisInstanceDefaultMemery().Expire(key, new TimeSpan(, , )); } }
在这里面,推送消息到客户端的时候,如果未找到标识对应的Websocket对象,则将消息广播到所有的集群节点上。我们知道Signalr里面的集群实现通过redis来做的,但在此处,因为
我项目里面已经搭建了Rabbitmq的高可用集群,所以我直接通过Rabbitmq来进行广播。这样不管我是在集群的那个节点上来推送消息,都可以保证消息被正确推送到客户端。
关于广播消息的订阅实现:
 public class WebSocketFanoutDto
{
public string conId { get; set; } public CustomWebSocketMessage data { get; set; }
} public class FanoutMesConsume : IMessageConsume
{
public void Consume(string message)
{
var condto = JsonConvert.DeserializeObject<WebSocketFanoutDto>(message);
var wsFactory = IOCManage.ServiceProvider.GetService<ICustomWebSocketFactory>();
var uws = wsFactory.Client(condto.conId);
if (uws != null)
{
//发送消息
var mesbuffer = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(condto.data));
var mescount = Encoding.UTF8.GetByteCount(JsonConvert.SerializeObject(condto.data));
uws.WebSocket.SendAsync(new ArraySegment<byte>(mesbuffer, , mescount), WebSocketMessageType.Text, true, CancellationToken.None);
}
}
}

最后在扩展类里面添加消息监视和注入Websocket中间件。

当然不要忘记 消息处理类的依赖注入

services.AddSingleton<ICustomWebSocketMessageHandler, CustomWebSocketMessageHandler>();

 public static IApplicationBuilder UseCustomWebSocketManager(this IApplicationBuilder app)
{
//添加针对分布式集群的消息监视
RabbitMQManage.Subscribe<FanoutMesConsume>(new MesArgs()
{
exchangeName = "reltimewsmes.exchange",
sendEnum = SendEnum.订阅模式
});
return app.UseMiddleware<CustomWebSocketMiddlewarr>();
}

至此这个框架搭建完成,最后在startup类中注入。

关于Rabbitmq的使用,发送和接收是我基于easynetq封装的一个帮助类,大家可以自行实现。

这里面最主要的逻辑就是每一个websocket实例都有一个对应的标识,然后在连接成功后,前端会发送用户信息,后端服务再把用户信息和连接标识关联。这样如果想推送信息到某个用户的话,就可以通过

用户信息来找到用户对应的连接信息。至于为什么整个流程会这么复杂的,就一言难尽(我能怎么办,我也很绝望啊)。大多数时候大家都可以直接通过token认证来绑定用户和socket连接。

目前还有几个问题一个广播消息的时候,发送消息方也会收到这个消息,这挺尴尬,目前我还没想到太好的解决办法。

第二个是采用单例list字段存储连接的websocket实例,少的时候还好,如果多的话,感觉可能会存在堆栈溢出的问题,但没实际测试过,所以目前还不知道最大的连接数多少。

NetCore下搭建websocket集群方案的更多相关文章

  1. Linux下搭建tomcat集群全记录(转)

    本文将讲述如何在Linux下搭建tomcat集群,以及搭建过程中可能的遇到的问题和解决方法.为简单起见,本文演示搭建的集群只有两个tomact节点外加一个apache组成,三者将安装在同一机器上:ap ...

  2. Windows下搭建Redis集群

    Redis集群: 如果部署到多台电脑,就跟普通的集群一样:因为Redis是单线程处理的,多核CPU也只能使用一个核, 所以部署在同一台电脑上,通过运行多个Redis实例组成集群,然后能提高CPU的利用 ...

  3. Redis集群搭建(转自一菲聪天的“Windows下搭建Redis集群”)

    配置Redis参考:http://blog.csdn.net/zsg88/article/details/73715947 使用Ruby配置集群参考:https://www.cnblogs.com/t ...

  4. Windows下 搭建redis集群

    Windows下搭建redis集群教程 一,redis集群介绍 Redis cluster(redis集群)是在版本3.0后才支持的架构,和其他集群一样,都是为了解决单台服务器不够用的情况,也防止了主 ...

  5. windows环境下搭建Redis集群

    转载请注明出处,原文章地址: https://www.cnblogs.com/tommy-huang/p/6240083.html Redis集群: 如果部署到多台电脑,就跟普通的集群一样:因为Red ...

  6. Linux下搭建tomcat集群全记录

    (转) Linux下搭建tomcat集群全记录 2011-10-12 10:23 6133人阅读 评论(1) 收藏 举报 tomcatlinuxapacheinterceptorsession集群 1 ...

  7. Windows下搭建REDIS集群

    Redis集群: 如果部署到多台电脑,就跟普通的集群一样:因为Redis是单线程处理的,多核CPU也只能使用一个核, 所以部署在同一台电脑上,通过运行多个Redis实例组成集群,然后能提高CPU的利用 ...

  8. Linux下搭建Hadoop集群

    本文地址: 1.前言 本文描述的是如何使用3台Hadoop节点搭建一个集群.本文中,使用的是三个Ubuntu虚拟机,并没有使用三台物理机.在使用物理机搭建Hadoop集群的时候,也可以参考本文.首先这 ...

  9. linux系统centOS7下搭建redis集群中ruby版本过低问题的解决方法

    问题描述: 在Centos7中,通过yum安装ruby的版本是2.0.0,但是如果有些应用需要高版本的ruby环境,比如2.2,2.3,2.4... 那就有点麻烦了,譬如:我准备使用redis官方给的 ...

随机推荐

  1. 2019年高级Java程序员面试题汇总

    目录 JDK Dubbo Zookeeper Strut2 Spring系列 Redis系列 Mysql系列 Java多线程 消息中间件 线程池 事物 JVM 设计模式 其他 程序设计 基础知识 编程 ...

  2. 从Go语言编码角度解释实现简易区块链

    区块链技术 人们可以用许多不同的方式解释区块链技术,其中通过加密货币来看区块链一直是主流.大多数人接触区块链技术都是从比特币谈起,但比特币仅仅是众多加密货币的一种. 到底什么是区块链技术? 从金融学相 ...

  3. 图论-最小生成树<Kruskal>

    昨天: 图论-最小生成树<Dijkstra,Floyd> 以上是昨天的Blog,有需要者请先阅读完以上再阅读今天的Blog. 可能今天的有点乱,好好理理,认真看完相信你会懂得 然而,文中提 ...

  4. 七、springBoot 简单优雅是实现文件上传和下载

    前言 好久没有更新spring Boot 这个项目了.最近看了一下docker 的知识,后期打算将spring boot 和docker 结合起来.刚好最近有一个上传文件的工作呢,刚好就想起这个脚手架 ...

  5. Linux对目录操作命令

    cd /home        进入 '/ home' 目录 cd ..        返回上一级目录 cd ../..        返回上两级目录 cd        进入个人的主目录 cd ~u ...

  6. 了解这一行的,腰包都鼓鼓的了,程序辅导,CS作业

    我们都知道计算机这类理科专业,可能你打开电脑聊几分钟微信,可能你就已经错过了成为程序员大佬的机会.就像数学专业的同学弯腰捡了一支笔,然后发现黑板被写满从此再也没有学懂数学.所以课后的作业自然也就没法顺 ...

  7. Linux系统 /etc目录下主要配置文件解释

    这些都是比较有实用性的系统配置,收藏下,以备不时之需!以下是etc下重要配置文件解释: 1./etc/hosts  #文件格式: IPaddress hostname aliases #文件功能: 提 ...

  8. vue 首次加载缓慢/刷新后加载缓慢 原因及解决方案

    # vue 首次加载缓慢/刷新后加载缓慢 原因及解决方案 最近做项目发现一个问题,页面每次刷新后加载速度都非常慢,20s左右,在开发环境则非常流畅,几乎感觉不到,本文参考望山的各种方案优化 1,关闭打 ...

  9. python:枚举类型

    1.什么是枚举类型? 枚举类型可以看做是一系列常量的集合,通常用于表示某些有限且固定的集合,例如月份(一年有12个月).星期(一星期有七天).季节(一年四个季节)等. 2.枚举的定义 定义枚举首先要导 ...

  10. 第三十章 System V信号量(一)

    信号量 信号量和P.V原语由Dijkstra(迪杰斯特拉)提出 信号量: 互斥: P.V在同一进程中 同步: P.V在不同进程中 信号量值含义 S>0 : S表示可用资源个数 S=0 : 表示无 ...