10

In the application that I build, there is a need for webserver that can serve, simultaneously, multiple clients.
For that I use the HttpListener object. with its Async methods\events BeginGetContext and EndGetContext.
In the delegated method, there is a call for the listener to start listening again, and it works.. mostly.

The code provided is a mix of code that i found here and there, and a delay, to simulate a data processing bottleneck.

The problem is, it starts to manage the next connection only AFTER the last one was served.. no use for me.

public class HtServer {


    public void startServer(){
        HttpListener HL = new HttpListener();
        HL.Prefixes.Add("http://127.0.0.1:800/");
        HL.Start();
        IAsyncResult HLC = HL.BeginGetContext(new AsyncCallback(clientConnection),HL);   
    }

    public void clientConnection(IAsyncResult res){
        HttpListener listener = (HttpListener)res.AsyncState;
        HttpListenerContext context = listener.EndGetContext(res);
        HttpListenerRequest request = context.Request;
        // Obtain a response object.
        HttpListenerResponse response = context.Response;
        // Construct a response. 
        // add a delay to simulate data process
        String before_wait = String.Format("{0}", DateTime.Now);
        Thread.Sleep(4000);
        String after_wait = String.Format("{0}", DateTime.Now);
        string responseString = "<HTML><BODY> BW: " + before_wait + "<br />AW:" + after_wait + "</BODY></HTML>";
        byte[] buffer = System.Text.Encoding.UTF8.GetBytes(responseString);
        // Get a response stream and write the response to it.
        response.ContentLength64 = buffer.Length;
        System.IO.Stream output = response.OutputStream;
        // You must close the output stream.
        output.Write(buffer, 0, buffer.Length);
        output.Close();
        listener.BeginGetContext(new AsyncCallback(clientConnection), listener);
    }
}

edit

    private static void OnContext(IAsyncResult ar)
    {
        var ctx = _listener.EndGetContext(ar);
        _listener.BeginGetContext(OnContext, null);

        Console.WriteLine(DateTime.UtcNow.ToString("HH:mm:ss.fff") + " Handling request");

        var buf = Encoding.ASCII.GetBytes("Hello world");
        ctx.Response.ContentType = "text/plain";

        // prevent thread from exiting.
        Thread.Sleep(3000);
        // moved these lines here.. to simulate process delay
        ctx.Response.OutputStream.Write(buf, 0, buf.Length);
        ctx.Response.OutputStream.Close();
        Console.WriteLine(DateTime.UtcNow.ToString("HH:mm:ss.fff") + " completed");
    }

the output is enter image description here

yossi
  • 3,090
  • 7
  • 45
  • 65

2 Answers2

22

Well. That's because you start to fetch the next context after you have processed the first. Don't do that. Get the next context directly:

public void clientConnection(IAsyncResult res){
    HttpListener listener = (HttpListener)res.AsyncState;
    HttpListenerContext context = listener.EndGetContext(res);

    //tell listener to get the next context directly.
    listener.BeginGetContext(clientConnection, listener);

    HttpListenerRequest request = context.Request;
    // Obtain a response object.
    HttpListenerResponse response = context.Response;
    // Construct a response. 
    // add a delay to simulate data process
    String before_wait = String.Format("{0}", DateTime.Now);
    Thread.Sleep(4000);
    String after_wait = String.Format("{0}", DateTime.Now);
    string responseString = "<HTML><BODY> BW: " + before_wait + "<br />AW:" + after_wait + "</BODY></HTML>";
    byte[] buffer = System.Text.Encoding.UTF8.GetBytes(responseString);
    // Get a response stream and write the response to it.
    response.ContentLength64 = buffer.Length;
    System.IO.Stream output = response.OutputStream;
    // You must close the output stream.
    output.Write(buffer, 0, buffer.Length);
    output.Close();
}

Here is my sample code that proves that it work (updated per request of the OP):

class Program
{
    private static HttpListener _listener;

    static void Main(string[] args)
    {
        _listener = new HttpListener();
        _listener.Prefixes.Add("http://localhost/asynctest/");
        _listener.Start();
        _listener.BeginGetContext(OnContext, null);

        Console.ReadLine();
    }

    private static void OnContext(IAsyncResult ar)
    {
        var ctx = _listener.EndGetContext(ar);
        _listener.BeginGetContext(OnContext, null);

        Console.WriteLine(DateTime.UtcNow.ToString("HH:mm:ss.fff") + " Handling request");

        var buf = Encoding.ASCII.GetBytes("Hello world");
        ctx.Response.ContentType = "text/plain";

        // simulate work
        Thread.Sleep(10000);

        ctx.Response.OutputStream.Write(buf, 0, buf.Length);
        ctx.Response.OutputStream.Close();


        Console.WriteLine(DateTime.UtcNow.ToString("HH:mm:ss.fff") + " completed");
    }
}

Generates:

enter image description here

Both requests starts to get processed directly.

Why the above code works

HTTP have something called pipelining. It means that all requests that are received over the same connection must get their responses in the same order. However, the built in HttpListener doesn't seem to support pipelining, instead it's completes the response for the first request before taking care of the second. It's therefore important that you make sure that every request is sent over a new connection.

The easiest way to do that is to use different browsers when trying out the code. I did that, and as you see both my requests are handled at the same time.

