Detect client disconnect with HttpListener

后端 未结 3 1583
独厮守ぢ
独厮守ぢ 2021-02-14 06:31

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

3条回答
  •  清歌不尽
    2021-02-14 06:55

    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?

    • 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.

提交回复
热议问题