Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Detect client disconnect with HttpListener

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 good practice.

How can I know if a client disconnected?

thanks!

like image 699
Schwertz Avatar asked Aug 25 '09 18:08

Schwertz


2 Answers

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<HttpListenerRequest, ulong> 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<ulong, IAsyncResult> 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?

  • creation of the HttpListenerHashtable derived type
  • storage of in-flight CancellationTokenSource instances
  • set or replace the HashTable field of HttpListener with the HttpListenerHashtable (it's best to do this right after creating the HttpListener instance)
  • handle the request of a disconnect token, and the client disconnects while the code is executing

All of which are addressed in the full source.

like image 182
George Tsiokos Avatar answered Oct 04 '22 16:10

George Tsiokos


Short answer: you can't. If a client stops talking, the underlying socket may stay open and won't ever close; it'll just timeout. The way to detect this is to attempt to perform an action on that connection and if the connection is no longer valid, it'll throw some sort of exception depending on what happened. If you use HttpListener asynchronously, it may clean up your code a bit in terms of a try/catch but unfortunately that's what you're stuck with. There is no event that will fire if the client disconnects.

like image 44
Jeff Tucker Avatar answered Oct 04 '22 18:10

Jeff Tucker