目录
介绍
计划
你需要什么
服务端
SquareService.cs
angular应用
square.ts
square-change-request.ts
socket-message.ts
Angular.json
app.module.ts
web-socket.ts
app.component.ts
app.component.html
SignalR和Socket.IO
其他有用的信息
本文的Github
Web套接字协议允许客户端(Web浏览器)与服务端之间进行连续的双向通信。这种通信方法允许服务端将信息推送到客户端,而无需客户端每次都进行请求。它是对旧方法重复ping服务端以获取信息的一种改进,方法是减少对客户端的推送,使其仅在发生适当的更改时才会发生。在.Net生态系统中,asp.net core平台的中间件使设置Web服务端以处理为网站提供服务并充当websocket服务端的过程变得更加容易。
最后,我们希望做到这一点:
当用户连接时,将为他们分配一个随机的用户名,并将其广播给所有人。当用户选择一种颜色并单击一个正方形时,正方形的颜色将更改,并且此更改将广播给所有人,并会显示一条消息,指出更改的内容和更改的人。
.Net Core Core SDK(我使用的是3.1,但应该可以使用3.0)
节点包管理器(Node Package Manager)
一个好的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
让我们检查一下我们对启动类所做的更改
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应用程序设置为ser SPAs相关而言,与我以前在其他文章(例如使用Firebase,Andgular和.Net Core保护网站的安全)中所做的相同。解释的很多过程在这里都会相同
在configure service方法中,主要更改是AddStaticFiles方法,在该方法中,我们将应用程序设置为服务端SPA(单页应用程序)。我们将客户端应用程序的根目录设置为wwwroot文件夹。在创建angular 应用程序时,我们将确保在构建过程中将输出目录设置为“wwwroot”。接下来,我们将“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");
}
});
}
在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 protocall发出的请求,例如,对ws://localhost/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客户端应用程序的更多操作。我们在开发环境中设置了客户端应用程序的源代码路径,并启动了“npm start”命令。当您在调试Angular应用程序时进行更改时,这将启动Angular的实时更新。
服务端的类别是此类,用于处理当前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 _users = new Dictionary();
private List _squares = new List(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();
do
{
socketResponse = await socket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None);
package.AddRange(new ArraySegment(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>()
{
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(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
{
MessageType = "name",
Payload = name
};
await Send(message.ToJson(), socket);
}
private async Task AnnounceNewUser(string name)
{
var message = new SocketMessage
{
MessageType = "announce",
Payload = $"{name} has joined"
};
await SendAll(message.ToJson());
}
private async Task AnnounceSquareChange(SquareChangeRequest request)
{
var message = new SocketMessage
{
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>()
{
MessageType = "squares",
Payload = _squares
};
await SendAll(message.ToJson());
}
}
}
这里还有更多要打开的包装。从顶部开始,我们有以下声明:
private ConcurrentDictionary _users = new ConcurrentDictionary();
private List _squares = new List(Square.GetInitialSquares());
“_user”字典用于维护当前用户及其Websocket连接的列表。接下来是“_squares”集合。这是将在用户界面中显示的正方形及其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();
do
{
socketResponse = await socket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None);
package.AddRange(new ArraySegment(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();
do
{
socketResponse = await socket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None);
package.AddRange(new ArraySegment(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()
接下来,我们的内部循环将从套接字读取数据。如果套接字消息没有结束,它将获取该数据并将其附加到当前包中。
do
{
socketResponse = await socket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None);
package.AddRange(new ArraySegment(buffer, 0, socketResponse.Count));
} while (!socketResponse.EndOfMessage);
最后,当我们从套接字完全接收了整个方法时,我们将获取该数据并将其转换为字符串。然后,我们使用此字符串并将其从JSON(我们将从客户端应用程序发送的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(stringAsBytes, 0, stringAsBytes.Length);
await theSocket.SendAsync(byteArraySegment, WebSocketMessageType.Text, true, CancellationToken.None);
}
}
发送消息是完成该类大部分工作的基本消息。无论发送什么内容,都要确保仅收到消息的消息仍处于打开状态。然后,它将循环遍历套接字以发送到,然后将字符串转换为字节,然后调用SendAsync方法以通过Web套接字发送消息。其他大多数方法只是使用“Send”方法发送信息的方法。
让我们回到提示并创建angular应用程序。首先,我们将获得Angular CLI工具(如果需要)并创建应用程序。
npm install -g angular/cli
ng new client-app
mkdir wwwroot
创建websocket服务
ng generate service WebSocket
我们还将创建用于将序列化和反序列化为JSON以便从服务端发送和接收的类。
ng生成类models/SocketMessage
ng生成类models/SquareChangeRequest
ng生成类models/Square
这些类的代码是:
export class Square {
Id: number;
Color: string;
}
export class SquareChangeRequest {
Id: number;
Color: string;
Name: string;
}
export class SocketMessage {
MessageType: string;
Payload: any
}
更新此文件上的此属性,以将“outputPath”更改为“wwwroot”。这将确保我们在.Net core SPA中间件中设置的目录是已编译的angular应用程序所在的目录。
[...]
"outputPath": "wwwroot"
[...]
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 { }
应用模块非常简单。主要的明显区别是我们对FormsModule的引用,并确保我们在providers集合中设置了WebSocketService。
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 = new BehaviorSubject([]);
announcement$: BehaviorSubject = new BehaviorSubject('');
name$: BehaviorSubject = new BehaviorSubject('');
private name: string;
constructor() { }
startSocket() {
this.socket = new WebSocket('wss://localhost: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服务端的大部分繁重工作一样,此service类负责繁琐的客户端工作。让我们分解一下正在发生的事情。
private socket: WebSocket;
squares$: BehaviorSubject = new BehaviorSubject([]);
announcement$: BehaviorSubject = new BehaviorSubject('');
name$: BehaviorSubject = new BehaviorSubject('');
private name: string;
首先,我们的声明。我们声明一个WebSocket对象来保存对我们的javascript websocket对象的引用。接下来,我们声明多个主题。当我们从服务端收到对正方形集合的更新时,将触发squares$主题。当我们收到来自服务端的公告更新时,会触发accounement$主题。最后,name$主题只是在我们成功连接套接字并从服务端接收到套接字后设置用户的名称时触发。
StartSocket()方法。发生魔术的方法:
this.socket = new WebSocket('wss://localhost: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;
}
}));
}
这是“messae”事件的事件侦听器。当我们收到来自服务端的消息时,我们确保来自服务端的消息具有相同的格式。所有消息都将反序列化到messageBox类中。此类具有“MessageType”属性,并且具有有效载荷,我们知道如何根据类型进行处理。当消息是“name”类型时,我们知道我们将在连接后收到设置用户名的消息,并在服务中设置该属性,然后将其广播到订阅“name $”订阅的任何组件。“announce”消息仅发送到订阅。最后,当消息为“squares”类型时,我们知道我们正在刷新正方形集合。
sendSquareChangeRequest(req: SquareChangeRequest) {
req.Name = this.name;
var requestAsJson = JSON.stringify(req);
this.socket.send(requestAsJson);
}
此方法仅用于将消息发送到服务端,以请求将颜色更改为正方形。我们设置进行更改的人的姓名,以及正方形的ID和新颜色。服务端将收到此请求,然后更新Square集合并将更改后的集合广播到所有人。
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);
}
}
在应用程序组件中,我们利用websocket服务将必要的订阅简单地绑定到我们的服务主题。
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;
});
}
该组件的构造函数只是将订阅设置为服务发生的事件,这些事件从服务端的消息中开始。
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);
}
单击一个正方形后,我们将创建一个正方形更改请求对象,并将此请求发送到服务端。
这是应用程序组件的模板,具有来自服务端的正方形的绑定以及显示当前正在滚动的消息的列表。此外,我们还具有用于选择颜色的下拉列表和单击事件绑定,这些绑定要求更改请求正方形的颜色。
Current Color:
{{square.Id}}
- {{message}}
这是完成工作的大部分代码。您可以通过以下步骤启动应用程序:
dotnet run
当应用程序启动时,你应该从服务器上得到一个带有可选颜色的下拉列表和一个正方形集合。您应该启动一个列表,该列表宣布您使用随机生成的名称与网站的连接。
接下来,在另一浏览器或另一台计算机上启动与网站的另一连接。当该用户连接时,第一个用户将看到其连接的通知。
现在,所有用户都可以选择一种颜色,然后单击一个正方形以将其更改为该颜色。用户应尝试将所有正方形更改为相同的颜色。
您可能会问的第一个问题是“嗯,为什么不使用SignalR”或“Socket.IO”?SignalR是.Net双向通信的实现。Socket是开源的websocket包装器的另一个实现。他们不仅在后台使用websocket,而且还落后于其他方法。我只想使用基本的“vanilla”网络套接字类和javascript实现即可。我发现的事实是这些库为您处理了多少事情。我遇到的第一个问题是与处理Websocket类产生的整个异步/同步问题有关的问题。SignalR可以为您完成所有这些工作。在客户端,普通的javascript是“OK”,但是像socket.io这样的客户端库会处理所有您必须非常小心的事情,例如断开连接和正确地排队消息。当然,另一件事是安全性。像SignalR这样的库在这里具有一定的优势,因为它们可以很好地插入整个.Net生态系统。这些库的作者在编写它们时就考虑了安全性和并发性。因此,我的建议是,除非您有一些主要的技术限制,这些限制仅要求原始websocket,否则我会很容易地建议您使用这些经过严格审查和维护良好的库之一。