你想做的事情是可能的,而且几乎是例行公事(至少对于 UDP)。这称为 NAT 打孔。它不适用于世界上的每个 NAT,但它适用于许多 NAT,包括大多数当前家用路由器的大多数设置。
基本思想是,对于大多数路由器,如果您发送一个传出数据包,它会将来自同一端点的所有传入数据包转发给您。否则,NAT 根本不起作用,对吧?因此,如果您和我都尝试同时与对方的公共地址通信,我们中的一个人将及时在他的 NAT 上“打一个洞”以接收消息。另一个人可能会错过最初的消息,因为他打洞太晚了,但他会收到另一个人发送的下一个回复,然后一切正常。
不过,这里有一些技巧。
首先,您需要知道您的公共地址。您可以直接看到的是您在 NAT 内的私人地址,因此您需要一个 NAT 外的服务器来告诉您您来自哪里。对此有一个标准,称为STUN。不仅有多种免费的实现你可以自己构建和运行,互联网上还有多个开放的 STUN 服务器;谷歌更新列表。
至少,你有一个私人地址和你的公共地址,你真的想把它们全部给另一个人,而不仅仅是一个。 (如果你只给我你的公共地址,而事实证明我们在同一个 NAT 上,我们可能无法相互通信。)如果你有多个 NIC,情况会变得更加复杂,或者你是在具有多层 NAT 等的公司 LAN 上。但是,如果您给我一大堆地址,我怎么知道该使用哪个?很简单,我可以尝试从我的每个本地地址连接到它们中的每一个,而第一个有效的是我一直使用的那个。我们显然需要为此制定某种协议,以便您可以告诉我其中一个何时起作用,但这可能非常简单。
另外,请注意,当我在这里说“地址”时,这不仅仅是指 IP 地址,而是指 IP 地址加上端口。毕竟,NAT 可以并且确实可以将端口与地址一起转换,并且您在 NAT 中打出的“洞”通常是特定于端口的。因此,您需要使用与实际通信相同的源端口和目标端口进行协商。 (实际上,这里有一些棘手的地方,在链接的文档中有解释,但现在让我们略读一下。)
当打孔不起作用并且您需要回退到通过服务器进行代理时,您不希望该代码看起来完全不同;你最终不得不把所有的东西都写两次,并且在调试方面遇到了很大的问题。幸运的是,有一个标准的解决方案,称为TURN,这使得对等方看起来就像他们实际上是在直接交谈,即使他们正在通过中继交谈。同样有免费的实现,但当然没有开放的 TURN 服务器供您使用(因为这会花费大量带宽)。
同时,一旦您知道了您的公共地址,您就必须告诉我它是什么,反之亦然。显然,我们需要一些边频道来交谈,比如特殊的“介绍人”或“大厅”服务器。通常,您会生成点对点连接作为某些服务器介导的连接的分支。例如,我们可能在一个 IRC 频道或 XMPP/Jabber 聊天网络中,并决定我们想要一个点对点通信(以避免窃听,或共享巨大的电子表格文件,或玩一个没有大量滞后)。
有一个名为ICE 的标准将这一切联系在一起;只要我们有一个共享的侧通道和一个 STUN(可能还有 TURN)服务器列表,ICE 就允许我们在可能的情况下协商点对点连接(如果没有,则回退到 TURN)。
ICE 支持是 SIP 和许多其他协议的一部分,因此,如果您使用其中一种协议的库,您可能只需要了解如何为其提供 STUN 和 TURN 服务器列表,这就是它。如果没有,可能有一些库可以做 ICE。如果没有,肯定有用于 STUN 和 TURN 的库(如果没有,它们都是非常简单的协议),因此您可以在它们之上自己进行协商。
当然,您不必使用这些标准中的任何一个,但您绝对应该阅读它们的作用以及它们这样做的原因。 (另外,请记住,人们现在专门制造路由器是为了让 ICE 更好地工作,而你发明的任何不同的东西都不是这样。)
我已经链接到上面的 Wikipedia 文章,因为它们从对技术如何工作及其背后的基本原理的非常简单的概述开始。对于详细的参考规范、入门示例代码、要使用的库等,有更好的资源,但通常 Wikipedia 提供了指向您需要的所有其他内容的链接。