UPNP

继承: RefCounted < Object

通用即插即用(UPnP)功能,用于网络设备的发现、查询及端口映射。

描述

这个类可用于在本地网络中发现兼容的 UPNPDevice 并在这些设备上执行命令,如管理端口映射(用于端口转发/NAT 穿透)和查询本地及远程网络 IP 地址。请注意,这个类的方法都是同步的,会阻塞调用线程。

要转发指定端口(此处为 7777,请注意 discoveradd_port_mapping 都可能返回错误,应进行检查):

  1. var upnp = UPNP.new()
  2. upnp.discover()
  3. upnp.add_port_mapping(7777)

要关闭指定端口(例如结束使用后):

  1. upnp.delete_port_mapping(port)

注意:UPnP 发现会阻塞当前线程。要在不阻塞主线程的前提下执行发现,请像这样使用 Thread

  1. # UPnP 端口映射建立完成时发出(无论成败)。
  2. signal upnp_completed(error)
  3. # 请将其替换为你自己的服务器端口号,在 1024 和 65535 之间。
  4. const SERVER_PORT = 3928
  5. var thread = null
  6. func _upnp_setup(server_port):
  7. # UPNP 查询比较耗时。
  8. var upnp = UPNP.new()
  9. var err = upnp.discover()
  10. if err != OK:
  11. push_error(str(err))
  12. emit_signal("upnp_completed", err)
  13. return
  14. if upnp.get_gateway() and upnp.get_gateway().is_valid_gateway():
  15. upnp.add_port_mapping(server_port, server_port, ProjectSettings.get_setting("application/config/name"), "UDP")
  16. upnp.add_port_mapping(server_port, server_port, ProjectSettings.get_setting("application/config/name"), "TCP")
  17. emit_signal("upnp_completed", OK)
  18. func _ready():
  19. thread = Thread.new()
  20. thread.start(_upnp_setup.bind(SERVER_PORT))
  21. func _exit_tree():
  22. # 游戏退出但线程还在运行时,在此处等待线程完成。
  23. thread.wait_to_finish()

术语:UPnP 网络中,“网关”(gateway,或称“互联网网关设备”,internet gateway device,简称 IGD)指的是在局域网中让计算机能够访问互联网(“广域网”,wide area network,WAN)的网络设备。这些网关经常也叫做“路由器”。

陷阱:

  • 前文解释过,这些调用都是阻塞的,不应该在主线程上执行,一次就能阻塞上很多秒。用用线程吧!

  • 网络是实打实的混乱。数据包可能会在传输过程中丢失或者被过滤掉,地址、空闲端口、端口映射有可能发生变化,设备可以随时离开或者加入网络。请考虑周全,老老实实地检查错误并进行处理,处理错误时请尽量友好:添加简洁的报错 UI、超时处理、重试机制。

  • 端口映射是随时会变的(也可以被删除),网关的远程/外部 IP 也可能发生改变。你应该考虑定期重新查询外部 IP、尝试更新/刷新端口映射(例如每隔 5 分钟或者在发生网络错误时执行)。

  • 并不是所有的设备都支持 UPnP,有些用户还会禁用 UPnP 支持。你需要处理这种情况(例如编写文档,要求用户手动进行端口映射,或者加入接力/镜像服务器、NAT 打洞、STUN/TURN 等 NAT 穿透的备用方案)。

  • 请考虑映射冲突时该怎么办。可能在同一个网络上同时有多个用户想要来玩你的游戏,或者有其他应用程序用了一样的端口。请把端口号做成可配置的,最好能够自动选择(失败时重试其他端口)。

拓展阅读:如果你想了解更多关于 UPnP(尤其是 Internet Gateway Device(IGD)和 Port Control Protocol(PCP)),可以首先查看维基百科,技术规范可以在 Open Connectivity 基金会找到,Godot 的实现基于的是 MiniUPnP 客户端

属性

bool

discover_ipv6

false

int

discover_local_port

0

String

discover_multicast_if

“”

方法

void

add_device(device: UPNPDevice)

int

add_port_mapping(port: int, port_internal: int = 0, desc: String = “”, proto: String = “UDP”, duration: int = 0) const

void

clear_devices()

int

delete_port_mapping(port: int, proto: String = “UDP”) const

