ASP.NET Core SignalR的基本使用

文章目录


前言

ASP.NET Core 中, SignalR 是用于实现实时、双向通信的技术。

一、SignalR

是什么?

一个由微软开发的高级库 ,构建在 ASP.NET Core 之上,用于简化向应用添加实时 Web 功能。

  • 核心目标 : 让开发者能够轻松实现服务器到客户端的实时推送(例如:聊天、通知、仪表盘更新、协作编辑)。

  • 抽象层 : 它在底层自动选择并使用最佳的传输协议 来建立实时连接。首选是 WebSocket ,但如果 WebSocket 不可用(例如旧浏览器、某些网络限制),它会自动优雅降级到其他技术,如 Server-Sent Events (SSE)Long Polling。开发者无需关心底层使用的是哪种传输方式。

  • 基于 Hub 的模型SignalR 的核心抽象是 HubHub 是一个高级管道,允许客户端和服务器直接相互调用方法(RPC 风格)。

ASP.NET Core 中的关键特性:

  • 自动传输协商与回退: 无缝处理连接建立和传输选择。
  • 连接管理: 内置管理连接的生命周期、连接组(Groups)和用户(Users),方便实现广播(所有客户端)、组播(特定组)、单播(特定客户端或用户)。
  • 自动重新连接: 提供客户端 API 在连接意外断开时尝试自动重新连接。
  • 简单的编程模型 (RPC)
    • 服务器端 : 定义继承自 Hub 的类,并在其中声明客户端可以调用的 public 方法。
    • 客户端: 提供多种语言的客户端库(JavaScript, .NET, Java 等),调用服务器 Hub 上的方法,并注册处理程序来响应服务器调用的方法。
  • 可扩展性 : 支持通过 SignalR Backplane(如 Azure SignalR Service, Redis)将消息分发到多个服务器实例,实现横向扩展。
  • ASP.NET Core 集成: 深度集成身份认证(如 [Authorize] 特性)、依赖注入等。

SignalR 工作原理简图:

复制代码
```csharp
[Client]  <---(首选 WebSocket, 次选 SSE/Long Polling)--->  [ASP.NET Core Server]
    |                                                         |
    |----(调用) ServerMethod(args) ------------------->| (Hub 方法)
    |<---(调用) ClientMethod(args) --------------------| (Clients.Caller, Clients.All, etc.)
```

二、使用步骤

1.创建ASP.NET Core web Api 项目

2.添加 SignalR 包

  1. 执行安装命令

    csharp 复制代码
    install-package Microsoft.AspNetCore.SignalR

3.创建 SignalR Hub

  1. MyHubService.cs

    csharp 复制代码
    using Microsoft.AspNetCore.SignalR;
    
    namespace SignalRDemo.HubService
    {
        public class MyHubService:Hub
        {
            public Task SendMessageAsync(string user,string content)
            {
                var connectionId=this.Context.ConnectionId;
                string msg = $"{connectionId},{DateTime.Now.ToString()}:{user}";
                return Clients.All.SendAsync("ReceivePubMsg", msg, content);
            }
        }
    }

4.配置服务与中间件

  1. Program.cs

    csharp 复制代码
    using SignalRDemo.HubService;
    
    var builder = WebApplication.CreateBuilder(args);
    
    // Add services to the container.
    
    builder.Services.AddControllers();
    // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
    // 添加 SignalR 服务
    builder.Services.AddSignalR();
    //跨域
    string[] urls = new[] { "http://localhost:5173" };
    builder.Services.AddCors(opt => 
            opt.AddDefaultPolicy(builder => builder.WithOrigins(urls)
            .AllowAnyMethod().AllowAnyHeader().AllowCredentials())
            );
            
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    if (app.Environment.IsDevelopment())
    {
        app.UseSwagger();
        app.UseSwaggerUI();
    }
    app.UseCors();
    app.UseHttpsRedirection();
    
    app.UseAuthorization();
    // 配置路由
    app.MapHub<MyHubService>("/Hubs/MyHubService");// SignalR 终结点
    app.MapControllers();
    
    app.Run();

