SignalR实时通信
SignalR
.NET8 使用API构建SignalR服务Demo
-
在
Program中注册和配置SignalR服务//注册SignalR服务 builder.Services.AddSignalR(); //添加跨域支持(如果时跨域Web请求SignalR服务需要配置) builder.Services.AddCors(options => { options.AddPolicy("AllowAllOrigins", p => p.WithOrigins("http://localhost:5287").AllowAnyMethod().AllowAnyHeader().AllowCredentials()); }); //配置SignalR服务 为当前应用程序地址+/myhub app.MapHub<MyHub>("/myhub").RequireCors("AllowAllOrigins"); //使用CORS策略 app.UseCors("AllowAllOrigins"); //AllowAllOrigins 表明使用的策略名称提示:在客户端中调用SignalR 无跨域问题,web端需要处理
-
实现MyHub类
[EnableCors("AllowAllOrigins")] public class MyHub : Hub { // 定义用户映射类(示例) private static readonly ConcurrentDictionary<string, string> _userConnections = new ConcurrentDictionary<string, string>(); public async Task SendMessage(paramsEntity param) { await Clients.All.SendAsync("ReceiveMessage", param); } /// <summary> /// 向其他用户发送消息 /// </summary> /// <param name="param"></param> /// <returns></returns> public async Task SendOtherMessage(paramsEntity param) { await Clients.Others.SendAsync("ReceiveMessage", param); } [HubMethodName("RegisterUser")] // Hub 方法:注册用户与连接的映射 public async Task RegisterUser(string userId) { // 检查用户是否已存在 if (_userConnections.TryGetValue(userId, out var oldConnectionId)) { // 断开旧连接 try { //向客户端发送断开连接标识 await Clients.Client(oldConnectionId).SendAsync("ForceDisconnect", "您的账号已在其他设备登录"); //服务端主动强制关闭连接 } catch (Exception ex) { // 记录错误但不影响新连接 Console.WriteLine($"断开旧连接失败: {ex.Message}"); } // 从映射中移除旧连接 _userConnections.TryRemove(userId, out _); } _userConnections.AddOrUpdate(userId, Context.ConnectionId, (key, oldValue) => Context.ConnectionId); // 方法1:直接使用已有的 ReceiveMessage 通知 await Clients.Others.SendAsync("ReceiveMessage", new paramsEntity { user = "系统通知", message = $"{userId} 已上线" }); // 方法2:如果客户端已注册 UserConnected,也可以同时发送 //await Clients.All.SendAsync("UserConnected", userId); } [HubMethodName("SendPrivateMessage")] // Hub 方法:向指定用户发送消息 public async Task SendToUser(string userId, paramsEntity param) { if (_userConnections.TryGetValue(userId, out var connectionId)) { await Clients.Client(connectionId).SendAsync("ReceiveMessage", param); } } [HubMethodName("Disconnected")] // 处理客户端断开连接 public override async Task OnDisconnectedAsync(Exception? exception) { // 从映射中移除断开的连接 var userId = _userConnections.FirstOrDefault(x => x.Value == Context.ConnectionId).Key; if (userId != null) { _userConnections.TryRemove(userId, out _); await Clients.Others.SendAsync("ReceiveMessage", new paramsEntity { user = "系统通知", message = $"{userId} 已下线" }); } await base.OnDisconnectedAsync(exception); } } public class paramsEntity { public string user { get; set; } public string message { get; set; } } -
Web前端实现
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>SignalR 测试工具</title> <script src="https://cdn.tailwindcss.com"></script> <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet"> <!--<script src="https://cdn.jsdelivr.net/npm/@microsoft/signalr@6.0.10/dist/browser/signalr.min.js"></script>--> <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.1/signalr.js"></script> <!-- Tailwind 配置 --> <script> tailwind.config = { theme: { extend: { colors: { primary: '#165DFF', secondary: '#0FC6C2', success: '#00B42A', warning: '#FF7D00', danger: '#F53F3F', dark: '#1D2129', 'dark-2': '#4E5969', 'light-1': '#F2F3F5', 'light-2': '#E5E6EB', }, fontFamily: { inter: ['Inter', 'system-ui', 'sans-serif'], }, }, } } </script> <style type="text/tailwindcss"> @layer utilities { .content-auto { content-visibility: auto; } .scrollbar-hide::-webkit-scrollbar { display: none; } .scrollbar-hide { -ms-overflow-style: none; scrollbar-width: none; } .message-appear { animation: fadeIn 0.3s ease-in-out; } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } } </style> </head> <body class="bg-gray-50 font-inter text-dark min-h-screen flex flex-col"> <!-- 导航栏 --> <header class="bg-white shadow-sm sticky top-0 z-10"> <div class="container mx-auto px-4 py-3 flex items-center justify-between"> <div class="flex items-center space-x-2"> <i class="fa fa-comments text-primary text-2xl"></i> <h1 class="text-xl font-bold text-dark">SignalR 测试工具</h1> </div> <div class="flex items-center space-x-4"> <button id="connectBtn" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition flex items-center"> <i class="fa fa-plug mr-2"></i> <span>连接</span> </button> <span id="connectionStatus" class="text-dark-2 text-sm hidden"> <i class="fa fa-circle-o text-warning"></i> 未连接 </span> </div> </div> </header> <!-- 主内容区 --> <main class="flex-grow container mx-auto px-4 py-6"> <div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> <!-- 消息发送区域 --> <div class="lg:col-span-1"> <div class="bg-white rounded-xl shadow-sm p-6 h-full flex flex-col"> <h2 class="text-lg font-semibold mb-4 flex items-center"> <i class="fa fa-paper-plane text-primary mr-2"></i> 发送消息 </h2> <div class="flex-grow"> <div class="mb-4"> <label for="messageType" class="block text-sm font-medium text-dark-2 mb-1">消息类型</label> <select id="messageType" class="w-full px-3 py-2 border border-light-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition"> <option value="broadcast">广播消息</option> <option value="private">私信消息</option> <option value="group">群组消息</option> </select> </div> <div id="userNameInputField" class="mb-4"> <label for="userNameInput" class="block text-sm font-medium text-dark-2 mb-1">用户名</label> <input type="text" id="userNameInput" placeholder="输入用户名" class="w-full px-3 py-2 border border-light-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition"> </div> <div id="recipientField" class="mb-4 hidden"> <label for="recipient" class="block text-sm font-medium text-dark-2 mb-1">接收者</label> <input type="text" id="recipient" placeholder="输入接收者ID" class="w-full px-3 py-2 border border-light-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition"> </div> <div class="mb-4"> <label for="messageContent" class="block text-sm font-medium text-dark-2 mb-1">消息内容</label> <textarea id="messageContent" rows="8" placeholder="输入要发送的消息..." class="w-full px-3 py-2 border border-light-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition resize-none"></textarea> </div> <div class="flex space-x-3"> <button id="sendMessageBtn" class="flex-grow px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed"> <i class="fa fa-send mr-2"></i> 发送消息 </button> <button id="clearInputBtn" class="px-4 py-2 border border-light-2 text-dark-2 rounded-lg hover:bg-light-1 transition"> <i class="fa fa-trash"></i> </button> </div> </div> </div> </div> <!-- 消息接收区域 --> <div class="lg:col-span-2"> <div class="bg-white rounded-xl shadow-sm p-6 h-full flex flex-col"> <div class="flex items-center justify-between mb-4"> <h2 class="text-lg font-semibold flex items-center"> <i class="fa fa-comments text-primary mr-2"></i> 消息接收 </h2> <div class="flex space-x-2"> <div class="relative"> <input type="text" id="messageFilter" placeholder="搜索消息..." class="pl-8 pr-3 py-1.5 text-sm border border-light-2 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition"> <i class="fa fa-search absolute left-3 top-2 text-dark-2/60"></i> </div> <button id="clearMessagesBtn" class="px-3 py-1.5 text-sm border border-light-2 text-dark-2 rounded-lg hover:bg-light-1 transition"> <i class="fa fa-eraser mr-1"></i> 清空 </button> </div> </div> <div id="messageContainer" class="flex-grow overflow-y-auto scrollbar-hide bg-light-1/50 rounded-lg p-4 mb-4"> <!-- 消息将动态添加到这里 --> <div class="text-center text-dark-2/60 py-10"> <i class="fa fa-comments-o text-3xl mb-2 block"></i> <p>尚未接收到任何消息</p> <p class="text-xs mt-1">连接服务器并发送消息后开始接收</p> </div> </div> <div class="flex items-center justify-between text-xs text-dark-2/60"> <div> <span id="totalMessages">0</span> 条消息 </div> <div> 最后更新: <span id="lastUpdate">-</span> </div> </div> </div> </div> </div> </main> <!-- 页脚 --> <footer class="bg-white py-4 border-t border-light-2"> <div class="container mx-auto px-4 text-center text-dark-2/70 text-sm"> <p>SignalR 测试工具 © 2023</p> </div> </footer> <script src="../lib/jquery/dist/jquery.js"></script> <script> // SignalR 连接对象 let connection = null; let isConnected = false; // DOM 元素 const connectBtn = document.getElementById('connectBtn'); const connectionStatus = document.getElementById('connectionStatus'); const sendMessageBtn = document.getElementById('sendMessageBtn'); const clearInputBtn = document.getElementById('clearInputBtn'); const clearMessagesBtn = document.getElementById('clearMessagesBtn'); const messageContent = document.getElementById('messageContent'); const messageContainer = document.getElementById('messageContainer'); const messageType = document.getElementById('messageType'); const recipientField = document.getElementById('recipientField'); const recipient = document.getElementById('recipient'); const totalMessages = document.getElementById('totalMessages'); const lastUpdate = document.getElementById('lastUpdate'); const messageFilter = document.getElementById('messageFilter'); let user = document.getElementById('userNameInput').value; // 初始化 init(); function init() { // 禁用发送按钮直到连接建立 sendMessageBtn.disabled = true; // 监听消息类型变化 messageType.addEventListener('change', () => { if (messageType.value === 'private') { recipientField.classList.remove('hidden'); } else { recipientField.classList.add('hidden'); } }); // 连接按钮点击事件 connectBtn.addEventListener('click', connectToSignalR); // 发送消息按钮点击事件 sendMessageBtn.addEventListener('click', sendMessage); // 清空输入按钮点击事件 clearInputBtn.addEventListener('click', () => { messageContent.value = ''; messageContent.focus(); }); // 清空消息按钮点击事件 clearMessagesBtn.addEventListener('click', () => { messageContainer.innerHTML = ` <div class="text-center text-dark-2/60 py-10"> <i class="fa fa-comments-o text-3xl mb-2 block"></i> <p>消息已清空</p> </div> `; totalMessages.textContent = '0'; }); // 消息过滤 messageFilter.addEventListener('input', filterMessages); } // 连接到 SignalR 服务器 function connectToSignalR() { if (isConnected) { disconnect(); return; } //$.ajax({ // url: "http://localhost:5299/WeatherForecast?message=123", // type: 'POST', // data: {}, // success: function () { // }, // dataType: "json", // contentType: "application/json" //}); // 创建 SignalR 连接 connection = new signalR.HubConnectionBuilder() .withUrl("http://localhost:5299/myhub") // 替换为您的 SignalR 服务器地址 .configureLogging(signalR.LogLevel.Information) .build(); // 定义接收消息的处理程序 connection.on("ReceiveMessage", (res) => { user = document.getElementById('userNameInput').value; if (res.user != user) { addMessage(res.user, res.message, "received"); } }); //监听强制断开连接 connection.on("ForceDisconnect", (message) => { alert(message); // 客户端主动断开连接 connection.stop().then(() => { // 更新UI状态 updateConnectionStatus("disconnected"); connectBtn.innerHTML = '<i class="fa fa-plug mr-2"></i><span>连接</span>'; connectBtn.classList.remove('bg-danger'); connectBtn.classList.add('bg-primary'); sendMessageBtn.disabled = true; }); }); // 连接状态变化 connection.onreconnecting((error) => { updateConnectionStatus("reconnecting", error); }); connection.onreconnected((connectionId) => { updateConnectionStatus("connected", connectionId); }); connection.onclose((error) => { updateConnectionStatus("disconnected", error); isConnected = false; connectBtn.innerHTML = '<i class="fa fa-plug mr-2"></i><span>连接</span>'; connectBtn.classList.remove('bg-danger'); connectBtn.classList.add('bg-primary'); sendMessageBtn.disabled = true; }); // 启动连接 connection.start() .then(() => { isConnected = true; updateConnectionStatus("connected"); connectBtn.innerHTML = '<i class="fa fa-plug mr-2"></i><span>断开</span>'; connectBtn.classList.remove('bg-primary'); connectBtn.classList.add('bg-danger'); sendMessageBtn.disabled = false; // 添加连接成功消息 addMessage("系统", "已成功连接到服务器", "system"); user = $('#userNameInput').val(); if (user == "") { alert("请先登录") } else { connection.invoke("RegisterUser", user); } }) .catch(error => { console.error("连接失败: ", error); updateConnectionStatus("disconnected", error); addMessage("系统", `连接失败: ${error.message}`, "error"); }); } // 断开连接 function disconnect() { if (connection) { connection.stop() .then(() => { isConnected = false; updateConnectionStatus("disconnected"); connectBtn.innerHTML = '<i class="fa fa-plug mr-2"></i><span>连接</span>'; connectBtn.classList.remove('bg-danger'); connectBtn.classList.add('bg-primary'); sendMessageBtn.disabled = true; addMessage("系统", "已断开与服务器的连接", "system"); }) .catch(error => { console.error("断开连接失败: ", error); addMessage("系统", `断开连接失败: ${error.message}`, "error"); }); } } // 更新连接状态显示 function updateConnectionStatus(status, info = null) { let statusText, statusIcon, statusColor; switch (status) { case "connected": statusText = "已连接"; statusIcon = "fa-circle"; statusColor = "text-success"; break; case "disconnected": statusText = "未连接"; statusIcon = "fa-circle-o"; statusColor = "text-warning"; break; case "reconnecting": statusText = "重连中..."; statusIcon = "fa-refresh fa-spin"; statusColor = "text-warning"; break; } connectionStatus.innerHTML = `<i class="fa ${statusIcon} ${statusColor}"></i> ${statusText}`; connectionStatus.classList.remove('hidden'); if (info) { console.log(`Connection ${status}:`, info); } } // 发送消息 function sendMessage() { const content = messageContent.value.trim(); if (!content) { alert("请输入消息内容"); return; } const type = messageType.value; let recipientValue = recipient.value.trim(); // 根据消息类型调用不同的方法 if (type === "private" && !recipientValue) { alert("请输入接收者ID"); return; } // 添加发送的消息到界面 addMessage("我", content, "sent"); // 调用服务器方法 //调用服务器接口发送消息 if (type === "private") { const userName = document.getElementById('userNameInput').value || '匿名用户' const recipient = document.getElementById('recipient').value || '匿名用户' //$.post("http://localhost:5299/SignalR", { receptID: recipient, param:{ user: userName, message: content } }, function (res) { //}, "json"); connection.invoke("SendPrivateMessage", recipientValue,{ user: userName, message: content }) .catch(error => { console.error("发送私信失败: ", error); addMessage("系统", `发送私信失败: ${error.message}`, "error"); }); } else if (type === "group") { connection.invoke("SendGroupMessage", "TestGroup", content) .catch(error => { console.error("发送群组消息失败: ", error); addMessage("系统", `发送群组消息失败: ${error.message}`, "error"); }); } else { // 广播消息 //connection.invoke("SendMessage", content) // .catch(error => { // console.error("发送广播消息失败: ", error); // addMessage("系统", `发送广播消息失败: ${error.message}`, "error"); // }); const userName = document.getElementById('userNameInput').value || '匿名用户' $.post("http://localhost:5299/WeatherForecast", { user: userName, message: content }, function (res) { },"json"); } // 清空输入框 messageContent.value = ''; messageContent.focus(); } // 添加消息到界面 function addMessage(sender, content, type) { // 移除空消息提示 if (messageContainer.querySelector('.text-center')) { messageContainer.innerHTML = ''; } // 创建消息元素 let messageClass, iconClass, bgColor; switch (type) { case "sent": messageClass = "ml-auto"; iconClass = "fa-paper-plane text-primary"; bgColor = "bg-primary/10"; break; case "received": messageClass = "mr-auto"; iconClass = "fa-comment-o text-secondary"; bgColor = "bg-white"; break; case "system": messageClass = "mx-auto text-center"; iconClass = "fa-info-circle text-dark-2"; bgColor = "bg-light-2/50"; break; case "error": messageClass = "mx-auto text-center"; iconClass = "fa-exclamation-circle text-danger"; bgColor = "bg-danger/10"; break; } const messageElement = document.createElement('div'); messageElement.className = `mb-3 max-w-[85%] ${messageClass} message-appear`; const timestamp = new Date().toLocaleTimeString(); if (type !== "system" && type !== "error") { messageElement.innerHTML = ` <div class="flex items-start"> <div class="mr-2 mt-1"> <i class="fa ${iconClass} text-xl"></i> </div> <div> <div class="text-xs text-dark-2/60 mb-0.5">${sender} <span class="ml-2">${timestamp}</span></div> <div class="${bgColor} p-3 rounded-lg shadow-sm break-words"> ${formatMessage(content)} </div> </div> </div> `; } else { messageElement.innerHTML = ` <div class="${bgColor} p-2 rounded-lg text-xs inline-block"> <i class="fa ${iconClass} mr-1"></i> ${content} <span class="ml-2">${timestamp}</span> </div> `; } messageContainer.appendChild(messageElement); // 滚动到底部 messageContainer.scrollTop = messageContainer.scrollHeight; // 更新消息计数和最后更新时间 updateMessageStats(); } // 格式化消息内容 function formatMessage(content) { // 简单的HTML转义 const escapedContent = content .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); // 替换换行符为<br> return escapedContent.replace(/\n/g, '<br>'); } // 更新消息统计信息 function updateMessageStats() { const count = messageContainer.querySelectorAll('.message-appear').length; totalMessages.textContent = count; lastUpdate.textContent = new Date().toLocaleTimeString(); } // 过滤消息 function filterMessages() { const filter = messageFilter.value.toLowerCase(); const messages = messageContainer.querySelectorAll('.message-appear'); messages.forEach(message => { const content = message.textContent.toLowerCase(); if (content.includes(filter)) { message.style.display = 'block'; } else { message.style.display = 'none'; } }); } </script> </body> </html> -
控制台实现
static async Task Main(string[] args) { Console.WriteLine("Hello, World!"); //创建连接 var connection = new HubConnectionBuilder().WithUrl("http://localhost:5299/myHub") .Build(); connection.On<paramsEntity>("ReceiveMessage", (res ) => { Console.WriteLine($"收到来自 {res.user} 的消息: {res.message}"); }); try { // 启动连接 await connection.StartAsync(); Console.WriteLine("已成功连接到SignalR服务!"); // 发送消息示例 Console.WriteLine("请输入您的名字:"); var userName = Console.ReadLine(); await connection.InvokeAsync("RegisterUser", userName); Console.WriteLine("请输入要发送的消息 (输入'退出'结束程序):"); while (true) { var message = Console.ReadLine(); if (message?.ToLower() == "退出") { break; } // 调用服务端方法 await connection.InvokeAsync("SendOtherMessage", new paramsEntity { user=userName, message=message }); } await connection.InvokeAsync("Disconnected"); } catch (Exception ex) { Console.WriteLine($"连接出错: {ex.Message}"); } finally { // 关闭连接 if (connection.State == HubConnectionState.Connected) { await connection.StopAsync(); } Console.WriteLine("已断开与SignalR服务的连接。"); } Console.WriteLine("按任意键退出..."); Console.ReadKey(); }-
总结:SignalR 在B/S中需要定义Hub服务,客户端与服务端通过约定的方法名,如上述中的
ReceiveMessageForceDisconnect服务端通过调用SendAsync 向ReceiveMessage发送消息param,客户端通过on监听接受消息。这种是由服务端向客户端发送消息。await Clients.All.SendAsync("ReceiveMessage", param); -
客户端需要向客户端发送消息
-
B/S中通过引用 signalr.js
<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.1/signalr.js"></script>创建 SignalR 连接
// 创建 SignalR 连接 connection = new signalR.HubConnectionBuilder() .withUrl("http://localhost:5299/myhub") // 替换为您的 SignalR 服务器地址 .configureLogging(signalR.LogLevel.Information) .build();通过
invoke方法调用后端服务MyHub定义的方法SendPrivateMessageconnection.invoke("SendPrivateMessage", recipientValue,{ user: userName, message: content }) .catch(error => { console.error("发送私信失败: ", error); addMessage("系统", `发送私信失败: ${error.message}`, "error"); });后端定义的
SendPrivateMessage指定客户端会话[HubMethodName("SendPrivateMessage")] // Hub 方法:向指定用户发送消息 public async Task SendToUser(string userId, paramsEntity param) { if (_userConnections.TryGetValue(userId, out var connectionId)) { await Clients.Client(connectionId).SendAsync("ReceiveMessage", param); } }
-
-

浙公网安备 33010602011771号