前言
本文所有讨论都基于.net core api前后端分离项目
服务端推送使用websocket,也可以使用轮询,但是解决高并发的玩法就多了,Nginx+Redis是流行方案,但是也有接入队列的,这样服务端不需要做比较大的改动,差点跑题~
AMQP
这是一个文本消息协议,RabbitMQ是基于这个协议的一种实现,贴个官网RabbitMQ官网,上面详细介绍了怎么在各种开发语言中接入的方法。本文重点谈前后端分离项目中怎么玩
开发环境准备
前端vue,脚手架版本4.2.3
后端.net core 3.1
vue接入signalr前端引入@microsoft/signalr这个依赖
这里我写了一个单独的页进行测试,可能后期不准备接入这玩意儿,几百个用户的访问量我不准备考虑。讲几点
this的指向问题
//这里存一个别名,防止在connection中获取不到vue的实例
let _self=this;
//创建一个Signalr连接
var connection = new signalR.HubConnectionBuilder().withUrl("/api/msg",{
//带个token过去后端鉴权和认证
accessTokenFactory: () => sessionStorage.getItem('token')
}).build();
后端采用终结点的方式提供服务入口,中间件方式试了一下会报错
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<MessageHub>("/api/msg")
.RequireCors(t => t.WithOrigins(new string[] { "http://localhost:5000", "https://localhost:5001" })
.AllowAnyMethod().AllowAnyHeader().AllowCredentials());
});
前端Signalr配置
connection.serverTimeoutInMilliseconds= 24e4;
connection.keepaliveintervalinmilliseconds = 12e4
后端引入RabbitMQ依赖(高亮),Signalr框架已经给我们集成了
后端Signalr服务注册进服务集合容器
services.AddSignalR(options =>
{
//客户端发保持连接请求到服务端最长间隔,默认30秒,改成4分钟,网页需跟着设置connection.keepaliveintervalinmilliseconds = 12e4;即2分钟
options.ClientTimeoutInterval = TimeSpan.FromMinutes(4);
//服务端发保持连接请求到客户端间隔,默认15秒,改成2分钟,网页需跟着设置connection.servertimeoutinmilliseconds = 24e4;即4分钟
options.KeepAliveInterval = TimeSpan.FromMinutes(2);
});
服务注册,托管服务只有注册了才会生效
services.AddHostedService<MessageHub>();
后端jwt配置
//TokenManagement
public class TokenManagement
{
[JsonProperty("secret")]
public string Secret { get; set; }
[JsonProperty("issuer")]
public string Issuer { get; set; }
[JsonProperty("audience")]
public string Audience { get; set; }
[JsonProperty("accessExpiration")]
public int AccessExpiration { get; set; }
[JsonProperty("refreshExpiration")]
public int RefreshExpiration { get; set; }
}
//这里是把配置文件中的section映射到自定义类型并获取实例
var token = Configuration.GetSection("tokenManagement").Get<TokenManagement>();
//身份认证代码段,基于jwt进行的身份认证
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
//这里配置是否一定要使用https请求
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,//是否验证秘钥签名
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(token.Secret)),//签名秘钥
ValidIssuer = token.Issuer,//使用者
ValidAudience = token.Audience,//颁发者
ValidateIssuer = false,
ValidateAudience = false,
// 是否验证Token有效期,使用当前时间与Token的Claims中的NotBefore和Expires对比
// ValidateLifetime = true,
//注意这是缓冲过期时间,总的有效时间等于这个时间加上jwt的过期时间,如果不配置,默认是5分钟
// ClockSkew = TimeSpan.FromSeconds(4)
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = (context) => {
if (!context.HttpContext.Request.Path.HasValue)
{
return Task.CompletedTask;
}
//重点在于这里;判断是Signalr的路径
var accessToken = context.HttpContext.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!(string.IsNullOrWhiteSpace(accessToken)) && path.StartsWithSegments("/api/msg"))
{
context.Token = accessToken;
return Task.CompletedTask;
}
return Task.CompletedTask;
}
};
});
贴个前后端完整代码吧还是,按需参考,这些代码在官网都能搞到
前端
<template>
<div>
<div class="container">
<div class="row"> </div>
<div class="row">
<div class="col-2">User</div>
<div class="col-4"><input type="text" id="userInput" /></div>
</div>
<div class="row">
<div class="col-2">Message</div>
<div class="col-4"><input type="text" id="messageInput" /></div>
</div>
<div class="row"> </div>
<div class="row">
<div class="col-6">
<input type="button" @click="" id="sendButton" value="Send Message" />
</div>
<div class="col-6">
<input type="button" @click="" id="sendButtonConfirm" value="Send Message" />
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<hr />
</div>
</div>
<div class="row">
<div class="col-6">
<ul id="messagesList"></ul>
</div>
</div>
</div>
</template>
<script>
import * as signalR from "@microsoft/signalr";
export default {
name: "Online",
data(){
return {
onLineUserList:[],
connectionId:"",
token:"",
selfConneId:""
}
},
created() {
// console.log(this)
},
mounted() {
let _self=this;
//.withAutomaticReconnect()
var connection = new signalR.HubConnectionBuilder().withUrl("/api/msg", {
accessTokenFactory: () => sessionStorage.getItem('token')
}).build();
document.getElementById("sendButton").disabled = true;
connection.serverTimeoutInMilliseconds= 24e4;
connection.keepaliveintervalinmilliseconds = 12e4
connection.on("ReceiveMessage", function (res) {
//var msg = res.data.msg.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
var encodedMsg = res.userName + " says " +res.msg;
var li = document.createElement("li");
li.textContent = encodedMsg;
document.getElementById("messagesList").appendChild(li);
});
// async function start () {
// try {
// await connection.start()
// console.log('connected')
// } catch (err) {
// console.log(err)
// setTimeout(() => start(), 5000)
// }
// }
connection.onclose(async (e) => {
_self.$message.error("连接断开");
})
connection.on("ConnectResponse",function (res) {
// console.log(res);
// var msg = res.msg.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
if(res.state===200){
this.onLineUserList=res.data;
this.selfConneId=res.connId;
this.token=res.token;
_self.$message.success(res.msg);
}else{
_self.$message.error("连接服务器失败~");
}
})
connection.start().then(function (e) {
// console.log(e)
document.getElementById("sendButton").disabled = false;
}).catch(function (err) {
return console.error(err.toString());
});
document.getElementById("sendButton").addEventListener("click", function (event) {
var user = document.getElementById("userInput").value;
var message = document.getElementById("messageInput").value;
connection.invoke("SendAllMessage", {ConnectionId:_self.selfConneId,Token:_self.token,UserName:user,Msg:message}).catch(function (err) {
return console.error(err.toString());
});
// connection.send("SendMessage",user,message).then(e=>{console.log(e)}).catch(e=>{
// this.$message.error('出错了');
// })
event.preventDefault();
});
document.getElementById("sendButtonConfirm").addEventListener("click", function (event) {
var user = document.getElementById("userInput").value;
var message = document.getElementById("messageInput").value;
connection.invoke("SendOneMessage", {ConnectionId:_self.selfConneId,Token:_self.token,UserName:user,Msg:message}).catch(function (err) {
return console.error(err.toString());
});
// connection.send("SendMessage",user,message).then(e=>{console.log(e)}).catch(e=>{
// this.$message.error('出错了');
// })
event.preventDefault();
});
}
}
</script>
<style scoped>
</style>
后端
using IntegratedServices.RabbitMQ;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Text;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
namespace IntegratedServices.Online
{
public class MessageHub : Hub, IHostedService
{
private IConnection _connection;
/// <summary>
/// The channel
/// </summary>
private IModel _channel;
private readonly QueueBaseOption _batchSendMessageQueue;
private readonly RabbitConnectOption _rabbitConnectOptions;
private readonly IServiceProvider _services;
private readonly ILogger<MessageHub> _logger;
public static List<UserModel> OnlineUser = new List<UserModel>() { };
//ISysUser user
public MessageHub(IServiceProvider services, IOptions<MessageQueueOption> messageQueueOption, ILogger<MessageHub> logger)
{
// _userService = user;
_logger = logger;
_services = services;
_batchSendMessageQueue = messageQueueOption.Value?.BatchSendMessageQueue;
_rabbitConnectOptions = messageQueueOption.Value?.RabbitConnect;
}
/// <summary>
/// 当连接成功时执行
/// </summary>
/// <returns></returns>
[Authorize]
public async override Task OnConnectedAsync()
{
string connId = Context.ConnectionId;
// Clients.All
_logger.LogWarning("SignalR已连接");
//验证Token
var token = Context.GetHttpContext().Request.Query["access_token"];
var user = (new JwtSecurityTokenHandler().ReadJwtToken(token));
string userName = user.Claims.FirstOrDefault(m => m.Type == "name").Value;
_logger.LogWarning("SignalR已连接,用户名:" + userName);
bool isexited = OnlineUser.Any(o => o.UserName == userName);
if (isexited)
OnlineUser.RemoveAll(ui => ui.UserName == userName);
//连接用户 这里可以存在Redis
//OnlineUser.Add(new UserModel{
//});
var model = new UserModel
{
ConnectionId = connId,
Token = token,
UserName = userName,
Msg = ""
};
//OnlineUser.Add(model);
OnlineUser.Add(model);
// Groups.AddToGroupAsync(connId, "UserList");
//给当前的连接分组 可以进行同一组接收消息 也可以用Token获取机构权限
//await Groups.AddToGroupAsync(Context.ConnectionId, "测试组");
//给当前连接返回消息 .Clients可以发多个连接ID
await Clients.Client(connId).SendAsync("ConnectResponse",
new
{
state = 200,
token = token,
connId = connId,
data = "",
msg = userName + "连接成功"
});
Process(Clients);
//await StartAsync(CancellationToken.None);
await base.OnConnectedAsync();
}
/// <summary>
/// 当连接断开时的处理
/// </summary>
public override async Task OnDisconnectedAsync(Exception exception)
{
var token = Context.GetHttpContext().Request.Query["access_token"];
var user = (new JwtSecurityTokenHandler().ReadJwtToken(token));
string userName = user.Claims.FirstOrDefault(m => m.Type == "name").Value;
string connId = Context.ConnectionId;
//var model = OnlineUser.FirstOrDefault(OU => OU.Key == userName);
OnlineUser.RemoveAll(ui => ui.UserName == userName);
await base.OnDisconnectedAsync(exception);
}
/// <summary>
/// 消息不直接发送接受者,先写入队列
/// </summary>
/// <returns></returns>
public async Task SendAllMessage(UserModel userModel)
{
var durable = true;//约定使用持久化
_logger.LogInformation(userModel.UserName + "写入消息" + userModel.Msg);
_channel.ExchangeDeclare(_batchSendMessageQueue.Exchange, _batchSendMessageQueue.ExchangeType, durable);
_channel.QueueDeclare(_batchSendMessageQueue.Queue, durable, false, false, null);
_channel.QueueBind(_batchSendMessageQueue.Queue, _batchSendMessageQueue.Exchange, _batchSendMessageQueue.RouteKey, null);
var sendBytes = Encoding.UTF8.GetBytes(userModel.Msg);
_channel.BasicPublish(_batchSendMessageQueue.Exchange, _batchSendMessageQueue.RouteKey, null, sendBytes);
await Task.CompletedTask;
//Send.SendMessage(userModel.Msg);
//Receiver.ReceiveMessage();
//await Clients.All.SendAsync("receiveMessage", new
//{
// userName = userModel.UserName,
// state = 200,
// msg = userModel.Msg
//});
//推送给所有连接ID的第一条数据
//await Clients.Clients(OnlineUser.Select(q => q.ConnectionId).ToList()).SendAsync("receiveMessage", new {
// userName=userModel.UserName,
// state= 200,
// msg = userModel.Msg
// });
}
public async Task SendOneMessage(UserModel userModel)
{
var durable = true;//约定使用持久化
var factory = new ConnectionFactory()
{
HostName = _rabbitConnectOptions.HostName,
Port = _rabbitConnectOptions.Port,
UserName = _rabbitConnectOptions.UserName,
Password = _rabbitConnectOptions.Password,
};
_connection = factory.CreateConnection();
_channel = _connection.CreateModel();
_logger.LogInformation(userModel.UserName + "写入消息" + userModel.Msg);
//_channel.ExchangeDeclare(_batchSendMessageQueue.Exchange, _batchSendMessageQueue.ExchangeType, durable);
_channel.QueueDeclare("Test", durable, false, false, null);
//_channel.QueueBind(_batchSendMessageQueue.Queue, _batchSendMessageQueue.Exchange, _batchSendMessageQueue.RouteKey, null);
var sendBytes = Encoding.UTF8.GetBytes(userModel.Msg);
// _channel.BasicPublish(_batchSendMessageQueue.Exchange, _batchSendMessageQueue.RouteKey,null,sendBytes);
_channel.BasicPublish("","Test", null, sendBytes);
await Task.CompletedTask;
//var conneId = OnlineUser.FirstOrDefault(ou => ou.UserName == userModel.UserName).ConnectionId;
以下方法用户dictionary的反序列化,转json
var conneId = Newtonsoft.Json.JsonConvert.DeserializeObject<UserModel>(receiver.Value.ToString()).ConnectionId;
//await Clients.Client(conneId).SendAsync("receiveMessage",
// new
// {
// userName = userModel.UserName,
// state = 200,
// msg = userModel.Msg
// });
}
public void Process(IHubCallerClients Clients)
{
_logger.LogInformation("调用ExecuteAsync");
using (var scope = _services.CreateScope())
{
var durable = true;//约定使用持久化
var noack = false;//消息手动确认,否则消费者在接收到消息后会自动应答
try
{
if (_rabbitConnectOptions == null) return;
var factory = new ConnectionFactory()
{
HostName = _rabbitConnectOptions.HostName,
Port = _rabbitConnectOptions.Port,
UserName = _rabbitConnectOptions.UserName,
Password = _rabbitConnectOptions.Password,
};
_connection = factory.CreateConnection();
_channel = _connection.CreateModel();
//我们在消费端 从新进行一次 队列和交换机的绑定 ,防止 因为消费端在生产端 之前运行的 问题。
// _channel.ExchangeDeclare(_batchSendMessageQueue.Exchange, _batchSendMessageQueue.ExchangeType, durable);
_channel.QueueDeclare("Test", durable, false, false, null);
//_channel.QueueBind(_batchSendMessageQueue.Queue, _batchSendMessageQueue.Exchange, _batchSendMessageQueue.RouteKey, null);
#region 通过事件的形式,如果队列中有消息,则执行事件。建议采用这种方式。
_logger.LogInformation("开始监听队列:" +"Test" );//_batchSendMessageQueue.Queue
// _channel.BasicQos(0, 1, false);//设置一个消费者在同一时间只处理一个消息,这个rabbitmq 就会将消息公平分发
var consumer = new EventingBasicConsumer(_channel);
// _channel.BasicQos(0, 1, false);
consumer.Received += (ch, ea) =>
{
try
{
var content = Encoding.UTF8.GetString(ea.Body.ToArray());
_logger.LogInformation("获取到消息:" + content);
// TODO: 向用户推送消息
Clients.All.SendAsync("receiveMessage", new
{
userName = "测试人员",
state = 200,
msg = content
});
}
catch (Exception ex)
{
_logger.LogError(ex, "");
}
finally
{
_channel.BasicAck(ea.DeliveryTag, false);
}
};
_channel.BasicConsume("Test", noack, consumer);
#endregion
}
catch (Exception ex)
{
_logger.LogError(ex, "");
}
}
}
public async Task StartAsync(CancellationToken cancellationToken)
{
try
{
//if (_rabbitConnectOptions == null) return Task.CompletedTask;
//var factory = new ConnectionFactory()
//{
// HostName = _rabbitConnectOptions.HostName,
// Port = _rabbitConnectOptions.Port,
// UserName = _rabbitConnectOptions.UserName,
// Password = _rabbitConnectOptions.Password,
//};
//_connection = factory.CreateConnection();
//_channel = _connection.CreateModel();
}
catch (Exception ex)
{
_logger.LogError(ex, "Rabbit连接出现异常");
}
await Task.CompletedTask;
}
/// <summary>
/// Triggered when the application host is performing a graceful shutdown.
/// </summary>
/// <param name="cancellationToken">Indicates that the shutdown process should no longer be graceful.</param>
/// <returns></returns>
public Task StopAsync(CancellationToken cancellationToken)
{
if (_connection != null)
this._channel.Close();
this._connection.Close();
return Task.CompletedTask;
}
}
public class UserModel
{
public string ConnectionId { get; set; }
public string Token { get; set; }
public string UserName { get; set; }
public string Msg { get; set; }
public string State { get; set; }
};
}
效果
前端写入消息
队列成功收到,我只想让请求排队,队列起到的作用只是作为一个缓冲
控制台输出,上面warn是log4net输出的内容
前端收到的反馈,后端通过集线器向所有在线的用户进行发送,这里逻辑好像有点问题,后续慢慢优化
不在线怎么办?不在线就写入数据库或者redis做持久化,下次用户连接的时候去库里面查询,查到就通过集线器直接推给用户
总结
服务端推送必然是要使用websocket协议,队列的作用是对请求进行排队,起到削减单次处理的并发量的作用,目前就这些,作者也是才入坑,说的不对的地方大家多多指出,谢谢~
推荐一篇关于服务托管的文章