IHostedService for tcp servers in .NET Core
Asked Answered
S

2

9

I am trying to build a small tcp server/daemon with asp.net core as a web frontend to interact with the server. I have found IHostedService/BackgroundService which seems to provide a low effort alternative to bundle the server and the frontend together.

The code looks basically like this at the moment (echo server for testing purposes):

public class Netcat : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        TcpListener listener = new TcpListener(IPAddress.Any, 8899);
        listener.Start();
        while(!stoppingToken.IsCancellationRequested)
        {
            TcpClient client = await listener.AcceptTcpClientAsync();
            NetworkStream stream = client.GetStream();

            while (!stoppingToken.IsCancellationRequested)
            {
                byte[] data = new byte[1024];
                int read = await stream.ReadAsync(data, 0, 1024, stoppingToken);

                await stream.WriteAsync(data, 0, read, stoppingToken);
            }
        }
    }
}

And is initialized in Startup.cs like this:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHostedService<Netcat>();
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    }

Is there a common pattern for how modern Asp.Net core applications and daemons should cooperate?

How would I interact with the running service itself from a Controller?

Is IHostedService even usable for this purpose or is it a better way that fully decouples the Asp.Net frontend and the service/server, e.g. by running the daemon and asp.net as seperate processes with some sort of IPC mechanism?

Shelby answered 26/9, 2018 at 19:13 Comment(1)
What route did you go down in the end for this?Ellita
U
8

Is there a common pattern for how modern Asp.Net core applications and daemons should cooperate?

Actually , the hosted service is not that powerful for the present . So people usually use a third product . However , it's possible to communicate with hosted service and controller . I'll use your code as an example to achieve these goals :

  1. The TcpServer is able to receive two commands so that we can switch the state of hosted service from a TcpClient.
  2. The controller of WebServer can invoke method of TcpServer indirectly (through a mediator ), and render it as html

enter image description here

It's not a good idea to couple controller with hosted service . To invoke method from hosted service , we can introduce a Mediator . A mediator is no more than a service that serves as a singleton (because it will referenced by hosted service) :

public interface IMediator{
    event ExecHandler ExecHandler ; 
    string Exec1(string status);
    string Exec2(int status);
    // ...
}

public class Mediator: IMediator{

    public event ExecHandler ExecHandler ;
    public string Exec1(string status)
    {
        if(this.ExecHandler==null) 
            return null;
        return this.ExecHandler(status);
    }

    public string Exec2(int status)
    {
        throw new System.NotImplementedException();
    }
}

A Hosted Service needs to realize the existence of IMediator and expose his method to IMediator in some way :

public class Netcat : BackgroundService
{
    private IMediator Mediator ;
    public Netcat(IMediator mediator){
        this.Mediator=mediator;
    }

    // method that you want to be invoke from somewhere else
    public string Hello(string status){
        return $"{status}:returned from service";
    }

    // method required by `BackgroundService`
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        TcpListener listener = new TcpListener(IPAddress.Any, 8899);
        listener.Start();
        while(!stoppingToken.IsCancellationRequested)
        {
            // ...
        }
    }
}

To allow control the status from the NetCat TcpServer , I make it able to receive two commands from clients to switch the state of background service :

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        TcpListener listener = new TcpListener(IPAddress.Any, 8899);
        listener.Start();
        while(!stoppingToken.IsCancellationRequested)
        {
            TcpClient client = await listener.AcceptTcpClientAsync();
            Console.WriteLine("a new client connected");
            NetworkStream stream = client.GetStream();

            while (!stoppingToken.IsCancellationRequested)
            {
                byte[] data = new byte[1024];
                int read = await stream.ReadAsync(data, 0, 1024, stoppingToken);
                var cmd= Encoding.UTF8.GetString(data,0,read);
                Console.WriteLine($"[+] received : {cmd}");

                if(cmd=="attach") { 
                    this.Mediator.ExecHandler+=this.Hello;
                    Console.WriteLine($"[-] exec : attached");
                    continue;
                }
                if(cmd=="detach") {
                    Console.WriteLine($"[-] exec : detached");
                    this.Mediator.ExecHandler-=this.Hello;
                    continue;
                }

                await stream.WriteAsync(data, 0, read, stoppingToken);
                stream.Flush();
            }
        }
    }

If you want to invoke the method of background service within a controller, simply inject the IMediator :

public class HomeController : Controller
{
    private IMediator Mediator{ get; }

    public HomeController(IMediator mediator){
        this.Mediator= mediator;
    }

    public IActionResult About()
    {
        ViewData["Message"] = this.Mediator.Exec1("hello world from controller")??"nothing from hosted service";

        return View();
    }
}
Unlookedfor answered 27/9, 2018 at 9:49 Comment(4)
@Shelby Because you cannot inject hosted service into controller directly , you should introduce an object serving as mediator . The IMediator is no more than a singleton service .Unlookedfor
Unfortunately, I could not get this to work. Could you elaborate a bit on how the Mediator is added and what the ExecHandler is for?Shelby
@Shelby The IMediator is no more than a plain singleton service, which is used to interact with your TCP Server from controllers . Probably you might have several hosted services to interact with, so I use a observer pattern here . All Hosted Services register event handlers throught an custom event of ExecHandler. To add the Mediator, simply register it as a singleton service: services.AddSingleton<IMediator,Mediator>() .Unlookedfor
@Shelby Oh I forgot to mention that the ExecHandler is a plain delegate, you can custom the delegate as you need. Since we expect something like string Exec1(string) here , we can define the ExecHandler as public delegate string ExecHandler(string status); .Unlookedfor
D
0

My suggestion is similar to @itminus

Depending on your desired scenario:

  1. If you want to access the service ONLY internally from the same app controllers/pages:

Do not create TCP Listener. Use the background queue for requests and background service for processing requests, invoked from the code as explained in the docs

https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.1#queued-background-tasks

  1. If you want to access the service both via TCP from other servers/clients etc AND internally from the hosting aspcore app:

Implement the separate processing service (logic server) as in point 1. You can inject it and invoke from both your TCP listener background service and controllers.

  1. Of course you can access your own service via HttpClient from the same app, but it would seem strange to use the whole TCP stack for internal calls.

  2. If the TCP processing is totally independent from the web application, then cut the TCP service out to separate server application. See docs on how to create "pure" service without asp/kestrel overhead in dotnet core 2.1.

Delineator answered 24/10, 2018 at 9:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.