介绍

最近在做一个基于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包。现在你可以自定义你自己的中间件了。

  1. /// <summary>
  2. /// websocket 协议扩展中间件
  3. /// </summary>
  4. public class CustomWebSocketMiddlewarr
  5. {
  6. private readonly RequestDelegate _next;
  7.  
  8. public CustomWebSocketMiddlewarr(RequestDelegate next)
  9. {
  10. _next = next;
  11. }
  12.  
  13. public async Task Invoke(HttpContext context, ICustomWebSocketFactory wsFactory, ICustomWebSocketMessageHandler wsmHandler)
  14. {
  15. if (context.WebSockets.IsWebSocketRequest)
  16. {
  17. string ConId = context.Request.Query["sign"];
  18. if (!string.IsNullOrEmpty(ConId))
  19. {
  20. WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
  21. CustomWebSocket userWebSocket = new CustomWebSocket()
  22. {
  23. WebSocket = webSocket,
  24. ConId = ConId
  25. };
  26. wsFactory.Add(userWebSocket);
  27. //await wsmHandler.SendInitialMessages(userWebSocket);
  28. await Listen(context, userWebSocket, wsFactory, wsmHandler);
  29.  
  30. }
  31. }
  32. else
  33. {
  34. context.Response.StatusCode = ;
  35. }
  36.  
  37. await _next(context);
  38. }
  39.      //监听客户端发送过来的消息
  40. private async Task Listen(HttpContext context, CustomWebSocket userWebSocket, ICustomWebSocketFactory wsFactory, ICustomWebSocketMessageHandler wsmHandler)
  41. {
  42. WebSocket webSocket = userWebSocket.WebSocket;
  43. var buffer = new byte[ * ];
  44. WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
  45. while (!result.CloseStatus.HasValue)
  46. {
  47. await wsmHandler.HandleMessage(result, buffer, userWebSocket, wsFactory);
  48. buffer = new byte[ * ];
  49. result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
  50. }
  51. wsFactory.Remove(userWebSocket.ConId);
  52. await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
  53. }
  54. }

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

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

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

中的  wsFactory.Add(userWebSocket)。

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

  1. public class CustomWebSocket
  2. {
  3.  
  4. public string ConId { get; set; }
  5.  
  6. public WebSocket WebSocket { get; set; }
  7. }

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

  1. //接口
    public interface ICustomWebSocketFactory
  2. {
  3. void Add(CustomWebSocket uws);
  4. void Remove(string conId);
  5. List<CustomWebSocket> All();
  6. List<CustomWebSocket> Others(CustomWebSocket client);
  7. CustomWebSocket Client(string conId);
  8. }
      

具体实现

  1. public class CustomWebSocketFactory: ICustomWebSocketFactory
  2. {
  3. List<CustomWebSocket> List;
  4. public CustomWebSocketFactory()
  5. {
  6. List = new List<CustomWebSocket>();
  7. }
  8. public void Add(CustomWebSocket uws)
  9. {
  10. List.Add(uws);
  11. }
  12. public void Remove(string conId)
  13. {
  14. List.Remove(Client(conId));
  15.  
  16. }
  17. public List<CustomWebSocket> All()
  18. {
  19. return List;
  20. }
  21.  
  22. public List<CustomWebSocket> Others(CustomWebSocket client)
  23. {
  24. return List.Where(c => c.ConId != client.ConId).ToList();
  25. }
  26. public CustomWebSocket Client(string conId)
  27. {
  28. var uws= List.FirstOrDefault(c => c.ConId == conId);
  29. return uws;
  30.  
  31. }
  32. }

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

