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系列目录 前言 也是好久没写博客了,近期确实很忙,嗯..几个项目..头要炸..今天忙里偷闲.
2114 0
|
JavaScript
AngularJS+ASP.NET MVC+SignalR实现消息推送
原文:AngularJS+ASP.NET MVC+SignalR实现消息推送 背景   OA管理系统中,员工提交申请单,消息实时通知到相关人员及时进行审批,审批之后将结果推送给用户。 技术选择   最开始发现的是firebase,于是很兴奋的开始倒腾起来。
1873 0
|
iOS开发
IOS消息推送
IOS消息推送
135 0
|
JSON 数据格式 iOS开发
APNS IOS 消息推送JSON格式介绍
在开发向苹果Apns推送消息服务功能,我们需要根据Apns接受的数据格式进行推送。下面积累了我在进行apns推送时候总结的 apns服务接受的Json数据格式 示例 1: 以下负载包含哦一个简单的 aps 字典。
3451 0
|
Android开发 iOS开发
了解iOS消息推送一文就够:史上最全iOS Push技术详解
本文作者:陈裕发, 腾讯系统测试工程师,由腾讯WeTest整理发表。 1、引言 开发iOS系统中的Push推送,通常有以下3种情况: 1)在线Push:比如QQ、微信等IM界面处于前台时,聊天消息和指令都会通过IM自建的网络长连接通道推送过来,这种Pu...
3432 0
|
搜索推荐 iOS开发
iOS小技能:消息推送扩展的使用
iOS小技能:消息推送扩展的使用
547 0
iOS小技能:消息推送扩展的使用
|
PHP 数据安全/隐私保护 iOS开发
分分钟搞定IOS远程消息推送(二)
分分钟搞定IOS远程消息推送
406 0
分分钟搞定IOS远程消息推送(二)