How to use websockets in asp.net core - c#

im trying to develop a game where i store a scoreboard in a text file which is stored on server (currently on localhost). I am using http get and post calls in order to communicate with the server and get and send the data that i want. Now i want to implement websockets in order to send a notification from the server to the c# client. The notification will just display on the console a message for the user, for example in mu case i want to display a message to the user each time a user is added to the scoreboard, each time the UpdateScoreBoard method is called. Based on tutorials i found online i have managed to build the following code, can anyone make it more clear for me how the i will build the websocket for the server and how i will initialize the websocket on the client? Thank you
Startup.cs (Server)
public void Configure(IApplicationBuilder app, IHostEnvironment env)
{
//deleted code
var webSocketOptions = new WebSocketOptions()
{
KeepAliveInterval = TimeSpan.FromSeconds(120),
ReceiveBufferSize = 4 * 1024
};
app.UseWebSockets(webSocketOptions);
app.Use(async (context, next) =>
{
if (context.Request.Path == "/ws")
{
if (context.WebSockets.IsWebSocketRequest)
{
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
await Echo(context, webSocket);
}
else
{
context.Response.StatusCode = 400;
}
}
else
{
await next();
}
});
}
private async Task Echo(HttpContext context, WebSocket webSocket)
{
var buffer = new byte[1024 * 4];
WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
while (!result.CloseStatus.HasValue)
{
await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
}
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
}
HttpClass.cs (Client) - where i call the http post request
public async override Task<List<Scoreboard>> UpdateScoreBoards(string username, int attempts, int seconds, DateTime date)
{
HttpResponseMessage response = null;
//Creating a new instance of object Scoreboard
//deleted code
var url = "http://localhost:5000/api/Scoreboard";
var socket_url = new Uri("ws://localhost:5000");
var exitEvent = new ManualResetEvent(false);
using (var client = new WebsocketClient(socket_url))
{
client.ReconnectTimeout = TimeSpan.FromSeconds(30);
client.ReconnectionHappened.Subscribe(info =>
Log.Information($"Reconnection happened, type: {info.Type}"));
client.MessageReceived.Subscribe(msg => Log.Information($"Message received: {msg}"));
await client.Start();
await Task.Run(() => client.Send("test"));
exitEvent.WaitOne();
}
// deleted code
}

can anyone make it more clear for me how the i will build the websocket for the server and how i will initialize the websocket on the client?
As the example that you referenced demonstrated, making use of WebSocket in ASP.NET Core, we can add the WebSockets middleware in the Configure method, then add/configure request delegate to check and handle incoming WebSocket requests.
And after transitioned a request to a WebSocket connection with AcceptWebSocketAsync() method, we can use the returned WebSocket object to send and receive messages.
In Echo method, we can also perform custom code logic to generate and send reply message/notification based on received message(s).
//received message
var mes = Encoding.UTF8.GetString(buffer, 0, result.Count);
//code logic here
//...
//create reply message
var reply_mes = $"You sent {mes}.";
byte[] reply_mes_buffer = Encoding.UTF8.GetBytes(reply_mes);
await webSocket.SendAsync(new ArraySegment<byte>(reply_mes_buffer, 0, reply_mes.Length), result.MessageType, result.EndOfMessage, CancellationToken.None);
Besides, ASP.NET Core SignalR is an open-source library that simplifies implementing real-time communication functionality. And it does support WebSockets transport and we can easily achieving push messages/notifications to all connected clients or specified subsets of connected clients.
For more information about ASP.NET Core SignalR, you can check this doc: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/websockets?view=aspnetcore-3.1

