SignalR服务器端消息推送

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 某些场景下,需要服务端向客户端发送请求。.net中采用封装了WebSocet的SignalR进行消息的处理。WebSocket独立于http,但是WebSocket服务器一般都部署在Web服务器上,所以需要借助http完成初始握手,并共享http的端口。

SignalR服务器端消息推送

某些场景下,需要服务端向客户端发送请求。.net中采用封装了WebSocet的SignalR进行消息的处理。WebSocket独立于http,但是WebSocket服务器一般都部署在Web服务器上,所以需要借助http完成初始握手,并共享http的端口。

SignalR基本使用

SignalR中一个重要的组件就是集线器hub,他用于在WebSocket服务器端和所有客户端之间进行数据交换,所有连接到同一个集线器上的程序都可以互相通信。

  1. 创建一个继承自Hub的类(Microsoft.AspNetCore.SignalR命名空间)的类,所有客户端和服务器都通过这个集线器进行通信。

publicclassChatRoomHub:Hub

{

   publicTaskSendPublicMessage(stringmessage)

   {

        stringconnId=this.Context.ConnectionId;//获得发送消息端的连接ID

        stringmsg=$"{connId} {DateTime.Now}:{message}";

       //发送到连接到集线器的所有客户端上

        returnClients.All.SendAsync("ReceivePublicMessage", msg);

   }

}

  1. 编辑Program.cs,在builder.Build之前调用

builder.Services.AddSignalR();

//如果是前后端分离项目,WebSocket初始化握手需要通过http,所以启用跨域

string[] urls=new[] { "http://localhost:3000" };

builder.Services.AddCors(options=>

   options.AddDefaultPolicy(builder=>builder.WithOrigins(urls)

       .AllowAnyMethod().AllowAnyHeader().AllowCredentials())

);

varapp=builder.Build();

app.UseCors();

//在MapControllers之前调用,启用中间件

//当客户端通过SignalRq请求/Hubs/ChatRoomHub时,由ChatRoomHub处理

app.MapHub<ChatRoomHub>("/Hubs/ChatRoomHub");

app.MapControllers();

  1. 前端vue组件

<template>

 <div>

   <inputtype="text"v-model="state.userMessage"v-on:keypress="txtMsgOnkeypress"/>

   <div><ul><liv-for="(msg,index) in state.messages" :key="index">{{msg}}</li> </ul>

   </div>

 </div>

</template>

<script>

import*assignalRfrom'@microsoft/signalr'

exportdefault {

 data() {

   return {

     name: "Login",

     state: {

       userMessage: "",

       messages: [],

     },

     connection: "",

   };

 },

 mounted() {

   this.connectInit();

 },

 methods: {

   asynctxtMsgOnkeypress(e) {

     if (e.keyCode!=13) return;

       //invoke调用集线器的方法,后面的方法名为集线器中定义的方法名

     awaitthis.connection.invoke("SendPublicMessage", this.state.userMessage);

     this.state.userMessage="";

   },

   asyncconnectInit() {

       //创建客户端到服务端的连接

     this.connection=newsignalR.HubConnectionBuilder()

       .withUrl("http://localhost:7112/Hubs/ChatRoomHub")//服务端的地址

       .withAutomaticReconnect()//断开后重新连接,但是ConnectionId会改变

       .build();//构建完成一个客户端到集线器的连接

     awaitthis.connection.start();//启动连接

       //用on来检测服务器使用SendAsync方法发送的消息,注意名称要相同

     this.connection.on("ReceivePublicMessage", (msg) => {

       this.state.messages.push(msg);

     });

   },

 },

};

</script>

 

<stylelang="less"scoped>

</style>

 

SignalR分布部署

假设聊天室被部署到两台服务器上,客户端1、2在A服务器,客户端3、4在B服务器上,此时,1只能和2通信,3只能和4通信。微软提供了Redis服务器来解决这个问题。

  1. Nugt安装Microsoft.AspNetCore.SignalR.StackExchangeRedis
  2. 在Program.cs中的builder.Services.AddSignalR()后面加上

