1)Startup.cs
cs
//添加 SignalR 服务
services.AddSignalR();
// 添加 CORS 策略(允许前端访问)
services.AddCors(options =>
{
options.AddPolicy("AllowAll", builder =>
{
builder
.WithOrigins(AppsettingsEntity.allowCors) // 前端地址
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials(); // SignalR 必须允许凭证
});
});
// 启用 CORS(一定要放在 UseRouting 之后,UseEndpoints 之前)
app.UseCors("AllowAll");
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
// 注册 ChatHub
endpoints.MapHub<ChatHub>("/chathub")
.RequireCors("AllowAll"); // Hub 也要应用 CORS 策略
});
2)ChatHub.cs
cs
using Common.PlatformEnum;
using Microsoft.AspNetCore.SignalR;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Platform.Api.Hubs
{
public class ChatHub : Hub
{
// 存储用户与连接ID的映射(一个用户可能有多个连接,比如多个浏览器标签页)
private static readonly ConcurrentDictionary<string, HashSet<string>> _userConnections = new();
// 当用户连接上时触发
public override Task OnConnectedAsync()
{
var user = Context.GetHttpContext()?.Request.Query["user"];
if (!string.IsNullOrEmpty(user))
{
_userConnections.AddOrUpdate(
user!,
_ => new HashSet<string> { Context.ConnectionId },
(_, connections) =>
{
connections.Add(Context.ConnectionId);
return connections;
});
}
return base.OnConnectedAsync();
}
// 当用户断开连接时触发
public override Task OnDisconnectedAsync(Exception? exception)
{
foreach (var kvp in _userConnections)
{
if (kvp.Value.Contains(Context.ConnectionId))
{
Logout(kvp.Key);
kvp.Value.Remove(Context.ConnectionId);
if (kvp.Value.Count == 0)
_userConnections.TryRemove(kvp.Key, out _);
break;
}
}
return base.OnDisconnectedAsync(exception);
}
// 向所有人发送(原始示例)
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
// 向指定用户发送消息
public async Task SendMessageToUser(string toUser, string formUser, SignalrEnum signalrEnum, string message)
{
if (_userConnections.TryGetValue(toUser, out var connections))
{
foreach (var connectionId in connections)
{
await Clients.Client(connectionId).SendAsync("ReceiveMessage", formUser,(int)signalrEnum, message);
}
}
}
/// <summary>
/// 学生进入竞赛场次
/// </summary>
/// <param name="formUser">学生signalr用户标识</param>
/// <returns></returns>
public async Task StudentLogin(string formUser)
{
string[] userSplit = formUser.Split('_');
string session = userSplit[0];
var _userConnection = _userConnections.FirstOrDefault(a => a.Key.StartsWith($"{session}_t"));
if (_userConnection.Key != null)
{
string toUser = _userConnection.Key;
await SendMessageToUser(toUser, formUser, SignalrEnum.学生进入竞赛场次, $"{formUser}进入竞赛场次");
}
}
/// <summary>
/// 老师进入竞赛场次
/// </summary>
/// <param name="formUser">老师signalr用户标识</param>
/// <returns></returns>
public async Task TeacherLogin(string formUser)
{
string[] userSplit = formUser.Split('_');
string session = userSplit[0];
var _tempUserConnections = _userConnections.Where(a => a.Key.StartsWith($"{session}_s"));
string studentids = string.Join(",", _tempUserConnections.Select(a => a.Key.Split('_')[2]));
await SendMessageToUser(formUser, formUser, SignalrEnum.老师进入竞赛场次, studentids);
}
/// <summary>
/// 学生离开竞赛场次
/// </summary>
/// <param name="formUser">学生signalr用户标识</param>
/// <returns></returns>
public async Task Logout(string formUser)
{
string[] userSplit = formUser.Split('_');
string session = userSplit[0];
string type = userSplit[1];
if (type == "s")
{
var _userConnection = _userConnections.FirstOrDefault(a => a.Key.StartsWith($"{session}_t"));
if (_userConnection.Key != null)
{
string toUser = _userConnection.Key;
await SendMessageToUser(toUser, formUser, SignalrEnum.学生离开竞赛场次, $"{formUser}离开竞赛场次");
}
}
}
/// <summary>
/// 收卷
/// </summary>
/// <param name="formUser">老师signalr用户标识</param>
/// <param name="toUserId">学生数据库主键,如果是多个用英文逗号隔开</param>
/// <returns></returns>
public async Task Finish(string formUser, string toUserId)
{
string[] userSplit = formUser.Split('_');
string session = userSplit[0];
if (string.IsNullOrEmpty(toUserId))
{
//全场收卷
var _tempUserConnections = _userConnections.Where(a => a.Key.StartsWith($"{session}_s"));
foreach (var _userConnection in _tempUserConnections)
{
string toUser = _userConnection.Key;
await SendMessageToUser(toUser, formUser, SignalrEnum.收卷, $"{formUser}收卷");
}
}
else
{
//单个收卷
foreach (string touserid in toUserId.Split(','))
{
var _userConnection = _userConnections.FirstOrDefault(a => a.Key.StartsWith($"{session}_s_{touserid}"));
if (_userConnection.Key != null)
{
string toUser = _userConnection.Key;
await SendMessageToUser(toUser, formUser, SignalrEnum.收卷, $"{formUser}收卷");
}
}
}
}
/// <summary>
/// 延迟
/// </summary>
/// <param name="formUser">老师signalr用户标识</param>
/// <param name="toUserId">学生数据库主键,如果是多个用英文逗号隔开</param>
/// <returns></returns>
public async Task Delay(string formUser, string toUserId)
{
string[] userSplit = formUser.Split('_');
string session = userSplit[0];
if (string.IsNullOrEmpty(toUserId))
{
//全场延迟
var _tempUserConnections = _userConnections.Where(a => a.Key.StartsWith($"{session}_s"));
foreach (var _userConnection in _tempUserConnections)
{
string toUser = _userConnection.Key;
await SendMessageToUser(toUser, formUser, SignalrEnum.延迟, $"{formUser}延迟");
}
}
else
{
//单个延迟
foreach (string touserid in toUserId.Split(','))
{
var _userConnection = _userConnections.FirstOrDefault(a => a.Key.StartsWith($"{session}_s_{touserid}"));
if (_userConnection.Key != null)
{
string toUser = _userConnection.Key;
await SendMessageToUser(toUser, formUser, SignalrEnum.延迟, $"{formUser}延迟");
}
}
}
}
}
}
3)老师端
javascript
<script src="signalr.min.js"></script>
<script>
// 当前用户标识(CompetitionSessionId_t_UserId)
// 场次1_老师_老师1
const currentUser = "p1_t_t1";
// 建立连接时,把 user 作为参数传给后端
const connection = new signalR.HubConnectionBuilder()
// .withUrl(`http://47.111.227.47:8082/chathub?user=${currentUser}`)
.withUrl(`http://localhost:8082/chathub?user=${currentUser}`)
.build();
// 接收消息事件
connection.on("ReceiveMessage", (user, type, message) => {
console.log(`收到来自 ${user} 的消息: signalr操作类型:${type},内容:${message}`);
});
// 启动连接
connection.start().then(() => {
console.log("已连接 SignalR Hub 作为用户:", currentUser);
// 老师进入竞赛场次
connection.invoke("TeacherLogin", currentUser).catch(console.error);
}).catch(console.error);
// 全场收卷
function finish() {
connection.invoke("Finish", currentUser, '').catch(console.error);
}
// 单个收卷(如果是多个用英文逗号隔开)
function singleFinish() {
connection.invoke("Finish", currentUser, 's1,s2').catch(console.error);
}
// 全场延迟
function delay() {
connection.invoke("Delay", currentUser, '').catch(console.error);
}
// 单个延迟(如果是多个用英文逗号隔开)
function singlDelay() {
connection.invoke("Delay", currentUser, 's1').catch(console.error);
}
</script>
场次1_老师_老师1
<button onclick="finish()">全场收卷</button>
<button onclick="singleFinish()">单收卷</button>
<button onclick="delay()">全场延时</button>
<button onclick="singlDelay()">单个延时</button>
4)学生端
javascript
<script src="signalr.min.js"></script>
<script>
// 当前用户标识(CompetitionSessionId_s_CompetitionUserId)
// 场次1_学生_学生1
const currentUser = "p1_s_s1";
// 建立连接时,把 user 作为参数传给后端
const connection = new signalR.HubConnectionBuilder()
.withUrl(`http://localhost:8082/chathub?user=${currentUser}`)
.build();
// 接收消息事件
connection.on("ReceiveMessage", (user, type, message) => {
console.log(`收到来自 ${user} 的消息: signalr操作类型:${type},内容:${message}`);
});
// 启动连接
connection.start().then(() => {
console.log("已连接 SignalR Hub 作为用户:", currentUser);
// 学生进入竞赛场次
connection.invoke("StudentLogin", currentUser).catch(console.error);
}).catch(console.error);
</script>
场次1_学生_学生1