65.9K
CodeProject 正在变化。 阅读更多。
Home

Angular 和 ASP.net Core 中的原始 Websockets 迷你游戏

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2020年2月19日

CPOL

13分钟阅读

viewsIcon

9698

使用 asp .net core 3.0、Angular 和使用原生 Javascript 实现的 websockets 创建一个小型“迷你游戏”。

本文的 Github

引言

Web socket 协议允许客户端(Web 浏览器)和服务器之间进行连续的双向通信。这种通信方式允许服务器将信息推送到客户端,而无需客户端每次都进行请求。这是对旧的反复 ping 服务器以获取信息的方法的改进,因为它仅在发生适当更改时才将信息推送到客户端。在 .Net 生态系统中,使用 asp.net core 平台的中间件可以更轻松地设置一个 Web 服务器来处理网站的提供以及充当 websocket 服务器。

计划

  • 我们将创建一个 asp.net core Web API 项目,并将其设置为提供 SPA 应用程序
  • 我们将创建一个 Angular Web 项目
  • 我们将使用 asp.net core 的 Websocket 中间件为我们的应用程序创建一个 websocket 端点
  • 我们将创建一个服务类,它将充当单例,处理所有已连接的用户及其相关信息。此类还将负责处理所有 websocket 请求和响应。
  • 我们将修改我们的 Angular 站点,使其包含一个创建 websocket 连接并处理与服务器之间所有消息的服务。
  • 我们将更新默认的 app 组件(主页)以使用 websocket 服务类创建一个非常简单的彩色方块游戏,当用户点击它们时,方块的颜色会发生变化。

最终,我们希望做到这一点

当用户连接时,他们会被分配一个随机用户名,并广播给所有人。当用户选择一种颜色并单击一个方块时,方块的颜色会发生变化,并且这个变化会被广播给所有人,并发送一条指示内容已更改以及谁更改它的消息。

您需要什么

.Net Core SDK(我使用的是 3.1,但 3.0 应该也可以)

Node 包管理器

一个好的 IDE。(我使用的是 Visual Studio 2019 社区版)

服务器

首先,我们将创建 Web 项目。打开您的命令提示符。我使用的是 powershell。

dotnet new sln --name WebSocketAndNetcore
dotnet new webapi --name WebSocketAndNetcore.Web --output .
dotnet sln add .\WebSocketAndNetcore.Web.csproj
rm .\WeatherForecaset.cs
rm .\Controllers\WeatherForecastController.cs
dotnet restore .\WebSocketAndNetcore.sln

安装提供 Angular 应用程序所需的 NuGet 包

dotnet add package Microsoft.AspNetCore.SpaServices
dotnet add package Microsoft.AspNetCore.SpaServices.Extensions

Startup.cs

让我们看看我们对 Startup 类所做的更改

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.SpaServices.AngularCli;
using System.Net.WebSockets;

namespace WebSocketAndNetCore.Web
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.AddSpaStaticFiles(config => config.RootPath = "wwwroot");
            services.AddSingleton(typeof(SquareService), new SquareService());
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseStaticFiles();
            app.UseSpaStaticFiles();

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });

            app.UseWebSockets();
            app.Use(async (context, next) =>
            {
                if (context.Request.Path == "/ws")
                {
                    if (context.WebSockets.IsWebSocketRequest)
                    {
                        var socket = await context.WebSockets.AcceptWebSocketAsync();
                        var squareService = (SquareService)app.ApplicationServices.GetService(typeof(SquareService));
                        await squareService.AddUser(socket);
                    }
                    else
                    {
                        context.Response.StatusCode = 400;
                    }
                }
                else
                {
                    await next();
                }
            });
            app.UseSpa(config =>
            {
                config.Options.SourcePath = "client-app";
                if (env.IsDevelopment())
                {
                    config.UseAngularCliServer("start");
                }
            });

        }
    }
}

让我们解释一下我们正在做什么

public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.AddSpaStaticFiles(config => config.RootPath = "wwwroot");
            services.AddSingleton(typeof(SquareService), new SquareService());
        }

该应用程序的许多初始设置,就其与设置 dot net core Web 应用程序以提供 SPAs 相关而言,与我之前在其他文章中的做法相同,例如 使用 Firebase、Andgular 和 .Net Core 保护网站。其中解释的许多过程与这里相同。

