如何在C#中创建一个随机端口的HttpListener类?

40

我希望创建一个应用程序,可以在同一台计算机上的多个实例中提供内部网页服务。为此,我想创建一个HttpListener,它监听随机选择且当前未使用的端口。

总体来说,我想要的是:

mListener = new HttpListener();
mListener.Prefixes.Add("http://*:0/");
mListener.Start();
selectedPort = mListener.Port;

我该如何完成这个任务?

7个回答

46

如果你将TcpListener绑定到端口0,则它会找到一个随机未使用的端口进行监听。

public static int GetRandomUnusedPort()
{
    var listener = new TcpListener(IPAddress.Any, 0);
    listener.Start();
    var port = ((IPEndPoint)listener.LocalEndpoint).Port;
    listener.Stop();
    return port;
}

2
不,它无法知道那个。 - Richard Dingwall
12
这并未回答关于HttpListener类的问题。 - jnm2
11
除了在你的方法结束时释放端口和启动HttpListener之间存在竞态条件外,我认为还算公平。 - jnm2
6
仅供您参考,谷歌在其所有OAuth代码和示例中使用了您的代码 :) (https://github.com/googlesamples/oauth-apps-for-windows/blob/a9c06c539adc21eab752537b234219c448356001/OAuthDesktopApp/OAuthDesktopApp/MainWindow.xaml.cs#L47) - frzsombor
2
竞态条件使得这个选项对我来说不可用。出现了太多的间歇性错误。 - Ken Lyon
显示剩余2条评论

18

这个怎么样:

    static List<int> usedPorts = new List<int>();
    static Random r = new Random();

    public HttpListener CreateNewListener()
    {
        HttpListener mListener;
        int newPort = -1;
        while (true)
        {
            mListener = new HttpListener();
            newPort = r.Next(49152, 65535); // IANA suggests the range 49152 to 65535 for dynamic or private ports.
            if (usedPorts.Contains(newPort))
            {
                continue;
            }
            mListener.Prefixes.Add(string.Format("http://*:{0}/", newPort));
            try
            {
                mListener.Start();
            }
            catch
            {
                continue;
            }
            usedPorts.Add(newPort);
            break;
        }

        return mListener;
    }

我不确定您如何找到该计算机上正在使用的所有端口,但如果您尝试在已被使用的端口上进行侦听,则应该会收到异常,在这种情况下,该方法将简单地选择另一个端口。


2
你可能想考虑使用MinPort/MaxPort常量,MSDN链接 - Joseph Lennox
@JosephLennox,MinPort常量的值为零。答案中给出的最小值更加合适。 - Drew Noakes
@Snooganz,catch块应该释放mListener。此外,usedPorts应该是一个Set<int>,以进行线性成员测试。个人建议在方法内部限定usedPorts的作用域(或者干脆将其删除),因为如果您在一个非常长时间运行的系统中使用它,最终可能会用尽端口,使得while循环永远运行,即使随着时间的推移可用端口变多。 - Drew Noakes

9
这里有一个答案,来自Snooganz的回答。它避免了在测试可用性和后绑定之间出现竞态条件的情况。
public static bool TryBindListenerOnFreePort(out HttpListener httpListener, out int port)
{
    // IANA suggested range for dynamic or private ports
    const int MinPort = 49215;
    const int MaxPort = 65535;

    for (port = MinPort; port < MaxPort; port++)
    {
        httpListener = new HttpListener();
        httpListener.Prefixes.Add($"http://localhost:{port}/");
        try
        {
            httpListener.Start();
            return true;
        }
        catch
        {
            // nothing to do here -- the listener disposes itself when Start throws
        }
    }

    port = 0;
    httpListener = null;
    return false;
}

在我的计算机上,这种方法平均需要15毫秒,对于我的使用情况来说是可以接受的。希望这能帮助其他人。

1
这太完美了,谢谢。这是唯一真正可靠的方法,因为在找到端口和使用它之间没有延迟,并且没有尝试手动维护正在使用的端口列表。我将使用它来使我的测试更加可靠。 - Ken Lyon
我刚刚想到一个小的改进。按顺序迭代端口更容易发生冲突,因为每个调用者都从相同的值开始。然而,每次在该范围内选择一个随机值可能会更加成功和一致。不过这只是一个小问题。我仍然喜欢它是原子性的。 :) - Ken Lyon
@KenLyon,随意选择端口的缺点是如果没有可用的端口,该方法将永远不会返回。相反,您可以跟踪已检查过的端口,但这将需要为16,320个可能的端口保留内存。每个端口使用一个位将占用2,040字节,这比我通常愿意在堆栈上保留的空间更多。 - Drew Noakes
没错,那将是最全面的解决方案。在我的情况下,问题本来就很罕见。它发生在我们构建过程中的单元测试中。我不想减慢测试速度,也不希望我们的测试服务器用尽端口。因此,我会尝试使用最多50个随机端口,然后在失败后停止。我希望这样可以在性能和可靠性之间取得平衡。 - Ken Lyon

