Communication between Topshelf service (acting as TCP server) and selfhosted OWIN WebAPI

1.4k views Asked by At

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.

3

There are 3 answers

1
arobin On

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

1
scottt732 On

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.

0
loxa On

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 :)