The only thing you need in your Startup is to add the UseWebsockets middleware.
Then you can define your own middleware and filter connections if they are websocket type like below:
Startup
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
app.UseWebSockets();
app.UseMiddleware<SocketWare>();
}
Middleware
public class SocketWare {
private RequestDelegate next;
public SocketWare(RequestDelegate _next) {
this.next = _next;
}
public async Task Invoke(HttpContext context) {
if (!context.WebSockets.IsWebSocketRequest) {
return;
}
var socket=await context.WebSockets.AcceptWebSocketAsync();
await RunAsync(socket);
}
private async Task RunAsync(WebSocket socket) {
try {
var client = new ChatClient(socket);
await client.RunAsync();
} catch (Exception ex) {
throw;
}
}
}
In my middleware i prefer to keep my business logic in a separate class that gets the Websocket injected in it like below:
Client
public class ChatClient
{
private Task writeTask;
private Task readTask;
private WebSocket socket;
private CancellationTokenSource cts=new CancellationTokenSource();
ChatClient(WebSocket socket)
{
this.socket=socket;
}
public async Task RunAsync()
{
this.readTask=Task.Run(async ()=>await ReadLoopAsync(cts.Token),cts.Token);
this.writeTask=Task.Run(async()=>await WriteLoopAsync(cts.Token),cts.Token);
await Task.WhenAny(this.readTask,this.writeTask);
}
public async Task WriteLoopAsync()
{
Memory<byte> buffer=ArrayPool<byte>.Shared.Rent(1024);
try {
while (true) {
var result= await this.socket.ReceiveAsync(buffer,....);
var usefulBuffer=buffer.Slice(0,result.Count).ToArray();
var raw=Encoding.Utf8.GetString(usefulBuffer);
//deserialize it to whatever you need
//handle message as you please (store it somwhere whatever)
}
} catch (Exception ex) {
//socket error handling
//break loop or continue with go to
}
}
public async Task ReadLoopAsync()
{
try {
while (true) {
var data = await this.[someMessageProvider].GetMessageAsync() //read below !!!
var bytes = Encoding.UTF8.GetBytes(data);
//send the message on the websocket
await this.socket.SendAsync(data, WebSocketMessageType.Text, true, CancellationToken.None);
}
} catch (Exception ex) {
//do incorrect message/socket disconnect logic
}
}
}
Now regarding producing messages and consuming them. In your case you could define your producers as some Controller routes like below .You would hit a route , produce a message and publish it to some message broker. I would use a Message Queue (RabbitMQ) or even a Redis Pub/Sub as a message bus.
You would publish messages from your route(s) and then consume them in your ReadLoopAsync method from WebSocketClient (look above).
Producing messages
public UpdateController:Controller
{
private IConnection
[HttpPost]
[someroute]
public void UpdateScoreboard(string someMessage)
{
this.connection.Publish("someChannel",someMessage);
}
[HttpPost]
[someotherroute]
public void DeletePlayer(string someOtherMessage)
{
this.connection.Publish("someChannel",someMessage);
}
}
Redis pub/sub Check redis pub/sub here Also check my repository
on github here in which i am
using exactly what you need (websockets, redis,pub sub)
RabbitMq Another option as a message bus is to use RabbitMQ , for more info regarding C# API
here
In Memory
You could also avoid using a third party and use some in memory data
structure like a BlockingCollection.You could inject it as a
Singleton service both in your Controller(s) and your socket
Middleware(s)

Related

ContinueWith doesn't work in Controller to log

