博客 (29)

SignalR 是一个开源的实时通信库,用于构建实时 Web 应用程序。它提供了一个简单的 API,可以在客户端和服务器之间建立持久连接,以便实时地推送数据。

与传统的 WebSocket 相比,SignalR 提供了更高级的功能和更简单的开发体验。下面是一些主要区别:

  • 支持多种传输方式:SignalR 可以使用多种传输方式,包括 WebSocket、Server-Sent Events(SSE)、长轮询和 Forever Frame。这使得 SignalR 在不同的环境中都能提供实时通信的能力,即使某些浏览器不支持 WebSocket,也可以使用其他传输方式。

  • 自动处理连接管理:SignalR 管理连接的生命周期,包括连接的建立、断开和重新连接。它会自动处理连接的失败和重新连接的逻辑,简化了开发人员的工作。

  • 服务器端推送:SignalR 允许服务器端主动推送消息给客户端,而不需要客户端发起请求。这使得实时通信变得更加高效和实时,适用于聊天应用、实时监控等场景。

  • 跨平台支持:SignalR 可以在多个平台上使用,包括 .NET、Java、JavaScript 等。这使得开发人员可以使用自己熟悉的语言和框架来构建实时应用程序。


微软官方提供了针对 ASP.NET Core Web 应用(Razor 页面)的详细教程,这里给出 MVC 版本入门教程。


最终将创建一个正常运行的聊天应用:

signalr-get-started-finished.png


创建 Web 应用项目

image.png

image.png


添加 SignalR 客户端库

在“解决方案资源管理器”>中,右键单击项目,然后选择“添加”“客户端库”。

在“添加客户端库”对话框中:

  • “提供程序”选择“unpkg”

  • “库”,请输入 @microsoft/signalr@latest。

  • 选择“选择特定文件”,展开“dist/browser”文件夹,然后选择 signalr.js 和 signalr.min.js。

  • 点击“安装” 。

image.png


创建 SignalR Hubs 类

using Microsoft.AspNetCore.SignalR;

/// <summary>
/// Hub 类管理连接、组和消息
/// </summary>
public class ChatHub : Hub
{
        /// <summary>
        /// 可通过已连接客户端调用 SendMessage,以向所有客户端发送消息
        /// </summary>
    public async Task SendMessage(string user, string message)
    {
        // Clients.All 向所有的客户端发送消息
        // ReceiveMessage 是客户端监听的方法
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}

其父类 Hub 可管理连接、组和消息。这里演示的是向所有客户端发送消息。


配置 SignalR

打开 Program.cs,添加注入:

builder.Services.AddSignalR();

添加路由:

app.MapHub<ChatHub>("/chatHub");


添加 SignalR 客户端代码

视图页面:

<div class="container">
    <div class="row p-1">
        <div class="col-1">用户</div>
        <div class="col-5"><input type="text" id="userInput" /></div>
    </div>
    <div class="row p-1">
        <div class="col-1">消息</div>
        <div class="col-5"><input type="text" class="w-100" id="messageInput" /></div>
    </div>
    <div class="row p-1">
        <div class="col-6 text-end">
            <input type="button" id="sendButton" value="发送消息" />
        </div>
    </div>
    <div class="row p-1">
        <div class="col-6">
            <hr />
        </div>
    </div>
    <div class="row p-1">
        <div class="col-6">
            <ul id="messagesList"></ul>
        </div>
    </div>
</div>
<script src="~/lib/microsoft/signalr/dist/browser/signalr.min.js"></script>
<script src="~/js/chat.js"></script>

chat.js 文件:

"use strict";

var connection = new signalR.HubConnectionBuilder().withUrl("/chatHub").build();

// 在连接建立之前禁用发送按钮
document.getElementById("sendButton").disabled = true;

connection.on("ReceiveMessage", function (user, message) {
    var li = document.createElement("li");
    document.getElementById("messagesList").appendChild(li);
    // 修改此处时应注意脚本注入问题
    li.textContent = `${user} says ${message}`;
});

connection.start().then(function () {
    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("SendMessage", user, message).catch(function (err) {
        return console.error(err.toString());
    });
    event.preventDefault();
});


完成。在线示例:https://xoyozo.net/Demo/SignalRDemo






xoyozo 5 个月前
434

推荐使用 SignalR 来平替 WebSocket,参考教程


【服务端(.NET)】

一、创建 WebSocketHandler 类,用于处理客户端发送过来的消息,并分发到其它客户端

public class WebSocketHandler
{
    private static List<WebSocket> connectedClients = [];

