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系列目录 前言 也是好久没写博客了,近期确实很忙,嗯..几个项目..头要炸..今天忙里偷闲.
2120 0
|
JavaScript
AngularJS+ASP.NET MVC+SignalR实现消息推送
原文:AngularJS+ASP.NET MVC+SignalR实现消息推送 背景   OA管理系统中,员工提交申请单,消息实时通知到相关人员及时进行审批,审批之后将结果推送给用户。 技术选择   最开始发现的是firebase,于是很兴奋的开始倒腾起来。
1883 0
|
2天前
|
SQL 弹性计算 安全
阿里云上云优选与飞天加速计划活动区别及购买云服务器后续必做功课参考
对于很多用户来说,购买云服务器通常都是通过阿里云当下的各种活动来购买,这就有必要了解这些活动的区别,同时由于活动内的云服务器购买之后还需要单独购买并挂载数据盘,还需要设置远程密码以及安全组等操作之后才能正常使用云服务器。本文就为大家介绍一下目前比较热门的上云优选与飞天加速计划两个活动的区别,以及通过活动来购买云服务器之后的一些必做功课,确保云服务器可以正常使用,以供参考。
|
4天前
|
弹性计算 安全 开发工具
灵码评测-阿里云提供的ECS python3 sdk做安全组管理
批量变更阿里云ECS安全组策略(批量变更)
|
22天前
|
存储 人工智能 弹性计算
阿里云弹性计算(ECS)提供强大的AI工作负载平台,支持灵活的资源配置与高性能计算,适用于AI训练与推理
阿里云弹性计算(ECS)提供强大的AI工作负载平台,支持灵活的资源配置与高性能计算,适用于AI训练与推理。通过合理优化资源分配、利用自动伸缩及高效数据管理,ECS能显著提升AI系统的性能与效率,降低运营成本,助力科研与企业用户在AI领域取得突破。
37 6
|
27天前
|
人工智能 弹性计算 编解码
阿里云GPU云服务器性能、应用场景及收费标准和活动价格参考
GPU云服务器作为阿里云提供的一种高性能计算服务,通过结合GPU与CPU的计算能力,为用户在人工智能、高性能计算等领域提供了强大的支持。其具备覆盖范围广、超强计算能力、网络性能出色等优势,且计费方式灵活多样,能够满足不同用户的需求。目前用户购买阿里云gpu云服务器gn5 规格族(P100-16G)、gn6i 规格族(T4-16G)、gn6v 规格族(V100-16G)有优惠,本文为大家详细介绍阿里云gpu云服务器的相关性能及收费标准与最新活动价格情况,以供参考和选择。
|
1月前
|
机器学习/深度学习 人工智能 弹性计算
什么是阿里云GPU云服务器?GPU服务器优势、使用和租赁费用整理
阿里云GPU云服务器提供强大的GPU算力,适用于深度学习、科学计算、图形可视化和视频处理等多种场景。作为亚太领先的云服务提供商,阿里云的GPU云服务器具备灵活的资源配置、高安全性和易用性,支持多种计费模式,帮助企业高效应对计算密集型任务。