//第一个参数为redis服务器连接字符串

builder.Services.AddSignalR().AddStackExchangeRedis("127.0.0.1", options=>

{

   options.Configuration.ChannelPrefix="Test1_";

});

SignalR身份验证

要求只有通过验证的用户才能连接集线器。

使用步骤如下(身份验证部分可参考(8.2 JWT(代替Session)):

  1. 在配置系统中配置一个名字为JWT的节点,配置相应的节点,并且创建一个JWTOption类。
  2. NuGet安装Microsoft.AspNetCore.Authentication.JwtBearer
  3. 对JWT进行配置在builder.Build之前添加

services.Configure<JWTOptions>(builder.Configuration.GetSection("JWT"));//实体配置类

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)//配置授权的各种属性

.AddJwtBearer(x=>//配置JWT的承载

{

   //配置JWT绑定到JWTOptions新的实例,返回一个JWTOptions实例

    JWTOptions?jwtOpt=builder.Configuration.GetSection("JWT").Get<JWTOptions>();

    byte[] keyBytes=Encoding.UTF8.GetBytes(jwtOpt.SigningKey);

    varsecKey=newSymmetricSecurityKey(keyBytes);

    x.TokenValidationParameters=new()//设置令牌验证参数

    {

        ValidateIssuer=false,

        ValidateAudience=false,

        ValidateLifetime=true,

        ValidateIssuerSigningKey=true,

        IssuerSigningKey=secKey

    };

    x.Events=newJwtBearerEvents

       {

           OnMessageReceived=context=>

           {

               //JWT默认放到了Authorization请求头中,但是WebSocket不支持请求头,

               //所以将JWT放到了URL中,然后在服务器中检测URL中的JWT

               varaccessToken=context.Request.Query["access_token"];

               varpath=context.HttpContext.Request.Path;

               if (!string.IsNullOrEmpty(accessToken) &&

                   (path.StartsWithSegments("/Hubs/ChatRoomHub")))

               {

                   //如果请求URL中有JWT并且请求路径为集线器

                   //就把JWT复制给Token,这样就可以直接解析和使用JWT了

                   context.Token=accessToken;

               }

               returnTask.CompletedTask;

           }

       };

});

 

  1. 在Program.cs中的app.UseAuthorization()前面加上app.UseAuthentication(),解决跨域和MapHub

 builder.Services.AddSignalR();

 //如果是前后端分离项目,WebSocket初始化握手需要通过http,所以启用跨域

 string[] urls=new[] { "http://localhost:3000" };

 builder.Services.AddCors(options=>

     options.AddDefaultPolicy(builder=>builder.WithOrigins(urls)

         .AllowAnyMethod().AllowAnyHeader().AllowCredentials())

 );

 varapp=builder.Build();

 app.UseCors();

 //在MapControllers之前调用,启用中间件

 //当客户端通过SignalRq请求/Hubs/ChatRoomHub时,由ChatRoomHub处理

 app.MapHub<ChatRoomHub>("/Hubs/ChatRoomHub");

 app.UseAuthentication();

 app.UseAuthorization();

 app.MapControllers();


  1. 在控制类中增加登陆并且创建JWT的操作方法 (参考8.2 JWT(代替Session)
  2. 在集线器类上增加[Authorize]

   [Authorize]

   publicclassChatRoomHub:Hub

   {

       publicTaskSendPublicMessage(stringmessage)

       {

           //可以直接拿到name

           stringname=this.Context.User!.FindFirst(ClaimTypes.Name)!.Value;

           stringmsg=$"{name} {DateTime.Now}:{message}";

           returnClients.All.SendAsync("ReceivePublicMessage", msg);

       }

   }

//[Authorize]可以加到集线器类上,也可以加到类中某个方法上

//如果加到方法上,则任意客户端可以连接到集线器,只是不能调用那个方法,这样不推荐

  1. 前端页面

<template>

 <div>

   <fieldset>

     <legend>登录</legend>

     <div>

       用户名:<input  type="text"  v-model="state.loginData.name"  />

     </div>

     <div>

       密码:<input  type="password"v-model="state.loginData.password">

     </div>

     <div>

       <inputtype="button"value="登录"v-on:click="loginClick"/>

     </div>

   </fieldset>

   公屏:<inputtype="text"  v-model="state.userMessage"v-on:keypress="txtMsgOnkeypress"/>

   <div>  <ul><liv-for="(msg,index) in state.messages"  :key="index"  >{{msg}}</li> </ul>

   </div>

 </div>

</template>

<script>

import*assignalRfrom"@microsoft/signalr";

importaxiosfrom'axios';

exportdefault {

 data() {

   return {

     connection: '',

     state: {

       accessToken: "",

       userMessage: "",

       messages: [],

       loginData: { name: "", password: "" },

       privateMsg: { destUserName: "", message: "" },

     },

   };

 },

 methods: {

   asyncstartConn() {

       consttransport=signalR.HttpTransportType.WebSockets;

       //skipNegotiation跳过协商

       //transport强制采用的通信方式

       constoptions= { skipNegotiation: true, transport: transport };

       //将JWT传递给服务器端

       options.accessTokenFactory= () =>this.state.accessToken;

     this.connection=newsignalR.

     HubConnectionBuilder()

                   .withUrl('http://localhost:7173/Hubs/ChatRoomHub', options)

                   .withAutomaticReconnect().build();

     try {

       awaitthis.connection.start();

     } catch (err) {

       alert(err);

       return;

     }

     this.connection.on("ReceivePublicMessage", (msg) => {

       this.state.messages.push(msg);

     });

     alert("登陆成功可以聊天了");

   },

   asyncloginClick() {

   

   const {data:resp} =awaitaxios.post('http://localhost:7173/api/Identity/Login',

                   this.state.loginData);

                   console.log(resp);

               this.state.accessToken=resp.data;

               this.startConn();

   },

   asynctxtMsgOnkeypress(e) {

     if (e.keyCode!=13) return;

     try {

       awaitthis.connection.invoke(

         "SendPublicMessage",

         this.state.userMessage

       );

     } catch (err) {

       alert(err);

       return;

     }

     this.state.userMessage="";

   }

   

 },

};

</script>

<stylescoped>

</style>

 

针对部分客户端的消息推送

之前使用了Clients.All.SendAsync向连接到当前集线器的所有客户端进行消息推送,但是某些场景需要针对特定用户进行消息推送。

进行客户端筛选的时候,有3个筛选参数,ConnectionId,组以及用户ID。

参数 说明
ConnectionId 是SignalR为每个客户端分配的Id
组有唯一的名字,对于连接到同一集线器的用户,可以自定义分组
用户ID 对应于Claim.NameIdentifier的Claim值

另外集线器(Hub)有一个Groups属性,他可以对组成员进行管理。在将连接加入到组中的时候,如果组不存在则自动创建,注意,当客户端重连之后,需要将连接重新加入组。

方法名 参数 说明
AddToGroupAsync string connectionId,string groupName 将connectionId放到groupName组中
RemoveFromGroupAsync string connectionId,string groupName 将connectionId从groupName组中移除

集线器(Hub)的Clients属性可以对当前集线器用户进行筛选。

方法名 参数 说明
Caller 只读属性 获取当前连接的客户端
Others 只读属性 获取除了当前连接外的所有客户端
OthersInGroup string groupName 获取组中除了当前连接之外的所有客户端
All 只读属性 获取所有客户端
AllExcept IReadOnlyList<string>excludedConnectionIds 所有客户端,除了ConnectionId在excludedConnectionIds之外的所有客户端
Client string connectionId 获取connectionId客户端
Clients IReadOnlyList<string>connectionIds 获取包含在connectionIds的客户端
Group string groupName groupName组中的客户端
Groups IReadOnlyList<string>groupNames 获取多个组的客户端
GroupsExcept string groupName,IReadOnlyList<string>excludedConnectionIds 获取所有组名为groupName的组中,除了ConnectionId在excludedConnectionIds中的客户端
User string userId 获取用户id为userId的客户端
Users IReadOnlyList<string> userIds 包含在userIds中的客户端

基于上面的代码,增加向特定客户端发送消息的功能

  1. 集线器类中增加

      //参数包含目标用户名

    publicasyncTask<string>SendPrivateMessage(stringdestUserName, stringmessage)

       {

           User?destUser=UserManager.FindByName(destUserName);//获取目标用户

           if (destUser==null)

           {

               return"DestUserNotFound";

           }

           stringdestUserId=destUser.Id.ToString();//目标用户的id

           stringsrcUserName=this.Context.User!.FindFirst(ClaimTypes.Name)!.Value;//发送端的用户

           stringtime=DateTime.Now.ToShortTimeString();

           //过滤出目标用户,并发送消息

           awaitthis.Clients.User(destUserId).SendAsync("ReceivePrivateMessage",

               srcUserName, time, message);

           return"ok";

       }

  1. 前端页面增加私聊功能

//在template中增加

...

<div>

     私聊给<input

       type="text"

       v-model="state.privateMsg.destUserName"

     />

     <input

       type="text"

       v-model="state.privateMsg.message"

       v-on:keypress="txtPrivateMsgOnkeypress"

     />

</div>

 

<script>

   //增加私聊接收方法

   ...

this.connection.on("ReceivePrivateMessage", (srcUser, time, msg) => {

       this.state.messages.push(srcUser+" "+time+"==="+msg);

     });

   //增加私聊发送方法

   ...

asynctxtPrivateMsgOnkeypress(e) {

     if (e.keyCode!=13) return;

     constdestUserName=this.state.privateMsg.destUserName;

     constmsg=this.state.privateMsg.message;

     try {

       constret=awaitconnection.invoke(

         "SendPrivateMessage",

         destUserName,

         msg

       );

       if (ret!="ok") {

         alert(ret);

       }

     } catch (err) {

       alert(err);

       return;

     }

     state.privateMsg.message="";

   }

</script>

注意:SignalR不会消息持久化,如果目标用户不在线就收不到消息,再次上线仍然收不到。如果需要持久化,则需要自行保存在数据库

外部向集线器推送消息

不通过集线器向客户端发送消息。

实现新增一个用户,向聊天室所有客户端推送欢迎xxx的消息。

  1. 在控制器中通过构造函数注入IHubContext服务,并向连接到ChatRoomHub集线器中的客户端推送消息。

publicclassTest1Controller : ControllerBase

   {

        privatereadonlyIHubContext<ChatRoomHub>hubContext;

        publicTest1Controller(IHubContext<ChatRoomHub>hubContext)

        {

            this.hubContext=hubContext;

        }

}

  1. 为控制器增加一个用于新增用户的操作。

        [HttpPost]

        publicasyncTask<IActionResult>AddUser(AddNewUserRequestreq)

        {

            //这里省略执行用户注册的代码

            awaithubContext.Clients.All.SendAsync("UserAdded", req.UserName);

            returnOk();

        }

  1. 在前端增加UserAdded的监听代码

this.connection.on("UserAdded", (userName) => {

       this.state.messages.push("系统消息:欢迎"+userName+"加入我们!");

     });

注意:IHubContext不能向“当前连接的所有客户端(Caller)”、“除了当前连接之外的客户端”推送消息(others),因为实在集线器之外调用,所以请求不在一个SignalR连接中,也就没有SignalR连接的概念

建议:在使用SignalR的时候,Hub类中不应该有数据库操作等比较好事的操作,Hub类只应该用于消息发布,且SignalR客户端给服务器端传递消息的时间不能超过30s,否则会报错

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
监控 测试技术
SignalR系列续集[系列8:SignalR的性能监测与服务器的负载测试]
原文:SignalR系列续集[系列8:SignalR的性能监测与服务器的负载测试] 目录 SignalR系列目录 前言 也是好久没写博客了,近期确实很忙,嗯..几个项目..头要炸..今天忙里偷闲.
2131 0
|
JavaScript
AngularJS+ASP.NET MVC+SignalR实现消息推送
原文:AngularJS+ASP.NET MVC+SignalR实现消息推送 背景   OA管理系统中,员工提交申请单,消息实时通知到相关人员及时进行审批,审批之后将结果推送给用户。 技术选择   最开始发现的是firebase,于是很兴奋的开始倒腾起来。
1899 0
|
21天前
|
弹性计算 数据挖掘 应用服务中间件
阿里云轻量应用服务器68元与云服务器99元和199元区别及选择参考
目前阿里云有三款特惠云服务器,第一款轻量云服务器2核2G68元一年,第二款经济型云服务器2核2G3M带宽99元1年,第三款通用算力型2核4G5M带宽199元一年。有的新手用户并不是很清楚他们之间的区别,因此不知道如何选择。本文来介绍一下它们之间的区别以及选择参考。
329 87
|
14天前
|
存储 弹性计算 应用服务中间件
阿里云轻量应用服务器出新品通用型实例了,全球26个地域可选
近日,阿里云再度发力,推出了首款全新升级的轻量应用服务器——通用型实例。这款服务器实例不仅标配了200Mbps峰值公网带宽,更在计算、存储、网络等基础资源上进行了全面优化,旨在为中小企业和开发者提供更加轻量、易用、普惠的云计算服务,满足其对于通用计算小算力的迫切需求。目前,这款新品已在全球26个地域正式上线,为全球用户提供了更加便捷、高效的上云选择。
114 27
|
5天前
|
机器学习/深度学习 人工智能 弹性计算
阿里云AI服务器价格表_GPU服务器租赁费用_AI人工智能高性能计算推理
阿里云AI服务器提供多种配置,包括CPU+GPU、FPGA等,适用于人工智能、机器学习和深度学习等计算密集型任务。本文整理了阿里云GPU服务器的优惠价格,涵盖NVIDIA A10、V100、T4等型号,提供1个月、1年和1小时的收费明细。具体规格如A10卡GN7i、V100-16G卡GN6v等,适用于不同业务场景,详情见官方页面。
50 11
|
6天前
|
存储 弹性计算 数据挖掘
阿里云服务器ECS通用算力型u1和ECS经济型e实例性能特点、使用及常见问题解答FAQ
阿里云ECS云服务器的经济型e实例和通用算力型u1实例深受开发者和中小企业青睐。e实例适合中小型网站、开发测试等轻量级应用,采用共享CPU调度模式,性价比高;u1实例则适用于中小型企业级应用,提供更高的性能保障和稳定性,支持固定CPU调度模式,计算性能更稳定。同等配置下,u1实例在网络带宽、IOPS等方面表现更优,价格也相对较高。个人用户可选择e实例,中小企业建议选择u1实例以确保业务稳定性。
|
2月前
|
机器学习/深度学习 人工智能 PyTorch
阿里云GPU云服务器怎么样?产品优势、应用场景介绍与最新活动价格参考
阿里云GPU云服务器怎么样?阿里云GPU结合了GPU计算力与CPU计算力,主要应用于于深度学习、科学计算、图形可视化、视频处理多种应用场景,本文为您详细介绍阿里云GPU云服务器产品优势、应用场景以及最新活动价格。
阿里云GPU云服务器怎么样?产品优势、应用场景介绍与最新活动价格参考