Я пытаюсь создать небольшой tcp-сервер / демон с ядром asp.net в качестве веб-интерфейса для взаимодействия с сервером. Я нашел 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 и демоны должны взаимодействовать?
Как мне взаимодействовать с самой запущенной службой из контроллера?
Можно ли использовать IHostedService для этой цели или это лучший способ полностью отделять интерфейс Asp.Net от службы / сервера, например запустив демон и asp.net как отдельные процессы с каким-то механизмом IPC?
Is there a common pattern for how modern Asp.Net core applications and daemons should cooperate?
На самом деле, хостинг-сервис пока не так силен. Поэтому люди обычно используют третий продукт. Однако можно взаимодействовать с размещенной службой и контроллером. Я буду использовать ваш код в качестве примера для достижения этих целей:
TcpServer
может получать две команды, чтобы мы могли переключать состояние размещенной службы с TcpClient
.WebServer
может вызывать метод TcpServer
косвенно (через посредника) и отображать его как html.Соединять контроллер с размещенной службой - не лучшая идея. Чтобы вызвать метод из размещенной службы, мы можем ввести посредника. Посредник - это не более чем служба, которая служит синглтоном (потому что на нее будет ссылаться размещенная служба):
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();
}
}
@agnsaft Поскольку вы не можете напрямую внедрить размещенную службу в контроллер, вам следует ввести объект, служащий посредником. IMediator - это не более чем одноэлементный сервис.
К сожалению, мне не удалось заставить это работать. Не могли бы вы подробнее рассказать, как добавляется посредник и для чего нужен ExecHandler?
@agnsaft IMediator
- это не более чем простая одноэлементная служба, которая используется для взаимодействия с вашим TCP-сервером с контроллеров. Возможно, у вас может быть несколько размещенных сервисов для взаимодействия, поэтому я использую здесь шаблон наблюдателя. Все размещенные службы регистрируют обработчики событий с помощью настраиваемого мероприятиеExecHandler
. Чтобы добавить посредника, просто зарегистрируйте его как одноэлементную службу: services.AddSingleton<IMediator,Mediator>()
.
@agnsaft О, я забыл упомянуть, что ExecHandler
- это простой делегат, вы можете настроить делегат по своему усмотрению. Поскольку мы ожидаем здесь что-то вроде string Exec1(string)
, мы можем определить ExecHandler
как public delegate string ExecHandler(string status);
.
Мое предложение похоже на @itminus
В зависимости от желаемого сценария:
Не создавайте TCP Listener. Используйте фоновую очередь для запросов и фоновую службу для обработки запросов, вызываемых из кода, как описано в документации.
Реализуйте отдельную службу обработки (логический сервер), как в пункте 1. Вы можете внедрить ее и вызвать как из фоновой службы прослушивателя TCP, так и из контроллеров.
Конечно, вы можете получить доступ к своей собственной службе через HttpClient
из того же приложения, но было бы странно использовать весь стек TCP для внутренних вызовов.
Если обработка TCP полностью независима от веб-приложения, отключите службу TCP для отдельного серверного приложения. См. документы о том, как создать «чистый» сервис без служебных данных asp / kestrel в ядре dotnet 2.1.
Каким путем вы в итоге пошли для этого?