services.AddSingleton<ICustomWebSocketFactory, CustomWebSocketFactory>();

  1. CustomWebSocketMessageHandle包含有关消息处理的逻辑(发送,接收)
  1. public interface ICustomWebSocketMessageHandler
  2. {
  3. Task SendInitialMessages(CustomWebSocket userWebSocket);
  4. Task HandleMessage(WebSocketReceiveResult result, byte[] buffer, CustomWebSocket userWebSocket, ICustomWebSocketFactory wsFactory);
  5. Task SendMessageInfo(string conId, object data, ICustomWebSocketFactory wsFactory);
  6.  
  7. }
  8.  
  9. public class CustomWebSocketMessageHandler:ICustomWebSocketMessageHandler
  10. {
  11. public async Task SendInitialMessages(CustomWebSocket userWebSocket)
  12. {
  13. WebSocket webSocket = userWebSocket.WebSocket;
  14. var msg = new CustomWebSocketMessage
  15. {
  16. MessagDateTime = DateTime.Now,
  17. Type = WSMessageType.连接响应
  18. };
  19.  
  20. string serialisedMessage = JsonConvert.SerializeObject(msg);
  21. byte[] bytes = Encoding.ASCII.GetBytes(serialisedMessage);
  22. await webSocket.SendAsync(new ArraySegment<byte>(bytes, , bytes.Length), WebSocketMessageType.Text, true, CancellationToken.None);
  23. }
  24. /// <summary>
  25. /// 推送消息到客户端
  26. /// </summary>
  27. /// <returns></returns>
  28. public async Task SendMessageInfo(string conId,object data, ICustomWebSocketFactory wsFactory)
  29. {
  30. var uws = wsFactory.Client(conId);
  31. CustomWebSocketMessage message = new CustomWebSocketMessage();
  32. message.DataInfo = data;
  33. message.Type = WSMessageType.任务数量;
  34. message.MessagDateTime = DateTime.Now;
  35. if (uws == null)
  36. {
  37. //广播到其他集群节点
  38. var listpush = new List<PushMsg>();
  39.  
  40. var push = new PushMsg()
  41. {
  42. sendjsonMsg = new WebSocketFanoutDto()
  43. {
  44. conId = conId,
  45. data = message
  46. },
  47. exchangeName = "saas.reltimewsmes.exchange",
  48. sendEnum = SendEnum.订阅模式
  49. };
  50. listpush.Add(push);
  51. BTRabbitMQManage.PushMessageAsync(listpush);
  52. return;
  53. }
  54.  
  55. var mesbuffer = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message));
  56. var mescount = Encoding.UTF8.GetByteCount(JsonConvert.SerializeObject(message));
  57. await uws.WebSocket.SendAsync(new ArraySegment<byte>(mesbuffer, , mescount), WebSocketMessageType.Text, true, CancellationToken.None);
  58. }
  59.  
  60. /// <summary>
  61. /// 处理接收到的客户端信息
  62. /// </summary>
  63. /// <param name="result"></param>
  64. /// <param name="buffer"></param>
  65. /// <param name="userWebSocket"></param>
  66. /// <param name="wsFactory"></param>
  67. /// <returns></returns>
  68. public async Task HandleMessage(WebSocketReceiveResult result, byte[] buffer, CustomWebSocket userWebSocket, ICustomWebSocketFactory wsFactory)
  69. {
  70. string msg = Encoding.UTF8.GetString(buffer);
  71. try
  72. {
  73. var message = JsonConvert.DeserializeObject<CustomWebSocketMessage>(msg);
  74. if (message.Type == WSMessageType.用户信息)
  75. {
  76. var logdto = JsonConvert.DeserializeObject<LoginInfoDto>(message.DataInfo.ToJsonString());
  77. await InitUserInfo(logdto, userWebSocket, wsFactory);
  78. }
  79.  
  80. }
  81. catch (Exception e)
  82. {
  83. var exbuffer = Encoding.UTF8.GetBytes(e.Message);
  84. var excount = Encoding.UTF8.GetByteCount(e.Message);
  85. await userWebSocket.WebSocket.SendAsync(new ArraySegment<byte>(exbuffer, , excount), result.MessageType, result.EndOfMessage, CancellationToken.None);
  86. }
  87. }
  88. /// <summary>
  89. /// 初始化用户连接关系
  90. /// </summary>
  91. /// <param name="dto"></param>
  92. /// <param name="userWebSocket"></param>
  93. /// <param name="wsFactory"></param>
  94. /// <returns></returns>
  95. private async Task InitUserInfo(LoginInfoDto dto, CustomWebSocket userWebSocket, ICustomWebSocketFactory wsFactory)
  96. {
  97. if (dto.userId == )
  98. return;
  99. var contectid = userWebSocket.ConId;
  100. var key = "";
  101. if (dto.tenantId.HasValue)
  102. key += "T_" + dto.userId + "_" + dto.tenantId + "_" + "tenant_";
  103. if (dto.bankId.HasValue)
  104. key += "B_" + dto.userId + "_" + dto.bankId + "_" + "bank_";
  105. key += dto.fromeType;
  106. //添加缓存
  107. CacheInstace<string>.GetRedisInstanceDefaultMemery().AddOrUpdate(key, contectid, r =>
  108. {
  109. r = contectid;
  110. return r;
  111. });
  112. CacheInstace<string>.GetRedisInstanceDefaultMemery().Expire(key, new TimeSpan(, , ));
  113.  
  114. }
  115.  
  116. }
  1. 在这里面,推送消息到客户端的时候,如果未找到标识对应的Websocket对象,则将消息广播到所有的集群节点上。我们知道Signalr里面的集群实现通过redis来做的,但在此处,因为
    我项目里面已经搭建了Rabbitmq的高可用集群,所以我直接通过Rabbitmq来进行广播。这样不管我是在集群的那个节点上来推送消息,都可以保证消息被正确推送到客户端。
    关于广播消息的订阅实现:
  1. public class WebSocketFanoutDto
  2. {
  3. public string conId { get; set; }
  4.  
  5. public CustomWebSocketMessage data { get; set; }
  6. }
  7.  
  8. public class FanoutMesConsume : IMessageConsume
  9. {
  10. public void Consume(string message)
  11. {
  12. var condto = JsonConvert.DeserializeObject<WebSocketFanoutDto>(message);
  13. var wsFactory = IOCManage.ServiceProvider.GetService<ICustomWebSocketFactory>();
  14. var uws = wsFactory.Client(condto.conId);
  15. if (uws != null)
  16. {
  17. //发送消息
  18. var mesbuffer = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(condto.data));
  19. var mescount = Encoding.UTF8.GetByteCount(JsonConvert.SerializeObject(condto.data));
  20. uws.WebSocket.SendAsync(new ArraySegment<byte>(mesbuffer, , mescount), WebSocketMessageType.Text, true, CancellationToken.None);
  21. }
  22. }
  23. }
  1.  

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

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

  1. services.AddSingleton<ICustomWebSocketMessageHandler, CustomWebSocketMessageHandler>();
  1.  
  1. public static IApplicationBuilder UseCustomWebSocketManager(this IApplicationBuilder app)
  2. {
  3. //添加针对分布式集群的消息监视
  4. RabbitMQManage.Subscribe<FanoutMesConsume>(new MesArgs()
  5. {
  6. exchangeName = "reltimewsmes.exchange",
  7. sendEnum = SendEnum.订阅模式
  8. });
  9. return app.UseMiddleware<CustomWebSocketMiddlewarr>();
  10. }

