Async httpListener is the callback initiated on the UI thread?
-
30-06-2021 - |
Question
I am looking to build an httpListener into a small server app. Whilst reading up on it, i encountered the following code snippet in this question on stackoverflow
Public Class HTTPServer
Shared Listener As HttpListener = New HttpListener
Public Shared Sub Start()
ServicePointManager.DefaultConnectionLimit = 500
ServicePointManager.Expect100Continue = False
ServicePointManager.MaxServicePoints = 500
Listener.Prefixes.Add("http://localhost/")
Listener.Start()
For i As Integer = 1 To (System.Environment.ProcessorCount * 2)
Dim NewThread As New System.Threading.Thread(AddressOf ListenerThread)
NewThread.Priority = ThreadPriority.Normal
NewThread.IsBackground = True
NewThread.Start()
Next
End Sub
Private Shared Sub ListenerThread()
Dim SyncResult As IAsyncResult
While True
SyncResult = Listener.BeginGetContext(New AsyncCallback(AddressOf ListenerCallback), Listener)
SyncResult.AsyncWaitHandle.WaitOne()
End While
End Sub
Private Shared Sub ListenerCallback(ByVal StateObject As IAsyncResult)
Dim Listener As HttpListener = DirectCast(StateObject.AsyncState, HttpListener)
Dim Context As HttpListenerContext = Listener.EndGetContext(StateObject)
Dim Request As HttpListenerRequest = Context.Request
Dim Response As HttpListenerResponse = Context.Response
Dim ResponseString As String = "OK"
Dim Buffer As Byte() = System.Text.Encoding.UTF8.GetBytes(ResponseString)
Response.ContentLength64 = Buffer.Length
Dim OutputStream As System.IO.Stream = Response.OutputStream
OutputStream.Write(Buffer, 0, Buffer.Length)
OutputStream.Close()
OutputStream.Dispose()
End Sub
End Class
Which seemed pretty simple, and seems much like the msdn example. However when testing it in a dummy project, i found a few things that confused me, for example UI objects can be accessed directly from the callback sub, which i thought should cause a cross threading exception.
To clarify, i modified this code slightly to run within the main form of a simple winforms project.
It seems i do not fully understand the code, for example AsyncWaitHandle.WaitOne() is completely new to me.
Could someone briefly walk me through this snippet please? Any help appreciated.
Solution
This snippet code looks like:
- It create a HttpListener and start linsening at the uri;
- It create some threads (number of System.Environment.ProcessorCount * 2) to send async request to listener try to get a context;
- Then the thread was blocked by call SyncResult.AsyncWaitHandle.WaitOne(), about the AsyncWaitHandle.WaitOne please check out Details of AsyncWaitHandle.WaitOne
- If some client request come in, the ListenerCallback() will be trigged, and it try to send response back to client
- Then go to the next round of loop.
Because it called SyncResult.AsyncWaitHandle.WaitOne() to block that thread, it will get the same result with Synchronously call.
In my opinion, it just returns a simple OK response, so we even don't need any other threads. I will show you the code soon (My code will not work very well if we have heavy operations, and please don't do this in form application.)
static void Main(string[] args)
{
ServicePointManager.DefaultConnectionLimit = 500;
ServicePointManager.Expect100Continue = false;
ServicePointManager.MaxServicePoints = 500;
HttpListener listener = new HttpListener();
listener.Prefixes.Add("http://localhost:999/");
listener.Start();
listener.BeginGetContext(ListenerCallBack, listener);
Console.ReadLine();
}
private static void ListenerCallBack(IAsyncResult result)
{
HttpListener httpListener = (HttpListener) result.AsyncState;
// Call EndGetContext to complete the asynchronous operation.
HttpListenerContext context = httpListener.EndGetContext(result);
HttpListenerRequest request = context.Request;
// Obtain a response object.
HttpListenerResponse response = context.Response;
// Construct a response.
string responseString = "<HTML><BODY> Hello world!</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;
output.Write(buffer,0,buffer.Length);
// You must close the output stream.
output.Close();
// we call BeginGetContext() to send async request again for next coming client
httpListener.BeginGetContext(ListenerCallBack, httpListener);
}
In C#5 and Net 4.5, we have sync method, it will much easier:
static void Main(string[] args)
{
ServicePointManager.DefaultConnectionLimit = 500;
ServicePointManager.Expect100Continue = false;
ServicePointManager.MaxServicePoints = 500;
HttpListener listener = new HttpListener();
listener.Prefixes.Add("http://localhost:999/");
listener.Start();
while (true)
{
var t = listener.GetContextAsync();
HttpListenerContext context = t.Result;
HttpListenerRequest request = context.Request;
// Obtain a response object.
HttpListenerResponse response = context.Response;
// Construct a response.
string responseString = "<HTML><BODY> Hello world!</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;
output.Write(buffer, 0, buffer.Length);
// You must close the output stream.
output.Close();
}
}
For your another question about "cross threading exception" for UI, you are right, the created new Thread will have a null value for SynchronizationContext.Current because it's a thread pool thread, we need do a post() in form's SynchronizationContext. Here are more information http://msdn.microsoft.com/en-us/magazine/gg598924.aspx and http://blogs.msdn.com/b/pfxteam/archive/2012/01/20/10259049.aspx.
OTHER TIPS
Simple C# WebServer with HttpListener (name: server), BackgroundWorker (name: bw_server), 2 buttons (name: btn_start & btn_stop) and TextBox (name: tb_log):
public partial class Form1 : Form
{
HttpListener server;
private void btn_start_Click(object sender, EventArgs e)
{
bw_server.RunWorkerAsync(@"http://+:80/");
}
private void bw_server_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
{
string prefix = e.Argument.ToString();
bw_server.ReportProgress(0, "Starting server...");
if (!bw_server.CancellationPending)
{
try
{
start_server(prefix);
}
catch (Exception ex)
{
bw_server.ReportProgress(0, ex.Message);
bw_server.CancelAsync();
}
}
else
{
e.Cancel = true;
return;
}
}
private void bw_server_ProgressChanged(object sender, System.ComponentModel.ProgressChangedEventArgs e)
{
log(e.UserState.ToString());
}
private void bw_server_RunWorkerCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
if (e.Cancelled)
{
log("Server was stopped");
}
else
{
log("Server work was completed");
}
}
private void log(string msg)
{
if (String.IsNullOrEmpty(msg)) return;
tb_log.AppendText((tb_log.Text == String.Empty ? String.Empty : Environment.NewLine) + DateTime.Now.ToLongTimeString() + " " + msg);
}
private void btn_stop_Click(object sender, EventArgs e)
{
WebRequest.Create("http://localhost:" + port + "/?stop=1").GetResponse();
bw_server.CancelAsync();
}
private void start_server(string prefix)
{
server = new HttpListener();
if (!HttpListener.IsSupported) return;
if (String.IsNullOrEmpty(prefix))
{
throw new ArgumentException("prefix");
}
server.Prefixes.Add(prefix);
server.Start();
while (server.IsListening)
{
HttpListenerContext context = server.GetContext();
HttpListenerRequest request = context.Request;
if (request.HttpMethod == "GET")
{
HttpListenerResponse response = context.Response;
response.ContentType = "text/html; charset=UTF-8";
if (request.QueryString["stop"] == "1")
{
write_to_resp(response, "server was stopped");
server.Stop();
}
else
{
write_to_resp(response, "bad params");
}
}
}
}
private static void write_to_resp(HttpListenerResponse response, string str_resp)
{
byte[] buffer = Encoding.UTF8.GetBytes(str_resp);
response.ContentLength64 = buffer.Length;
response.OutputStream.Write(buffer, 0, buffer.Length);
}
}