2

由于您正在使用HttpListener(因此使用TCP连接),因此可以使用IPGlobalProperties对象的GetActiveTcpListeners方法获取活动TCP侦听器列表,并检查其Port属性。

可能的解决方案如下:

private static bool TryGetUnusedPort(int startingPort, ref int port)
{
    var listeners = IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners();

    for (var i = startingPort; i <= 65535; i++)
    {
        if (listeners.Any(x => x.Port == i)) continue;
        port = i;
        return true;
    }

    return false;
}

这段代码将从startingPort端口号开始查找第一个未使用的端口,并返回true。如果所有端口都已被占用(这是非常不可能的),则该方法返回false

还要注意可能发生的竞争条件,即在您之前其他进程占用了找到的端口。


有趣。在我的机器上,这个方法花费了大约130毫秒(第一次调用),这在某些情况下可能是不可接受的。这里仍然存在竞争条件,因此调用代码需要防范。此外,为什么不使用out而是使用ref - Drew Noakes
也许他不喜欢输出。无论如何,我会点赞这个帖子,因为它对于在启动Web服务器之前在.NET Core中识别可用端口非常有用。如果我已经运行了一个现有的服务器(例如:XScheduler的设置页面),Core将覆盖它,而这将允许我防止它发生。 - John Lord
那真是很久以前的事了...除非你想传递起始值并且摆脱第一个参数,否则绝对不需要使用 ref - dodbrian

1

很遗憾,这是不可能的。正如Richard Dingwall所建议的那样,您可以创建一个TCP监听器并使用该端口。这种方法有两个可能的问题:

  • 理论上可能会发生竞争条件,因为另一个TCP监听器可能在关闭后使用此端口。
  • 如果您没有以管理员身份运行,则需要分配前缀和端口组合以允许绑定到它。如果您没有管理员权限,则无法管理此操作。实质上,这将强制您使用管理员权限运行HTTP服务器(这通常是一个坏主意)。

我亲身经历过竞态条件。在我的情况下,我们有几个测试同时运行,都使用一个TcpListener,在创建HttpListener之前停止。偶尔会出现错误,因为端口已经被占用。顺便说一句,加1赞,因为你实际上回答了这个问题。 :) - Ken Lyon

1
我建议尝试 Grapevine。它允许您在应用程序中嵌入REST/HTTP服务器。它包括一个 RestCluster 类,可以让您在单个位置管理所有 RestServer 实例。
将每个实例设置为使用随机的开放端口号,例如:
using (var server = new RestServer())
{
    // Grab the next open port (starts at 1)
    server.Port = PortFinder.FindNextLocalOpenPort();

    // Grab the next open port after 2000 (inclusive)
    server.Port = PortFinder.FindNextLocalOpenPort(2000);

    // Grab the next open port between 2000 and 5000 (inclusive)
    server.Port = PortFinder.FindNextLocalOpenPort(200, 5000);
    ...
}

入门指南:https://sukona.github.io/Grapevine/en/getting-started.html


0

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