    public static async Task Handle(WebSocket webSocket)
    {
        connectedClients.Add(webSocket);

        byte[] buffer = new byte[1024];
        WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
        while (!result.CloseStatus.HasValue)
        {
            string message = Encoding.UTF8.GetString(buffer, 0, result.Count);
            Console.WriteLine($"Received message: {message}");

            // 处理接收到的消息,例如广播给其他客户端
            await BroadcastMessage(message, webSocket);

            result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
        }

        connectedClients.Remove(webSocket);
        await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
    }

    private static async Task BroadcastMessage(string message, WebSocket sender)
    {
        foreach (var client in connectedClients)
        {
            if (client != sender && client.State == WebSocketState.Open)
            {
                await client.SendAsync(Encoding.UTF8.GetBytes(message), WebSocketMessageType.Text, true, CancellationToken.None);
            }
        }
    }
}

二、在 Program.cs 或 Startup.cs 中插入:

// 添加 WebSocket 路由
app.UseWebSockets();
app.Use(async (context, next) =>
{
    if (context.Request.Path == "/chat")
    {
        if (context.WebSockets.IsWebSocketRequest)
        {
            WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
            await WebSocketHandler.Handle(webSocket);
        }
        else
        {
            context.Response.StatusCode = 400;
        }
    }
    else
    {
        await next();
    }
});

这里的路由路径可以更改,此处将会创建一个 WebSocket 连接,并将其传递给 WebSocketHandler.Handle 方法进行处理。

也可以用控制器来接收 WebSocket 消息。


【客户端(JS)】

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
    <title>WebSocket 示例(JS + ASP.NET)</title>
    <style>
        #list { width: 100%; max-width: 500px; }
        .tip { color: red; }
    </style>
</head>
<body>
    <h2>WebSocket 示例(JS + ASP.NET)</h2>
    <textarea id="list" readonly rows="20"></textarea>
    <div>
        <input type="text" id="msg" />
        <button onclick="fn_send()">发送</button>
        <button onclick="fn_disconnect()">断开</button>
    </div>
    <p id="tip_required">请输入要发送的内容</p>
    <p id="tip_closed">WebSocket 连接未建立</p>

    <script>
        var wsUrl = "wss://" + window.location.hostname + ":" + window.location.port + "/chat";
        var webSocket;
        var domList = document.getElementById('list');

        // 初始化 WebSocket 并连接
        function fn_ConnectWebSocket() {
            webSocket = new WebSocket(wsUrl);

            // 向服务端发送连接请求
            webSocket.onopen = function (event) {
                var content = domList.value;
                content += "[ WebSocket 连接已建立 ]" + '\r\n';
                domList.innerHTML = content;

                document.getElementById('tip_closed').style.display = 'none';

                webSocket.send(JSON.stringify({
                    msg: 'Hello'
                }));
            };

            // 接收服务端发送的消息
            webSocket.onmessage = function (event) {
                if (event.data) {
                    var content = domList.value;
                    content += event.data + '\r\n';
                    domList.innerHTML = content;
                    domList.scrollTop = domList.scrollHeight;
                }
            };

            // 各种情况导致的连接关闭或失败
            webSocket.onclose = function (event) {
                var content = domList.value;
                content += "[ WebSocket 连接已关闭,3 秒后自动重连 ]" + '\r\n';
                domList.innerHTML = content;

                document.getElementById('tip_closed').style.display = 'block';

                setTimeout(function () {
                    var content = domList.value;
                    content += "[ 正在重连... ]" + '\r\n';
                    domList.innerHTML = content;
                    fn_ConnectWebSocket();
                }, 3000); // 3 秒后重连
            };
        }