在 configure service 方法中,主要的变化是 AddStaticFiles 方法,我们在其中设置应用程序以提供 SPA(单页应用程序)。我们将客户端应用程序的根目录设置为 wwwroot 文件夹。当我们创建 Angular 应用程序时,我们将在构建过程中确保将输出目录设置为“wwwroot”。接下来,我们将“SquareService”类添加到 services 集合中,并将其标记为单例实例。此类是服务器应用程序的核心,它跟踪方块游戏的当前状态以及玩家与其连接的 websockets 之间的关系。我们稍后会回顾它。

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseStaticFiles();
            app.UseSpaStaticFiles();

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });

            app.UseWebSockets();
            app.Use(async (context, next) =>
            {
                if (context.Request.Path == "/ws")
                {
                    if (context.WebSockets.IsWebSocketRequest)
                    {
                        var socket = await context.WebSockets.AcceptWebSocketAsync();
                        var squareService = (SquareService)app.ApplicationServices.GetService(typeof(SquareService));
                        await squareService.AddUser(socket);
                    }
                    else
                    {
                        context.Response.StatusCode = 400;
                    }
                }
                else
                {
                    await next();
                }
            });
            app.UseSpa(config =>
            {
                config.Options.SourcePath = "client-app";
                if (env.IsDevelopment())
                {
                    config.UseAngularCliServer("start");
                }
            });

        }

在 Configure 方法中,我们首先添加为我们处理 SPA 设置的中间件

app.UseStaticFiles();
app.UseSpaStaticFiles();

接下来,我们将查看负责处理我们 websocket 请求的设置和处理部分

app.UseWebSockets();
            app.Use(async (context, next) =>
            {
                if (context.Request.Path == "/ws")
                {
                    if (context.WebSockets.IsWebSocketRequest)
                    {
                        var socket = await context.WebSockets.AcceptWebSocketAsync();
                        var squareService = (SquareService)app.ApplicationServices.GetService(typeof(SquareService));
                        await squareService.AddUser(socket);
                    }
                    else
                    {
                        context.Response.StatusCode = 400;
                    }
                }
                else
                {
                    await next();
                }
            });

首先,我们通过使用提供的中间件扩展方法来设置我们的应用程序以处理 websocket 请求

 app.UseWebSockets();

接下来,我们创建一个动态自定义中间件函数

[...]
app.Use(async (context, next) =>
            {
                if (context.Request.Path == "/ws")
                {
                    if (context.WebSockets.IsWebSocketRequest)
                    {
                        var socket = await context.WebSockets.AcceptWebSocketAsync();
                        var squareService = (SquareService)app.ApplicationServices.GetService(typeof(SquareService));
                        await squareService.AddUser(socket);
                    }
                    else
                    {
                        context.Response.StatusCode = 400;
                    }
                }
                else
                {
                    await next();
                }
            });
[...]

首先,我们将通知我们的应用程序,在处理对“ws”路径的请求时,我们将做一些不同的事情