jgauffin
  • 99,844
  • 45
  • 235
  • 372
  • 3
    Are you sure that you tried it with multiple connections? Browsers tend to reuse existing connections and that will not work. – jgauffin Feb 02 '15 at 08:18
  • @yossi if you tried that post the current code, not some outdated code. – usr Feb 02 '15 at 08:20
  • No. Browsers should allow 3 concurrent http requests. The problem is most likely because you are using synchronous-single threaded code. – Aron Feb 02 '15 at 08:20
  • @jgauffin yes, now i tried it from 3 different browsers simultaneously. 4 second time gap between them all. can it be the `Stream`? if so, how can i change it? – yossi Feb 02 '15 at 08:22
  • @Aron: Should allow, yes. For debugging purposes it's probably better to use different instances instead of opening new tabs. I've at least had problems with browsers reusing the same connection (i.e. pipelining requests) – jgauffin Feb 02 '15 at 08:23
  • @usr if it didn't work and had no impect? i have more code that i tried.. i can't post it all. so i used the most generic version to start with – yossi Feb 02 '15 at 08:23
  • @Aron i am new to C#, but as i understood - the Async call in the `BeginGetContext`, is sending it to another thread. https://msdn.microsoft.com/en-us/library/system.net.httplistener.begingetcontext%28v=vs.110%29.aspx – yossi Feb 02 '15 at 08:28
  • @yossi Depends on the `SynchronizationContext.Current`. In general, you should not be mixing blocking code (like `Thread.Sleep`) with async code. – Aron Feb 02 '15 at 08:30
  • 1
    @yossi well, given this answer the code that is posted in the question is clearly wrong. The approach of this answer is right. So post the best code that you have. – usr Feb 02 '15 at 09:00
  • 1) well explained. understood! 2) assuming that I can limit the system to.. lets say - 50 connections, is there a way to send the next request to another `httplistener` (or similar solution)? – yossi Feb 02 '15 at 09:59
  • @yossi: It depends on how many requests you send from the same client within a certain time frame. It's only a problem if you need to send requests within the same amount of time that it takes for the server to respond on a request. i.e. If it takes 3 seconds for the server to reply and you only send a request every 4th second it's not a problem. – jgauffin Feb 02 '15 at 10:20
  • The app will serve up to 50 clients/tabs simul' - every one of them can send a request every 3 to 30 seconds. some of the requests will end in a single second, others can take up to a minute.. – yossi Feb 02 '15 at 10:55
  • If it's your own application that sends a request it's typically not a problem. Just create a new HTTP client for every request. – jgauffin Feb 02 '15 at 11:38
  • @jgauffin it is not. the idea is of a central process, that reacts with browsers & native clients, on the same machine and from the internet. than the data is channeled and processed by other processes( or threads). the number of clients for each app is unknown, but estimated to be up to 50.. – yossi Feb 03 '15 at 09:02
7

Try this instead..

This will use asynchronous coding to ensure that there is no blocking. Blocking means when a thread, sleeps, which is typically how programs tend to "freeze". By using this code, you run non-blocking, which means that its almost impossible to "freeze" the application.

public async Task handleClientConnection(HttpListener listener){
    HttpListenerContext context = await listener.GetContextAsync();
    var ret = handleClientConnection(listener);

    HttpListenerRequest request = context.Request;
    // Obtain a response object.
    HttpListenerResponse response = context.Response;
    // Construct a response. 
    // add a delay to simulate data process
    String before_wait = String.Format("{0}", DateTime.Now);
    await Task.Wait(4000);
    String after_wait = String.Format("{0}", DateTime.Now);
    string responseString = "<HTML><BODY> BW: " + before_wait + "<br />AW:" + after_wait + "</BODY></HTML>";
    byte[] buffer = System.Text.Encoding.UTF8.GetBytes(responseString);
    // Get a response stream and write the response to it.
    response.ContentLength64 = buffer.Length;
    using(System.IO.Stream output = response.OutputStream)
        output.Write(buffer, 0, buffer.Length);

    await ret;
}

public void startServer(){
    HttpListener HL = new HttpListener();
    HL.Prefixes.Add("http://127.0.0.1:800/");
    HL.Start();
    await handleClientConnection(HL);
}
Aron
  • 15,464
  • 3
  • 31
  • 64
  • oh, guess i'm really new to .NET. i'm using vs2010 on xp pro (client demands) thanks! – yossi Feb 02 '15 at 08:45
  • BTW in this case we are still single threaded. If multithreading is something you might want...just replace `await listener.GetContextAsync();` with `await listener.GetContextAsync().ConfigureAwait(false);` – Aron Feb 02 '15 at 08:51
  • 1
    You never say why your code should work. So what does he learn from your answer more than another way of using asynchronous IO? – jgauffin Feb 02 '15 at 09:38
  • This is .Net 4.5, am i correct? if so, i can't use it on xp-pro, which is a requirement of the client. – yossi Feb 02 '15 at 10:00
  • @yossi That would be correct. I would however ask, why this would be a requirement. xp is no longer supported by Microsoft. It is extremely dangerous to be running XP now (unless you are one of those banks that are paying obscene amounts for private support from Microsoft). – Aron Feb 02 '15 at 10:09
  • There are .. about 20k xp stations that will use the app, and can't be upgraded for many reasons (old apps and some special hardware). – yossi Feb 02 '15 at 10:30