SignalR 是一个面向 .NET 开发人员的库,可简化向应用程序添加实时 Web 功能的过程。 实时 Web 功能是让服务器代码在可用时立即将内容推送到连接的客户端,而不是让服务器等待客户端请求新数据。
近期我给我的网站(闲蛋)集成了微语实时推送到主页的功能,就是在不用刷新页面的情况下,也能实时获取到最新动态的消息。这种技术的实现,在.net里面可以说是很简单的,因为有SignalR。下面我就分享一下我实现的过程。
1、安装Signalr的安装包
貌似好像.net6已经集成了Signalr,不需要你手动安装了,但是我这里因为把它单独抽离了出来,所以还是要安装的。
2、注册Signalr服务
新建一个SignalrHub类
Hub 是 SignalR 的核心组件,它是一个类,用作处理客户端和服务器之间通信的高级管道。Hub 允许客户端和服务器分别调用对方的方法,SignalR 自动处理跨计算机边界的调度。代码比较简单,如下
public class SignalrHub : Hub
{
//连接中断时执行,微软这样描述的:
//重写 OnDisconnectedAsync 虚方法,以便在客户端断开连接时执行操作。 如果客户端故意断开 连接(例如,通过调用 connection.stop()),exception 参数将 null。
//但是,如果客户端由于错误(例如网络故障)而断开连接,则 exception 参数将包含描述失败的 异常
public override Task OnDisconnectedAsync(Exception exception)
{
//todo 你自己的代码
return base.OnDisconnectedAsync(exception);
}
}
然后service.AddSignalR()注册服务,同时 创建对应的路由 app.MapHub("/SignalrHub")。
实现SignalrContext
SignalrContext是我自己封装的一个类,主要是通过构造函数完成IHubContext的注入然后获取Hub实例,然后业务代码通过调用SignalrContext.SendAllClientsMessage向所有客户端推送了消息,前提是客户端未关闭页面,当然你可以可以向指定或单个客户端发送消息
public class SignalrContext : ISignalrContext
{
private IHubContext<SignalrHub> _hubContext;
public SignalrContext(IHubContext<SignalrHub> hubContext)
{
_hubContext = hubContext;
}
#region 向客户端发送消息
/// <summary>
/// 向所有客户端发送消息
/// </summary>
/// <param name="message"></param>
/// <param name="method"></param>
/// <returns></returns>
public async Task SendAllClientsMessage<T>(T message, string method = "AllReviceMesage")
{
await _hubContext.Clients.All.SendAsync(method, message);
}
/// <summary>
/// 向指定的客户端集合发送消息
/// </summary>
/// <param name="connectionIds"></param>
/// <param name="message"></param>
/// <returns></returns>
public async Task SendSomeClientsMessage<T>(IReadOnlyList<string> connectionIds, T message)
{
if (connectionIds != null && connectionIds.Count >0)
await _hubContext.Clients.Clients(connectionIds).SendAsync("SendClientMessage", message);
}
/// <summary>
/// 向指定的单个开户端发送消息
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public async Task SendClientMessage<T>(T message,string revicer)
{
if (string.IsNullOrEmpty(revicer))
throw new ArgumentNullException("指定的客户端连接为空");
IReadOnlyList<string> connectionsByUser = SignalrConnectionMap.GetConnectionIds(revicer);
await _hubContext.Clients.Clients(connectionsByUser).SendAsync("ReviceMesage", message);
}
#endregion
}
我自己封装的一个
internal class SignalrConnectionMap
{
/// <summary>
/// 连接对象集合
/// </summary>
public static ConcurrentDictionary<string, string> ConnectionMaps = new ConcurrentDictionary<string, string>();
/// <summary>
/// 添加连接对象
/// </summary>
/// <param name="connectionId"></param>
/// <param name="value"></param>
public static void SetConnectionMaps(string connectionId, string value)
{
ConnectionMaps.AddOrUpdate(connectionId, value, (string s, string y) => value);
}
/// <summary>
/// 删除对象集合
/// </summary>
/// <param name="key"></param>
public static void Remove(string key)
{
if (ConnectionMaps.ContainsKey(key))
ConnectionMaps.TryRemove(key, out string value);
}
/// <summary>
/// 获取连接
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static List<string> GetConnectionIds(string value)
{
List<string> connectionIds = new List<string>();
foreach (KeyValuePair<string, string> item in ConnectionMaps)
{
if (item.Value == value)
connectionIds.Add(item.Key);
}
return connectionIds;
}
}
前端部分
安装Signalr
直接使用pnpm i @microsoft/signalr
监听Signalr
安装完成后,我们先import * as signalR from "@microsoft/signalr",然后创建于后端的连接
const connection = new signalR.HubConnectionBuilder()
.withUrl("http://localhost:5002/SignalrHub")//对应上面的app.MapHub<SignalrHub>("/SignalrHub")。
.build();
connection.start();
然后通过on方法监听,on方法第一个参数就是对应的我们后台的SendAllClientsMessage里面的method参数,一定要对应,然后第二个参数是回调函数,就是后端推送的数据。
connection.on("AllReviceMesage", (message) => {
console.log(message);
})
当然,如果你想创建连接成功时,做些别的事情,如把当前的登录人传递到后端,则你需要定义一个变量接收 connection.start()的返回的Promise对象,如下
let promise = connection.start();
promise.then(con => {
connection.invoke("SetConnectionMaps", "11")
})
然后你要在你的SignalrHub类里面添加一个对应的方法
public void SetConnectionMaps(string account)
{
string connectionid = Context.ConnectionId;
//todo 你自己的代码
}
运行测试
一切就需完成后,运行前端以及后端,然后F12查看浏览器,你可能会出现以下两个错误
解决办法就是在跨域里面设置允许包含请求头"x-requested-with","x-signalr-user-agent"
services.AddCors(s =>
{
IConfigurationSection section = ConfigureProvider.configuration.GetSection("policy");
string[] origins = section.GetSection("origins").Value.Split(',');
s.AddPolicy("cors",
p => p.AllowAnyMethod()
.AllowCredentials()
.WithOrigins(origins)
.WithHeaders("x-requested-with","x-signalr-user-agent")
);
});
让后重新运行,出现这个消息时,代码创建连接成功
然后调试后台推送一个消息过来,前端虽然接收到了消息,但是我们看到时间字段没有格式化,并且userId也没有转为字符串
据我所了解的,Signalr默认是使用Json的格式,所以我们这里可以使用AddJsonProtocol去设置格式,我这里没有使用Newtonsoft.Json,而是使用System.Text.Json设置,如果你也想使用System.Text.Json,那么就需要引入Microsoft.AspNetCore.SignalR.Protocol,才可以像我下面这样设置
service.AddSignalR().AddJsonProtocol(option =>
{
option.PayloadSerializerOptions = new JsonSerializerOptions()
{
//首字母小写
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
//不格式化输出
WriteIndented = false,
Converters = {
new DateTimeConverter(),
new LongToStringConverter()
}
};
});
internal class DateTimeConverter: JsonConverter<DateTime>
{
private readonly string _dateFormat = "yyyy-MM-dd HH:mm:ss";
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return DateTime.ParseExact(reader.GetString(), _dateFormat, null);
}
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToUniversalTime().ToString(_dateFormat));
}
}
internal class LongToStringConverter : JsonConverter<long>
{
public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return long.Parse(reader.GetString());
}
public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
}
以上设置完成后就可以按照我想要的格式了接收消息了。
Ubunu部署
我是通过nginx反向代理到我的服务器上的,默认的nginx是不支持Signalr的协议的,所以你需要在nginx里面添加
map $http_connection $connection_upgrade { "~*Upgrade" $http_connection; default keep-alive; }
可以参考我的配置
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx-access.log;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server_tokens off;
map $http_connection $connection_upgrade {
"~*Upgrade" $http_connection;
default keep-alive;
}
server {
listen 80;
server_name www.xiandanplay.com ;
rewrite ^(.*) https://$server_name$1 permanent;
}
server {
#SSL 默认访问端口号为 443
listen 443 ssl;
#请填写绑定证书的域名
server_name xiandanplay.com;
location /signalr {
proxy_pass http://127.0.0.1:5051/SignalrHub;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# proxy_redirect default;
}
}
}
作者:程序员奶牛
个人主页:https://www.xiandanplay.com
源码地址:https://gitee.com/MrHanchichi/xian-dan