if (context.Request.Path == "/ws")
{
 [...]

当对此路径发出请求时,我们将检查它是否是 websocket 请求。这会检查通过 websocket 协议发出的请求,例如对 ws:///ws 的调用将被此中间件函数拦截

if (context.WebSockets.IsWebSocketRequest)
{
[...]
[..]
var socket = await context.WebSockets.AcceptWebSocketAsync();
var squareService = (SquareService)app.ApplicationServices.GetService(typeof(SquareService));
await squareService.AddUser(socket);
[..]                    

在此部分,在客户端完成握手过程并接受连接后,我们首先获取到套接字连接的引用。接下来,我们获取 SquareService 类的引用。我们使用 ApplicationService.GetService 方法来完成此操作。由于我们在 ConfigureServices 方法中将该服务声明为单例,因此每个用户都将获得对同一实例的引用。这使我们能够跟踪我们拥有的当前连接、当前用户以及“游戏”的当前状态。然后,我们将 SquareService 的 AddUser 方法传递当前请求的套接字连接。我们将看到此方法启动了 Web 套接字请求/响应处理循环。

app.UseSpa(config =>
            {
                config.Options.SourcePath = "client-app";
                if (env.IsDevelopment())
                {
                    config.UseAngularCliServer("start");
                }
            });

我们在这里做的另一个更改是为了处理 SPA 客户端应用程序。我们设置了客户端应用程序的源代码路径,并在 Development 环境中启动“npm start”命令。这将启动 Angular 的实时更新,以便在调试 Angular 应用程序时进行更改。

SquareService.cs

服务器的核心是此类,它负责当前连接的 websockets 的 Response/Request 循环。

using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;

namespace WebSocketAndNetCore.Web
{
    public class SquareService
    {
        private Dictionary<string, WebSocket> _users = new Dictionary<string, WebSocket>();
        private List<Square> _squares = new List<Square>(Square.GetInitialSquares());
        public async Task AddUser(WebSocket socket)
        {
            try
            {
                var name = GenerateName();
                var userAddedSuccessfully = _users.TryAdd(name, socket);
                while (!userAddedSuccessfully)
                {
                    name = GenerateName();
                    userAddedSuccessfully = _users.TryAdd(name, socket);
                }
                GiveUserTheirName(name, socket).Wait();
                AnnounceNewUser(name).Wait();
                SendSquares(socket).Wait();
                while (socket.State == WebSocketState.Open)
                {
                    var buffer = new byte[1024 * 4];
                    WebSocketReceiveResult socketResponse;
                    var package = new List<byte>();
                    do
                    {
                        socketResponse = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
                        package.AddRange(new ArraySegment<byte>(buffer, 0, socketResponse.Count));
                    } while (!socketResponse.EndOfMessage);
                    var bufferAsString = System.Text.Encoding.ASCII.GetString(package.ToArray());
                    if (!string.IsNullOrEmpty(bufferAsString))
                    {
                        var changeRequest = SquareChangeRequest.FromJson(bufferAsString);
                        await HandleSquareChangeRequest(changeRequest);
                    }
                }
                await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
            }
            catch (Exception ex)
            { }
        }

        private string GenerateName()
        {
            var prefix = "WebUser";
            Random ran = new Random();
            var name = prefix + ran.Next(1, 1000);
            while (_users.ContainsKey(name))
            {
                name = prefix + ran.Next(1, 1000);
            }
            return name;
        }

        private async Task SendSquares(WebSocket socket)
        {
            var message = new SocketMessage<List<Square>>()
            {
                MessageType = "squares",
                Payload = _squares
            };

            await Send(message.ToJson(), socket);
        }

        private async Task SendAll(string message)
        {
            await Send(message, _users.Values.ToArray());
        }

        private async Task Send(string message, params WebSocket[] socketsToSendTo)
        {
            var sockets = socketsToSendTo.Where(s => s.State == WebSocketState.Open);
            foreach (var theSocket in sockets)
            {
                var stringAsBytes = System.Text.Encoding.ASCII.GetBytes(message);
                var byteArraySegment = new ArraySegment<byte>(stringAsBytes, 0, stringAsBytes.Length);
                await theSocket.SendAsync(byteArraySegment, WebSocketMessageType.Text, true, CancellationToken.None);
            }
        }

        private async Task GiveUserTheirName(string name, WebSocket socket)
        {
            var message = new SocketMessage<string>
            {
                MessageType = "name",
                Payload = name
            };
            await Send(message.ToJson(), socket);
        }

        private async Task AnnounceNewUser(string name)
        {
            var message = new SocketMessage<string>
            {
                MessageType = "announce",
                Payload = $"{name} has joined"
            };
            await SendAll(message.ToJson());
        }

        private async Task AnnounceSquareChange(SquareChangeRequest request)
        {
            var message = new SocketMessage<string>
            {
                MessageType = "announce",
                Payload = $"{request.Name} has changed square #{request.Id} to {request.Color}"
            };
            await SendAll(message.ToJson());
        }

        private async Task HandleSquareChangeRequest(SquareChangeRequest request)
        {
            var theSquare = _squares.First(sq => sq.Id == request.Id);
            theSquare.Color = request.Color;
            await SendSquaresToAll();
            await AnnounceSquareChange(request);
        }

        private async Task SendSquaresToAll()
        {
            var message = new SocketMessage<List<Square>>()
            {
                MessageType = "squares",
                Payload = _squares
            };

            await SendAll(message.ToJson());
        }
    }
}

这里还有一些需要进一步解释的。从顶部开始,我们有这些声明

private ConcurrentDictionary<string, WebSocket> _users = new ConcurrentDictionary<string, WebSocket>();
private List<Square> _squares = new List<Square>(Square.GetInitialSquares());

“_user”字典用于维护当前用户及其 websocket 连接的列表。接下来是“_squares”集合。这是 UI 中方块的集合,包括它们的 id 和颜色。由于此类被实例化为单例服务,因此该集合及其状态将在所有用户之间共享。

        public async Task AddUser(WebSocket socket)
        {
            try
            {
                var name = GenerateName();
                var userAddedSuccessfully = _users.TryAdd(name, socket);
                while (!userAddedSuccessfully)
                {
                    name = GenerateName();
                    userAddedSuccessfully = _users.TryAdd(name, socket);
                }
                GiveUserTheirName(name, socket).Wait();
                AnnounceNewUser(name).Wait();
                SendSquares(socket).Wait();
                while (socket.State == WebSocketState.Open)
                {
                    var buffer = new byte[1024 * 4];
                    WebSocketReceiveResult socketResponse;
                    var package = new List<byte>();
                    do
                    {
                        socketResponse = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
                        package.AddRange(new ArraySegment<byte>(buffer, 0, socketResponse.Count));
                    } while (!socketResponse.EndOfMessage);
                    var bufferAsString = System.Text.Encoding.ASCII.GetString(package.ToArray());
                    if (!string.IsNullOrEmpty(bufferAsString))
                    {
                        var changeRequest = SquareChangeRequest.FromJson(bufferAsString);
                        await HandleSquareChangeRequest(changeRequest);
                    }
                }
                await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
            }
            catch (Exception ex)
            { }
        }

大循环。“AddUser”方法是套接字连接成功建立后请求首先调用的。我们首先调用 GenerateName 方法,该方法将为用户生成一个随机的未使用的名称。然后,我们调用“GiveUserTheirName”,它将用户添加到“_users”字典中,并创建用户与其套接字连接之间的关系。然后,我们将调用“AnnounceNewUser”方法,该方法将遍历当前套接字并发送一条消息,宣布新用户的连接。接下来,我们调用“SendSquares”方法。这将向用户发送当前方块集合。正如您所看到的,我们是同步地执行这些发送函数。这是因为 websocket 类的性质。通过实验发现,我们似乎无法异步发送。因此,我们基本上以这样一种方式编写它,即确保在执行另一个发送之前完全发送一个发送。不过,我们可以异步进行双向通信。

接下来,让我们看看我们如何实现接收循环

                while (socket.State == WebSocketState.Open)
                {
                    var buffer = new byte[1024 * 4];
                    WebSocketReceiveResult socketResponse;
                    var package = new List<byte>();
                    do
                    {
                        socketResponse = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
                        package.AddRange(new ArraySegment<byte>(buffer, 0, socketResponse.Count));
                    } while (!socketResponse.EndOfMessage);
                    var bufferAsString = System.Text.Encoding.ASCII.GetString(package.ToArray());
                    if (!string.IsNullOrEmpty(bufferAsString))
                    {
                        var changeRequest = SquareChangeRequest.FromJson(bufferAsString);
                        await HandleSquareChangeRequest(changeRequest);
                    }
                }

首先,我们不断检查连接状态以确保它仍然打开。接下来,我们创建一个内部循环以从客户端接收。我们首先创建一个缓冲区来保存从套接字接收到的数据。

var buffer = new byte[1024 * 4];
WebSocketReceiveResult socketResponse;
var package = new List<byte>()

接下来,我们的内部循环将从套接字读取数据。如果套接字消息未结束,它将获取该数据并将其附加到当前包中。

do
{
   socketResponse = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
   package.AddRange(new ArraySegment<byte>(buffer, 0, socketResponse.Count));
} while (!socketResponse.EndOfMessage);

最后,当我们完全接收到套接字中的整个消息时,我们将该数据转换为字符串。然后,我们将该字符串从 JSON(我们将从客户端应用程序发送)转换为表示更改特定方块颜色的方块集合状态请求的对象。

if (!string.IsNullOrEmpty(bufferAsString))
{
   var changeRequest = SquareChangeRequest.FromJson(bufferAsString);
   await HandleSquareChangeRequest(changeRequest);
}
        private async Task HandleSquareChangeRequest(SquareChangeRequest request)
        {
            var theSquare = _squares.First(sq => sq.Id == request.Id);
            theSquare.Color = request.Color;
            await SendSquaresToAll();
            await AnnounceSquareChange(request);
        }

此 HandleSquareChangeReqeuest 方法基本上接受用户方块更改请求,并将方块更改为新颜色,然后将更改后的方块集合广播给所有用户,然后向所有用户宣布更改。

        private async Task Send(string message, params WebSocket[] socketsToSendTo)
        {
            var sockets = socketsToSendTo.Where(s => s.State == WebSocketState.Open);
            foreach (var theSocket in sockets)
            {
                var stringAsBytes = System.Text.Encoding.ASCII.GetBytes(message);
                var byteArraySegment = new ArraySegment<byte>(stringAsBytes, 0, stringAsBytes.Length);
                await theSocket.SendAsync(byteArraySegment, WebSocketMessageType.Text, true, CancellationToken.None);
            }
        }

Send message 是此类中大部分工作的基础消息。无论发送什么,它都会确保只将消息发送给仍然打开的连接。然后,它将循环遍历要发送的套接字,然后将字符串转换为字节,然后调用 SendAsync 方法通过 web socket 发送消息。大多数其他方法只是使用“Send”方法发送信息的各种方法。

Angular 应用程序

让我们回到提示,创建 Angular 应用程序。首先,我们将获取 Angular CLI 工具(如果需要)并创建应用程序。

npm install -g angular/cli
ng new client-app
mkdir wwwroot

创建 websocket 服务

ng generate service WebSocket

我们还将创建用于序列化和反序列化为 JSON 以发送和接收来自服务器的类的类

ng generate class models/SocketMessage

ng generate class models/SquareChangeRequest

ng generate class models/Square

这些类的代码是

square.ts

export class Square {
  Id: number;
  Color: string;
}

square-change-request.ts

export class SquareChangeRequest {
  Id: number;
  Color: string;
  Name: string;
}

socket-message.ts

export class SocketMessage {
  MessageType: string;
  Payload: any
}

angular.json

在此文件的此属性上更新,将“outputPath”更改为“wwwroot”。这将确保 .Net Core SPA 中间件中设置的目录是编译后的 Angular 应用程序所在的目录。

[...]
"outputPath": "wwwroot"
[...]

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { WebSocketService } from './web-socket.service';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule
  ],
  providers: [
    WebSocketService
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

app 模块非常简单。主要可注意的区别是我们对 FormsModule 的引用,并确保我们将 WebSocketService 设置在 providers 集合中。

web-socket.ts

import { Injectable } from '@angular/core';
import { SocketMessage } from './models/socket-message';
import { BehaviorSubject } from 'rxjs';
import { Square } from './models/square';
import { SquareChangeRequest } from './models/square-change-request';

@Injectable({
  providedIn: 'root'
})
export class WebSocketService {
  private socket: WebSocket;
  squares$: BehaviorSubject<Square[]> = new BehaviorSubject<Square[]>([]);
  announcement$: BehaviorSubject<string> = new BehaviorSubject<string>('');
  name$: BehaviorSubject<string> = new BehaviorSubject<string>('');
  private name: string;
  constructor() { }

  startSocket() {
    this.socket = new WebSocket('wss://:5001/ws');
    this.socket.addEventListener("open", (ev => {
      console.log('opened')
    }));
    this.socket.addEventListener("message", (ev => {
      var messageBox: SocketMessage = JSON.parse(ev.data);
      console.log('message object', messageBox);
      switch (messageBox.MessageType) {
        case "name":
          this.name = messageBox.Payload;
          this.name$.next(this.name);
          break;
        case "announce":
          this.announcement$.next(messageBox.Payload);
          break;
        case "squares":
          this.squares$.next(messageBox.Payload);
          break;
        default:
          break;
      }
    }));
  }

  sendSquareChangeRequest(req: SquareChangeRequest) {
    req.Name = this.name;
    var requestAsJson = JSON.stringify(req);
    this.socket.send(requestAsJson);
  }
}

就像我们的 SquareService.cs 类负责 websocket 服务器端的大部分繁重工作一样,这个服务类负责客户端的繁重工作。让我们分解一下里面的内容。

private socket: WebSocket;
squares$: BehaviorSubject<Square[]> = new BehaviorSubject<Square[]>([]);
announcement$: BehaviorSubject<string> = new BehaviorSubject<string>('');
name$: BehaviorSubject<string> = new BehaviorSubject<string>('');
private name: string;

首先,我们的声明。我们声明一个 WebSocket 对象来保存对我们的 javascript websocket 对象的引用。接下来,我们声明多个主题。当从服务器接收到方块集合的更新时,squares$ 主题将触发。当从服务器接收到公告的更新时,accounement$ 主题将触发。最后,name$ 主题只是在成功连接我们的套接字并从服务器接收到一个名称后用于设置用户名称。

StartSocket() 方法。魔法发生的方法

this.socket = new WebSocket('wss://:5001/ws');

首先,我们通过使用 websocket 协议调用我们的服务器来创建套接字,并将路径设置为“ws”。这将确保我们的连接请求被 Startup.cs 类中定义的 websocket 中间件捕获。

this.socket.addEventListener("open", (ev => {
      console.log('opened')
    }));

在这里,我们添加了 websocket 的“open”事件的监听器。我只是简单地记录下来。其他消息是我们感兴趣的消息。

this.socket.addEventListener("message", (ev => {
      var messageBox: SocketMessage = JSON.parse(ev.data);
      console.log('message object', messageBox);
      switch (messageBox.MessageType) {
        case "name":
          this.name = messageBox.Payload;
          this.name$.next(this.name);
          break;
        case "announce":
          this.announcement$.next(messageBox.Payload);
          break;
        case "squares":
          this.squares$.next(messageBox.Payload);
          break;
        default:
          break;
      }
    }));
  }

这是“message”事件的事件监听器。当我们从服务器收到消息时,我们已确保来自服务器的消息格式相同。所有消息都将被反序列化为 messageBox 类。这个类有一个“MessageType”属性和一个有效负载,我们可以根据类型来处理它。当消息是“name”类型时,我们知道我们收到的是连接后设置用户名的消息,我们将该属性设置在服务中,然后将其广播给任何订阅“name$”订阅的组件。“announce”消息只是发送给订阅。最后,当消息是“squares”类型时,我们知道我们正在刷新我们的方块集合。

sendSquareChangeRequest(req: SquareChangeRequest) {
    req.Name = this.name;
    var requestAsJson = JSON.stringify(req);
    this.socket.send(requestAsJson);
  }

此方法仅用于将消息发送到服务器,以请求更改方块的颜色。我们设置了更改者姓名以及方块的 id 和新颜色。服务器将接收此请求,然后更新方块集合并将更改后的集合广播给所有人。

app.component.ts

import { Component } from '@angular/core';
import { WebSocketService } from './web-socket.service';
import { Square } from './models/square';
import { SquareChangeRequest } from './models/square-change-request';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  announcementSub;
  messages: string[] = [];
  squares: Square[] = [];
  colors: string[] = ["red", "green", "blue"];
  currentColor: string = "red";
  name: string = "";
  constructor(private socketService: WebSocketService) {
    this.socketService.announcement$.subscribe(announcement => {
      if (announcement) {
        this.messages.unshift(announcement);
      }
    });
    this.socketService.squares$.subscribe(sq => {
      this.squares = sq;
    });
    this.socketService.name$.subscribe(n => {
      this.name = n;
    });
  }

  ngOnInit() {
    this.socketService.startSocket();

  }

  squareClick(event, square: Square) {
    if (square.Color === this.currentColor)
      return;
    var req = new SquareChangeRequest();
    req.Id = square.Id;
    req.Color = this.currentColor;
    this.socketService.sendSquareChangeRequest(req);

  }
}

