From a0d35d1c47cc9c92c6db15a4461aa0bee63585a2 Mon Sep 17 00:00:00 2001 From: Alexis DRAI Date: Sat, 11 Feb 2023 10:33:23 +0100 Subject: [PATCH] :socks: Implement WebSocket! --- ApiGateway/Program.cs | 2 ++ ApiGateway/ocelot.json | 14 +++++++++ README.md | 32 ++++++++++++++++---- Tests/Controllers/CatsControllerTest.cs | 4 +-- cat_cafe/ApiGateway.csproj | 15 ++++++++++ cat_cafe/Controllers/CatsController.cs | 14 +++++++-- cat_cafe/Program.cs | 40 +++++++++++++++++++++++++ cat_cafe/WeSo/WebSocketHandler.cs | 28 +++++++++++++++++ cat_cafe/cat_cafe.csproj | 1 + 9 files changed, 141 insertions(+), 9 deletions(-) create mode 100644 cat_cafe/ApiGateway.csproj create mode 100644 cat_cafe/WeSo/WebSocketHandler.cs diff --git a/ApiGateway/Program.cs b/ApiGateway/Program.cs index 2801331..3536aa9 100644 --- a/ApiGateway/Program.cs +++ b/ApiGateway/Program.cs @@ -32,6 +32,8 @@ app.UseAuthorization(); app.MapControllers(); +app.UseWebSockets(); + await app.UseOcelot(); app.Run(); diff --git a/ApiGateway/ocelot.json b/ApiGateway/ocelot.json index 8451b05..d1b8387 100644 --- a/ApiGateway/ocelot.json +++ b/ApiGateway/ocelot.json @@ -127,6 +127,20 @@ "Port": 7229 } ] + }, + + { + "UpstreamPathTemplate": "/gateway/ws", + "UpstreamHttpMethod": [ "Get", "Post" ], + "DownstreamPathTemplate": "/ws", + "DownstreamScheme": "wss", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 7229 + } + ], + "UseWebSockets": true } ] } \ No newline at end of file diff --git a/README.md b/README.md index e0c95c9..8d96eed 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ### Concept -This application attempts to modelize a cat café, with cafés (called "bars" here), cats, and customers. The cats can go "meow". +This application attempts to modelize a cat café, with cafés (called "bars" here), cats, and customers. ### API REST @@ -18,8 +18,21 @@ We used an ASP .NET Web API, with a Swagger configuration to visualize the inter ### WebSocket -... +A websocket was set up to notify clients (who subscribe to it) whenever a Cat is `POST`ed. + +Clients need to subscribe by typing the following code in the console of their browser, in developer mode : +```js +new WebSocket("wss://localhost:5003/gateway/ws").onmessage = function (event) { + if (event.data === "entity-created") { + alert("A new entity was created!"); + } +}; +``` +*Note*: +- while the app uses port `7229` in our default config, **you should use port `5003` anyway** to subscribe to our WebSocket through our *API Gateway* +- `"entity-created"` is a hard-coded event ID and should not be changed. +- you are free to change the content of the `Alert` itself, of course ### API Gateway An [Ocelot](https://ocelot.readthedocs.io/en/latest/) API Gateway manages the whole system. @@ -54,12 +67,21 @@ Overall, the architecture may be summed up like so: #### Routes The Gateway routes offer access to the REST API in a similar way as the REST API itself, with a small transformation: there is a new port, and the word "gateway" replaces "api". The REST API's Swagger UI will give you all the information required about those routes. -| REST(old) | Gateway(current) | +| REST(old) | Gateway(current) | |--|--| -| `.../7229/api/...` | `.../5003/gateway/...` | | `GET` on `https://localhost/7229/api/cats` | `GET` on `https://localhost/5003/gateway/cats` | | `POST` on `https://localhost/7229/api/bars/{id}` | `GET` on `https://localhost/5003/gateway/bars/{id}` | - + +...and for the websocket: +- old : +```js +new WebSocket("wss://localhost:7229/ws").onmessage = function (event) {...}; +``` + +- new : +```js +new WebSocket("wss://localhost:5003/gateway/ws").onmessage = function (event) {...}; +``` #### Caching The gateway uses caching to ensure that the entire list of customers is only queried from the database once every 10 seconds. The rest of the time, clients sending `GET`-all requests get served the contents of a cache. diff --git a/Tests/Controllers/CatsControllerTest.cs b/Tests/Controllers/CatsControllerTest.cs index 98d3ba2..66f207d 100644 --- a/Tests/Controllers/CatsControllerTest.cs +++ b/Tests/Controllers/CatsControllerTest.cs @@ -18,7 +18,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using FluentAssertions; - +using cat_cafe.WeSo; namespace cat_cafe.Controllers.Tests { @@ -64,7 +64,7 @@ namespace cat_cafe.Controllers.Tests { mapper = mapperConf.CreateMapper(); context = new CatCafeContext(options); - controller = new CatsController(context, mapper, logger); + controller = new CatsController(context, mapper, logger, new WebSocketHandler(new List())); aliceDto = mapper.Map(alice); bobDto = mapper.Map(bob); } diff --git a/cat_cafe/ApiGateway.csproj b/cat_cafe/ApiGateway.csproj new file mode 100644 index 0000000..1f9898b --- /dev/null +++ b/cat_cafe/ApiGateway.csproj @@ -0,0 +1,15 @@ + + + + net6.0 + enable + enable + + + + + + + + + diff --git a/cat_cafe/Controllers/CatsController.cs b/cat_cafe/Controllers/CatsController.cs index 0857fa9..033ab58 100644 --- a/cat_cafe/Controllers/CatsController.cs +++ b/cat_cafe/Controllers/CatsController.cs @@ -12,6 +12,7 @@ using cat_cafe.Dto; using Serilog; using Newtonsoft.Json; using Microsoft.Extensions.Logging.Abstractions; +using cat_cafe.WeSo; namespace cat_cafe.Controllers { @@ -22,12 +23,19 @@ namespace cat_cafe.Controllers private readonly CatCafeContext _context; private readonly IMapper _mapper; private readonly ILogger _logger; - - public CatsController(CatCafeContext context, IMapper mapper, ILogger logger) + private readonly WebSocketHandler _webSocketHandler; + + public CatsController( + CatCafeContext context, + IMapper mapper, + ILogger logger, + WebSocketHandler webSocketHandler + ) { _mapper = mapper; _context = context; _logger = logger; + _webSocketHandler = webSocketHandler; } // GET: api/Cats @@ -94,6 +102,8 @@ namespace cat_cafe.Controllers _context.Cats.Add(cat); await _context.SaveChangesAsync(); + await _webSocketHandler.BroadcastMessageAsync("entity-created"); + return CreatedAtAction("GetCat", new { id = catDto.Id }, _mapper.Map(cat)); } diff --git a/cat_cafe/Program.cs b/cat_cafe/Program.cs index 47a5e4b..e53f6f9 100644 --- a/cat_cafe/Program.cs +++ b/cat_cafe/Program.cs @@ -2,13 +2,19 @@ using Microsoft.EntityFrameworkCore; using cat_cafe.Repositories; using Serilog; using Serilog.Sinks.File; +using System.Net.WebSockets; +using cat_cafe.WeSo; var builder = WebApplication.CreateBuilder(args); Log.Logger = new LoggerConfiguration().MinimumLevel.Information().WriteTo.File("log.txt").CreateLogger(); +List _sockets = new(); + // Add services to the container. +builder.Services.AddSingleton>(x => _sockets); +builder.Services.AddSingleton(); builder.Services.AddControllers(); builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("CatCafe")); builder.Services.AddEndpointsApiExplorer(); @@ -33,6 +39,40 @@ app.UseAuthorization(); app.MapControllers(); +app.UseWebSockets(); + + +app.Use(async (context, next) => +{ + if (context.Request.Path == "/ws") + { + if (context.WebSockets.IsWebSocketRequest) + { + WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync(); + + _sockets.Add(webSocket); + + var buffer = new byte[1024 * 4]; + WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + while (!result.CloseStatus.HasValue) + { + await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); + + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + } + await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + } + else + { + context.Response.StatusCode = 400; + } + } + else + { + await next(); + } +}); + Log.Information("program start"); app.Run(); diff --git a/cat_cafe/WeSo/WebSocketHandler.cs b/cat_cafe/WeSo/WebSocketHandler.cs new file mode 100644 index 0000000..d50dd41 --- /dev/null +++ b/cat_cafe/WeSo/WebSocketHandler.cs @@ -0,0 +1,28 @@ +using System.Net.WebSockets; +using System.Text; + +namespace cat_cafe.WeSo +{ + public class WebSocketHandler + { + private readonly List _sockets; + + public WebSocketHandler(List sockets) + { + _sockets = sockets; + } + + public async Task BroadcastMessageAsync(string message) + { + var buffer = Encoding.UTF8.GetBytes(message); + foreach (var socket in _sockets) + { + if (socket.State == WebSocketState.Open) + { + await socket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, CancellationToken.None); + } + } + } + } + +} diff --git a/cat_cafe/cat_cafe.csproj b/cat_cafe/cat_cafe.csproj index 31cfb39..4aff7dd 100644 --- a/cat_cafe/cat_cafe.csproj +++ b/cat_cafe/cat_cafe.csproj @@ -7,6 +7,7 @@ + -- 2.36.3