int

discover(timeout: int = 2000, ttl: int = 2, device_filter: String = “InternetGatewayDevice”)

UPNPDevice

get_device(index: int) const

int

get_device_count() const

UPNPDevice

get_gateway() const

String

query_external_address() const

void

remove_device(index: int)

void

set_device(index: int, device: UPNPDevice)


枚举

enum UPNPResult: 🔗

UPNPResult UPNP_RESULT_SUCCESS = 0

UPNP 命令或发现成功。

UPNPResult UPNP_RESULT_NOT_AUTHORIZED = 1

未授权在 UPNPDevice 上使用该命令。当用户在其路由器上禁用 UPNP 时,可能会被返回。

UPNPResult UPNP_RESULT_PORT_MAPPING_NOT_FOUND = 2

在给定的 UPNPDevice 上没有找到给定端口、协议组合的端口映射。

UPNPResult UPNP_RESULT_INCONSISTENT_PARAMETERS = 3

参数不一致。

UPNPResult UPNP_RESULT_NO_SUCH_ENTRY_IN_ARRAY = 4

数组中没有此条目。如果在 UPNPDevice 上没有找到给定的端口、协议组合,可能会被返回。

UPNPResult UPNP_RESULT_ACTION_FAILED = 5

操作失败。

UPNPResult UPNP_RESULT_SRC_IP_WILDCARD_NOT_PERMITTED = 6

UPNPDevice 不允许源 IP 地址的通配符值。

UPNPResult UPNP_RESULT_EXT_PORT_WILDCARD_NOT_PERMITTED = 7

UPNPDevice 不允许外部端口的通配符值。

UPNPResult UPNP_RESULT_INT_PORT_WILDCARD_NOT_PERMITTED = 8

UPNPDevice 不允许内部端口的通配符值。

UPNPResult UPNP_RESULT_REMOTE_HOST_MUST_BE_WILDCARD = 9

远程主机值必须是通配符。

UPNPResult UPNP_RESULT_EXT_PORT_MUST_BE_WILDCARD = 10

外部端口值必须是通配符。

UPNPResult UPNP_RESULT_NO_PORT_MAPS_AVAILABLE = 11

没有可用的端口映射。如果端口映射功能不可用,也可能被返回。

UPNPResult UPNP_RESULT_CONFLICT_WITH_OTHER_MECHANISM = 12

与其他机制冲突。如果一个端口映射与现有的冲突,可能会被返回,而不是UPNP_RESULT_CONFLICT_WITH_OTHER_MAPPING

UPNPResult UPNP_RESULT_CONFLICT_WITH_OTHER_MAPPING = 13

与现有的端口映射相冲突。

UPNPResult UPNP_RESULT_SAME_PORT_VALUES_REQUIRED = 14

外部和内部端口值必须相同。

UPNPResult UPNP_RESULT_ONLY_PERMANENT_LEASE_SUPPORTED = 15

只支持永久租用。在添加端口映射时,不要使用 duration 参数。

UPNPResult UPNP_RESULT_INVALID_GATEWAY = 16

无效网关。

UPNPResult UPNP_RESULT_INVALID_PORT = 17

无效端口。

UPNPResult UPNP_RESULT_INVALID_PROTOCOL = 18

无效协议。

UPNPResult UPNP_RESULT_INVALID_DURATION = 19

无效持续时间。

UPNPResult UPNP_RESULT_INVALID_ARGS = 20

无效参数。

UPNPResult UPNP_RESULT_INVALID_RESPONSE = 21

无效响应。

UPNPResult UPNP_RESULT_INVALID_PARAM = 22

无效参数。

UPNPResult UPNP_RESULT_HTTP_ERROR = 23

HTTP 错误。

UPNPResult UPNP_RESULT_SOCKET_ERROR = 24

套接字错误。

UPNPResult UPNP_RESULT_MEM_ALLOC_ERROR = 25

分配内存时出错。

UPNPResult UPNP_RESULT_NO_GATEWAY = 26

没有可用的网关。你可能需要先调用 discover ,否则发现没有检测到任何有效的 IGD(InternetGatewayDevices)。

UPNPResult UPNP_RESULT_NO_DEVICES = 27