在 app 组件中,我们利用 websocket 服务将必要的订阅简单地绑定到我们服务的受众。

announcementSub;
  messages: string[] = [];
  squares: Square[] = [];
  colors: string[] = ["red", "green", "blue"];
  currentColor: string = "red";
  name: string = "";

我们有一个订阅,用于接收公告。激活时,我们将这些公告附加到我们的“messages”数组中。square 数组将用于显示方块。当我们从服务器接收到更新时,我们将用服务器发送的数组替换此数组。colors 是我们可以切换到的可选颜色,current color 用于绑定用户当前使用的颜色。name 字符串是……用户随机生成的名称。

constructor(private socketService: WebSocketService) {
    this.socketService.announcement$.subscribe(announcement => {
      if (announcement) {
        this.messages.unshift(announcement);
      }
    });
    this.socketService.squares$.subscribe(sq => {
      this.squares = sq;
    });
    this.socketService.name$.subscribe(n => {
      this.name = n;
    });
  }

此组件的构造函数只是将订阅设置为从服务中发生的事件,这些事件是由服务器消息触发的。

squareClick(event, square: Square) {
    if (square.Color === this.currentColor)
      return;
    var req = new SquareChangeRequest();
    req.Id = square.Id;
    req.Color = this.currentColor;
    this.socketService.sendSquareChangeRequest(req);

  }