I can't find a solution to the problem despite many similar questions.
There is a Web API. On POST I need
read DB
make a HTTP call to other service to subscribe on notification (let's say it takes 5s)
return the data from the DB
In the step 2, I don't need to wait, I don't need to block the client (for 5sec), so the client should not wait for the response.
However, the server have to wait on result from 2 and log it. So far I've tried
[HttpPost("{callId}")]
public async Task<IActionResult> CreateSubs([FromRoute] string callId)
{
var data = await ...// read the DB
_ = SubscribeForUpdates(callId);
return Ok(data);
}
private async Task SubscribeForUpdates(string callId)
{
_logger.LogInformation("Subscribe client {ConnectionId} notifications", callId);
var requestMessage = new HttpRequestMessage
{
RequestUri = new Uri(_httpClient.BaseAddress, $"subscribe/{callId}"),
Method = HttpMethod.Get,
};
var result = await SendAsync<SubscriptionResponse>(requestMessage);
if (result.IsSuccess)
{
Console.WriteLine("Success");
}
else
{
Console.WriteLine("Fail");
}
}
SendAsync is from some library and so smth like _httpClient.SendAsync
In this case the request will not be blocked, the internal HTTP request is successful but I there is no Success from Console.WriteLine("Success");. Only if I put a breakpoint there it logs.
Could you please help me to understand why this is not log and how to fix that?
I've tried ContinueWith - no result
await SendAsync<ServerSubscriptionResponse>(requestMessage)
.ContinueWith(t =>
{
if (t.Result.IsSuccess)
{
Console.WriteLine("Success");
}
else
{
Console.WriteLine("Fail");
}
})
When I use await SubscribeForUpdates(callId) inasted of _ = SubscribeForUpdates(callId) it works and logs but the blocks a client. I need to avoid that

Can't connect secured Asp.Net Core Web Socket hosted on Azure Web App (using TLS)

I spent a whole day searching for a solution but I didn't solve my problem. As you can see from the title, I implemented a basic Web Socket using Asp.Net Core (3.1) framework and I deployed it on Azure (on a Web App service). I successfully make it works without the TLS protocol (so I think the ws has been configured in a good manner) but when I try to connect using wss I receive this errors on the client side:
System.Net.WebSockets.WebSocketException : Unable to connect to the remote server
System.Net.WebException: The underlying connection was closed: An unexpected error occurred on a send.
System.IO.IOException: Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host.
System.Net.Sockets.SocketException: An existing connection was forcibly closed by the remote host
I tried to switch the "HTTPS Only" trigger on azure portal but it keep refusing any client to connect.
Do you have any idea how to let wss works with Azure Web App? Do I need to configure a certificate? I read that azure provide a certificate if the user didn't have one. Thanks and regards
UPDATE: the code has been "copied" from this great Les Jackson's tutorial and is available on git hub at this precise address. The important part of the code is here:
/* THIS IS THE SERVER PART WHERE THE CONNECTION IS ACCEPTED */
namespace WebSocketServer.Middleware
{
public class WebSocketServerMiddleware
{
private readonly RequestDelegate _next;
private WebSocketServerConnectionManager _manager;
public WebSocketServerMiddleware(RequestDelegate next, WebSocketServerConnectionManager manager)
{
_next = next;
_manager = manager;
}
public async Task InvokeAsync(HttpContext context)
{
if (context.WebSockets.IsWebSocketRequest)
{
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
await Receive(webSocket, async (result, buffer) =>
{
if (result.MessageType == WebSocketMessageType.Text)
{
Console.WriteLine($"Receive->Text");
return;
}
else if (result.MessageType == WebSocketMessageType.Close)
{
await sock.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
return;
}
});
}
else
{
await _next(context);
}
}
}
}
/* THIS IS THE STARTUP FILE*/
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddWebSocketServerConnectionManager();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseWebSockets();
app.UseWebSocketServer();
}
}
/* THIS IS THE CLIENT (WRITTEN IN NET FULLFRAMEWORK) */
Console.Write("Connecting....");
var cts = new CancellationTokenSource();
var socket = new ClientWebSocket();
string wsUri = "wss://testingwebsocket123123.azurewebsites.net";
await socket.ConnectAsync(new Uri(wsUri), cts.Token);
Console.WriteLine(socket.State);
await Task.Factory.StartNew(
async () =>
{
var rcvBytes = new byte[1024 * 1024];
var rcvBuffer = new ArraySegment<byte>(rcvBytes);
while (true)
{
WebSocketReceiveResult rcvResult = await socket.ReceiveAsync(rcvBuffer, cts.Token);
byte[] msgBytes = rcvBuffer.Skip(rcvBuffer.Offset).Take(rcvResult.Count).ToArray();
string rcvMsg = Encoding.UTF8.GetString(msgBytes);
Console.WriteLine("Received: {0}", rcvMsg);
}
}, cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
Thank you for reading
As disussed over the comments, it works fine with the provided javascript client in the sample. The error in .net client happens because of TLS version when you connect from c# client with full framework. The screenshot of your Azure web app enforces min TLS 1.2. Set that in the .net client like below:
System.Net.ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;

http2 request docker container memory goes up and up - doesn't release

I have .NET Core 3.1 Web API in a docker container on Linux.
I use test tool that makes 1000 sequential requests to the Web API.
The Web API controller looks like this:
public MyController(ISendService sservice)
{
_sservice = sservice;
}
[HttpPost()]
public async Task<IActionResult> SendMessage([FromBody] SendMessageRequest request)
{
await _sservice.SendIt(request.Message);
}
My Send Service looks like this:
public class SendService: ISendService
{
private readonly HttpClient _client;
public SendService(HttpClient client)
{
_client = client;
}
public async Task SendMessage(string data)
{
var request= new HttpRequestMessage(HttpMethod.Post, "https://somelocation/test") { Version = new Version(2, 0) };
request.Content = new StringContent(data);
var response = await _client.SendAsync(request);
//Log response
}
}
I add the SendService in Startup like so:
services.AddHttpClient<ISendService, SendService>().ConfigurePrimaryHttpMessageHandler(() =>
{
var handler = new HttpClientHandler { SslProtocols = SslProtocols.Tls12 };
var store = new Store.GetStore();
handler.ClientCertificates.Add(store.certificate);
return handler;
});
My problem is that whenever SendMessage is called, the memory usage inside docker container goes up with each request. i.e. I call it 10 times, the memory will go up and stay there. I call it 1000 times, the memory goes up and up, beyond 85% (read that the limit should be 75% in .NET Core 3.1) and stay there even waiting 20 minutes with each test scenario.
Why does it not appear to garbage collect or release the memory? I running tests but I think it will reach 100% and the service will stop which is not good. Thank you
Right now you are blocking on every call to your service by doing
_client.SendAsync(request).GetAwaiter().GetResult()
and thats a big no-no.
You can read more about it here: https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html
Change your ISendService interface from
void SendMessage(string data)
to
Task SendMessage(string data)
Then in your SendService add async in your method declaration and replace the following row
var response = _client.SendAsync(request).GetAwaiter().GetResult();
With
var response = await _client.SendAsync(request);
Then in your controller add the async keyword and await it like this:
[HttpPost()]
public async Task<IActionResult> SendMessage([FromBody] SendMessageRequest request)
{
await _sservice.SendIt(request.Message);
}
Also, regarding the memory usage, are you sure that it's an issue? Unused memory is worthless... :)

Open Web Socket depend on current route C# asp net core

Is it possible to open a web socket and connect to the end user, but only through a particular route?
Let's assume that I have a web API which has the following functionalities:
article section on route: /articles
chat module on route: /chat
At this moment I have a web socket middleware which handles all the ws request independently on root which is used by the end user.
So, theoretically web socket connection could be established in any place of the front layer of an application if I just write the code which will be able to do it.
I don't want it, so how can I prevent those situations?
Below is my current solution:
using System;
using System.IO;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace chat.Framework
{
public class WebSocketMiddleware
{
private readonly RequestDelegate _next;
private WebSocketHandler _webSocketHandler;
public WebSocketMiddleware(RequestDelegate next, WebSocketHandler webSocketHandler)
{
_next = next;
_webSocketHandler = webSocketHandler;
}
public async Task Invoke(HttpContext context)
{
if (!context.WebSockets.IsWebSocketRequest)
{
await _next.Invoke(context);
return;
}
var socket = await context.WebSockets.AcceptWebSocketAsync();
await _webSocketHandler.OnConnected(socket);
await Recive(socket, async(result, buffer) =>
{
if(result.MessageType == WebSocketMessageType.Text)
{
await _webSocketHandler.Recive(socket, result, buffer);
return;
}
else if (result.MessageType == WebSocketMessageType.Close)
{
await _webSocketHandler.OnDisconnected(socket);
Console.WriteLine("Disconnect");
return;
}
});
}
private async Task Recive(WebSocket socket, Action<WebSocketReceiveResult, byte[]> handle)
{
var buffer = new ArraySegment<byte>(new Byte[1024 * 4]);
WebSocketReceiveResult result = null;
var ms = new MemoryStream();
while(socket.State == WebSocketState.Open)
{
do
{
result = await socket.ReceiveAsync(buffer, CancellationToken.None);
} while (!result.EndOfMessage);
var newBuffer = new byte[result.Count];
Array.Copy(buffer.Array, newBuffer, result.Count);
handle(result, newBuffer);
Console.WriteLine(newBuffer.Length);
}
}
}
}
ASP.NET Core Middleware run on every single request, so if you want to do routing logic, you need to implement it inside your middleware. For your two routes, this should be pretty simple, just check context.Request.Path in your Invoke method before accepting the WebSocket in order to determine which kind of connection to handle.

HttpClient PostAsync not working in CRM plugin

I am trying to send json to a web API using HttpClient.PostAsync. It works from a console application but not from my CRM plugin. Doing some research I noted that it is probably something to do with the context the plugin runs in and threading. Anyway here is my calling code:
public async void Execute(IServiceProvider serviceProvider)
{
IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
if (context.InputParameters.Contains("Target"))
{
if (context.InputParameters["Target"] is Entity)
{
Entity entity = (Entity)context.InputParameters["Target"];
if (entity.LogicalName == "new_product")
{
IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);
try
{
if (entity.Contains("new_begindate") && entity.Contains("new_expirationdate"))
{
await OnlineSignUp(entity, service);
}
}
catch (InvalidPluginExecutionException)
{
throw;
}
catch (Exception e)
{
throw new InvalidPluginExecutionException(OperationStatus.Failed, "Error signing up: " + e.Message);
}
}
}
}
}
And here is the relevant code for sending the json:
private async Task<HttpResponseMessage> OnlineSignUp(Entity license, IOrganizationService service)
{
...
var json = JsonConvert.Serialize(invitation);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Token", "token=7d20f3f09ef24067ae64f4323bc95163");
Uri uri = new Uri("http://signup.com/api/v1/user_invitations");
var response = await httpClient.PostAsync(uri, content).ConfigureAwait(false);
int n = 1;
return response;
}
}
The exception is thrown with a message "Thread was being aborted". Can anyone tell me what I am doing wrong?
I would guess this is going to fail somewhat randomly based on use of async/await. I wouldn't think CRM really supports plugins returning control before they are complete. When it fails, it looks like the thread was in the process of being cleaned up behind the scenes.
CRM is already handling multi-threading of plugins and supports registering plugin steps as asynchronous if they are long running (or don't need to be run in the synchronous pipeline). It would make more sense to use a synchronous HTTP call here like:
var response = httpClient.PostAsync(uri, content).Result;
EDIT: To illustrate, this is an overly-trivialized example of what is most likely happening when CRM goes to kickoff your plugin and you're using async/await (from LinqPad).
static async void CrmInternalFunctionThatKicksOffPlugins()
{
var plugin = new YourPlugin();
//NOTE Crm is not going to "await" your plugin execute method here
plugin.Execute();
"CRM is Done".Dump();
}
public class YourPlugin
{
public async void Execute()
{
await OnlineSignUp();
}
private async Task<HttpResponseMessage> OnlineSignUp()
{
var httpClient = new HttpClient();
var r = await httpClient.PostAsync("http://www.example.com", null);
"My Async Finished".Dump();
return r;
}
}
Which will print:
CRM is Done My Async Finished
looks like you are using Json.NET, when you use external assemblies there are some things to take care of (merging) and not always the result works.
Try to serialize using DataContractJsonSerializer
example: http://www.crmanswers.net/2015/02/json-and-crm-sandbox-plugins.html

Categories