首页 文章

为什么重定向进程的输入流会影响TcpListener的行为?

提问于
浏览
1

当一个线程被TcpListener.AcceptTcpClient()调用阻塞而TcpListener被第二个线程调用Stop()时,预期的行为是从调用AcceptTcpClient()抛出的SocketException .

当重定向创建的进程的输入流时,这似乎受到对Process.Start(StartupInfo)的调用的影响 . 这可以通过以下代码显示 .

void Main() {
    TcpListener server = new TcpListener(IPAddress.Any, 1339);
    server.Start();

    Stopwatch sw = new Stopwatch();

    Thread t = new Thread(() => {
        Thread.Sleep(1000);

        String cmdExe = Environment.ExpandEnvironmentVariables(@"%SYSTEMROOT%\system32\cmd.exe");
        ProcessStartInfo info = new ProcessStartInfo(cmdExe, "/Q");

        // This problem does not show up when this true
        info.UseShellExecute = false;

        // The exception is only delayed when this is false
        info.RedirectStandardInput = true;

        info.RedirectStandardOutput = true;
        info.RedirectStandardError = true;

        Process p = Process.Start(info);

        server.Stop();

        //Start a timer as soon as the server is stopped
        sw.Start();
    });

    t.Start();

    try {
        server.AcceptTcpClient();
    }
    catch (Exception) { }

    // Print how long between the server stopping and the exception being thrown
    sw.Stop();
    sw.Elapsed.Dump();
}

当UseShellExecute为true时,一切都按预期工作 . 当UseShellExecute为false时,停止侦听器和抛出的异常之间会有一段延迟~25 - 30秒 . 当UseShell Execute为false且RedirectStandardInput为true时,似乎永远不会抛出异常,直到进程终止 .

在调用Stop()之后,调试器显示侦听器确实已停止且套接字不再绑定 . 但任何incomming连接抛出一个不同的SocketExceptions,说“对象不是一个套接字” .

我已经通过切换到似乎不受所有这些影响的异步方法解决了这个问题,但我无法理解这两个调用是如何连接的 .

Update 使用下面RbMm提供的信息,我通过更改侦听套接字的继承标志重新解决了这个问题 . 用于创建套接字的标志是hard coded到框架中,但是在创建监听器后,可以通过p/Invoking SetHandleInformation()立即更改继承标志 . 请注意,每次调用Stop()时都会创建一个新套接字,因此如果要重新启动侦听器,则需要再次调用它 .

TcpListener server = new TcpListener(IPAddress.Any, 1339);
SetHandleInformation(server.Server.Handle, HANDLE_FLAGS.INHERIT, HANDLE_FLAGS.None);
server.Start();

1 回答

  • 2

    TcpListener.AcceptTcpClient在套接字文件对象上开始I / O请求 . 内部IRP已分配并与此文件对象关联 .

    TcpListener.Stop关闭套接字文件句柄 . 当文件的 last 句柄关闭时 - 调用IRP_MJ_CLEANUP处理程序 . DispatchCleanup例程取消与调用清除的文件对象关联的每个IRP(I / O请求) .

    所以通常只存在一个文件(套接字)句柄,你在调用 AcceptTcpClient 时使用它 . 当调用 Stop 时(在客户端连接之前) - 它关闭此句柄 . 如果句柄是单一的 - 这是 last 句柄关闭,结果所有I / O请求被取消, AcceptTcpClient 完成错误(取消) .

    但如果此套接字的句柄将被复制 - 在 Stop 中关闭 not last 句柄不生效,I / O将不会被取消 .

    套接字句柄是如何以及为何重复的?由于未知原因,所有套接字句柄默认都是可继承的 . 仅从带有SP1的Windows 7开始添加标志WSA_FLAG_NO_HANDLE_INHERIT,这将创建一个不可继承的套接字 .

    直到你没有调用CreateProcessbInheritHandles 设置为 true 这不起作用 . 但是在这样的调用之后 - 所有可继承的句柄(包括所有的套接字句柄)都将被复制到子进程 .

    重定向输入流实现使用可继承的命名管道来输入/输出/错误流 . 并在 bInheritHandles 设置为 true 的情况下启动进程 . 这对网络代码有致命的影响 - 监听的套接字句柄被复制到子级和 Stop 关闭 not last 套接字句柄(否则一个将在子进程中 - 在您的情况下为cmd) . 结果 AcceptTcpClient 将无法完成 .

    在进程终止之前,似乎永远不会抛出异常 .

    当然 . 子进程终止时 - 套接字的 last 句柄将被关闭, AcceptTcpClient 结束 .

    什么是解决方案?在c上从win7 sp1开始 - 始终使用 WSA_FLAG_NO_HANDLE_INHERIT 创建套接字 . 在早期系统上 - 调用SetHandleInformation删除 HANDLE_FLAG_INHERIT .

    也可以从vista开始,当我们需要使用一些重复的句柄启动子进程时,将 bInheritHandles 设置为true,将所有可继承的句柄复制到子进程,我们可以通过使用PROC_THREAD_ATTRIBUTE_HANDLE_LIST显式设置子进程继承的句柄数组 .

    通过切换到似乎不受所有这些影响的异步方法

    没有 . 绝对与您在此处使用的同步或异步I / O(套接字句柄)无关 . 无论如何I / O请求都不会被取消 . 只是当你使用同步调用时 - 这是非常明显的 - 调用不返回 . 如果您使用异步调用和托管环境 - 这里有更多问题请注意这一点 . 如果你使用回调,必须在 AcceptTcpClient 完成时调用 - 这个回调不会被调用 . 如果您将事件与此io操作关联 - 将不会设置事件 . 等等

相关问题