5.创建控制器(模拟服务器向客户端发送消息)

  1. TestController.cs

    csharp 复制代码
    using Microsoft.AspNetCore.Http;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.SignalR;
    using SignalRDemo.Entity;
    using SignalRDemo.HubService;
    
    namespace SignalRDemo.Controllers
    {
        [Route("api/[controller]/[action]")]
        [ApiController]
        public class TestController : ControllerBase
        {
            private readonly IHubContext<MyHubService> _hubContext;
    
            public TestController(IHubContext<MyHubService> hubContext)
            {
                _hubContext = hubContext;
            }
    
            [HttpPost("broadcast")]
            public async Task<IActionResult> BroadcastMessage([FromBody] MessageModel msg)
            {
                // 从服务端主动推送消息
                await _hubContext.Clients.All.SendAsync("ReceivePubMsg", msg.User, msg.Content);
                return Ok();
            }
        }
    }

6.创建Vue前端项目(模拟客户端发送消息)

  1. 打开文件夹(D:\Project\MyProject\SignalRProject\SignalRDemo)

  2. 创建文件夹:Front

  3. 当前文件夹下运行cmd

  4. 执行命令

    • npm create vite@latest SignalRClient1
    • 输入y,回车
    • 选择JavaScript
    • 等待项目创建完成
    • npm
    • npm run dev
  5. 进入前端项目文件夹D:\Project\MyProject\SignalRProject\SignalRDemo\Front\SignalClient1\src\components,编辑HelloWorld.vue文件。

  6. HelloWorld.vue

    csharp 复制代码
    <template>
        <div style="padding: 20px; max-width: 800px; margin: 0 auto;">
          <h2 style="color: #2c3e50;">SignalR 聊天室</h2>
          
          <div style="margin-bottom: 20px; display: flex; align-items: center;">
            <label style="margin-right: 10px; font-weight: bold; min-width: 80px;">用户:</label>
            <input 
              type="text" 
              v-model="state.userMsg" 
              @keydown.enter="sendMessage"
              placeholder="输入消息后按回车发送"
              :disabled="!state.isConnected || state.isConnecting"
              style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; flex: 1;"
            />
     	<label style="margin-right: 10px; font-weight: bold; min-width: 80px;">消息内容:</label>
            <input 
              type="text" 
              v-model="state.contentMsg" 
              @keydown.enter="sendMessage"
              placeholder="输入消息后按回车发送"
              :disabled="!state.isConnected || state.isConnecting"
              style="padding: 10px; border: 1px solid #ddd; border-radius: 4px; flex: 1;"
            />
          </div>
          
          <div style="margin-bottom: 20px; background: #f8f9fa; padding: 15px; border-radius: 4px;">
            <div style="display: flex; margin-bottom: 10px;">
              <label style="margin-right: 10px; font-weight: bold; min-width: 80px;">服务器:</label>
              <input
                type="text"
                v-model="state.serverUrl"
                placeholder="输入 SignalR Hub URL"
                style="padding: 8px; border: 1px solid #ddd; border-radius: 4px; flex: 1;"
              />
            </div>
            <button 
              @click="reconnect"
              style="padding: 8px 15px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer;"
            >
              {{ state.isConnected ? '重新连接' : '连接' }}
            </button>
          </div>
          
          <div style="border: 1px solid #e0e0e0; border-radius: 4px; overflow: hidden; margin-bottom: 20px;">
            <div style="background: #f0f0f0; padding: 10px; font-weight: bold;">消息记录</div>
            <div style="max-height: 300px; overflow-y: auto; padding: 10px; background: white;">
              <div v-for="(msg, index) in state.messages" :key="index" style="padding: 8px 0; border-bottom: 1px solid #f5f5f5;">
               	  {{ msg }}
              </div>
               <div v-if="state.messages.length === 0" style="text-align: center; color: #999; padding: 20px;">
                暂无消息
              </div>
            </div>
          </div>
          
          <div :style="{
            padding: '12px',
            borderRadius: '4px',
            marginBottom: '15px',
            backgroundColor: state.connectionStatus.includes('失败') ? '#ffebee' : 
                            state.connectionStatus.includes('连接') ? '#e8f5e9' : '#e3f2fd',
            color: state.connectionStatus.includes('失败') ? '#b71c1c' : 
                   state.connectionStatus.includes('连接') ? '#1b5e20' : '#0d47a1',
            border: state.connectionStatus.includes('失败') ? '1px solid #ffcdd2' : 'none'
          }">
            <div style="font-weight: bold; margin-bottom: 5px;">连接状态:</div>
            <div>{{ state.connectionStatus }}</div>
            <div v-if="state.errorDetails" style="margin-top: 10px; font-size: 0.9em; color: #b71c1c;">
              <div style="font-weight: bold;">错误详情:</div>
              <div style="word-break: break-all;">{{ state.errorDetails }}</div>
            </div>
          </div>
        </div>
      </template>
      
      <script>
      import { reactive, onMounted, onUnmounted } from 'vue';
      import * as signalR from '@microsoft/signalr';
      
      export default {
        setup() {
          const state = reactive({
            userMsg: "",
            contentMsg:"",
            messages: [],
            connectionStatus: "正在初始化...",
            isConnected: false,
            isConnecting: false,
            serverUrl: "https://localhost:7183/Hubs/MyHubService",
            errorDetails: "",
            connection: null,
            retryCount: 0
          });
      
          const sendMessage = async () => {
            if (!state.userMsg.trim()) return;
            
            if (!state.isConnected || !state.connection) {
              state.connectionStatus = "连接尚未建立,无法发送消息";
              return;
            }
            
            try {
            // 尝试多种可能的服务端方法名
            const possibleMethods = [
              "SendMessage",          // 标准命名
              "SendMessageAsync",     // Async后缀命名
              "BroadcastMessage",     // 其他可能命名
              "SendToAll",            // 另一种常见命名
              "PublishMessage"        // 备用命名
            ];
            
            let lastError = null;
            
            // 依次尝试所有可能的方法名
            for (const method of possibleMethods) {
              try {
                await state.connection.invoke(method, state.userMsg,state.contentMsg);
                state.userMsg = "";
                state.contentMsg="";
                return; // 成功发送则退出
              } catch (error) {
                lastError = error;
                console.log(`尝试调用 ${method} 失败:`, error.message);
              }
            }
            
            // 所有方法都失败
            state.connectionStatus = `发送失败: 未找到服务端方法`;
            state.errorDetails = `尝试的方法: ${possibleMethods.join(", ")}\n错误: ${lastError.message}`;
            
            } catch (error) {
                state.connectionStatus = `发送失败: ${error.message}`;
                state.errorDetails = error.toString();
            }
          };
      
          const initSignalRConnection = async () => {
            state.isConnecting = true;
            state.connectionStatus = "正在连接...";
            state.errorDetails = "";
            
            try {
              // 清理现有连接
              if (state.connection) {
                await state.connection.stop();
                state.connection = null;
              }
              
              // 创建新连接
              state.connection = new signalR.HubConnectionBuilder()
                .withUrl(state.serverUrl, {
                  skipNegotiation: true, // 尝试跳过协商步骤
                  transport: signalR.HttpTransportType.WebSockets // 强制使用 WebSockets
                })
                .withAutomaticReconnect({
                  nextRetryDelayInMilliseconds: retryContext => {
                    state.retryCount = retryContext.previousRetryCount + 1;
                    return Math.min(1000 * Math.pow(2, state.retryCount), 30000);
                  }
                })
                .configureLogging(signalR.LogLevel.Debug) // 启用详细调试日志
                .build();
              
              // 消息接收处理
              state.connection.on('ReceiveMessage', rcvMsg => {
                state.messages.push(rcvMsg);
              });
              
              state.connection.on('ReceivePubMsg', (rcvMsg,rcvContent) => {
                state.messages.push(rcvMsg,rcvContent);
              });
              
              // 连接状态变化
              state.connection.onreconnecting(() => {
                state.isConnected = false;
                state.isConnecting = true;
                state.connectionStatus = "连接丢失,正在重连...";
              });
              
              state.connection.onreconnected(connectionId => {
                state.isConnected = true;
                state.isConnecting = false;
                state.retryCount = 0;
                state.connectionStatus = `已重新连接 (ID: ${connectionId})`;
              });
              
              state.connection.onclose(error => {
                state.isConnected = false;
                state.isConnecting = false;
                state.connectionStatus = error 
                  ? `连接关闭: ${error.message}` 
                  : "连接已关闭";
              });
              
              // 启动连接
              await state.connection.start();
              state.isConnected = true;
              state.isConnecting = false;
              state.retryCount = 0;
              state.connectionStatus = `已连接 (ID: ${state.connection.connectionId})`;
              
              console.log("SignalR 连接详情:", state.connection);
            } catch (error) {
              console.error("SignalR 连接失败:", error);
              state.isConnected = false;
              state.isConnecting = false;
              state.connectionStatus = `连接失败: ${error.message}`;
              state.errorDetails = error.toString();
              
              // 提供详细的错误诊断
              if (error.message.includes("Failed to fetch")) {
                state.errorDetails += "\n\n可能的原因:\n" +
                  "1. CORS 问题 - 确保服务器已启用 CORS\n" +
                  "2. URL 错误 - 检查服务器地址是否正确\n" +
                  "3. 证书问题 - 尝试访问服务器URL查看证书是否有效\n" +
                  "4. 服务器未运行 - 确保后端服务正在运行";
              }
            }
          };
      
          const reconnect = async () => {
            await initSignalRConnection();
          };
      
          onMounted(() => {
            initSignalRConnection();
          });
      
          onUnmounted(() => {
            if (state.connection) {
              state.connection.stop();
            }
          });
      
          return { state, sendMessage, reconnect };
        }
      }
      </script>
      
      <style>
      body {
        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        background-color: #f5f7fa;
        margin: 0;
        padding: 20px;
        color: #333;
      }
      
      input {
        font-size: 1rem;
        transition: border-color 0.3s;
      }
      
      input:focus {
        outline: none;
        border-color: #3498db;
        box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
      }
      
      button {
        font-weight: 500;
        transition: background-color 0.3s;
      }
      
      button:hover {
        background-color: #2980b9 !important;
      }
      
      button:disabled {
        background-color: #bdc3c7 !important;
        cursor: not-allowed;
      }
      </style>

