How to receive http-requests with Windows Service

lenden picture lenden · Oct 24, 2014 · Viewed 9.6k times · Source

I'm new at winservice developing. I need to create service, that will contain some functions and api: it should do something when some requests are coming.

As I understand, I need to use HttpListener class.

Base of this problem is described here: How to create a HTTP request listener Windows Service in .NET But it isn't finished code and I have more questions.

Here's my code:

partial class MyService
{
    private System.ComponentModel.IContainer components = null;
    private System.Diagnostics.EventLog eventLog1;
    private HttpListener listener;


    protected override void Dispose(bool disposing)
    {
        if (disposing && (components != null))
        {
            components.Dispose();
        }
        base.Dispose(disposing);
    }

    private void InitializeComponent()
    {
        this.eventLog1 = new System.Diagnostics.EventLog();
        ((System.ComponentModel.ISupportInitialize)(this.eventLog1)).BeginInit();

        this.ServiceName = Globals.ServiceName;
        ((System.ComponentModel.ISupportInitialize)(this.eventLog1)).EndInit();

        HttpListener listener = new HttpListener();
        listener.Prefixes.Add("http://localhost:1111/");
        listener.Start();
        listener.BeginGetContext(new AsyncCallback(OnRequestReceive), listener);

    }
}

public partial class MyService : ServiceBase
{
    public MyService()
    {
        InitializeComponent();
        if (!System.Diagnostics.EventLog.SourceExists("MySource"))
        {
            System.Diagnostics.EventLog.CreateEventSource(
                "MySource", "MyNewLog");
        }
        eventLog1.Source = "MySource";
        eventLog1.Log = "MyNewLog";
    }

    // smth about onstart and onstop

    private void OnRequestReceive(IAsyncResult result)
    {
        eventLog1.WriteEntry("Connection catched!");

        HttpListener listener = (HttpListener)result.AsyncState;
        eventLog1.WriteEntry("1");
        HttpListenerContext context = listener.GetContext();
        eventLog1.WriteEntry("2");
        HttpListenerRequest request = context.Request;
        eventLog1.WriteEntry("3");

        HttpListenerResponse response = context.Response;
        eventLog1.WriteEntry("4");

        string responseString = "<HTML><BODY> Hello world!</BODY></HTML>";
        byte[] buffer = System.Text.Encoding.UTF8.GetBytes(responseString);
        response.ContentLength64 = buffer.Length;


        Stream output = response.OutputStream;
        output.Write(buffer, 0, buffer.Length);
        output.Close();
    }
}

So, I added some log-writtings to understand what is happening:

When I reinstall and start service I open http://localhost:1111/ and -sometimes everything is OK, I see HelloWorld in my browser and I see 1234 in MyLog -sometimes it stucks and I wait and wait and wait. In that time I can see "1" in log, but not "2". Nothing happens, if I just wait. But if I load localhost again(or press f5), I can see Hello World... And log just adds 234 and it is 1234 and not 11234 summary..

After that, it just stucks and nothing changes in log till I restart service.

So, I understood, that problem may be with listener.EndGetContext, that is not used yet.

I tested this changing in code(last two lines):

private void OnRequestReceive(IAsyncResult result)
    {
        eventLog1.WriteEntry("Connection catched!");

        HttpListener listener = (HttpListener)result.AsyncState;
        eventLog1.WriteEntry("1");
        HttpListenerContext context = listener.GetContext();
        eventLog1.WriteEntry("2");
        HttpListenerRequest request = context.Request;
        eventLog1.WriteEntry("3");

        HttpListenerResponse response = context.Response;
        eventLog1.WriteEntry("4");

        string responseString = "<HTML><BODY> Hello world!</BODY></HTML>";
        byte[] buffer = System.Text.Encoding.UTF8.GetBytes(responseString);
        response.ContentLength64 = buffer.Length;


        Stream output = response.OutputStream;
        output.Write(buffer, 0, buffer.Length);
        output.Close();

        listener.EndGetContext(result);
        listener.BeginGetContext(new AsyncCallback(OnRequestReceive), listener);
    }

This bad code works: when HelloWorld is shown, I can reload page and see it again(and log shows 1234 many time). But I still have problem with 1......234(for some loadings of HelloWorld it appears, for some doesn't)

Of course, using BeginGetContext in the end of callback is very bad, but it is just my test.

Also, I need to work with parallel requests

So, how to do it in right way? I can't find anything helpfull about winservices requests receiving in google.. Just that question about HttpListener..

UPDATE:

I tried new way:

HttpListener listener = new HttpListener();
listener.Prefixes.Add(Globals.URL);
listener.Start();

listenerThread = new Thread(() =>
{
    while (listener.IsListening)
    {
         HandleRequest(listener.GetContext());
    }

});
listenerThread.Start();

It is allmost ok, it waits when context appeas, works with it and waits again. But this way creates a queue of requests. If 1000 requests come in one time, the last will be processed after 999. It is better than nothing, but I want to process requests parallel. Also, very strange things happen: I've created simple web-server with some routes, but after some time of using, it hangs. Nothing changes till I reinstall(not restart!) service..

So, I'm still waiting for any really good realization, that will contain

  • Parallel processing incoming requests
  • Clear and simple code for understanding callbacks and so on
  • Stopping service shouldn't make any problems with while loop

Answer

Luaan picture Luaan · Oct 29, 2014

You really want to use the asynchronous API for this - in fact, this is almost exactly what it has been designed for.

The basic code is relatively simple:

void Main()
{
    var cts = new CancellationTokenSource();

    var task = StartListener(cts.Token);

    Console.ReadLine();

    cts.Cancel();

    task.Wait();
}

async Task StartListener(CancellationToken token)
{
  var listener = new HttpListener();
  listener.Start();

  token.Register(() => listener.Abort());

  while (!token.IsCancellationRequested)
  {
    HttpListenerContext context;

    try
    {
      context = await listener.GetContextAsync().ConfigureAwait(false);

      HandleRequest(context); // Note that this is *not* awaited
    }
    catch
    {
      // Handle errors
    }
  }
}

async Task HandleRequest(HttpListenerContext context)
{
  // Handle the request, ideally in an asynchronous way.
  // Even if not asynchronous, though, this is still run 
  // on a different (thread pool) thread
}

If you have no experience with the await pattern, you might want to read up a bit on the particulars. And of course, this is definitely not production code, just a sample. You need to add a lot more error handling for starters.

Also of note is that because HandleRequest is not awaited, you can't handle its exceptions in the StartListener method - you'll either have to add a continuation for handling those, or you should just catch the exceptions in HandleRequest directly.

The basic idea is simple - by not awaiting the HandleRequest method, we're immediately going back to waiting for a new request (by asynchronous I/O, i.e. thread-less). If HttpListener is really meant to be used this way (and I believe it is, although I haven't tested it), it will allow you to process many requests in parallel, both with asynchronous I/O and CPU work.

The ConfigureAwait makes sure you're not going to get synchronized to a SynchronizationContext. This isn't strictly necessary in a service / console application, because those often don't have any synchronization contexts, but I tend to write it out explicitly anyway. The continuation of the GetContextAsync method is thus posted to a different task scheduler - by default, a thread pool task scheduler.

When implementing this in your code, you'd just use the Main code before ReadLine for OnStart, and the code after for OnStop. The task.Wait() is there to make sure you shutdown cleanly - it might take a while to actually shut everything down. Also, any exceptions in StartListener itself will be rethrown by the Wait, so you want to have some error logging there as well.