I've tried to define a gRPC service where client can subscribe to receive broadcasted messages and they can also send them.
syntax = "proto3";
package Messenger;
service MessengerService {
rpc SubscribeForMessages(User) returns (stream Message) {}
rpc SendMessage(Message) returns (Close) {}
}
message User {
string displayName = 1;
}
message Message {
User from = 1;
string message = 2;
}
message Close {}
My idea was that when a client requests to subscribe to the messages, the response stream would be added to a collection of response streams, and when a message is sent, the message is sent through all the response streams.
However, when my server attempts to write to the response streams, I get an exception System.InvalidOperationException: 'Response stream has already been completed.'
Is there any way to tell the server to keep the streams open so that new messages can be sent through them? Or is this not something that gRPC was designed for and a different technology should be used?
The end goal service would be allows multiple types of subscriptions (could be to new messages, weather updates, etc...) through different clients written in different languages (C#, Java, etc...). The different languages part is mainly the reason I chose gRPC to try this, although I intend on writing the server in C#.
Implementation example
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Grpc.Core;
using Messenger;
namespace SimpleGrpcTestStream
{
/*
Dependencies
Install-Package Google.Protobuf
Install-Package Grpc
Install-Package Grpc.Tools
Install-Package System.Interactive.Async
Install-Package System.Linq.Async
*/
internal static class Program
{
private static void Main()
{
var messengerServer = new MessengerServer();
messengerServer.Start();
var channel = Common.GetNewInsecureChannel();
var client = new MessengerService.MessengerServiceClient(channel);
var clientUser = Common.GetUser("Client");
var otherUser = Common.GetUser("Other");
var cancelClientSubscription = AddCancellableMessageSubscription(client, clientUser);
var cancelOtherSubscription = AddCancellableMessageSubscription(client, otherUser);
client.SendMessage(new Message { From = clientUser, Message_ = "Hello" });
client.SendMessage(new Message { From = otherUser, Message_ = "World" });
client.SendMessage(new Message { From = clientUser, Message_ = "Whoop" });
cancelClientSubscription.Cancel();
cancelOtherSubscription.Cancel();
channel.ShutdownAsync().Wait();
messengerServer.ShutDown().Wait();
}
private static CancellationTokenSource AddCancellableMessageSubscription(
MessengerService.MessengerServiceClient client,
User user)
{
var cancelMessageSubscription = new CancellationTokenSource();
var messages = client.SubscribeForMessages(user);
var messageSubscription = messages
.ResponseStream
.ToAsyncEnumerable()
.Finally(() => messages.Dispose());
messageSubscription.ForEachAsync(
message => Console.WriteLine($"New Message: {message.Message_}"),
cancelMessageSubscription.Token);
return cancelMessageSubscription;
}
}
public static class Common
{
private const int Port = 50051;
private const string Host = "localhost";
private static readonly string ChannelAddress = $"{Host}:{Port}";
public static User GetUser(string name) => new User { DisplayName = name };
public static readonly User ServerUser = GetUser("Server");
public static readonly Close EmptyClose = new Close();
public static Channel GetNewInsecureChannel() => new Channel(ChannelAddress, ChannelCredentials.Insecure);
public static ServerPort GetNewInsecureServerPort() => new ServerPort(Host, Port, ServerCredentials.Insecure);
}
public sealed class MessengerServer : MessengerService.MessengerServiceBase
{
private readonly Server _server;
public MessengerServer()
{
_server = new Server
{
Ports = { Common.GetNewInsecureServerPort() },
Services = { MessengerService.BindService(this) },
};
}
public void Start()
{
_server.Start();
}
public async Task ShutDown()
{
await _server.ShutdownAsync().ConfigureAwait(false);
}
private readonly ConcurrentDictionary<User, IServerStreamWriter<Message>> _messageSubscriptions = new ConcurrentDictionary<User, IServerStreamWriter<Message>>();
public override async Task<Close> SendMessage(Message request, ServerCallContext context)
{
await Task.Run(() =>
{
foreach (var (_, messageStream) in _messageSubscriptions)
{
messageStream.WriteAsync(request);
}
}).ConfigureAwait(false);
return await Task.FromResult(Common.EmptyClose).ConfigureAwait(false);
}
public override async Task SubscribeForMessages(User request, IServerStreamWriter<Message> responseStream, ServerCallContext context)
{
await Task.Run(() =>
{
responseStream.WriteAsync(new Message
{
From = Common.ServerUser,
Message_ = $"{request.DisplayName} is listening for messages!",
});
_messageSubscriptions.TryAdd(request, responseStream);
}).ConfigureAwait(false);
}
}
public static class AsyncStreamReaderExtensions
{
public static IAsyncEnumerable<T> ToAsyncEnumerable<T>(this IAsyncStreamReader<T> asyncStreamReader)
{
if (asyncStreamReader is null) { throw new ArgumentNullException(nameof(asyncStreamReader)); }
return new ToAsyncEnumerableEnumerable<T>(asyncStreamReader);
}
private sealed class ToAsyncEnumerableEnumerable<T> : IAsyncEnumerable<T>
{
public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default)
=> new ToAsyncEnumerator<T>(_asyncStreamReader, cancellationToken);
private readonly IAsyncStreamReader<T> _asyncStreamReader;
public ToAsyncEnumerableEnumerable(IAsyncStreamReader<T> asyncStreamReader)
{
_asyncStreamReader = asyncStreamReader;
}
private sealed class ToAsyncEnumerator<TEnumerator> : IAsyncEnumerator<TEnumerator>
{
public TEnumerator Current => _asyncStreamReader.Current;
public async ValueTask<bool> MoveNextAsync() => await _asyncStreamReader.MoveNext(_cancellationToken);
public ValueTask DisposeAsync() => default;
private readonly IAsyncStreamReader<TEnumerator> _asyncStreamReader;
private readonly CancellationToken _cancellationToken;
public ToAsyncEnumerator(IAsyncStreamReader<TEnumerator> asyncStreamReader, CancellationToken cancellationToken)
{
_asyncStreamReader = asyncStreamReader;
_cancellationToken = cancellationToken;
}
}
}
}
}
The problem you're experiencing is due to the fact that MessengerServer.SubscribeForMessages returns immediately. Once that method returns, the stream is closed.
You'll need an implementation similar to this to keep the stream alive:
public class MessengerService : MessengerServiceBase
{
private static readonly ConcurrentDictionary<User, IServerStreamWriter<Message>> MessageSubscriptions =
new Dictionary<User, IServerStreamWriter<Message>>();
public override async Task SubscribeForMessages(User request, IServerStreamWriter<ReferralAssignment> responseStream, ServerCallContext context)
{
if (!MessageSubscriptions.TryAdd(request))
{
// User is already subscribed
return;
}
// Keep the stream open so we can continue writing new Messages as they are pushed
while (!context.CancellationToken.IsCancellationRequested)
{
// Avoid pegging CPU
await Task.Delay(100);
}
// Cancellation was requested, remove the stream from stream map
MessageSubscriptions.TryRemove(request);
}
}
As far as unsubscribing / cancellation goes, there are two possible approaches:
The client can hold onto a CancellationToken and call Cancel() when it wants to disconnect
The server can hold onto a CancellationToken which you would then store along with the IServerStreamWriter in the MessageSubscriptions dictionary via a Tuple or similar. Then, you could introduce an Unsubscribe method on the server which looks up the CancellationToken by User and calls Cancel on it server-side
Similar to Jon Halliday's answer, an indefinately long Task.Delay(-1) could be used and passed the context's cancellation token.
A try catch can be used to remove end the server's response stream when the task is cancelled.
public override async Task SubscribeForMessages(User request, IServerStreamWriter<Message> responseStream, ServerCallContext context)
{
if (_messageSubscriptions.ContainsKey(request))
{
return;
}
await responseStream.WriteAsync(new Message
{
From = Common.ServerUser,
Message_ = $"{request.DisplayName} is listening for messages!",
}).ConfigureAwait(false);
_messageSubscriptions.TryAdd(request, responseStream);
try
{
await Task.Delay(-1, context.CancellationToken);
}
catch (TaskCanceledException)
{
_messageSubscriptions.TryRemove(request, out _);
}
}
Related
I've two APIs.
When one of the endpoints are called in API #1, message is sent to queue in Azure Service Bus.
API #2 should listen and make some changes in DB after this message appears in queue.
Message is sent to queue successfully.
Listener part doesn't work because the listener method is never called (and I do not understand how to call it).
Listener in API #2 :
public class MessageConsumer : IMessageConsumer
{
const string connectionString = "stringTakenFromAzure";
private static IQueueClient queueClient;
private CartingDbContext context;
public MessageConsumer(CartingDbContext context)
{
this.context = context;
}
public async Task Consume()
{
queueClient = new QueueClient(connectionString, "cartqueue");
var options = new MessageHandlerOptions(ExceptionReceivedHandler)
{
MaxConcurrentCalls = 1,
AutoComplete = false
};
queueClient.RegisterMessageHandler(ProcessMessageAsync, options);
await queueClient.CloseAsync();
}
private async Task ProcessMessageAsync(Microsoft.Azure.ServiceBus.Message message, CancellationToken cancellationToken)
{
var jsonBody = Encoding.UTF8.GetString(message.Body);
var categoryItem = JsonSerializer.Deserialize<CategoryItem>(jsonBody);
//update item in DB.
var categoryItemInDb = context.CategoryItems.Where(x => x.Id == categoryItem.Id).FirstOrDefault();
if (categoryItemInDb == null)
{
context.CategoryItems.Add(categoryItem);
}
else
{
context.CategoryItems.Update(categoryItem);
}
context.SaveChanges();
await queueClient.CompleteAsync(message.SystemProperties.LockToken);
}
private static Task ExceptionReceivedHandler(ExceptionReceivedEventArgs args)
{
return Task.CompletedTask;
}
}
Program.cs
builder.Services.AddTransient<IMessageConsumer, MessageConsumer>();
I'm very close to implementing a system in C# which allows me to send a push-notification with the click of a button from an interface written in ASP.net to a C# console application acting as client. This all using WebSockets.
After reading a lot of tutorials and reusing code found online I'm already able to successfully establish a WebSocket connection. I'm yet not able to actually send a Notification.
The part I'm struggling with is the function that get's triggered as soon as a button is clicked:
//Close ticket and send push-notification over websocket
public void Close(int id) {
//Ticket ticket = mgr.GetTicket(id);
//Create a new notification
Notification notif = new Notification();
notif.message = "Rofl test123 Notification lol";
//Initialize WebSocketMiddleware here??
//WebSocketsMiddleware wsm = new WebSocketsMiddleware(what parameter??);
//wsm.Invoke(what HttpContext parameter???)
NotificationManager notifMgr;
//notifMgr.AddSubscriber(wsm);
//notifMgr.SendNotificationAsync(notif);
return;
}
The specific questions/problems I'm encountering are:
How to initialize the class WebSocketsMiddleware? Does it need to be initialized, if yes, what is the parameter with type RequestDelegate? What do I pass to that parameter?
WebSocketsMiddleware has an Invoke function with parameter context of type HttpContext. Do I just need to pass new HttpContext() to this? Is that sufficient?
Someone made a class NotificationManager, this class uses the middleware to actually send a notification. Do I just need to pass the initialized WebSocketsMiddleware variable as parameter for NotificationManager.AddSubscriber()? Will the notifications of each client be nicely separated then?
Can I after that just use SendNotificationAsync() to send the notification?
Bonus question: Say that each client has it's own button. When I click a client's button only that client may receive a push-notification. How to make sure that all the other client's don't receive the same notification as well?
To be able to help me with these questions you'll need the following classes. The question is merely about WebSockets but more about how to initiate and use the classes I gathered from the tutorials.
Notification.cs - Class representing a Notification (notification text, send-date,...):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SC.UI.MVC.Models
{
public class Notification
{
public Guid? notificationId { get; set; }
public int id { get; set; }
public DateTime timestamp { get; set; }
public string message { get; set; }
public string type { get; set; }
public Notification()
{
// add a new guid as a unique identifier for the notification in the db
notificationId = Guid.NewGuid();
}
}
}
WebSocketsMiddleware.cs - Has the low-level part of the WebSockets handled, invoking connection etc:
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
namespace NotificationsApi.Notifications
{
public class WebSocketsMiddleware
{
// private variable to track the next delegate to call in the request chain
private readonly RequestDelegate _next;
public WebSocketsMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
CancellationToken ct = context.RequestAborted;
string currentSubscriberId = null;
WebSocket currentSocket = null;
// we want to listen on a specific path for websocket communications
if (context.Request.Path == "/notifications/ws")
{
// make sure the request is a websocket request
if (context.WebSockets.IsWebSocketRequest)
{
currentSocket = await context.WebSockets.AcceptWebSocketAsync();
currentSubscriberId = NotificationManager.Instance.AddSubscriber(currentSocket);
// keep the socket open until we get a cancellation request
while (true)
{
if (ct.IsCancellationRequested)
{
break;
}
}
}
else // return an HTTP bad request status code if anything other a web socket request is made on this URI
{
context.Response.StatusCode = 400;
}
}
// clean up the socket
if (!string.IsNullOrWhiteSpace(currentSubscriberId))
{
NotificationManager.Instance.RemoveSubscriber(currentSubscriberId);
if (currentSocket != null)
{
await currentSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
currentSocket.Dispose();
}
}
// call the next delegate in the pipeline
await _next(context);
return;
}
}
}
NotificationManager.cs - Interface/Class with three functions to add and remove subscribers, and to actually send a notification. Uses the WebSocket middleware to achieve this:
using SC.UI.MVC.Models;
//using NotificationsApi.Persistence;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace NotificationsApi.Notifications
{
// interface for NotificationManager for dependency injection
public interface INotificationManager
{
string AddSubscriber(WebSocket subscriber);
void RemoveSubscriber(string subscriberId);
Task SendNotificationAsync(Notification notification);
}
public class NotificationManager : INotificationManager
{
// static instance of the NotificationManager class
private static INotificationManager _instance;
public static INotificationManager Instance { get { return _instance ?? (_instance = new NotificationManager()); } set { _instance = value; } }
// static dictionary to keep track of all notification subscribers
private static ConcurrentDictionary<string, WebSocket> _subscribers = new ConcurrentDictionary<string, WebSocket>();
// adds a subscriber to receive notifications
public string AddSubscriber(WebSocket subscriber)
{
var subscriberId = Guid.NewGuid().ToString();
_subscribers.TryAdd(subscriberId, subscriber);
return subscriberId.ToString();
}
// removes a notifications subscriber
public void RemoveSubscriber(string subscriberId)
{
WebSocket empty;
_subscribers.TryRemove(subscriberId, out empty);
}
// sends a notification to all subscribers
public async Task SendNotificationAsync(Notification notification)
{
// add the notification to the persistence store
//await PersistenceManager.Instance.AddNotificationAsync(notification);
// send the notification to all subscribers
foreach (var s in _subscribers)
{
if (s.Value.State == WebSocketState.Open)
{
var jsonNotification = JsonConvert.SerializeObject(notification);
await SendStringAsync(s.Value, jsonNotification);
}
}
}
// sends a string via web socket communication
private async Task SendStringAsync(WebSocket socket, string data, CancellationToken ct = default(CancellationToken))
{
var buffer = Encoding.UTF8.GetBytes(data);
var segment = new ArraySegment<byte>(buffer);
await socket.SendAsync(segment, WebSocketMessageType.Text, true, ct);
}
}
}
Client.cs - Client receiving the push-notification. Not really a problem here I guess:
/* WEBSOCKET PART */
//Variables for websocket
private static object consoleLock = new object();
private const int sendChunkSize = 256;
private const int receiveChunkSize = 256;
private const bool verbose = true;
private static readonly TimeSpan delay = TimeSpan.FromMilliseconds(30000);
//Function to check if a ticket from this client is closed/solved
public void checkTicketSolved() {
Thread.Sleep(1000);
Connect("ws://localhost:5050/notifications/ws").Wait();
Console.WriteLine("Press any key to exit...");
}
public static async Task Connect(string uri)
{
ClientWebSocket webSocket = null;
try
{
webSocket = new ClientWebSocket();
await webSocket.ConnectAsync(new Uri(uri), CancellationToken.None);
await Task.WhenAll(Receive(webSocket), Send(webSocket));
}
catch (Exception ex)
{
Console.WriteLine("Exception: {0}", ex);
}
finally
{
if (webSocket != null)
webSocket.Dispose();
Console.WriteLine();
lock (consoleLock)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("WebSocket closed.");
Console.ResetColor();
}
}
}
static UTF8Encoding encoder = new UTF8Encoding();
private static async Task Send(ClientWebSocket webSocket)
{
//byte[] buffer = encoder.GetBytes("{\"op\":\"blocks_sub\"}"); //"{\"op\":\"unconfirmed_sub\"}");
byte[] buffer = encoder.GetBytes("{\"op\":\"unconfirmed_sub\"}");
await webSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
while (webSocket.State == WebSocketState.Open)
{
LogStatus(false, buffer, buffer.Length);
await Task.Delay(delay);
}
}
private static async Task Receive(ClientWebSocket webSocket)
{
byte[] buffer = new byte[receiveChunkSize];
while (webSocket.State == WebSocketState.Open)
{
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Close)
{
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
}
else
{
LogStatus(true, buffer, result.Count);
}
}
}
private static void LogStatus(bool receiving, byte[] buffer, int length)
{
lock (consoleLock)
{
Console.ForegroundColor = receiving ? ConsoleColor.Green : ConsoleColor.Gray;
//Console.WriteLine("{0} ", receiving ? "Received" : "Sent");
if (verbose)
Console.WriteLine(encoder.GetString(buffer));
Console.ResetColor();
}
}
}
You can also find this code on Github.
The relevant parts of the code are located in:
WebServer/UI-MVC/Controllers/TicketController.cs -> Contains function triggered when a button is clicked.
WebServer/UI-MVC/Notifications -> Contains NotificationManager.cs and WebSocketsMiddleware.cs
WebServer/UI-MVC/Models -> Contains Notification.cs
Client/ contains all the code for the client's console application
To give you some context about the application:
This application represents a ticketing system which allows clients/customers which use my software to open support tickets. The WebServer-part is for administrators/employees of me to answer and manage tickets. The Console Application is what my customers/clients need to have installed in order to contact my support service and open a support ticket. When an administrator closes the ticket of a client by clicking a button, it means the ticket and thus the client's problem was resolved and closed. Resulting in the client getting a push-notification about that.
I am not looking for references to other tutorials about WebSockets or suggestions using SignalR instead or whatever, I've already read all of them and I've already used SignalR but am interested in pure WebSockets now. I would be very grateful for someone who could help me working out the first part of code posted in this question (the Close-function) and explains what he has done. Thanks!
I found the solution myself.
First I made a new controller called NotificationsController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using SC.UI.MVC.Models;
using NotificationsApi.Notifications;
//using NotificationsApi.Persistence;
using System.Net.Http;
// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
namespace NotificationsApi.Controllers
{
[Route("api/notifications")]
public class NotificationsController : Controller
{
// GET api/notifications
[HttpGet]
public ActionResult Get()
{
try
{
var notifications = new List<Notification>();
//notifications = PersistenceManager.Instance.GetNotifications();
return Ok(notifications);
}
catch (Exception exception)
{
// log exception
// TODO: implement logging
// return a 500
return StatusCode(500);
}
}
// POST api/notifications
[HttpPost]
public async Task<ActionResult> Post(string message)
{
Notification notification = new Notification();
notification.message = message;
Console.WriteLine(message);
try
{
// return a 400 if we didn't get a valid json payload in the body
if (notification == null)
return BadRequest();
await NotificationManager.Instance.SendNotificationAsync(notification);
// we aren't returning the object to reference because POSTing a notification is fire and forget
return Created(string.Empty, null);
}
catch (Exception exception)
{
// log the error
// TODO: implement logging
// return a 500
return StatusCode(500);
}
return Ok();
}
}
}
Then I commented out the unnecessary properties in Notification.cs so only Guid and message remain. Now I'm able to just send a notification by calling the NotificationsController with a POST request carrying the message-parameter as data.
I'm setting websocket on .net core using middlewares (it looks something like that https://radu-matei.com/blog/aspnet-core-websockets-middleware/), but I have one issue
for instance my client communicates with websocket via http protocol, in other words, websocket is wrapped in http, when my client sends http request, it goes to the service method where it finally sends message using websocket.
Problem: is it possible to get that websocket reply of messages before returning response?
public async Task SendMessage(WebSocket socket, string response)
{
if (socket.State != WebSocketState.Open)
return;
await socket.SendAsync(buffer: new ArraySegment<byte>(array: Encoding.UTF8.GetBytes(response),
offset: 0,
count: response.Length),
messageType: WebSocketMessageType.Text,
endOfMessage: true,
cancellationToken: CancellationToken.None);
await socket.ReceiveAsync() // When I finally receive some reply message, stop listening and return http response to my client
}
From what you said i understood that you need to share the websocket between multiple services, each using it how it sees fit.Based on this scenario the proposed implementation has a Middleware that contains the different services that require the socket . Special care must be taken when deciding which service does the writing and which does the reading.The websocket is thread-safe in the context of reading-writing at the same time but not in the other scenarios.(writing-writing,reading-reading)
Startup
public void ConfigureServices(IServiceCollection colllection){
collection.AddSingleton<Sender>();
collection.AddSingleton<Receiver>();
}
public void Configure(IApplicationBuilder app)
{
app.UseWebsockets();
app.UseMiddleware<DispatcherMiddleware>(); //receives socket here
}
Middleware
public class DispatcherMiddleware{
private Sender sender;
private Receiver receiver;
private RequestDelegate next;
public Dispatcher(RequestDelegate req,Sender _sender,Receiver _receiver)
{
this.sender=_sender;
this.receiver=_receiver;
this.next=req;
}
public async Task Invoke(HttpContext context){
if(!context.WebSockets.IsWebSocketRequest){
return;
}
await DispatchAsync(context.WebSockets);
}
public async Task DispatchAsync(WebsocketManager manager){
WebSocket socket=await manager.AcceptWebSocketAsync();
Task t1=Task.Run(async()=>await this.sender.SendAsync(socket));
Task t2=Task.Run(async()=>await this.receiver.ReceiveAsync(socket));
await Task.WhenAll(t1,t2);
}
}
Websocket services (example)
public class Sender(){
public async Task SendAsync(WebSocket ws){
try{
while(true){
// ws.SendAsync()
}
}
catch(Exception ex){
}
}
}
public class Receiver{
public async Task ReceiveAsync(WebSocket ws){
try
{
while(true){
//ws.ReceiveAsync(buffer)
}
}
catch (System.Exception)
{
throw;
}
}
}
Edit
You can not perform concurrent reads /writes on the same socket.With this said if you want to use the same socket what you can do is make it thread-safe.Since the operation is async i suggest using the SemaphoreSlim class.
Below is an implementation of a shared socket:
public class SafeSocket {
private const int BUFFER_SIZE = 1024;
private WebSocket socket { get; set; }
private SemaphoreSlim #lock = new SemaphoreSlim(1);
public SafeSocket(WebSocket socket) {
this.socket = socket;
}
public async Task<byte[]> ReadAsync() {
byte[] buffer = ArrayPool<byte>.Shared.Rent(BUFFER_SIZE);
await #lock.WaitAsync();
try {
await this.socket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None);
return buffer;
} catch (Exception) {
throw;
} finally {
#lock.Release();
}
}
}
public class DispatcherMiddleware {
private List<Sender> senders;
private Receiver receiver;
private RequestDelegate next;
public DispatcherMiddleware(RequestDelegate req, List<Sender> _senders, Receiver _receiver) {
this.senders = _senders;
this.receiver = _receiver;
this.next = req;
}
public async Task Invoke(HttpContext context) {
if (!context.WebSockets.IsWebSocketRequest) {
return;
}
await DispatchAsync(context.WebSockets);
}
public async Task DispatchAsync(WebSocketManager manager) {
WebSocket socket = await manager.AcceptWebSocketAsync();
SafeSocket commonSocket = new SafeSocket(socket);
Task[] senderTasks = new Task[this.senders.Count];
for (int senderIndex = 0; senderIndex < senderTasks.Length; senderIndex++) {
int index = senderIndex;// careful at index ! , make copy and use it inside closure !
senderTasks[senderIndex] = Task.Run(async () => {
await commonSocket.ReadAsync();
});
}
}
Keep in mind that the messages order will not be preserved.The same can be applied to the receivers.
So what you will end up with is N senders and K receivers , that at time T :
1 sender will write
1 receiver will read
N-1 senders will wait for the lock
K-1 receivers will wait for the lock
So in the end there will be just 2 operations at any given time.
I do not know if that is what you need though.
I am using Azure Queues to perform a bulk import.
I am using WebJobs to perform the process in the background.
The queue dequeues very frequently. How do I create a delay between 2 message
reads?
This is how I am adding a message to the Queue
public async Task<bool> Handle(CreateFileUploadCommand message)
{
var queueClient = _queueService.GetQueueClient(Constants.Queues.ImportQueue);
var brokeredMessage = new BrokeredMessage(JsonConvert.SerializeObject(new ProcessFileUploadMessage
{
TenantId = message.TenantId,
FileExtension = message.FileExtension,
FileName = message.Name,
DeviceId = message.DeviceId,
SessionId = message.SessionId,
UserId = message.UserId,
OutletId = message.OutletId,
CorrelationId = message.CorrelationId,
}))
{
ContentType = "application/json",
};
await queueClient.SendAsync(brokeredMessage);
return true;
}
And Below is the WebJobs Function.
public class Functions
{
private readonly IValueProvider _valueProvider;
public Functions(IValueProvider valueProvider)
{
_valueProvider = valueProvider;
}
public async Task ProcessQueueMessage([ServiceBusTrigger(Constants.Constants.Queues.ImportQueue)] BrokeredMessage message,
TextWriter logger)
{
var queueMessage = message.GetBody<string>();
using (var client = new HttpClient())
{
client.BaseAddress = new Uri(_valueProvider.Get("ServiceBaseUri"));
var stringContent = new StringContent(queueMessage, Encoding.UTF8, "application/json");
var result = await client.PostAsync(RestfulUrls.ImportMenu.ProcessUrl, stringContent);
if (result.IsSuccessStatusCode)
{
await message.CompleteAsync();
}
else
{
await message.AbandonAsync();
}
}
}
}
As far as I know, azure webjobs sdk enable concurrent processing on a single instance(the default is 16).
If you run your webjobs, it will read 16 queue messages(peeklock and calls Complete on the message if the function finishes successfully, or calls Abandon) and create 16 processes to execute the trigger function at same time. So you feel the queue dequeues very frequently.
If you want to disable concurrent processing on a single instance.
I suggest you could set ServiceBusConfiguration's MessageOptions.MaxConcurrentCalls to 1.
More details, you could refer to below codes:
In the program.cs:
JobHostConfiguration config = new JobHostConfiguration();
ServiceBusConfiguration serviceBusConfig = new ServiceBusConfiguration();
serviceBusConfig.MessageOptions.MaxConcurrentCalls = 1;
config.UseServiceBus(serviceBusConfig);
JobHost host = new JobHost(config);
host.RunAndBlock();
If you want to create a delay between 2 message reads, I suggest you could create a custom ServiceBusConfiguration.MessagingProvider.
It contains CompleteProcessingMessageAsync method, this method completes processing of the specified message, after the job function has been invoked.
I suggest you could add thread.sleep method in CompleteProcessingMessageAsync to achieve delay read.
More detail, you could refer to below code sample:
CustomMessagingProvider.cs:
Notice: I override the CompleteProcessingMessageAsync method codes.
public class CustomMessagingProvider : MessagingProvider
{
private readonly ServiceBusConfiguration _config;
public CustomMessagingProvider(ServiceBusConfiguration config)
: base(config)
{
_config = config;
}
public override NamespaceManager CreateNamespaceManager(string connectionStringName = null)
{
// you could return your own NamespaceManager here, which would be used
// globally
return base.CreateNamespaceManager(connectionStringName);
}
public override MessagingFactory CreateMessagingFactory(string entityPath, string connectionStringName = null)
{
// you could return a customized (or new) MessagingFactory here per entity
return base.CreateMessagingFactory(entityPath, connectionStringName);
}
public override MessageProcessor CreateMessageProcessor(string entityPath)
{
// demonstrates how to plug in a custom MessageProcessor
// you could use the global MessageOptions, or use different
// options per entity
return new CustomMessageProcessor(_config.MessageOptions);
}
private class CustomMessageProcessor : MessageProcessor
{
public CustomMessageProcessor(OnMessageOptions messageOptions)
: base(messageOptions)
{
}
public override Task<bool> BeginProcessingMessageAsync(BrokeredMessage message, CancellationToken cancellationToken)
{
// intercept messages before the job function is invoked
return base.BeginProcessingMessageAsync(message, cancellationToken);
}
public override async Task CompleteProcessingMessageAsync(BrokeredMessage message, FunctionResult result, CancellationToken cancellationToken)
{
if (result.Succeeded)
{
if (!MessageOptions.AutoComplete)
{
// AutoComplete is true by default, but if set to false
// we need to complete the message
cancellationToken.ThrowIfCancellationRequested();
await message.CompleteAsync();
Console.WriteLine("Begin sleep");
//Sleep 5 seconds
Thread.Sleep(5000);
Console.WriteLine("Sleep 5 seconds");
}
}
else
{
cancellationToken.ThrowIfCancellationRequested();
await message.AbandonAsync();
}
}
}
}
Program.cs main method:
static void Main()
{
var config = new JobHostConfiguration();
if (config.IsDevelopment)
{
config.UseDevelopmentSettings();
}
var sbConfig = new ServiceBusConfiguration
{
MessageOptions = new OnMessageOptions
{
AutoComplete = false,
MaxConcurrentCalls = 1
}
};
sbConfig.MessagingProvider = new CustomMessagingProvider(sbConfig);
config.UseServiceBus(sbConfig);
var host = new JobHost(config);
// The following code ensures that the WebJob will be running continuously
host.RunAndBlock();
}
Result:
I am developing an application in C# using HttpClient. My code is flowing through a lot of functions and then finally it does a PostAsyc.
What I want to do is that I want to have a EventHandler which is called when PostAsyc is done. and in that event handler I want to capture and print everything which the client has sent to the server .
Is this possible in .NET HTTPClient?
public void PostData(string data, Action<string> callback)
{
var client = new HttpClient();
var task = client.PostAsync("uri", new StringContent(data));
task.ContinueWith((t) =>
{
t.Result.Content.ReadAsStringAsync().ContinueWith((trep) =>
{
string response = trep.Result;
callback(response);
});
});
}
Instead of using Action<string> callback you can define an event delegate and use that also, this gives you more flexibility of attaching multiple receivers.
public class PostEventArgs : EventArgs { public string Data { get; set; } }
public event EventHandler<PostEventArgs> postDone;
public void PostData(string data)
{
var client = new HttpClient();
var task = client.PostAsync("uri", new StringContent(data));
task.ContinueWith((t) =>
{
t.Result.Content.ReadAsStringAsync().ContinueWith((trep) =>
{
string response = trep.Result;
if (postDone != null)
postDone(this, new PostEventArgs() { Data = response });
});
});
}
Usage:
First Case
serviceObj.PostData("some data", (response)=> { Console.WriteLine(response); });
Second case
serviceObj.postDone += (obj,response)=>{ Console.WriteLine(response); }; // register only once
serviceObj.PostData("some data");
Updated with Task.ContinueWith.
Create a message handler class like this,
public class LoggingMessageHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = await base.SendAsync(request, cancellationToken);
if (request.Method == HttpMethod.Post)
{
// Log whatever you want here
Console.WriteLine(request.ToString());
Console.WriteLine(response.ToString());
}
return response;
}
}
and then create your HttpClient with the message handler as part of the request/response pipeline
var client = new HttpClient(new LoggingMessageHandler() {InnerHandler = new HttpClientHandler()});
client.PostAsync(...) // Whatever
Any request you make from this point on will pass through the LoggingRequestHandler.
By taking this approach you do not need to wrap the HttpClient object and it is also easy to retrofit into existing code.