当方块被点击时,我们将创建一个方块更改请求对象并将此请求发送到服务器。

app.component.html

这是 app 组件的模板,它具有来自服务器的方块的绑定以及一个显示当前传入消息的列表。此外,我们还有用于选择颜色的下拉菜单,以及调用更改方块颜色的请求的单击事件绑定。

<style>
  .flex-container {
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    margin-right: 5px;
    margin-left: 5px;
    padding: 5px;
    font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif
  }

  .flex-item {
    height: 500px;
    margin: 3px;
  }

    .flex-item ul {
      list-style: none;
    }

    .flex-item li {
      background-color: black;
      color: white;
      margin: 3px;
      padding:3px;
    }
</style>
<div class="flex-container">
  <div>
    Current Color:
    <select [(ngModel)]="currentColor">
      <option *ngFor="let color of colors" [value]="color">{{color}}</option>
    </select>
  </div>
  <div class="flex-item">
    <label>Your name:{{name}}</label>
    <div *ngFor="let square of squares" [style.background-color]="square.Color" style="color:white;font-size:24px;width:50px;height:50px;margin:2px;" (click)="squareClick($event,square)">
      {{square.Id}}
    </div>
  </div>

  <div class="flex-item">
    <ul>
      <li *ngFor="let message of messages">{{message}}</li>
    </ul>
  </div>