        // 检查连接状态
        function fn_IsWebSocketConnected() {
            if (webSocket.readyState === WebSocket.OPEN) {
                document.getElementById('tip_closed').style.display = 'none';
                return true;
            } else {
                document.getElementById('tip_closed').style.display = 'block';
                return false;
            }
        }

        // 发送内容
        function fn_send() {
            if (fn_IsWebSocketConnected()) {
                var message = document.getElementById('msg').value;
                document.getElementById('tip_required').style.display = message ? 'none' : 'block';
                if (message) {
                    webSocket.send(JSON.stringify({
                        msg: message
                    }));
                }
            }
        }

        // 断开连接
        function fn_disconnect() {
            if (fn_IsWebSocketConnected()) {
                // 部分浏览器调用 close() 方法关闭 WebSocket 时不支持传参
                // webSocket.close(001, "Reason");
                webSocket.close();
            }
        }

        // 执行连接
        fn_ConnectWebSocket();
    </script>
</body>
</html>


【在线示例】

https://xoyozo.net/Demo/JsNetWebSocket


【其它】

  • 服务端以 IIS 作为 Web 服务器,那么需要安装 WebSocket 协议

  • 一个连接对应一个客户端(即 JS 中的 WebSocket 对象),注意与会话 Session 的区别

  • 在实际项目中使用应考虑兼容性问题

  • 程序设计应避免 XSS、CSRF 等安全隐患

  • 参考文档:https://learn.microsoft.com/zh-cn/aspnet/core/fundamentals/websockets

xoyozo 5 个月前
411

有一个项目,在 VS2022 中正常打开,在 VS2019 中无法加载项目,项目目标框架是 .NET Standard 2.0。报错内容:

error: 无法打开项目文件。 .NET SDK 的版本 7.0.306 至少需要 MSBuild 的 17.4.0 版本。当前可用的 MSBuild 版本为 16.11.2.50704。请将在 global.json 中指定的 .NET SDK 更改为需要当前可用的 MSBuild 版本的旧版本。

解决方法:

运行 Visual Studio Installer,选择 Visual Studio 2019 进行修改,切换到“单个组件”,勾选“.NET SDK (out of support)”,会联动勾选“.NET 5.0 Runtime (Out of support)”与“.NET Core 3.1 Runtime (Out of support)”。

QQ截图20230728135007.png

xoyozo 10 个月前
5,554

您需要将 IWebHostEnvironment 注入到类中才能访问属性值 ApplicationBasePath:阅读有关依赖关系注入的信息。成功注入依赖项后,wwwroot 路径应该可供您使用。例如:

private readonly IWebHostEnvironment _appEnvironment;

public ProductsController(IWebHostEnvironment appEnvironment)
{
   _appEnvironment = appEnvironment;
}

用法:

 [HttpGet]
 public IEnumerable<string> Get()
 {
    FolderScanner scanner = new FolderScanner(_appEnvironment.WebRootPath);
    return scanner.scan();
 }

编辑:在 ASP.NET 的更高版本中,IHostingEnvironment 已被替换为 IWebHostEnvironment

O
转自 Oluwafemi 2 年前
2,886

POST

axios.post('AjaxCases', {
    following: true,
    success: null,
    page: 1,
}).then(function (response) {
    console.log(response.data)
}).catch(function (error) {
    console.log(error.response.data);
});
[HttpPost]
public IActionResult AjaxCases([FromBody] AjaxCasesRequestModel req)
{
    bool? following = req.following;
    bool? success = req.success;
    int page = req.page;
}

GET

axios.get('AjaxCase', {
    int: 123,
}).then(function (response) {
    console.log(response.data)
}).catch(function (error) {
    console.log(error.response.data);
});
[HttpGet]
public IActionResult AjaxCases([FromQuery] int id)
{
}


* 若服务端获取参数值为 null,可能的情况如下:

  • 请检查相关枚举类型是否已设置 [JsonConverter] 属性,参:C# 枚举(enum)用法笔记,且传入的值不能为空字符串,可以传入 null 值,并将服务端 enum 类型改为可空。可以以 string 方式接收参数后再进行转换。

