HttpListener:请求的地址在此上下文中无效

HttpListener: The requested address is not valid in this context

使用

创建HttpListener对象时
var server = new HttpListener();
server.Prefixes.Add("http://*:8080/");
server.Start();

一切正常。但是,当我使用

var server = new HttpListener();
server.Prefixes.Add("http://demindiro.com:8080/");
server.Start();

它抛出 System.Net.Sockets.SocketException (0x80004005): The requested address is not valid in this context(下面是完整的堆栈跟踪)。

在谷歌搜索异常后,很明显它与使用的地址有关。在 Mono 源代码中挖掘 HttpListener and EndPointManager I determined the issue probably lies within this section of code (at GetEPListener 之后:

static EndPointListener GetEPListener (string host, int port, HttpListener listener, bool secure)
{
    IPAddress addr;
    if (host == "*")
        addr = IPAddress.Any;
    else if (IPAddress.TryParse(host, out addr) == false){
        try {
#pragma warning disable 618
            IPHostEntry iphost = Dns.GetHostByName(host);
#pragma warning restore 618
            if (iphost != null)
                addr = iphost.AddressList[0]; // <---
            else
                addr = IPAddress.Any;
        } catch {
            addr = IPAddress.Any;
        }
        // ...    
    }

我发现几乎在所有情况下都使用 ÌPAddress.Any,除非它可以将外部 IP 地址与给定的主机名相关联。

但是,我只能假设这是故意的,因为它写得相当明确,而且似乎 HttpListener 对其他开发人员来说工作得很好,所以在这一点上,我不知道出了什么问题这里。


堆栈跟踪:

Unhandled Exception:
System.Net.Sockets.SocketException (0x80004005): The requested address is not valid in this context
  at System.Net.Sockets.Socket.Bind (System.Net.EndPoint localEP) [0x00043] in <50d80b08c1a5449282b22aedf03ce925>:0 
  at System.Net.EndPointListener..ctor (System.Net.HttpListener listener, System.Net.IPAddress addr, System.Int32 port, System.Boolean secure) [0x00047] in <50d80b08c1a5449282b22aedf03ce925>:0 
  at System.Net.EndPointManager.GetEPListener (System.String host, System.Int32 port, System.Net.HttpListener listener, System.Boolean secure) [0x0009d] in <50d80b08c1a5449282b22aedf03ce925>:0 
  at System.Net.EndPointManager.AddPrefixInternal (System.String p, System.Net.HttpListener listener) [0x0005e] in <50d80b08c1a5449282b22aedf03ce925>:0 
  at System.Net.EndPointManager.AddListener (System.Net.HttpListener listener) [0x0009c] in <50d80b08c1a5449282b22aedf03ce925>:0 
  at System.Net.HttpListener.Start () [0x0000f] in <50d80b08c1a5449282b22aedf03ce925>:0 
  at Playground.Playground.StartHttpListener () [0x00016] in <d51cc6c047ee47c9a05c5e174876cbec>:0 
  at Playground.Playground.Main (System.String[] args) [0x00000] in <d51cc6c047ee47c9a05c5e174876cbec>:0 
[ERROR] FATAL UNHANDLED EXCEPTION: System.Net.Sockets.SocketException (0x80004005): The requested address is not valid in this context
  at System.Net.Sockets.Socket.Bind (System.Net.EndPoint localEP) [0x00043] in <50d80b08c1a5449282b22aedf03ce925>:0 
  at System.Net.EndPointListener..ctor (System.Net.HttpListener listener, System.Net.IPAddress addr, System.Int32 port, System.Boolean secure) [0x00047] in <50d80b08c1a5449282b22aedf03ce925>:0 
  at System.Net.EndPointManager.GetEPListener (System.String host, System.Int32 port, System.Net.HttpListener listener, System.Boolean secure) [0x0009d] in <50d80b08c1a5449282b22aedf03ce925>:0 
  at System.Net.EndPointManager.AddPrefixInternal (System.String p, System.Net.HttpListener listener) [0x0005e] in <50d80b08c1a5449282b22aedf03ce925>:0 
  at System.Net.EndPointManager.AddListener (System.Net.HttpListener listener) [0x0009c] in <50d80b08c1a5449282b22aedf03ce925>:0 
  at System.Net.HttpListener.Start () [0x0000f] in <50d80b08c1a5449282b22aedf03ce925>:0 
  at Playground.Playground.StartHttpListener () [0x00016] in <d51cc6c047ee47c9a05c5e174876cbec>:0 
  at Playground.Playground.Main (System.String[] args) [0x00000] in <d51cc6c047ee47c9a05c5e174876cbec>:0

PS:我明确想使用 demindiro.com,而不是 *

PS2:我知道有很多关于这个特殊异常的话题,但是 none 似乎关注 HttpListener.Start() 抛出这个异常。


更新 我又用谷歌搜索了我的问题,但这次我专门查找 TcpListener 并找到了这个答案:

The TcpListener can only be bound to a local IP Address of the computer that runs it. So the IP you're specifying isn't an IP of the local machine. Your public IP isn't the same IP as your local machine, especially if you're using some kind of NAT.

If I recall correctly, it's common to just do IPAddress.Any as your IP to initialise the listener.

所以如果我明白这一点,这意味着要绑定到我的外部 IP,我必须以某种方式从这里开始

Server <-- local IP --> Modem <-- external IP --> The Internet

至此

Server <-- external IP --> The Internet

这是正确的吗?如果是,如何在没有调制解调器的情况下将服务器连接到互联网?或者我还应该做什么?

您可以像下面这样可视化您的网络

local machine -> local interface 
              -> interface 1 (Private IP) <-> External IP
              -> interface 2 (Private IP2) <-> External IP

现在在大多数情况下,云中的 VM 将具有 1 个网络接口和一个本地接口。本地接口是环回接口,interface 1 将有一个 Private IP

现在,当您在代码中使用以下代码时

server.Prefixes.Add("http://*:8080/");

这意味着绑定到您机器上的所有接口。这意味着如果你这样做

curl http://localhost:8080

curl http://<privateIP>:8080

两者都可以。

现在,如果您将 example.com 映射到 externalIP 运行 下面的命令,它也会起作用

curl http://example.com:8080

这是因为您的 example.com 映射到 <extrenalIP>,这会将所有流量转发到 <privateIP>。现在,当您在 .代码

server.Prefixes.Add("http://example.com:8080/");

翻译成

server.Prefixes.Add("http://<externalIP>:8080/");

这不是上下文中的有效 IP,只有机器的 127.0.0.1<PrivateIP> 有效。所以如果你有多个接口那么只有你应该担心不使用 *。现在,如果您有多个接口和一个特定的接口来侦听您的域,那么您将在 /etc/hosts 文件中输入相同的条目,如下所示

<PrivateIP> example.com

然后你可以使用

server.Prefixes.Add("http://example.com:8080/");

现在,如果您担心有人可以使用不同的主机名指向您的机器,并且您不希望服务器在这种情况下做出响应。然后你将不得不检查你的代码中的 Host header 并拒绝基于相同的请求。或者你可以使用像 nginx 这样的网络服务器,只响应指向 example.com

的请求

考虑到 http://demindiro.com 似乎有效,这可能是多余的。然而...

这里有几点您需要注意:

通过主机名绑定是错误的

通常按主机名绑定是一个坏主意™。如果主机名解析为多个地址 - localhost 例如,当你启用了 IPv6,或者当你在同一台机器上有多个 IPv4 地址时,等等 - 你无法控制它绑定到哪些地址.在 localhost 的情况下,您可能会(有时令人不快地)惊讶地发现它绑定到 ::1 而不是 127.0.0.1.

这里的问题是 demindiro.com 是一个无法解析为计算机上存在的内部 IP 地址的名称,因此您无法直接绑定到它。准确地说,demindiro.com 是一个主机名,它(通过 DNS)解析为 IPv4 地址 109.132.169.132,这(几乎可以肯定)不是您的计算机拥有的 IP 地址。

绑定到主机名不会绑定到主机名

相反,它绑定到主机名在机器上解析到的第一个 IP 地址。不多也不少

假设您修改主机文件,使 demindiro.com 解析(仅在您的机器上)到有效的本地 IPv4 地址,例如 192.168.0.123。如果我在你的网络上并且连接到那个地址,我就会得到你的服务。我尝试访问的主机名无关紧要,只要它解析为该地址,我就可以建立连接。我可以通过 IP 或任何解析为该 IP 的主机名直接连接,而你永远不会知道。

只有在建立连接并解析请求后,您才能(有时)检测到哪个主机名是请求的目标。

对于 HTTP(S) 请求,主机名通常作为请求的一部分发送。这允许 Web 服务器在同一地址上托管多个站点。 Web 服务器接收请求,确定它应该在本地站点之间路由到哪里,然后将请求发送到已解析的站点。在 HTTPS 中做起来有点困难,但这个问题主要由 SNI.

解决

但是,如果我使用没有主机名的绝对路径向您的服务发出 HTTP 1.0 请求...那会怎样?

绑定只能是本地的

换句话说,您不能绑定到机器上不存在的地址 运行 您的代码。更重要的是,您不能直接绑定到互联网连接的 public 地址,除非它直接在您的机器上终止。由于我们中没有多少人 运行 拨号或桥接 PPPoE 到我们的桌面,您尝试绑定的地址可能不可用。

一般来说,您需要做的是在您的 Internet 连接上设置某种 NAT 重定向,以将端口 8080 上的传入请求转换为 public 地址 109.132.169.132 并将它们重定向到您的内部地址(192.168.0.123 或其他)。

假设 demindiro.com 解析为您的 public IP 地址。如果没有,那么您就会遇到各种其他问题。跨越 public 网络的 NAT 是……奇怪的。您在链中添加的每个 NAT 都会增加更多的复杂性、更多的故障点等。