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

时间:2018-02-19 00:13:32

标签: c# .net winapi

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

当重定向创建的进程的输入流时,这似乎受到对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()之后,调试器显示侦听器确实已停止且套接字不再绑定。但任何输入连接都会抛出一个不同的SocketExceptions,表示" Object不是套接字"。

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

更新 使用下面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 个答案:

答案 0 :(得分:2)

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

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

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

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

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

直到你不CreateProcessbInheritHandles设为true这个不扮演角色。但是在这样的调用之后 - 所有可继承的句柄(包括所有套接字句柄)都将被复制到子进程。

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

  

异常似乎永远不会抛出,直到进程为止   终止。

当然。当子进程终止时 - 套接字的最后句柄将被关闭,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操作关联 - 将不会设置事件。等。