  • 模型中属性名称不能重复(大小写不同也不行)。

  • 布尔型属性值必须传递布尔型值,即在 JS 中,字符串 "true" 应 JSON.parse("true") 为 true 后再回传给服务端。

xoyozo 2 年前
1,940

直接运行项目启动程序(在发布目录中,以项目名称为文件名,扩展名为 .exe 的可执行程序)

启动后可在“Now listening on:”所在行看到访问入口 URL(如图中 http://localhost:5000)。

在同服浏览器中打开该地址,访问到报错页面,回到该命令行窗口可见“fail”异常的详细信息。

QQ截图20211003173208.png

xoyozo 3 年前
1,685

当 ASP.NET 发布一个新的小版本更新时(以 .NET 5.0.10 为例),生产环境服务器端可以直接下载安装 .NET 5.0.10 来自动替换 .NET 5.0.9。

倘若这时直接使用 VS 将 nuget 中版本为 5.0.9 的组件更新到 5.0.10 发布到服务器上的话,可能会发生错误:

HTTP Error 500.30 - ASP.NET Core app failed to start

这里我们可以在 nuget 回滚相关组件的版本至 5.0.9,等 VS 发现新的版本更新时,更新 VS 开发工具后,再更新 nuget 组件并进行发布即可。

着急的小伙伴可以直接前往 Microsoft .NET 网站下载安装包含 Runtime 5.0.10 的 SDK:

image.png

该情况同样发生在 ASP.NET Core 3.1 等大版本上。

若仍未生效,请重启服务器

----------------

后注:安装完成后重启服务器可避免以上烦恼!

xoyozo 3 年前
1,473

本文写于 2021 年 7 月。

WebBrowser 不能改用 Chrome 内核。

在 .NET Frameworks 和 .NET Core 年代,常用的第三方组件有:WebKit.NET、CefSharp 等。

而并未找到适用于 .NET 5 的成熟的 Chromium 内核的浏览器组件。

不过还是推荐一款微软官方出品的 WebView2,需要安装 Microsoft Edge Canary 才能正常运行。


xoyozo 3 年前
1,973

本文使用 Oracle 官方提供的 MySql.EntityFrameworkCore,如使用 Pomelo.EntityFrameworkCore.MySql 请移步

MySql.EntityFrameworkCore 是 MySql.Data.EntityFrameworkCore 的升级版


本文以 Visual Studio 2019、ASP.NET Core 5.0 开发环境为例。

  1. 新建 ASP.NET Core Web 应用程序。

  2. 安装 NuGet 包:

    MySql.EntityFrameworkCore

    Microsoft.EntityFrameworkCore.Design

    image.png

  3. 根据已有数据库创建数据模型。在 NuGet 的程序包管理(Package Manager)控制台中(PowerShell)执行命令:

    Scaffold-DbContext "server=数据库服务器;port=3306;user=数据库用户名;password=数据库密码;database=数据库名" MySql.EntityFrameworkCore -OutputDir Data -f

    .Net Core CLi:

    dotnet ef dbcontext scaffold "server=数据库服务器;port=3306;user=数据库用户名;password=数据库密码;database=数据库名" MySql.EntityFrameworkCore -o Data -f
  4. 搞定。


注:开发环境和生产环境都不需要安装 Connector/NET,只需要安装 ASP.NET Core。

补充:其它数据库提供程序请参考:https://docs.microsoft.com/zh-cn/ef/core/providers/

更多高级用法请参考官方文档


xoyozo 3 年前
3,695

获取表名:

using Microsoft.EntityFrameworkCore;
var entityType = _context.Model.FindEntityType(typeof(表实体));
var tableName = entityType.GetTableName();

获取字段名:

using Microsoft.EntityFrameworkCore;
var entityType = _context.Model.FindEntityType(typeof(表实体));
var dic = new Dictionary<string, string>();
foreach (var p in entityType.GetProperties())
{
    dic.Add(p.Name, p.GetColumnBaseName());
}


xoyozo 3 年前
3,656