没有可用的设备。你可能需要先调用 discover,或者发现没有检测到任何有效的 UPNPDevice

UPNPResult UPNP_RESULT_UNKNOWN_ERROR = 28

未知错误。


属性说明

bool discover_ipv6 = false 🔗

  • void set_discover_ipv6(value: bool)

  • bool is_discover_ipv6()

如果为 true,则 IPv6 用于 UPNPDevice 发现。


int discover_local_port = 0 🔗

  • void set_discover_local_port(value: int)

  • int get_discover_local_port()

如果为 0,系统会自动选择用于发现的本地端口。如果为 1,将从源端口 1900 进行发现(与目的端口相同)。否则,将使用该值作为端口。


String discover_multicast_if = "" 🔗

  • void set_discover_multicast_if(value: String)

  • String get_discover_multicast_if()

用于发现的多播接口。如果为空,则使用默认的多播接口。


方法说明

void add_device(device: UPNPDevice) 🔗

将给定的 UPNPDevice 添加到已发现设备的列表中。


int add_port_mapping(port: int, port_internal: int = 0, desc: String = “”, proto: String = “UDP”, duration: int = 0) const 🔗

添加映射,针对给定的协议 proto"TCP""UDP",默认为 UDP),将默认网关(见 get_gateway)上的外部端口 port(在 1 到 65535 之间,不过推荐使用 1024 以上的端口)映射到本机上的内部端口 port_internal。如果该网关上已经存在给定的端口与协议的组合,这个方法会尝试进行覆盖。如果不希望如此,你可以使用 get_gateway 手动获取网关,获取到后调用其 add_port_mapping 方法。请注意,使用 UPnP 转发公认端口(1024 以下)在有些设备上可能会失败。

如果端口的映射已存在,有些网关设备可能会对其进行更新,有些则会因为冲突而拒绝这个命令,尤其当现有端口映射不是由 UPnP 创建的,或者指向的是别的网络地址(或设备)的时候。

如果 port_internal0(默认),表示内外部端口相同(使用 port 的值)。

描述(desc)会显示在一些路由器的管理界面上,可以用来识别添加映射的程序。

映射的租赁时长 duration 可以通过指定秒数来限定。默认的 0 表示没有时长,即永久租赁,有些设备只支持这种永久租赁。请注意,无论是否永久都只是一种请求,网关仍然可以随时移除映射(通常发生在重启网关后外部 IP 地址发生变化时,也有些型号会在映射不再活动,即若干分钟无流量时移除)。如果非 0(永久),技术规格所允许的范围是 120(2 分钟)到 86400 秒(24 小时)。

可能的返回值见 UPNPResult


void clear_devices() 🔗

清除已发现设备的列表。


int delete_port_mapping(port: int, proto: String = “UDP”) const 🔗

如果默认网关上存在对给定端口和协议组合的端口映射,则将其删除(见 get_gateway)。port 必须是 1 和 65535 之间的有效端口,proto 可以是 "TCP""UDP"。拒绝的原因可能是映射指向其他地址、端口为公认端口(1024 以下)、映射不是由 UPnP 添加的。可能的返回值见 UPNPResult


int discover(timeout: int = 2000, ttl: int = 2, device_filter: String = “InternetGatewayDevice”) 🔗

发现本地的 UPNPDevice。清除先前发现的设备的列表。

默认情况下会过滤 IGD(InternetGatewayDevice)类型的设备,因为这些设备管理端口转发。timeout 是等待响应的时间,单位为毫秒。ttl 是生存时间;请在你知道自己在做什么的时候才碰这个参数。

可能的返回值见 UPNPResult


UPNPDevice get_device(index: int) const 🔗

返回给定 index 处的 UPNPDevice


int get_device_count() const 🔗

返回已发现的 UPNPDevice 的数量。


UPNPDevice get_gateway() const 🔗

返回默认网关。这是第一个发现的UPNPDevice,也是一个有效的IGD(InternetGatewayDevice)。


String query_external_address() const 🔗

返回默认网关的外部 IP 地址字符串(见 get_gateway)。错误时返回空字符串。


void remove_device(index: int) 🔗

index 处的设备从已发现的设备列表中移除。


void set_device(index: int, device: UPNPDevice) 🔗

index 处的设备从已发现的设备列表中设置为 device