7.运行使用

  1. 客户端推送消息
    • 访问前端地址: http://localhost:5173/ (打开多个浏览器窗口测试消息广播)
    • 输入消息,回车

  2. 服务端推送 (多个客户端都可接收到服务端推送的消息)


三、关键配置说明

  • 跨域支持 (CORS)
    若客户端在不同域,在 Program.cs 添加:

    csharp 复制代码
    .....
    //跨域
    string[] urls = new[] { "http://localhost:5173" };
    builder.Services.AddCors(opt => 
            opt.AddDefaultPolicy(builder => builder.WithOrigins(urls)
            .AllowAnyMethod().AllowAnyHeader().AllowCredentials())
            );
     ....
    
    app.UseCors();
    app.UseHttpsRedirection();
    ......
  • 配置路由
    在 Program.cs 添加:

    csharp 复制代码
    app.MapHub<MyHubService>("/Hubs/MyHubService");// SignalR 终结点
    app.MapControllers();

四、故障排查

  • 连接失败 404

    检查终结点路由是否匹配 app.MapHub("/chatHub")

  • 跨域问题

    确保启用 CORS 并正确配置

  • HTTPS 证书

    开发环境下信任本地证书或改用 HTTP


总结

SignalR : 是构建在 WebSocket (和其他传输) 之上的高级框架。它抽象了底层复杂性,提供了极其便利的编程模型(Hub)、内置的连接管理、自动传输回退和重连机制,极大地简化了在 ASP.NET Core 中开发各种实时功能的过程。它是大多数需要服务器主动推送消息的 ASP.NET Core 实时应用的首选。

