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

时间:2009-08-25 18:01:54

标签: c# httplistener

我有一个使用HttpListener的应用程序,我需要知道客户端何时断开连接,现在我的所有代码都在try / catch块中,这非常难看并且不是一个好习惯。

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

谢谢!

3 个答案:

答案 0 :(得分:9)

简短的回答:你做不到。如果客户端停止通话,则底层套接字可能保持打开状态并且不会关闭;它会超时。检测这种情况的方法是尝试对该连接执行操作,如果连接不再有效,它将根据发生的情况抛出某种异常。如果您异步使用HttpListener,它可能会在try / catch方面稍微清理一下您的代码,但不幸的是,这就是您所困扰的。如果客户端断开连接,则不会触发任何事件。

答案 1 :(得分:1)

可以!我发现有两个选项是reflectionunsafe code。这两个选项都为您提供了一个客户端断开令牌,该令牌与方法类似:

CancellationToken GetClientDisconnectToken(HttpListenerRequest request)

要通过复制实现此目的,我发现HttpListener实际上为内置身份验证实现实现了客户端断开连接通知。我创建了一个类型,该类型从Hashtable派生而来,该结构HttpListener用于处理未完成的客户端断开连接通知,以超出其预期目的使用该代码。

对于每个请求,ConnectionId使用一个HTTP.SYS。该代码使用反射并创建一个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()调用私有方法。此方法将此结构的状态从“ inHandleAuthentication”更改为“ Normal”,这是调用IO完成回调(将在Remove上调用HashTable时所需要的状态)。拦截此调用非常简单-创建派生类型并覆盖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实例的存储
  • 使用HashTable设置或替换HttpListener的HttpListenerHashtable字段(最好在创建HttpListener实例后立即执行此操作)
  • 处理断开令牌的请求,并且在代码执行期间客户端断开连接

所有这些都在full source中解决。

答案 2 :(得分: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");