.NET TcpListener Stop 方法在存在子进程时无法停止监听器

6
我正在处理一些使用套接字直接工作的旧版TCP服务器代码,因为它是在.NET 2.0及以前版本中编写的。该服务器具有“停止”和“启动”接受客户端连接的功能。
为了解决问题,我以管理员用户身份在控制台模式下运行服务器。除此之外,我从方程式中排除了套接字接受线程,所有代码只执行类似以下操作:
tcpListener = new TcpListener(IPAddress.Any, this.Port);
tcpListener.Start();

并且

tcpListener.Stop();

这个代码从不同的方法中调用。我已经调试了代码,非常确定代码只执行一次。然而,问题在于对Stop的调用实际上并没有释放套接字地址,随后对Start的调用因此失败,并显示错误信息“通常只允许使用每个套接字地址(协议/网络地址/端口)一次”。我还可以从ProcessExplorer中确认服务器仍在监听服务器端口。
当我编写一个小型控制台应用程序,使用相同的代码片段时,一切都正常工作。我甚至尝试跟踪.NET网络和套接字库,但没有错误或任何指示出现问题。
对我来说,不清楚为什么调用Stop不会释放套接字地址?
更新: 经过进一步调查,发现子进程启动到TcpListener存在一些奇怪的影响。我已经制作了一个“裸骨”示例代码,说明了这个问题:
using System;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Threading;

namespace TcpListenerStartStop
{
    class MyTcpListener
    {
        public static void Main(string[] args)
        {
            Int32 port = 13000;

            if (args.Length > 0) // indicates child process
            {
                Thread.Sleep(4000); // as a child do nothing and wait for a few seconds
            }
            else // parent will play with the TcpListener
            {
                //LaunchChildProcess(); // launch child here and listener restart is fine?!?

                var tcpListener = new TcpListener(IPAddress.Any, port);
                tcpListener.Start();

                Console.WriteLine("Starting test in 2 seconds...");
                Thread.Sleep(2000);

                LaunchChildProcess(); // launch child here and listener restart is not fine?!?

                tcpListener.Stop();
                Console.WriteLine("Stopped.");
                Thread.Sleep(1000);

                tcpListener.Start();
                Console.WriteLine("Started");
            }

            Console.WriteLine("All is good, no exceptions :)");
        }

        private static void LaunchChildProcess()
        {
            Process process = new Process();
            var processStartInfo = new ProcessStartInfo
            {
                CreateNoWindow = true,
                FileName = Assembly.GetExecutingAssembly().Location,
                UseShellExecute = false, // comment out this line out listener restart is fine?!?
                Arguments = "child"
            };
            process.StartInfo = processStartInfo;

            process.Start();
        }
    }
}

从代码中可以看出,如果在创建监听器之前启动子进程,则一切正常,但如果在监听器重启之后启动子进程,则会失败。这与子进程的 UseShellExecute = false 选项有关。

不确定这是 .NET 的 bug 还是我不知道的某种特殊行为?


从文档中可以看到:调用者需要注意,Stop方法还会关闭底层Socket,并为TcpListener创建一个新的Socket。如果在调用Stop方法之前在底层Socket上设置了任何属性,则这些属性将不会传递到新的Socket。因此,调用Stop()方法会释放socket,但它会立即启动一个新的socket。编辑:不过,这是一个未绑定的socket。 - Scott Chamberlain
1个回答

6
这种行为的真正原因是TcpListener套接字句柄与许多其他句柄一起被子进程继承。有关此主题的一些讨论可以在这里这里找到。
一个明显的解决方案是在初始化TcpListener之前启动子进程。
另一个解决方案是使用UseShellExecute = true来避免这种套接字句柄继承。
理想的解决方案是设置套接字句柄选项以防止子进程继承。在.NET中,可以通过对TcpListener套接字句柄进行P/Invoke来实现:
    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool SetHandleInformation(IntPtr hObject, uint dwMask, uint dwFlags);
    private const uint HANDLE_FLAG_INHERIT = 1;

    private static void MakeNotInheritable(TcpListener tcpListener)
    {
        var handle = tcpListener.Server.Handle;
        SetHandleInformation(handle, HANDLE_FLAG_INHERIT, 0);
    }

2
很棒的问题,很棒的答案!还有一个好的复现示例。 - Lorenzo Dematté
1
非常好的答案。我被这个问题困扰了一天以上。P/Invoke解决了它。 - Kamyar

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