</div>

这是大部分工作的代码。您可以通过在 powershell 中执行以下操作来启动应用程序

dotnet run

当应用程序启动时,您应该会看到一个下拉菜单,其中包含可选颜色和来自服务器的方块集合。您应该会看到一个列表,宣布您的连接到网站并显示您的随机生成的名称。

接下来,在另一个浏览器或另一台计算机上启动到该网站的另一个连接。当此用户连接时,第一个用户将看到他们连接的公告。

现在,所有用户都可以选择一种颜色并单击一个方块,将其更改为该颜色。用户应该尝试将所有方块更改为相同的颜色。

SignalR 和 Socket.IO

您可能会问的第一个问题是“嗯,你为什么不使用 SignalR”或“Socket.IO”? SignalR 是双向通信的 .Net 实现。Socket.IO 是一个开源的 websocket 包装器实现。它们都在底层使用 websockets,但也有回退到其他方法的机制。我只想使用基本的“原生”web socket 类和 Javascript 实现来做到这一点。我确实发现的是,这些库为您处理了很多事情。我遇到的第一个问题是与设置 websocket 类带来的异步/同步问题有关。SignalR 在将所有这些包装起来方面做得很好。在客户端,原生 Javascript “还可以”,但像 socket.io 这样的客户端库会处理您必须非常注意的事情,比如断开连接和正确地排队消息。另一个担忧当然是安全性。像 SignalR 这样的库在这方面具有明显优势,因为它们能很好地集成到整个 .Net 生态系统中。这些库的作者在编写它们时已经考虑到了安全性、并发性等问题。所以,我的建议是,除非您有主要的技��限制需要原始 websockets,否则我很容易建议使用这些经过充分验证且维护良好的库之一。

其他有用的信息

© . All rights reserved.