相关推荐
CRMEB定制开发17 分钟前
PHP 支付系统扩展实战:从微信 / 支付宝到银联的多驱动架构设计
后端
史文豪17 分钟前
nacos2.5.1版本基于docker的自定义部署(适配人大金仓)
后端
aiopencode18 分钟前
移动端抓包指南:为什么真机HTTPS总是抓不到?(多工具对比 + Sniffmaster实战)
后端
冰茶_24 分钟前
结构型设计模式之桥接模式
学习·设计模式·微软·c#·.netcore·桥接模式
追逐时光者28 分钟前
一款 .NET 开源、免费的适用于 Windows 下 PC 版微信/QQ/TIM的防撤回补丁(我已经看到了,撤回也没用了)!!
后端·.net
IT_陈寒41 分钟前
开发者必看!5个VSCode隐藏技巧让你的编码效率提升200% 🚀
前端·人工智能·后端
保持学习ing1 小时前
黑马Java面试笔记之 微服务篇(SpringCloud)
java·笔记·后端·阿里云·面试·负载均衡·springcloud
Chan161 小时前
【 SpringCloud | 微服务 网关 】
java·spring boot·后端·spring·spring cloud·微服务
Moment1 小时前
别小看 FAQ,用户体验和转化都离不开它!
前端·后端·面试
三分恶1 小时前
支付新手常犯的十个错误
后端