SignalR(WebSockets)通过.NET Core使用的初学者指南
学习如何使用 SignalR 广播异步实时更新所有 Web 客户端。
- 下载 pawns_v006.zip - 59.3 KB
- 下载 pawns_v005.zip - 58.8 KB
- 下载 pawns_v004.zip - 58.1 KB
- 下载 pawns_v003.zip - 57.4 KB
- 下载 pawns_v002.zip - 56.3 KB
- 下载 pawns_v001.zip - 32.8 KB
引言
注意
这是我于2017年5月(7年前!)撰写的文章的全面更新。您可以在这里查看:SignalR 通过 ASP.NET 使用初学者指南[^]
使用 SignalR 比那时更容易了,但我真的很喜欢最初的理念:在本地移动一个棋子,然后其他人(通过互联网)在他们的浏览器中看到它移动,所以我保留了原始示例并对其进行了更新。
灵光一现!:额外内容
当我正要发布这篇包含基本示例的文章时,我突然灵光一现。
这篇文章本来会以示例 v005 结束,该示例只是在所有人的屏幕上移动棋子。然而,就在我即将发布时,我想到一个更酷的示例:只在私密伙伴的屏幕上移动棋子。
在私密伙伴的屏幕上移动棋子
我想到了一种只在私密伙伴的屏幕上移动棋子的方法,代码已包含在 v006 示例中,所以即使您不阅读文章的其余部分,也要阅读最后一节并查看最后一个示例,因为这是那种让我想到很多其他想法的创意之一。
Visual Studio Code
本文的另一个重大区别是,上一篇文章使用了运行在 Win10 上的 Visual Studio 2017,而本文将使用 Visual Studio Code,并引导您通过 dotnet
命令行完成所有工作。
我99%的工作现在都是在我运行 Ubuntu 22.04.4 LTS 的台式机或我的 Mac Pro M3 笔记本电脑(目前运行 MacOS Sonoma)上完成的,但现在你可以在任何平台上进行所有这些“微软”开发,这真的很棒。
文章主要内容
我一直在思考使用 SignalR 会有多简单,所以我决定尝试完成 Microsoft 的一个快速入门教程。在操作过程中,我遇到了一些挑战,因此我想尝试自己解释 SignalR 的工作原理。
主要观点:实时更新远程客户端
使用 SignalR 等技术的全部意义在于实时更新远程客户端。我想要一个简单的例子,让读者了解它如何工作。不久前,我使用 Firebase(稍后会详细介绍)创建了一个简单的例子,它
- 允许用户在 HTML5 Canvas 上抓取并移动一个棋子
- 更新所有客户端,以便远程用户可以在其浏览器中看到棋子移动
这是它运行时的动态 GIF。(但我认为您必须点击图像才能看到动画)。
您现在就可以尝试
只需打开两个不同的浏览器窗口,并将它们指向我的 InterServer.net 托管站点:
现在,在任一浏览器窗口中移动其中一个棋子,相应的棋子也会在另一个浏览器窗口中移动。
现在你已经看到它工作了,让我们在 Visual Studio 中设置我们的 SignalR 项目并开始吧。
Visual Studio Code Web 项目
如前所述,我通过 Microsoft 的一个教程学习了 SignalR。你可以在这里看到那个教程:ASP.NET Core SignalR 入门 | Microsoft Learn[^] 然而,这篇文章提出了一些挑战。
- 它引导您安装一个并非完全必要的新包管理器工具 (LibMan)。(我的文章将向您展示如何使用 dotnet 命令行和内置 nuget 来完成相同的工作。)
- 我们将手动下载 SignalR JS 代码并将其放置在正确的位置。文章让 LibMan 为我们做这件事。
让我们打开一个终端(控制台、命令行界面 (CLI)、文本用户界面 (TUI) 等),然后从那里创建我们的项目。只要您的计算机上正确安装了 .NET Core,这将在任何平台(MacOS、Linux、Windows)上运行。
创建棋子项目
在您的机器上打开一个终端,然后进入一个您想要创建新项目文件夹的目录,该文件夹将包含我们项目的所有文件,并运行以下命令
$ dotnet new web -o pawns
当您按下 <ENTER> 时,您的项目文件夹和项目所需的初始文件将被创建。
现在您可以 CD(更改目录)到该项目文件夹中
$ cd pawns
进入该目录后,您可以查看为您创建的基本文件(在 Windows 上是 c:\dir
,在 Linux / MacOS 上是 $ ls -al
)。
之后,您可以使用以下命令尝试运行此初始项目
$ dotnet run
您会看到 dotnet 为您启动了一个 Web 服务器,您应该能够 CTRL-点击显示的 URL,它会在您的浏览器中加载这个(非常)基本的页面。
这是您将看到的极其基本的网页
实际上,如果您查看源代码,它甚至都不是一个真正的网页。它只是文本“Hello World!”。它甚至不包含任何 HTML。
目前没关系。我们只是想确保 dotnet 在您的机器上正常运行。现在让我们继续将 SignalR 库添加到我们的项目中,以便我们拥有项目继续进行所需的一切。
通过 Nuget(dotnet 命令行)添加 SignalR
初始项目,可从本文轻松下载
注意:如果其中任何一个给您带来问题,请不要担心,因为我将压缩我们项目的第一个版本,以便您可以在这些第一步(包括添加 SignalR 组件和 SignalR JS 文件)之后直接下载项目,该文件位于本文的第一个附件压缩文件中。
要使用 dotnet 获取 SignalR 组件,我只需在网上搜索:nuget signalr .net core
我看到了一些链接,并且从 nuget 中看到了两个不同的链接。
您可能会认为它是列出的第一个。然而,那是应该与 .NET Framework(那是 .NET Core 之前的旧版本 .NET)一起使用的旧版本。
如果您查看第三个链接(显示 8.0.4),您会在 Nuget 网站上看到它是您应该用于 .NET Core 项目的链接。
当您访问 Nuget 网站时,它会为您提供我们将用于向项目添加组件的 dotnet 命令
$ dotnet add package Microsoft.AspNetCore.SignalR.Client --version 8.0.4
继续在项目文件夹内的终端中运行它。如果您仍在运行上一步中的 Web 服务器,则在运行命令之前键入 CTRL-C
关闭 Web 服务器。
您应该会在终端中看到一些输出。为了确保一切正常,您可以通过输入或 cat 到终端来快速查看 pawns.csproj 文件。
$ cat pawns.csproj
c:\<your-path>\type pawns.csproj
完成此操作后,您应该会在 csproj XML 中看到对新添加的包的引用。
SignalR 的又一步
我们还需要做一件事,以便拥有使用 SignalR 所需的一切。
我们需要下载 SignalR JavaScript。
Microsoft 提供的 SignalR JavaScript
该源位于:
https://unpkg.com/@microsoft/signalr@latest/dist/browser/signalr.js[^]
JS 已保存到特定位置
我们需要将该 JS 源文件放置在我们项目中的一个非常特定的位置。
然而,该位置目前在我们的项目中不存在。
位置是:wwwroot/js
该位置将在我们的主 pawns 项目目录中。让我们转到终端并用一个命令创建这个目录结构
$ mkdir -p wwwroot/js
这将创建初始的 wwwroot 目录及其下面的所有子目录。
一个非常相似的命令在 Windows 上也有效(只是使用了不同的目录分隔符)
c:\<your-path>\>mkdir -p wwwroot\js
获取 JavaScript 源代码
要将源代码导入我们的项目,我们需要遵循以下步骤
- 点击链接并在新的浏览器标签页或窗口中打开它。
- 右键单击新标签页/窗口中的内容
- 选择“另存为...”
将文件保存到我们之前创建的(Microsoft 要求的)项目目录:wwwroot/js
我们稍后还会在此 js
目录中保存另一个文件,因为我们所有的 JavaScript 都将从该目录运行。
项目还剩一件事
现在,让我们对 Program.cs
做一些修改,使其不再只返回“Hello World!”,而是返回 index.htm
文件。
打开现有的 Program.cs 并用以下代码替换所有代码
using Microsoft.AspNetCore.Http.HttpResults;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddSignalR();
var app = builder.Build();
// will automatically redirect to the wwwroot/index.htm
// when you load https://:<port>
app.MapGet("/", () => Results.Redirect("/index.htm"));
app.UseStaticFiles();
app.UseDefaultFiles();
app.Run();
现在,当您运行时,应用程序将自动重定向到 wwwroot/index.htm 文件,该文件将成为我们 SPA(单页应用程序)的内容。
我们只需确保一切都能构建。
$ dotnet build
现在我们有了包含所有依赖项的项目,让我们将其保存到一个 zip 文件中进行快照,您可以下载该文件以确保您已正确设置所有内容。
获取源代码 - v001
如果您在项目创建过程中遇到任何问题,可以从本文顶部获取 v001 压缩文件并解压,一切就绪。当然,我删除了下载的 nuget 包,所以压缩包只包含源代码,但当您构建 ($ dotnet build) 或运行 ($ dotnet run
) 项目时,包将自动恢复。
完成所有前面的步骤后,您就拥有了在项目中使 SignalR 所需的一切。
项目尚未正常运行
此外,如果您现在运行项目并在浏览器中加载它,您会发现浏览器告诉您 index.htm
文件丢失了。没关系,因为在下一步中我们将解决这个问题。
现在,让我们添加一些代码。
添加新的 HTML 页面
在 Microsoft 教程中,作者告诉您做的第一件事是添加一个实现 SignalR 行为的新类。然而,我喜欢在过程中逐步构建,以了解它们如何协同工作。所以首先,我们将设置一个 HTML 页面,作为我们应用程序的用户界面。
让我们从向项目添加一个名为 index.htm
的新(主)HTML 文件开始。
如我所说,我正在使用 Visual Studio Code (VSC),所以请在 VSC 中打开项目文件夹,以便我们可以添加文件。
在 VSC 左侧的项目树中选择 wwwroot
文件夹,然后添加文件。
当您添加文件时,VSC 将添加一个空的 HTML 文件,该文件只会向用户显示一个空白页面。当然,为了我们的目的,我们希望在一个蓝色网格背景上显示三个不同颜色的棋子。
现在让我们通过粘贴以下代码来修改空白的 index.htm
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>pawns</title>
</head>
<style>
body, html {
margin: 0;
padding: 0;
}
</style>
<body>
<img style="visibility:hidden;display:none;" src="assets/redBlueGreenPawn.png" id="allPawns" />
<canvas id="gamescreen">You're browser does not support HTML5.</canvas>
</body>
</html>
通常,我会将 CSS(层叠样式表)数据保存在一个单独的文件中,但我正在尝试简化本教程,并且实际上只有一个样式可以移除任何边距或填充,所以我已将其添加到我们的 index.htm 中。
图片资源
接下来,您会看到我引用了一张您没有的图片。如果您喜欢,可以在这里查看并下载它
您可以看到,尽管有三个可以移动的独立棋子,但图像本身实际上是一个 PNG 文件。这是因为您可以使用 Canvas API 轻松地将图像的各个部分作为单独的 HTML5 Canvas 对象进行引用。
添加资源文件夹
我还会向项目添加一个名为 wwwroot\assets
的新文件夹,并将 redGreenBluePawn.png 放置在该文件夹中,使其成为项目的一部分。您将在下一个(v002)下载中获得该文件。
最后,我们设置了一个 HTML Canvas
元素,我们的网格和棋子将在此处绘制。
实际工作,用 JavaScript 完成
然而,真正的工作是由 JavaScript 完成的。我知道你们很多人对此有自己的看法,但这就是 Web 的方式,所以习惯它吧。🤓
我们将项目的自定义 JavaScript 分开保存,所以我将创建一个名为 pawns.js
的新 JavaScript 文件并将其添加到 wwwroot\js
文件夹中。我将向您展示它,让您自己弄清楚如何做同样的事情。仔细查看我在 HTM 文件中添加 pawns.js
引用的位置。这有点重要,因为该代码引用了 Canvas,并且我们正在确保 Canvas 元素已加载。
SignalR 参考
另请注意我在下面的 HTML 示例中加粗的行。过去这部分要复杂得多,但现在你只需在页面顶部(在 head 部分)添加一个脚本元素来引用 signalr.js
文件,以便它在页面其余部分加载之前加载。
Pawns.js 参考
接下来,在页面底部,我们确保引用了 pawns.js
文件,以便它可以与页面的 Canvas
元素进行交互。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>pawns</title>
<script src="js/signalr.js"></script>
</head>
<style>
body, html {
margin: 0;
padding: 0;
}
</style>
<body>
<img style="visibility:hidden;display:none;" src="assets/redBlueGreenPawn.png" id="allPawns" />
<canvas id="gamescreen">You're browser does not support HTML5.</canvas>
<script src="js/pawns.js"></script>
</body>
</html>
获取代码 v002
您也可以在本文顶部下载代码的 v002 版本,这样您将是最新版本,并准备好编写代码来绘制棋子和网格。
代码将构建并运行,但当然显示的页面将完全空白。让我们看看如何绘制棋子和网格。
绘制棋子和网格
我将快速浏览大部分内容,因为它只是间接属于我想谈论的部分。
首先,我需要设置一些我将要使用的变量,并设置 load
事件,该事件将在浏览器加载目标页面 (index.htm) 及其所有相关资源(图像和 JavaScript 文件)后触发。
// pawns.js
var ctx = null;
var theCanvas = null;
var firebaseTokenRef = null;
window.addEventListener("load", initApp);
var mouseIsCaptured = false;
var LINES = 20;
var lineInterval = 0;
var allTokens = [];
// hoverToken -- token being hovered over with mouse
var hoverToken = null;
var pawnR = null;
//$.on("mousemove", mouseMove
function token(userToken){
this.size = userToken.size;
this.imgSourceX = userToken.imgSourceX;
this.imgSourceY = userToken.imgSourceY;
this.imgSourceSize = userToken.imgSourceSize;
this.imgIdTag = userToken.imgIdTag;
this.gridLocation = userToken.gridLocation;
}
function gridlocation(value){
this.x = value.x;
this.y = value.y
}
我还添加了我创建的几种类型(token
和 gridLocation
),以便更容易跟踪事物。稍后您将看到它们的使用方式。
当浏览器加载所有内容时,initApp()
函数将运行。让我们看看它。
function initApp()
{
theCanvas = document.getElementById("gamescreen");
ctx = theCanvas.getContext("2d");
ctx.canvas.height = 650;
ctx.canvas.width = ctx.canvas.height;
initBoard();
}
我们开始设置 Canvas,我们将在其上绘制网格。
接下来,我们调用 initBoard()
中的自定义代码。
function initBoard(){
lineInterval = Math.floor(ctx.canvas.width / LINES);
console.log(lineInterval);
initTokens();
}
我使用画布宽度和线条之间的距离来计算线条间隔。之后,我调用 initTokens()
来准备绘制棋子(tokens)。
function initTokens(){
if (allTokens.length == 0)
{
allTokens = [];
var currentToken =null;
// add 3 pawns
for (var i = 0; i < 3;i++)
{
currentToken = new token({
size:lineInterval,
imgSourceX:i*128,
imgSourceY:0*128,
imgSourceSize:128,
imgIdTag:'allPawns',
gridLocation: new gridlocation({x:i*lineInterval,y:3*lineInterval})
});
allTokens.push(currentToken);
}
console.log(allTokens);
}
draw();
}
在这里,我们只是确保 allTokens
数组被初始化为空。接下来,我们将所有令牌添加到数组中,同时从我们的 PNG 图像中获取正确的部分。使用一些数学运算很容易实现,因为每个令牌的宽度都是 128 像素。
最后,我们调用 draw()
函数,它会将我们所有的图形绘制到我们的 Canvas 元素中。
function draw() {
ctx.globalAlpha = 1;
// fill the canvas background with white
ctx.fillStyle = "white";
ctx.fillRect(0, 0, ctx.canvas.height, ctx.canvas.width);
// draw the blue grid background
for (var lineCount = 0; lineCount < LINES; lineCount++) {
ctx.fillStyle = "blue";
ctx.fillRect(0, lineInterval * (lineCount + 1), ctx.canvas.width, 2);
ctx.fillRect(lineInterval * (lineCount + 1), 0, 2, ctx.canvas.width);
}
// draw each token it its current location
for (var tokenCount = 0; tokenCount < allTokens.length; tokenCount++) {
drawClippedAsset(
allTokens[tokenCount].imgSourceX,
allTokens[tokenCount].imgSourceY,
allTokens[tokenCount].imgSourceSize,
allTokens[tokenCount].imgSourceSize,
allTokens[tokenCount].gridLocation.x,
allTokens[tokenCount].gridLocation.y,
allTokens[tokenCount].size,
allTokens[tokenCount].size,
allTokens[tokenCount].imgIdTag
);
}
// if the mouse is hovering over the location of a token, show yellow highlight
if (hoverToken !== null) {
ctx.fillStyle = "yellow";
ctx.globalAlpha = .5
ctx.fillRect(hoverToken.gridLocation.x, hoverToken.gridLocation.y,
hoverToken.size, hoverToken.size);
ctx.globalAlpha = 1;
drawClippedAsset(
hoverToken.imgSourceX,
hoverToken.imgSourceY,
hoverToken.imgSourceSize,
hoverToken.imgSourceSize,
hoverToken.gridLocation.x,
hoverToken.gridLocation.y,
hoverToken.size,
hoverToken.size,
hoverToken.imgIdTag
);
}
}
基本上,我们所做的只是
- 遍历
allTokens
数组 - 根据其
gridLocation
值,在当前位置绘制令牌 - 如果令牌被悬停,则在其周围绘制一个淡黄色阴影,以便用户知道她可以抓取令牌。
draw()
函数确实使用了另一个名为 drawClippedAsset()
的辅助方法,它允许我轻松地引用图像中的令牌。它看起来像这样
function drawClippedAsset(sx,sy,swidth,sheight,x,y,w,h,imageId)
{
var img = document.getElementById(imageId);
if (img != null)
{
ctx.drawImage(img,sx,sy,swidth,sheight,x,y,w,h);
}
else
{
console.log("couldn't get element");
}
}
一旦您添加了所有这些代码,您最终将绘制出背景网格并将棋子绘制在它们的初始位置。
获取代码:v003
如果您在本文顶部获取代码 v003,您将是最新的,可以继续阅读文章。
至此,我们已经绘制了网格和网格上的棋子。但是,我们还无法与应用程序进行交互,因为我们还没有添加允许用户抓取和移动网格上棋子的代码。
我们现在在哪里?
我们现在离使用 SignalR 已经很近了,但首先我们必须添加本地代码,它将允许我们抓取一个棋子并移动它。一旦我们做到这一点,我们将允许更新的值广播到其他客户端。
现在让我们添加完成这项工作的代码。是的,它仍然是更多的 JavaScript。
我们需要一些事件处理程序,它们会在鼠标点击 (mousedown) 和鼠标移动时执行一些工作。
回到我们的 initApp() 函数中,我们想为这两个事件添加事件监听器。现在 initApp() 将看起来像下面这样:(我添加了加粗的行)
function initApp()
{
theCanvas = document.getElementById("gamescreen");
ctx = theCanvas.getContext("2d");
ctx.canvas.height = 650;
ctx.canvas.width = ctx.canvas.height;
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mousedown", mouseDownHandler);
initBoard();
}
这是添加这些监听器的纯 JavaScript 方法。你也可以用 jQuery 来做,但这样做也足够简单。
现在,我们已经注册了事件监听器,我们需要实现 handleMouseMove()
和 mousedownHandler()
方法。
function handleMouseMove(e)
{
if (mouseIsCaptured)
{
if (hoverItem.isMoving)
{
var tempx = e.clientX - hoverItem.offSetX;
var tempy = e.clientY - hoverItem.offSetY;
hoverItem.gridLocation.x = tempx;
hoverItem.gridLocation.y = tempy;
if (tempx < 0)
{
hoverItem.gridLocation.x = 0;
}
if (tempx + lineInterval > 650)
{
hoverItem.gridLocation.x = 650 - lineInterval;
}
if (tempy < 0)
{
hoverItem.gridLocation.y = 0;
}
if (lineInterval + tempy > 650)
{
hoverItem.gridLocation.y = 650 - lineInterval;
}
allTokens[hoverItem.idx]=hoverItem;
pawnR.server.send(hoverItem.gridLocation.x, hoverItem.gridLocation.y,hoverItem.idx);
}
draw();
}
// otherwise user is just moving mouse / highlight tokens
else
{
hoverToken = hitTestHoverItem({x:e.clientX,y:e.clientY}, allTokens);
draw();
}
}
只要鼠标移动,此函数就会运行。我本可以更具体地说,只在鼠标移动且鼠标位于 Canvas
元素上方时运行,但这也能正常工作。
我做的第一件事是检查 mouseIsCaptured
是否为 true。当 mouseDownHandler
触发(用户点击)并且该方法确定用户在三个棋子之一上方时,该值就会被设置。这项工作在 mouseDownHandler
中完成,所以让我们看看。
function mouseDownHandler(event) {
var currentPoint = getMousePos(event);
for (var tokenCount = allTokens.length - 1; tokenCount >= 0; tokenCount--) {
if (hitTest(currentPoint, allTokens[tokenCount])) {
currentToken = allTokens[tokenCount];
// the offset value is the diff. between the place inside the token
// where the user clicked and the token's xy origin.
currentToken.offSetX = currentPoint.x - currentToken.gridLocation.x;
currentToken.offSetY = currentPoint.y - currentToken.gridLocation.y;
currentToken.isMoving = true;
currentToken.idx = tokenCount;
hoverItem = currentToken;
console.log("b.x : " + currentToken.gridLocation.x + " b.y : "
+ currentToken.gridLocation.y);
mouseIsCaptured = true;
window.addEventListener("mouseup", mouseUpHandler);
break;
}
}
}
在 mouseDownHandler
中,我们只需遍历 allTokens
数组并检查它们的 gridLocation
。如果我们确定鼠标指针在该区域内,我们就将 mouseIsCaptured
布尔值设置为 true。
我将检查鼠标位置是否在任何令牌位置内的代码提取出来,并将其放置在一个名为 hitTest()
的方法中。
function hitTest(mouseLocation, hitTestObject)
{
var testObjXmax = hitTestObject.gridLocation.x + hitTestObject.size;
var testObjYmax = hitTestObject.gridLocation.y + hitTestObject.size;
if ( ((mouseLocation.x >= hitTestObject.gridLocation.x) && (mouseLocation.x <= testObjXmax)) &&
((mouseLocation.y >= hitTestObject.gridLocation.y) && (mouseLocation.y <= testObjYmax)))
{
return true;
}
return false;
}
您可以看到我们只需传入鼠标位置和要测试的对象,函数会遍历并确定是否命中,并返回 true 或 false。这使得使用起来非常简单。
里面还有几个辅助方法,它们将决定哪个棋子被抓取,以及哪个将持续调用 draw() 方法,以便在鼠标移动时绘制棋子。此时,用户可以抓取任何一个棋子并在屏幕上移动它。
获取代码:用户可以抓取和拖动任意棋子 v004
下载本文顶部的 v004 压缩文件,您可以尝试拖动棋子。
最后,我们来到了主要挑战
我们终于准备好尝试解决主要挑战了。
我们想做的是
更新所有正在查看我们网页的客户端,以便当其中一个棋子移动时,所有其他客户端都会看到棋子移动。
为了实现这一点,我们首先需要创建一个表示 SignalR Hub 的 C# 类。
这是在服务器端运行的代码,它将作为主机转发发送给它的数据(当用户在屏幕上移动棋子时)。
添加 PawnHub
- 创建一个新目录(名为
Hubs
) - 在 Hubs 目录中创建一个新文件(名为
PawnHub.cs
) - 添加以下代码
using Microsoft.AspNetCore.SignalR;
namespace Pawns.Hubs
{ public class PawnHub : Hub
{
public async Task SendData( int x, int y, int idx){
var drawData = new {x=x,y=y,idx=idx};
await Clients.Others.SendAsync("ReceiveData", drawData );
}
}
}
如您所见,我们的 PawnHub
是一个 SignalR Hub
,它要求我们实现 SendData
方法。
客户端代码(JavaScript)将调用此方法
稍后我们将看到 JavaScript 代码将调用此方法。当 JavaScript 调用此方法时,它还将向 SendData 传递三个值
x
- 所选棋子的 x(屏幕)位置y
- 所选棋子的 y(屏幕)位置idx
- 当前用户正在移动的棋子图像的零基索引(0,1,2)。
Hub 接收数据,发送给其他人
当 Hub 接收到数据时,我采取两个步骤
- 将数据封装在一个简单的对象 (
drawData
) 中 - 将对象 (
drawData
) 传递给所有其他客户端,以便他们可以实时更新移动棋子的位置。
当调用代码行发送数据时(下面显示的代码行),我们还会传递一个字符串,表示将要调用的客户端方法名称(作为 SendAsync
函数的第一个参数。以下代码行调用名为 ReceiveData
的客户端函数,并向其发送当前的 drawData
。
await Clients.Others.SendAsync("ReceiveData", drawData );
对 Program.cs 进行更改
现在,我们只需要在 Program.cs 文件中添加一行代码,并添加一个新的 using 语句(以引用新代码行中将使用的类)。
- 在文件顶部添加 using 语句 -
- 添加新代码行,注册 pawnHub 类的路径,该路径将用于向客户端发送数据。
现在整个 Program.cs 将如下所示
using Microsoft.AspNetCore.Http.HttpResults;
using Pawns.Hubs;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddSignalR();
var app = builder.Build();
app.MapGet("/", () => Results.Redirect("/index.htm"));
app.UseStaticFiles();
app.UseDefaultFiles();
app.MapHub<Pawns.Hubs.PawnHub>("/pawnHub");
app.Run();
C# 方面我们需要做的工作就这些了。现在让我们在 pawns.js
文件中添加 JavaScript 代码。
设置与 pawnHub 的连接
我们添加到 pawns.js 的第一行代码将作为文件的第一行代码。
var connection = new signalR.HubConnectionBuilder().withUrl("/pawnHub").build();
这会连接到我们在 Program.cs
中映射的 pawnHub
。
这会将 JavaScript 客户端与 C# 服务器绑定,这样当我们调用连接上的方法时,数据就会发送到服务器并广播给所有客户端。
初始化 JavaScript 方法
现在,我们只需确保我们的 JS 方法已正确初始化,我们将在设置上述连接后立即执行此操作。我们将在 initApp()
JavaScript 方法中执行此操作。
以下大部分代码已经存在,因为它是设置 HTML Canvas 并设置所有绘图组件以绘制网格和棋子的代码。
现在我们添加代码行来注册一个 JavaScript 函数,以便在接收到数据时执行工作。
在这种情况下,我们告诉 Hub Connection
,当调用“ReceiveData”函数时,我们将使用 handleData()
函数执行工作。之后,我们定义 handleData()
方法的作用。
最后,我们必须确保 Hub 已启动,我们通过 connection.start()
函数来实现这一点。所有这些代码都只是在 Web 应用程序启动时运行。
function initApp() {
theCanvas = document.getElementById("gamescreen");
ctx = theCanvas.getContext("2d");
ctx.canvas.height = 650;
ctx.canvas.width = ctx.canvas.height;
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mousedown", mouseDownHandler);
connection.on("ReceiveData", handleData);
function handleData(drawData) {
allTokens[drawData.idx].gridLocation.x = drawData.x;
allTokens[drawData.idx].gridLocation.y = drawData.y;
draw();
};
connection.start().then(function () {
console.log("Hub is started.");
}).catch(function (err) {
return console.error(err.toString());
});
initBoard();
}
一切都已设置好,客户端已准备好接收数据,服务器 Hub 已准备好将该数据重新广播给所有客户端。然而,我们没有任何试图发送数据的操作。
只剩一件事要做
我们想调用 Hub SendData()
方法,以便每当棋子在屏幕上移动时,它都会将数据广播到所有其他客户端,并在所有其他客户端屏幕上也移动棋子。
要做到这一点,我们只需在 pawns.js
中的 handleMouseMove()
方法中添加一行(长)代码。
它再次使用 connection
对象,但这次它调用 SendData()
方法,同时传递棋子的 x
、y
位置和代表正在移动的棋子的 idx
值。
添加的代码行如下所示
connection.invoke("SendData", hoverItem.gridLocation.x,
hoverItem.gridLocation.y,
hoverItem.idx)
.catch(function (error){
return console.error(error.toString());
});
hoverItem
是当前选定的棋子,其中包含其 x
、y
和 idx
值,您可以看到我们将所有这些值传递给 SendData() 方法。
完整解决方案:v005
在本文顶部的 v005 下载中获取完整解决方案。
构建并运行,然后打开两个指向相同 URL 的独立浏览器窗口并移动一个棋子。它也会在另一个浏览器窗口中移动。
刷新页面重置棋子位置
此外,如果您刷新页面,棋子总是会回到它们的原始位置。那是因为我没有将这些值持久化到数据存储中。
然而,它不会重置远程棋子的位置,因为 SendData()
方法仅在客户端鼠标移动时调用。有一些方法可以通过将数据保存到服务器上的数据库来解决这个问题,但这超出了本文的范围。
在私密伙伴的屏幕上移动棋子:v006 必看
正如我们所看到的,v005 示例将数据发送给所有简单地将页面加载到浏览器中的任何人(和每个人)。
分享私钥?
我开始思考如何只与“私密伙伴”共享数据:即,我与一个或多个人共享了密钥,这样只有他们才能看到我如何移动棋子(也只有我能看到他们如何移动)。
v006 示例将做什么
如果您不提供密钥,该示例仍将正常工作——因此每个人都将处于一个“全球组”中,可以查看其他人移动棋子。但是,它将增加生成或设置 UUID(通用唯一 ID)的功能,这样只有共享该 ID 的用户才能看到彼此的棋子移动。
我们添加了一个新字段(文本输入)来保存 UUID 和一个按钮来生成它,因此新应用程序(v006)将如下所示
当用户点击按钮时,将生成一个随机 UUID。这是一个非常基本的方法,允许我们做到这一点,我将让您在源代码中进行检查。它在 pawns.js
中,函数名为 uuidv4()
。
UUID 如何使用
一旦伙伴生成了 UUID,她必须与她的伙伴分享,以便伙伴可以将其粘贴到她的网络浏览器中,然后点击 [Gen/Set UUID] 按钮。
Gen/Set UUID 按钮的工作原理
这个按钮是这样工作的:当 UUID 文本框为空时,代码会生成一个随机 UUID 并将 currentUuid
变量设置为该值。然而,当该字段被设置为某个值时,currentUuid
变量只是被设置为该值。
UUID 值如何使用?
如果生成或设置了该值,应用程序将随其他棋子数据 (x,y,idx) 一起发送 UUID。
C# PawnHub 更改
在这个新示例中,我修改了 PawnHub 代码以期望一个额外的参数。现在的代码如下所示
public class PawnHub : Hub
{
public async Task SendData( int x, int y, int idx, string uuid){
var drawData = new {x=x,y=y,idx=idx,uuid=uuid};
await Clients.Others.SendAsync("ReceiveData", drawData );
}
}
如您所见,SendData()
函数现在接受一个名为 uuid
的额外 String
值。
我现在还将这个新项封装到 drawData
对象中,我们将在调用 SendAsync
方法时传递该对象。
这意味着现在我们可以使用该值来过滤浏览器接收到的数据,这样只有拥有密钥的用户才能获取数据并看到棋子移动。
JavaScript 更改
以下是 JavaScript 方面(在 pawns.js
中)的更改:
在 handleMouseMove() 函数中,现在当 currentUuid 已设置或生成时,我们需要发送 UUID。
if (currentUuid == ""){
connection.invoke("SendData", hoverItem.gridLocation.x,
hoverItem.gridLocation.y, hoverItem.idx, "")
.catch(function (error){
return console.error(error.toString());
});
}
else{
connection.invoke("SendData", hoverItem.gridLocation.x,
hoverItem.gridLocation.y, hoverItem.idx, currentUuid)
.catch(function (error){
return console.error(error.toString());
});
}
代码解释
如果 currentUuid
未设置,那么我们只是发送带有空白 UUID
的数据,该数据将被忽略,任何查看该网站的用户都将能够看到棋子移动,并且能够移动所有人都看到的棋子。
然而,如果 currentUuid
被设置,那么只有那些在其端设置了 UUID
的用户才能看到棋子移动,并在那个“秘密通道”上移动棋子。
这是另一部分,浏览器接收数据并进行过滤。
(并非完全)秘密通道的工作原理
这一切都发生在 handleData()
函数中。
当应用程序接收到数据时,它将检查 uuid
是否存在,并查看它是否与 currentUuid
匹配。如果匹配,则使用数据。但是,如果不匹配,则棋子不会移动。
function handleData(drawData) {
if (currentUuid == ""){
allTokens[drawData.idx].gridLocation.x = drawData.x;
allTokens[drawData.idx].gridLocation.y = drawData.y;
draw();
}
else{
if (currentUuid == drawData.uuid){
allTokens[drawData.idx].gridLocation.x = drawData.x;
allTokens[drawData.idx].gridLocation.y = drawData.y;
draw();
}
}
};
并非完全秘密的 UUID
当我思考上面的代码时,我开始思考数据是如何发送的。这让我发现
- 数据发送到所有客户端
- 然而,没有 UUID 的客户端不使用数据。
为了能够看到这一点,我在上面的 handleData()
函数中添加了以下代码行作为第一行。
console.log(`${drawData.x} : uuid: ${drawData.uuid}`);
无论哪个客户端移动棋子,无论他们是否设置了 UUID,那行代码都会被触发。这意味着数据会发送给所有指向应用程序主 URL 的客户端。当然,如果发送方使用 UUID 而接收方不使用,那么他们的棋子就不会移动。
此外,我不认为有人能够在不直接修改 JavaScript 代码的情况下获取这些数据。
一个有趣的问题
这非常有趣,因为它让我们深入了解 SignalR(WebSockets)的工作原理。数据会发送给所有客户端,因此在使用时必须考虑到这一点
SignalR。
考虑 HTTPS 安全性
然而,它还很有趣,因为在应用程序部署到包含 HTTPS 连接的服务器(如我的生产服务器,网址为 https://newlibre.com/pawns.index.htm)的情况下,黑客无法抓取数据,因为它受到 HTTPS 证书的保护(加密)。
黑客能窃听吗?
但是,由于所有数据都发送给每个客户端,黑客有没有办法“窃听”不属于他的数据?在不侵入实际网站及其背后的 JS 的情况下,我认为至少有一个原因会使其变得困难。
- 消息只发送一次就消失了
如果你想到了办法,请告诉我
如果数据在收到时未被捕获,则无法“重播”数据。这意味着黑客必须以某种方式插入 JavaScript,以读取浏览器中接收到的 drawData 对象。可能有一种方法可以做到这一点。如果您想到了,请告诉我。
获取 v006 代码并试用
下载代码并试一试。它非常酷,开始激发关于我们如何创建一个“游戏会话”,玩家拥有不同的状态等想法。我希望这个额外的示例能让您像我一样兴奋。这很有趣。
部署到 InterServer.net 网站
将应用程序部署到我的 InterServer.net 网站实际上非常简单。
我所要做的就是使用以下命令构建发布版本
$ dotnet publish -r win-x64 -c Release
我必须这样做,因为我的网站托管在 Windows x64 上。
然后我将二进制文件复制到一个目录中,并将该目录转换为一个虚拟目录,ASP.NET 引擎可以在其中运行。
我必须确保我的配置文件设置为以进程外方式运行应用程序 DLL,如下所示
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2"
resourceType="Unspecified" />
</handlers>
<aspNetCore processPath=".\Pawns.exe" stdoutLogEnabled="false"
stdoutLogFile=".\logs\stdout" hostingModel="outofprocess" />
</system.webServer>
我还必须进行更改,因为我的网络主机已使用安全证书全部重定向到 HTTPS。
这意味着我必须确保 pawnHub 能够被注册,即使所有内容都通过 HTTPS 重定向。我必须在 pawns.js
中将 connection
初始化更改为以下内容。
var connection = new signalR.HubConnectionBuilder()
.withUrl("https://newlibre.com/pawns/pawnHub").build();
我希望您发现本文对使用 SignalR 很有帮助,并作为入门指南。
历史
2024-05-11:文章和代码首次发布