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



当一个线程被对 TcpListener.AcceptTcpClient() 的调用阻塞并且 TcpListener 被第二个线程 Stop()'d 时,预期的行为是从调用 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%system32cmd.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() 后,调试器显示侦听器确实已停止,套接字不再绑定。但是任何传入的连接都会抛出不同的套接字异常,说"对象不是套接字"。

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

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

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

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

TcpListener.Stop关闭套接字文件句柄。 关闭文件的最后一个句柄时 - 调用IRP_MJ_CLEANUP处理程序。调度清理例程取消与调用清理的文件对象关联的每个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 关联操作 - 不会设置事件。等。

最新更新