使用HttpListener检测客户端断开连接

12

我有一个使用HttpListener的应用程序,我需要知道客户端何时断开连接,目前我所有的代码都放在try/catch块中,这样做很丑陋,也不是一个好的实践。

如何知道客户端是否已断开连接?

谢谢!

3个回答

10

简短回答:你无法关闭客户端的连接。如果客户端停止通信,底层套接字可能会保持打开状态而永远不会关闭;它只会超时。检测这种情况的方法是尝试在该连接上执行操作,如果该连接不再有效,则会引发某种异常,具体取决于发生了什么。如果异步使用HttpListener,可能会在try/catch方面使您的代码更加整洁,但不幸的是,这就是您所困扰的问题。没有触发客户端断开连接事件的事件。


2
你可以!我找到了两个选项:使用反射不安全的代码。这两种方法都能从一个类似于这样的方法中获得客户端断开连接的令牌:
CancellationToken GetClientDisconnectToken(HttpListenerRequest request)

为了通过反射实现这一点,我发现HttpListener实际上为内置身份验证实现了客户端断开连接通知。我创建了一个从Hashtable派生的类型,用于连接到HttpListener用于未完成的客户端断开连接通知的结构,以便在其预期目的之外利用该代码。
对于每个请求,都有一个由HTTP.SYS使用的ConnectionId。此代码使用反射并创建一个Func<>,以获取任何HttpListenerRequest的此ID:
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);
}

下面的代码有点复杂:
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;
}

这个反射创建了一个 Func<>,它的输入是一个 ConnectionId,返回一个包含正在进行请求状态的 IAsyncResult。在内部,它调用了 HttpListener 上的一个私有方法:

private unsafe void RegisterForDisconnectNotification(ulong connectionId,
      ref HttpListener.DisconnectAsyncResult disconnectResult)

正如其名称所示,此方法调用Win32 API以通知客户端断开连接。紧接着,使用该方法的结果,如果连接仍然打开,则调用嵌套类型上的私有方法:void HttpListener.DisconnectedAsyncResult.FinishOwningDisconnectHandling()。该方法将此结构的状态从“在HandleAuthentication中”更改为“正常”,这是需要调用IO完成回调的状态,该回调将调用HashTable上的Remove。拦截此调用非常简单-创建派生类型并覆盖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());
}

调用Cancel往往会抛出异常,因此我们使用TPL来调用它,这样您就可以通过订阅TaskScheduler.UnobservedTaskException来捕获任何取消期间抛出的异常。
还剩下什么?
  • 创建HttpListenerHashtable派生类型
  • 存储正在进行中的CancellationTokenSource实例
  • HttpListenerHashTable字段设置或替换为HttpListenerHashtable(最好在创建HttpListener实例后立即执行此操作)
  • 处理断开连接令牌的请求,并且客户端在代码执行时断开连接
所有这些都在完整源代码中得到解决。

0

我遇到了同样的问题,我执行了一条长的 SQL 查询,如果用户不断按 F5,它就会崩溃。

HttpListener 不公开其底层套接字,因此您无法轮询它。

所以我想出了这个解决方法:

IDbCommand cmd = LongSqlQuery();
while (!taskCompleted)
{
    try
    {
        //send disposable data at times to check connection
        byte[] dummy = new byte[1] { 0xFF };
        outputStream.Write(dummy,0,1);

        Task.Delay(100).Wait();
    }
    catch
    {
        //Client disconnect
        cmd.Cancel();
    }
}

//Redirect client to results page
response.Redirect("http://yourserver.com/results?query=yourquery");

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接