I have an application that uses HttpListener, I need to know when the client disconnected, right now I have all my code inside a try/catch block which is pretty ugly and not a g
You can! Two options I've found are with reflection or unsafe code. Both options give you a client disconnect token from a method that looks like this:
CancellationToken GetClientDisconnectToken(HttpListenerRequest request)
To implement this via refection, I found HttpListener
actually implements client disconnect notifications for the built-in authentication implementation. I created a type who derives from Hashtable
, the structure HttpListener
uses for outstanding client disconnect notifications, to tap in to that code outside of its intended purpose.
For every request there is a ConnectionId
used by HTTP.SYS
. This code uses reflection and creates a Func<>
to obtain this id for any HttpListenerRequest
:
private static Func GetConnectionId()
{
var field = typeof(HttpListenerRequest).GetField("m_ConnectionId",
BindingFlags.Instance | BindingFlags.NonPublic);
if (null == field)
throw new InvalidOperationException();
return request => (ulong)field.GetValue(request);
}
The next bit of code is a little more complicated:
private static Func GetRegisterForDisconnectNotification(HttpListener httpListener)
{
var registerForDisconnectNotification = typeof(HttpListener)
.GetMethod("RegisterForDisconnectNotification", BindingFlags.Instance | BindingFlags.NonPublic);
if (null == registerForDisconnectNotification)
throw new InvalidOperationException();
var finishOwningDisconnectHandling =
typeof(HttpListener).GetNestedType("DisconnectAsyncResult", BindingFlags.NonPublic)
.GetMethod("FinishOwningDisconnectHandling", BindingFlags.Instance | BindingFlags.NonPublic);
if (null == finishOwningDisconnectHandling)
throw new InvalidOperationException();
IAsyncResult RegisterForDisconnectNotification(ulong connectionId)
{
var invokeAttr = new object[] { connectionId, null };
registerForDisconnectNotification.Invoke(httpListener, invokeAttr);
var disconnectedAsyncResult = invokeAttr[1];
if (null != disconnectedAsyncResult)
finishOwningDisconnectHandling.Invoke(disconnectedAsyncResult, null);
return disconnectedAsyncResult as IAsyncResult;
}
return RegisterForDisconnectNotification;
}
This reflection creates a Func<>
whose input is a ConnectionId
and returns an IAsyncResult
containing the state of the inflight request. Internally, this calls a private method on HttpListener
:
private unsafe void RegisterForDisconnectNotification(ulong connectionId,
ref HttpListener.DisconnectAsyncResult disconnectResult)
As the name implies, this method calls a Win32 API to be notified of a client disconnect. Immediately after, using the result of that method I call a private method on a nested type: void HttpListener.DisconnectedAsyncResult.FinishOwningDisconnectHandling()
, if the connection is still open. This method changes the state of this structure from "in HandleAuthentication" to "Normal", which is the state it needs to be in to invoke the IO completion callback who will call Remove
on the HashTable
. Intercepting this call turns out to be pretty simple - create a derived type and override Remove
:
public override void Remove(object key)
{
base.Remove(key);
var connectionId = (ulong)key;
if (!_clientDisconnectTokens.TryRemove(connectionId, out var cancellationTokenSource))
return;
Cancel(cancellationTokenSource);
}
private static void Cancel(CancellationTokenSource cancellationTokenSource)
{
// Use TaskScheduler.UnobservedTaskException for caller to catch exceptions
Task.Run(() => cancellationTokenSource.Cancel());
}
Calling Cancel
tends to throw, so we invoke this using TPL so you can catch any exception thrown during cancel by subscribing to TaskScheduler.UnobservedTaskException
.
What left?
HttpListenerHashtable
derived typeCancellationTokenSource
instancesHashTable
field of HttpListener with the HttpListenerHashtable
(it's best to do this right after creating the HttpListener
instance)All of which are addressed in the full source.