至此这个框架搭建完成,最后在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. 新手也能看懂的 SpringBoot 异步编程指南

    本文已经收录自 springboot-guide : https://github.com/Snailclimb/springboot-guide (Spring Boot 核心知识点整理. 基于 S ...

  2. LeetCode初级算法--树02:验证二叉搜索树

    LeetCode初级算法--树02:验证二叉搜索树 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:https://blog.csdn.ne ...

  3. 百万年薪python之路 -- 异常处理

    异常处理 1.错误的分类: 1.语法错误:(这种错误,根本过不了python解释器的语法检测,必须在程序执行前就改正) #语法错误示范一 if #语法错误示范二 def test: pass #语法错 ...

  4. 解决html连续字符或数字换行的问题

    word-break: break-all; word-wrap:break-word; 强制换行

  5. Solr分片机制以及Solrcloud搭建及分片操作

    Solr分片描述 分片是集合的逻辑分区,包含集合中文档的子集,这样集合中的每个文档都正好包含在一个分片中.集合中包含每个文档的分片取决于集合的整体"分片"策略. 当您的集合对于一个 ...

  6. TCP/IP和Socket开发经验分享

    当前与网络相关的业务主要是基于tcp/ip或http,熟悉j2ee的同学一定会对http场景下的开发比较了解.但是,精通tcp/ip以及如何构建一个直接基于tcp/ip层通讯的知识却不太多见.恰巧,最 ...

  7. Alpha阶段--第六周Scrum Meeting

    任务内容 本次会议为第六周的Scrum Meeting会议 召开时间为周四上午10点,在信南B317召开,召开时间约为30分钟,进行的项目规划和分工 队员 任务 张孟宇 进行用户登录界面的代码编写 吴 ...

  8. Vim任意代码执行漏洞(CVE-2019-12735)

    Vim通过Modelines执行任意代码 漏洞概要: 在8.1.1365之前的Vim和在0.3.6之前的Neovim很容易通过打开特制的文本文件而通过模型执行任意代码. 复现条件: 确保未禁用mode ...

  9. 【原创】(十)Linux内存管理 - zoned page frame allocator - 5

    背景 Read the fucking source code! --By 鲁迅 A picture is worth a thousand words. --By 高尔基 说明: Kernel版本: ...

  10. iSCSI 共享存储

         iSCSI(Internet Small Computer System Interface,发音为/ˈаɪskʌzi/),Internet小型计算机系统接口,又称为IP-SAN,是一种基于 ...