.NET Core中用于TCP服务器的IHostedService

9
我正在尝试使用asp.net core构建一个小型tcp服务器/守护进程,并使用web前端与服务器进行交互。我发现IHostedService/BackgroundService似乎提供了一种低成本的替代方案,可以将服务器和前端捆绑在一起。
目前代码基本如下所示(用于测试目的的回显服务器):
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);
            }
        }
    }
}

并且在Startup.cs中像这样初始化:

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

现代Asp.Net Core应用程序和守护程序之间是否存在常见的协作模式?

我如何从Controller与正在运行的服务本身交互?

IHostedService是否可用于此目的,还是有更好的方式可以完全解耦Asp.Net前端和服务/服务器,例如通过使用某种IPC机制将守护程序和asp.net作为分离的进程运行?


你最终选择了哪种方案? - A Houghton
2个回答

8

现代Asp.Net Core应用程序和守护程序之间是否有常见的协作模式?

实际上,目前托管服务并不是很强大。因此,人们通常使用第三方产品。但是,与托管服务和控制器进行通信是可能的。我将使用您的代码作为示例来实现这些目标:

  1. TcpServer能够从TcpClient接收两个命令,以便我们可以切换托管服务的状态。
  2. WebServer的控制器可以通过中介者间接调用TcpServer的方法,并将其呈现为HTML。

enter image description here

把控制器和托管服务耦合在一起并不是一个好主意。为了从托管服务中调用方法,我们可以引入一个中介者。中介者只是充当单例服务的服务(因为它将被托管服务引用):

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

一个托管服务需要意识到IMediator的存在,并以某种方式将其方法暴露给IMediator
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)
        {
            // ...
        }
    }
}

为了允许从NetCat的TcpServer控制状态,我使其能够接收来自客户端的两个命令以切换后台服务的状态:
    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();
            }
        }
    }

如果你想在控制器中调用后台服务的方法,只需注入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();
    }
}

1
由于您无法直接将托管服务注入控制器中,因此应引入充当中介的对象。IMediator不过是一个单例服务。 - itminus
很遗憾,我无法让它工作。您能详细说明一下中介者是如何添加的,执行处理程序的作用是什么吗? - agnsaft
@agnsaft IMediator 只是一个普通的单例服务,用于从控制器与您的 TCP 服务器进行交互。可能您有多个托管服务需要进行交互,因此我在这里使用了观察者模式。所有托管服务都通过自定义 事件ExecHandler 注册事件处理程序。要添加 Mediator,只需将其注册为单例服务:services.AddSingleton<IMediator,Mediator>() - itminus
@agnsaft 噢,我忘了提到ExecHandler是一个普通委托,您可以根据需要自定义委托。由于我们希望在这里得到像string Exec1(string)这样的东西,因此我们可以将ExecHandler定义为public delegate string ExecHandler(string status); - itminus

0

我的建议与@itminus类似

根据您的需求场景:

  1. 如果您只想从同一应用程序控制器/页面内部访问服务:

不要创建TCP监听器。使用后台队列处理请求和后台服务处理请求,按照文档中所述的代码调用。

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

  1. 如果您想从其他服务器/客户端等通过TCP访问服务,并且还想从托管的aspcore应用程序内部访问:

按照第1点实现单独的处理服务(逻辑服务器)。您可以将其注入并从TCP监听器后台服务和控制器中调用。

  1. 当然,您可以通过HttpClient从同一应用程序访问自己的服务,但是对于内部调用使用整个TCP堆栈似乎很奇怪。

  2. 如果TCP处理与Web应用程序完全独立,则将TCP服务剥离到单独的服务器应用程序中。请参见docs,了解如何在dotnet core 2.1中创建“纯”服务而不带有asp/kestrel开销。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接