I am having concerns about how to use SgnalR in the following scenario:
There is a non-hub service project that runs a time-consuming task periodically.
The clients should be notified about the progress of the running task. After making some research, SignalR seemed to be the right choice for this purpose.
The problem is, I want the Service-Hub-Clients system to be as loosely-coupled as possible. So, I hosted the Hub in IIS and as a SignalR documentation suggests, added a reference to the Hub context in the outside project and called the client method:
hubContext = GlobalHost.ConnectionManager.GetHubContext<TheHub>()
hubContext.Clients.All.progress(n, i);
Client side:
private void InitHub()
{
hubConnection = new HubConnection(ConfigurationManager.AppSettings["hubConnection"]);
hubProxy = hubConnection.CreateHubProxy("TheHub");
hubConnection.Start().Wait();
}
hubProxy.On<int, int>("progress", (total, done) =>
{
task1Bar.Invoke(t => t.Maximum = total);
task1Bar.Invoke(t => t.Value = done);
});
On the client side the method isn't being invoked and after two days of research I can't get it working, although when making a call from the Hub itself, it works fine. I suspect I'm missing some configuration
You can't use the GlobalHost.Connection manager in your Hub class or service, if the caller is going to be any project other than the Web project.
GlobalHost.ConnectionManager.GetHubContext<TheHub>()
You should instead create a service class that would abstract the hub from the callers. The service class should have something like:
// This method is what the caller sees, and abstracts the communication with the Hub
public void NotifyGroup(string groupName, string message)
{
Execute("NotifyGroup", groupName, message);
}
// This is the method that calls the Hub
private void Execute(string methodName, params object[] parameters)
{
using (var connection = new HubConnection("http://localhost/"))
{
_myHub = connection.CreateHubProxy("TheHub");
connection.Start().Wait();
_myHub.Invoke(methodName, parameters);
connection.Stop();
}
}
The last bit which is the hub itself, should be something like:
public void NotifyGroup(string groupName, string message)
{
var group = Clients.Group(groupName);
if (group == null)
{
Log.IfWarn(() => $"Group '{groupName}' is not registered");
return;
}
group.NotifyGroup(message);
}
Related
My goal: Pass data do specific client who is connected to server and get results without calling Server method.
I tried use SignalR to do this (because It's very easy tool for me), but I can't get results (now I know why). I am working on ASP.NET Core 3.1.
My question: Is there SignalR alternative with "return value to server" functionality (call method with params on target client and get results)?
SignalR is usually used in a setup where there are multiple clients and a single server the clients connect to. This makes it a normal thing for clients to call the server and expect results back. Since the server usually does not really care about what individual clients are connected, and since the server usually broadcasts to a set of clients (e.g. using a group), the communication direction is mostly used for notifications or broadcasts. Single-target messages are possible but there isn’t a built-in mechanism for a request/response pattern.
In order to make this work with SignalR you will need to have a way for the client to call back the server. So you will need a hub action to send the response to.
That alone doesn’t make it difficult but what might do is that you will need to link a client-call with an incoming result message received by a hub. For that, you will have to build something.
Here’s an example implementation to get you starting. The MyRequestClient is a singleton service that basically encapsulates the messaging and offers you an asynchronous method that will call the client and only complete once the client responded by calling the callback method on the hub:
public class MyRequestClient
{
private readonly IHubContext<MyHub> _hubContext;
private ConcurrentDictionary<Guid, object> _pendingTasks = new ConcurrentDictionary<Guid, object>();
public MyRequestClient(IHubContext<MyHub> hubContext)
{
_hubContext = hubContext;
}
public async Task<int> Square(string connectionId, int number)
{
var requestId = Guid.NewGuid();
var source = new TaskCompletionSource<int>();
_pendingTasks[requestId] = source;
await _hubContext.Clients.Client(connectionId).SendAsync("Square", nameof(MyHub.SquareCallback), requestId, number);
return await source.Task;
}
public void SquareCallback(Guid requestId, int result)
{
if (_pendingTasks.TryRemove(requestId, out var obj) && obj is TaskCompletionSource<int> source)
source.SetResult(result);
}
}
In the hub, you then need the callback action to call the request client to complete the task:
public class MyHub : Hub
{
private readonly ILogger<MyHub> _logger;
private readonly MyRequestClient _requestClient;
public MyHub(ILogger<MyHub> logger, MyRequestClient requestClient)
{
_logger = logger;
_requestClient = requestClient;
}
public Task SquareCallback(Guid requestId, int number)
{
_requestClient.SquareCallback(requestId, number);
return Task.CompletedTask;
}
// just for demo purposes
public Task Start()
{
var connectionId = Context.ConnectionId;
_ = Task.Run(async () =>
{
var number = 42;
_logger.LogInformation("Starting Square: {Number}", number);
var result = await _requestClient.Square(connectionId, number);
_logger.LogInformation("Square returned: {Result}", result);
});
return Task.CompletedTask;
}
}
The Start hub action is only for demo purposes to have a way to start this with a valid connection id.
On the client, you then need to implement the client method and have it call the specified callback method once it’s done:
connection.on('Square', (callbackMethod, requestId, number) => {
const result = number * number;
connection.invoke(callbackMethod, requestId, result);
});
Finally, you can try this out by invoking the Start action by a client:
connection.invoke('Start');
Of course, this implementation is very basic and will need a few things like proper error handling and support for timing out the tasks if the client didn’t respond properly. It would also be possible to expand this to support arbitrary calls, without having you to create all these methods manually (e.g. by having a single callback method on the hub that is able to complete any task).
I have a Topshelf windows service that acts as a TCP server. Inside this service, I also have a self-hosted (OWIN) WebAPI.
My goal is to somehow allow the WebAPI to communicate with the TCP server that's contained and running in the same service. Naturally I could simply use something like a "trigger" file or a shared DB that could be polled frequently, though I'd like to know of any more optimal/native ways to achieve this.
To get a better idea of the project, think of a single page application consuming my API and making certain calls with arbitrary string parameters. This data should then be passed to clients (C++ console apps using winsock) that are connected to the running TCP server.
The following Container is instantiated and passed to the Topshelf HostConfigurator
class ContainerService
{
private APIService _apiService;
private EngineService _engineService;
protected IDisposable WebAppHolder { get; set; }
public bool Start(HostControl hostControl)
{
var host = hostControl;
_apiService = new APIService();
_engineService = new EngineService();
// Initialize API service
if (WebAppHolder == null)
{
WebAppHolder = _apiService.Initialize();
}
// Initialize Engine service
_engineService.Initialize();
return true;
}
public bool Stop(HostControl hostControl)
{
// Stop API service
if (WebAppHolder != null)
{
WebAppHolder.Dispose();
WebAppHolder = null;
}
// Stop Engine service
_engineService.Stop();
return true;
}
}
Standard Topshelf stuff in program entry point (as mentioned above):
HostFactory.Run(hostConfigurator =>
{
hostConfigurator.Service<ContainerService>(containerService =>
{
containerService.WhenStarted((service, control) => service.Start(control));
containerService.WhenStopped((service, control) => service.Stop(control));
});
hostConfigurator.RunAsLocalSystem();
hostConfigurator.SetServiceName("Educe Service Host");
hostConfigurator.SetDisplayName("Communication Service");
hostConfigurator.SetDescription("Responsible for API and Engine services");
});
TCP Server:
public void Initialize()
{
_serverListener = new TcpListener(new IPEndPoint(hostAddress, (int)port));
_serverListener.Start();
_threadDoBeginAcceptTcpClient = new Thread(() => DoBeginAcceptTcpClient(_serverListener));
_threadDoBeginAcceptTcpClient.Start();
}
...
public void DoBeginAcceptTcpClient(TcpListener listener)
{
while(!_breakThread)
{
// Set the event to nonsignaled state.
TcpClientConnected.Reset();
// Start to listen for connections from a client.
Console.WriteLine("Waiting for a connection...");
// Accept the connection.
listener.BeginAcceptTcpClient(DoAcceptTcpClientCallback, listener);
// Wait until a connection is made and processed before continuing.
TcpClientConnected.WaitOne();
}
}
// Process the client connection.
public void DoAcceptTcpClientCallback(IAsyncResult ar)
{
// Get the listener that handles the client request.
TcpListener listener = (TcpListener)ar.AsyncState;
// End the operation and display the received data on the console.
Console.WriteLine("Client connection completed");
Clients.Add(listener.EndAcceptTcpClient(ar));
// Signal the calling thread to continue.
TcpClientConnected.Set();
}
WebAPI Controller:
public class ValuesController : ApiController
{
// GET api/values/5
public string Get(int id)
{
return $"Foo: {id}";
}
}
As mentioned earlier, what I seek is "communication" between the WebAPI and the windows service. How can I pass the "id" parameter from the WebAPI call to the _engineService object in my windows service? Perhaps something similar to WPF's MVVM Light Messenger? The idea is that it would then be parsed and sent to the appropriate TcpClient that is stored in the Clients List.
Any advice on how to achieve this will be appreciated. Please feel free to ask for clarification/more code.
Did you find any answer to your issue yet ?
I don't quite understand what you try to achieve looking for a communication between the two of them ? Do you want to somehow rely on TCP/IP to relay this id or in-memory ?
Potentially, you could consider a Mediator pattern and use this kind of library that seems quite useful in the case I understood : https://github.com/jbogard/MediatR
In a simpler approach, I would rely on events to achieve what you are trying to do, which is having a reactive communication from the HTTP request to the c++ users.
Did I understand you needs ? I am quite curious about the solution
I'm assuming you are trying to take an HTTP GET request's ID parameter and send it to TCP clients who are connected to the EngineService. If your EngineService is initialized before your ApiService, I think this is a question of how to get a handle to the one-and-only EngineService instance from within an ApiService's controller instances.
If I'm following you, you could make the EngineService a public static property of your ContainerService and reference it as ContainerService.EngineService from the controller (or anywhere in the app for that matter) or better register your EngineService as a singleton in a DI container an inject it into the ApiService.
Solution (calls to WebAPI trigger EngineService)
I now use RabbitMQ/EasyNetQ to achieve communication between the WebApi and the EngineService object containing my TCP clients.
I have incidentally split them into two separate Projects/Topshelf services now.
The following is the new "communication" component and it is instantiated in the EngineService constructor.
public class Communication
{
private readonly Logger _logger;
private readonly IBus _bus;
public delegate void ReceivedEventHandler(string data);
public event ReceivedEventHandler Received;
protected virtual void OnReceive(string data)
{
Received?.Invoke(data);
}
public Communication()
{
_logger = new Logger();
_bus = RabbitHutch.CreateBus("host=localhost", reg => reg.Register<IEasyNetQLogger>(log => _logger));
SubscribeAllQueues();
}
private void SubscribeAllQueues()
{
_bus.Receive<Message>("pipeline", message =>
{
OnReceive(message.Body);
});
}
public void SubscribeQueue(string queueName)
{
_bus.Receive<Message>(queueName, message =>
{
OnReceive(message.Body);
});
}
}
An event handler is then added.
This means that as soon as a message arrives to the bus, the data will be relayed to the event handler which will subsequently relay it to the first connected TCP client in the list.
public void Handler(string data)
{
//Console.WriteLine(data);
_clients[0].Client.Send(Encoding.UTF8.GetBytes(data));
}
...
_comPipe.Received += Handler;
And finally on the WebApi's controller:
public string Get(int id)
{
ServiceCom.SendMessage("ID: " + id);
return "value";
}
ServiceCom class. Allows sending a string message on the bus.
public static class ServiceCom
{
public static void SendMessage(string messageBody)
{
var messageBus = RabbitHutch.CreateBus("host=localhost");
messageBus.Send("pipeline", new Message { Body = messageBody });
}
}
Now that this is done, I am now looking to implement a way for the connected TCP clients to trigger updates/events in an additional SPA project that will act as a Portal / Client Management App.
My approach will probably make use of KnockOut.js and SignalR to achieve dynamic Views where TCP client events are displayed immediately and similarly actions on to WebAPI will trigger events in the TCP clients. I know it sounds like a bizarre combination of processes but it is all according to plan and working out as expected :)
I've followed this guide, ASP.NET SignalR Hubs API Guide (How to manage group membership from the Hub class) and yet am unable to get my server side ShipmentHub methods to execute.
My ShipmentHub class looks like this:
public class ShipmentHub : Hub
{
IShipmentLogic shipmentLogic;
public ShipmentHub(IShipmentLogic shipmentLogic)
{
this.shipmentLogic = shipmentLogic;
}
public void CreateShipment(IEnumerable<Shipment> shipments)
{
// Clients.All.createShipment(shipments.OrderByDescending(s => s.CreatedDate));
Clients.Group(shipments.FirstOrDefault().ShipmentId)
.createShipment(shipments.OrderByDescending(s => s.CreatedDate));
}
public async Task WatchShipmentId(string shipmentId)
{
await Groups.Add(Context.ConnectionId, shipmentId);
Clients.Group(shipmentId).createShipment(shipmentLogic.Get(shipmentId, true));
}
public Task StopWatchingShipmentId(string shipmentId)
{
return Groups.Remove(Context.ConnectionId, shipmentId);
}
}
My client, more or less, looks like this:
var shipmentHub = $.connection.shipmentHub;
$.connection.hub.logging = true;
$.connection.hub.start();
var shipmentId = "SHP-W-GE-100122";
if (previousShipmentId) {
shipmentHub.server.stopWatchingShipmentId(previousShipmentId);
}
if (shipmentId.length) {
previousShipmentId = shipmentId;
shipmentHub.server.watchShipmentId(shipmentId);
}
In the SignalR client logs I see that these are being called:
SignalR: Invoking shipmenthub.WatchShipmentId
SignalR: Invoking shipmenthub.StopWatchingShipmentId
SignalR: Invoking shipmenthub.WatchShipmentId
And, aside from just the logs, these methods are being hit:
proxies['shipmentHub'].server = {
createShipment: function (shipments) {
return proxies['shipmentHub'].invoke.apply(proxies['shipmentHub'], $.merge(["CreateShipment"], $.makeArray(arguments)));
},
stopWatchingShipmentId: function (shipmentId) {
return proxies['shipmentHub'].invoke.apply(proxies['shipmentHub'], $.merge(["StopWatchingShipmentId"], $.makeArray(arguments)));
},
watchShipmentId: function (shipmentId) {
return proxies['shipmentHub'].invoke.apply(proxies['shipmentHub'], $.merge(["WatchShipmentId"], $.makeArray(arguments)));
}
};
And, as a final note, before I added the Watch and StopWatching methods, everything else worked (i.e., CreateShipment would call the Client.All.createShipment method without issue).
You need to wait for the connection to the server to be established before you can start calling methods on the server from the client. hub.start() returns a promise, here is the basic pattern for doing something once that promise is resolved.
var shipmentHub = $.connection.shipmentHub;
$.connection.hub.logging = true;
$.connection.hub.start().done(talkToServer);
var talkToServer=function(){
var shipmentId = "SHP-W-GE-100122";
if (previousShipmentId) {
shipmentHub.server.stopWatchingShipmentId(previousShipmentId);
}
if (shipmentId.length) {
previousShipmentId = shipmentId;
shipmentHub.server.watchShipmentId(shipmentId);
}
}
The issue is due to the parameterized constructor in ShipmentHub. According to Dependency Injection in SignalR:
By default, SignalR expects a hub class to have a parameterless constructor. However, you can easily register a function to create hub instances, and use this function to perform DI. Register the function by calling GlobalHost.DependencyResolver.Register.
So, you need to modify your Startup.Configuration(IAppBuilder app) method to resolve the dependency for you:
GlobalHost
.DependencyResolver
.Register(
typeof(ShipmentHub),
() => new ShipmentHub(new ShipmentLogic()));
This is my Hub code:
public class Pusher : Hub, IPusher
{
readonly IHubContext _hubContext = GlobalHost.ConnectionManager.GetHubContext<Pusher>();
public virtual Task PushToOtherInGroup(dynamic group, dynamic data)
{
return _hubContext.Clients.Group(group).GetData(data);
}
}
I want call this method in another project with this code:
var pusher = new Pusher.Pusher();
pusher.PushToOtherInGroup("Test", new {exchangeTypeId, price});
I want call PushToOtherInGroup,when calling the method i don't get any error.but pusher does not work.
This is my Ui Code:
$(function() {
hub = $.connection.pusher;
$.connection.hub.start()
.done(function() {
hub.server.subscribe('newPrice');
console.log('Now connected, connection ID=' + $.connection.hub.id);
})
.fail(function() { console.log('Could not Connect!'); });
});
(function() {
hub.client.GetData = function (data) {
debugger;
};
});
What is my problem?
You can't instantiate and call a hub class directly like that. There is much plumbing provided around a Hub class by the SignalR runtime that you are bypassing by using it as a "plain-old class" like that.
The only way to interact with a SignalR hub from the outside is to actually get an instance of an IHubContext that represents the hub from the SignalR runtime. You can only do this from within the same process, so as long as your other "project" is going to be running in process with the SignalR code it will work.
If your other project is going to be running in another process then what you would want to do is expose a sort of "companion" API which is either another SignalR hub or a regular old web service (with ASP.NET web API) that you can call from this other application to trigger the behavior you want. Whichever technology you choose, you would probably want to secure this so that only your authenticated applications can call it.
Once you decide which approach you're going to take, all you would do to send messages out via the Pusher hub would be:
// Get the context for the Pusher hub
IHubContext hubContext = GlobalHost.ConnectionManager.GetHubContext<Pusher>();
// Notify clients in the group
hubContext.Clients.Group(group).GetData(data);
If you're looking to call a method in your hub from another project then it needs to reside within the same app domain. If it does here's how you can do it:
Call a hub method from a controller's action (don't mind the title, it works for your scenario)
Take a look at this link at the topic of (How to call client methods and manage groups from outside the Hub class).
Code example simply creates a singleton instance of the caller class and pass in the IHubContext into it's constructor. Then you have access to desired context.Clients in caller class's methods:
// This sample only shows code related to getting and using the SignalR context.
public class StockTicker
{
// Singleton instance
private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>()));
private IHubContext _context;
private StockTicker(IHubContext context)
{
_context = context;
}
// This method is invoked by a Timer object.
private void UpdateStockPrices(object state)
{
foreach (var stock in _stocks.Values)
{
if (TryUpdateStockPrice(stock))
{
_context.Clients.All.updateStockPrice(stock);
}
}
}
The methods within Hub are supposed to be called FROM a CLIENT.
If you want to send something TO a CLIENT - indeed, you have to use hubContext.
My objective is to implement an asynchronous self hosted WCF service which will run all requests in a single thread and make full use of the new C# 5 async features.
My server will be a Console app, in which I will setup a SingleThreadSynchronizationContext, as specified here, create and open a ServiceHost and then run the SynchronizationContext, so all the WCF requests are handled in the same thread.
The problem is that, though the server was able to successfully handle all requests in the same thread, async operations are blocking the execution and being serialized, instead of being interlaced.
I prepared a simplified sample that reproduces the issue.
Here is my service contract (the same for server and client):
[ServiceContract]
public interface IMessageService
{
[OperationContract]
Task<bool> Post(String message);
}
The service implementation is the following (it is a bit simplified, but the final implementation may access databases or even call other services in asynchronous fashion):
public class MessageService : IMessageService
{
public async Task<bool> Post(string message)
{
Console.WriteLine(string.Format("[Thread {0} start] {1}", Thread.CurrentThread.ManagedThreadId, message));
await Task.Delay(5000);
Console.WriteLine(string.Format("[Thread {0} end] {1}", Thread.CurrentThread.ManagedThreadId, message));
return true;
}
}
The service is hosted in a Console application:
static void Main(string[] args)
{
var syncCtx = new SingleThreadSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(syncCtx);
using (ServiceHost serviceHost = new ServiceHost(typeof(MessageService)))
{
NetNamedPipeBinding binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None);
serviceHost.AddServiceEndpoint(typeof(IMessageService), binding, address);
serviceHost.Open();
syncCtx.Run();
serviceHost.Close();
}
}
As you can see, the first thing I do is to setup a single threaded SynchronizationContext. Following, I create, configure and open a ServiceHost. According to this article, as I've set the SynchronizationContext prior to its creation, the ServiceHost will capture it and all the client requests will be posted in the SynchronizationContext. In the sequence, I start the SingleThreadSynchronizationContext in the same thread.
I created a test client that will call the server in a fire-and-forget fashion.
static void Main(string[] args)
{
EndpointAddress ep = new EndpointAddress(address);
NetNamedPipeBinding binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None);
IMessageService channel = ChannelFactory<IMessageService>.CreateChannel(binding, ep);
using (channel as IDisposable)
{
while (true)
{
string message = Console.ReadLine();
channel.Post(message);
}
}
}
When I execute the example, I get the following results:
Client
Server
The messages are sent by the client with a minimal interval ( < 1s).
I expected the server would receive the first call and run it in the SingleThreadSynchronizationContext (queueing a new WorkItem. When the await keyword was reached, the SynchronizationContext would be once again captured, the continuation posted to it, and the method would return a Task at this point, freeing the SynchronizationContext to deal with the second request (at least start dealing with it).
As you can see by the Thread's id in the server log, the requests are being correctly posted in the SynchronizationContext. However, looking at the timestamps, we can see that the first request is being completed before the second is started, what totally defeats the purpose of having a async server.
Why is that happening?
What is the correct way of implementing a WCF self hosted async server?
I think the problem is with the SingleThreadSynchronizationContext, but I can't see how to implement it in any other manner.
I researched the subject, but I could not find more useful information on asynchronous WCF service hosting, especially using the Task based pattern.
ADDITION
Here is my implementation of the SingleThreadedSinchronizationContext. It is basically the same as the one in the article:
public sealed class SingleThreadSynchronizationContext
: SynchronizationContext
{
private readonly BlockingCollection<WorkItem> queue = new BlockingCollection<WorkItem>();
public override void Post(SendOrPostCallback d, object state)
{
this.queue.Add(new WorkItem(d, state));
}
public void Complete() {
this.queue.CompleteAdding();
}
public void Run(CancellationToken cancellation = default(CancellationToken))
{
WorkItem workItem;
while (this.queue.TryTake(out workItem, Timeout.Infinite, cancellation))
workItem.Action(workItem.State);
}
}
public class WorkItem
{
public SendOrPostCallback Action { get; set; }
public object State { get; set; }
public WorkItem(SendOrPostCallback action, object state)
{
this.Action = action;
this.State = state;
}
}
You need to apply ConcurrencyMode.Multiple.
This is where the terminology gets a bit confusing, because in this case it doesn't actually mean "multi-threaded" as the MSDN docs state. It means concurrent. By default (single concurrency), WCF will delay other requests until the original operation has completed, so you need to specify multiple concurrency to permit overlapping (concurrent) requests. Your SynchronizationContext will still guarantee only a single thread will process all the requests, so it's not actually multi-threading. It's single-threaded concurrency.
On a side note, you might want to consider a different SynchronizationContext that has cleaner shutdown semantics. The SingleThreadSynchronizationContext you are currently using will "clamp shut" if you call Complete; any async methods that are in an await are just never resumed.
I have an AsyncContext type that has better support for clean shutdowns. If you install the Nito.AsyncEx NuGet package, you can use server code like this:
static SynchronizationContext syncCtx;
static ServiceHost serviceHost;
static void Main(string[] args)
{
AsyncContext.Run(() =>
{
syncCtx = SynchronizationContext.Current;
syncCtx.OperationStarted();
serviceHost = new ServiceHost(typeof(MessageService));
Console.CancelKeyPress += Console_CancelKeyPress;
var binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None);
serviceHost.AddServiceEndpoint(typeof(IMessageService), binding, address);
serviceHost.Open();
});
}
static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e)
{
if (serviceHost != null)
{
serviceHost.BeginClose(_ => syncCtx.OperationCompleted(), null);
serviceHost = null;
}
if (e.SpecialKey == ConsoleSpecialKey.ControlC)
e.Cancel = true;
}
This will translate Ctrl-C into a "soft" exit, meaning the application will continue running as long as there are client connections (or until the "close" times out). During the close, existing client connections can make new requests, but new client connections will be rejected.
Ctrl-Break is still a "hard" exit; there's nothing you can do to change that in a Console host.