首先说一下业务,我们的webapi需要从redis订阅消息,并把订阅到的消息推送给web前端。要求不同的用户建立不同的websocket连接,并把websocket要把消息分发给不同的用户。
Redis的消息订阅与发布并不复杂,这里不再赘述。主要讲解如何通过webSocke将消息推送给前端。
我们使用的是 .Net Core 3.1 + 原生 webSocket ,前端使用的是VueJS。
在websocket中订阅redis消息,当收到订阅消息时,处理并发送给前端。
1、配置websocket中间键
首先,安装 System.Net.WebSockets包,可在Nuget中安装,也可以在程序包管理控制台安装。
然后,在 Startup 类的 Configure 方法中添加 WebSocket 中间件: app.UseWebSockets();
可以配置以下属性:
KeepAliveInterval - 向客户端发送“ping”帧的频率,以确保代理保持连接处于打开状态。 ReceiveBufferSize - 用于接收数据的缓冲区的大小。 高级用户可能需要对其进行更改,以便根据数据大小调整性能。
app.UseWebSockets(new WebSocketOptions()
{
KeepAliveInterval = TimeSpan.FromSeconds(120000),
ReceiveBufferSize = 4 * 1024
});
2、新建一个WebSocketsHelper类,代码如下:
public class WebSocketsHelper
{
///
/// 日志接口
///
private ILogger _logger;
///
/// 下一级管道
///
private RequestDelegate _next;
///
/// 缓冲区大小
///
private const int bufferSize = 1024 * 4;
///
/// URL地址后缀
///
private const string routePostfix = "/ws";
///
/// Socket列表
///
/// typeof(string),用户名
/// typeof(WebSocket),WebSocket
///
private static ConcurrentDictionary _socketsList
= new ConcurrentDictionary();
//private static ConcurrentDictionary> _meterList = new ConcurrentDictionary>();
private const string Channel_1 = "TestChannel1";
private const string Channel_2 = "TestChannel2";
private readonly RedisHelper _helper;
///
/// 构造函数
///
/// 下一级管道
/// 日志接口
public WebSocketsHelper(RequestDelegate next, ILogger logger, RedisHelper helper)
{
_next = next;
_logger = logger;
_helper = helper;
}
public async Task Invoke(HttpContext context)
{
//判断当前请求是否为WebSocket
if (!IsWebSocket(context))
{
await _next.Invoke(context);
return;
}
//将请求转换为 WebSocket 连接
var webSocket = await context.WebSockets.AcceptWebSocketAsync();
while (webSocket.State == WebSocketState.Open)
{
//接收消息
var entity = await Receiveentity(webSocket);
HandRecMsg(entity, webSocket);
//订阅redis消息
await _helper.UnsubscribeAsync(Channel_1 );
await _helper.SubscribeAsync(Channel_1 , async (channel, msg) => await HandleChannelMsg(webSocket, channel, msg));
//await _helper.UnsubscribeAsync(Channel_2);
//await _helper.SubscribeAsync(Channel_2, async (channel, msg) => await HandleChannelMsg(webSocket, channel, msg));
}
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Close", default(CancellationToken));
}
///
/// 处理低订阅到的信息,并发送给用户
///
///
///
///
///
private async Task HandleChannelMsg(WebSocket webSocket, string channelName, string msg)
{
_logger.LogError("收到订阅结果:" + msg);
MeterReadResult res = JsonConvert.DeserializeObject(msg);
//消息类型
MReadViewModel mod = new MReadViewModel()
{
MeterAddr = res.Meter,
readTime = DateTime.Now
};
mod.Flag = res.Status;
SocketMessageEntity entity = new SocketMessageEntity()
{
Receiver = res.UserId,
Sender = "Amr-Api",
Message = mod
};
await HandleSend(webSocket, entity);
}
///
/// 处理Socket收到的信息
///
///
///
private void HandRecMsg(SocketMessageEntity msg, WebSocket webSocket)
{
_logger.LogError("收到socket消息:" + JsonConvert.SerializeObject(msg));
if (msg != null && msg.Receiver != "" && msg.Receiver != null)
{
AddUser(msg.Receiver, webSocket);
}
else
{
//_helper.Unsubscribe(Channel_2);
_helper.Unsubscribe(Channel_1);
//webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Close", default(CancellationToken));
}
}
private bool RemoveUser(string UserId, WebSocket webSocket)
{
WebSocket socket1 = null;
_socketsList.TryRemove(UserId, out webSocket);
webSocket = null;
return true;
}
///
/// 添加用户
///
///
///
///
private bool AddUser(string UserId, WebSocket webSocket)
{
var res = false;
//不存在
if (!_socketsList.TryGetValue(UserId, out WebSocket socket))
{
res = _socketsList.TryAdd(UserId, webSocket);
}
else
{
//webSocket = null;
WebSocket socket1 = null;
_socketsList.TryRemove(UserId, out socket1);
res = _socketsList.TryAdd(UserId, webSocket);
}
return res;
}
///
/// 处理WebSocket聊天
///
/// WebSocket
/// Entity of Message
///
private async Task HandleSend(WebSocket webSocket, SocketMessageEntity entity)
{
_logger.LogError(entity.Sender + ";" + entity.Receiver + ";" + entity.Message);
await SendOne(entity.Sender, entity.Receiver, entity.Message);
}
///
/// 给指定用户发送消息
///
/// 发送者
/// 接收者
/// 消息内容
///
private async Task SendOne(string sender, string receiver, MReadViewModel message, MessageType type = MessageType.Chat)
{
if (sender == receiver) return;
if (message == null) return;
if (!ValidateUser(receiver)) return;
var socket = _socketsList[receiver];
var chatEntity = new SocketMessageEntity() { Receiver = receiver, Sender = sender, Message = message };
await SendMessage(socket, chatEntity);
}
///
/// 当前请求是否为WebSocket
///
/// Http上下文
///
private bool IsWebSocket(HttpContext context)
{
return context.WebSockets.IsWebSocketRequest &&
context.Request.Path == routePostfix;
}
///
/// 验证用户是否存在
///
/// 用户名
///
private bool ValidateUser(string userName)
{
return _socketsList.ContainsKey(userName);
}
///
/// 发送消息
///
///
///
///
///
private async Task SendMessage(WebSocket webSocket, TEntity entity)
{
try
{
if (webSocket.State == WebSocketState.Open)
{
var Json = JsonConvert.SerializeObject(entity);
_logger.LogError("Json数据:" + Json);
var bytes = Encoding.UTF8.GetBytes(Json);
await webSocket.SendAsync(
new ArraySegment(bytes),
WebSocketMessageType.Text,
true,
CancellationToken.None
);
}
}
catch
{
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Close", default(CancellationToken));
}
}
///
/// 接收消息
///
/// WebSocket
/// typeof(TEntity)
///
private async Task Receiveentity(WebSocket webSocket)
{
try
{
if (webSocket.State == WebSocketState.Open)
{
var buffer = new ArraySegment(new byte[bufferSize]);
var result = await webSocket.ReceiveAsync(buffer, CancellationToken.None);
while (!result.EndOfMessage)
{
result = await webSocket.ReceiveAsync(buffer, default(CancellationToken));
}
var json = Encoding.UTF8.GetString(buffer.Array);
json = json.Replace("\0", "").Trim();
return JsonConvert.DeserializeObject(json, new JsonSerializerSettings()
{
DateTimeZoneHandling = DateTimeZoneHandling.Local
});
}
else
{
return default;
}
}
catch
{
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Close", default(CancellationToken));
return default;
}
}
}
注意:
1、在添加user时,一定要判断当前用户的socket链接是否存在,如果已存在,需要移除老的连接,并重新添加新连接,否则会导致用户收不到消息
2、用户建立多个socket连接时,在Invoke方法中会执行多次,所以防止订阅多次,要先取消订阅,然后再进行订阅。其次,在收到断开连接的消息时,最好也进行取消订阅。
3、新建MiddlewareExtenstion 类,注册中间件,代码如下:
public static class MiddlewareExtenstion
{
///
/// 添加应用 中间件注册
///
///
public static void UseWebSocketHelper(this IApplicationBuilder app)
{
app.UseMiddleware
}
}
4、最后,在Startup 中的 Configure 方法中,添加自定义的 socket中间件:
//添加socket
app.UseWebSocketHelper();
前端使用 ws://ip:端口/ws,来建立WebSocket连接,需要注意的是当后台服务使用域名时,要换成wss://ip:端口/ws 来访问。
例如:ws://127.0.0.1:80/ws 改为 wss://csdn.test.com:80/ws