Search code examples
c#asp.net-core.net-coretcplistenerasp.net-core-hosted-services

IHostedService for tcp servers in .NET Core


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?


Solution

  • 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();
        }
    }