Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

UPNP

继承: RefCounted < Object

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

描述

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

要转发一个特定的端口(这里是 7777,注意 discover()add_port_mapping() 都有可能返回错误,代码中应该对这些错误进行检查):

var upnp = UPNP.new()
upnp.discover()
upnp.add_port_mapping(7777)

To close a specific port (e.g. after you have finished using it):

upnp.delete_port_mapping(port)

Note: UPnP discovery blocks the current thread. To perform discovery without blocking the main thread, use Threads like this:

# 当 UPnP 端口映射设置完成时发出该信号(无论成功还是失败)。
signal upnp_completed(error)

# 请将其替换为你自己的服务器端口号(范围在 1024 到 65535 之间)。
const SERVER_PORT = 3928
var thread = null

func _upnp_setup(server_port):
    # UPNP queries take some time.
    var upnp = UPNP.new()
    var err = upnp.discover()

    if err != UPNP.UPNP_RESULT_SUCCESS:
        push_error(str(err))
        upnp_completed.emit(err)
        return

    if upnp.get_gateway() and upnp.get_gateway().is_valid_gateway():
        upnp.add_port_mapping(server_port, server_port, ProjectSettings.get_setting("application/config/name"), "UDP")
        upnp.add_port_mapping(server_port, server_port, ProjectSettings.get_setting("application/config/name"), "TCP")
        upnp_completed.emit(UPNP.UPNP_RESULT_SUCCESS)

func _ready():
    thread = Thread.new()
    thread.start(_upnp_setup.bind(SERVER_PORT))

func _exit_tree():
    # 在此处等待线程执行完毕,以便在线程仍在运行时妥善处理游戏退出。
    thread.wait_to_finish()

术语说明: 在 UPnP 网络的语境中,“网关”(或称“互联网网关设备”,简称 IGD)指的是那些允许本地网络中的计算机访问互联网(即“广域网”,WAN)的网络设备。这些网关通常也被称为“路由器”。

常见陷阱:

正如上文所述,这些调用都是阻塞式的,因此不应该在主线程上运行,尤其是考虑到它们可能会一次性卡住好几秒钟。请务必使用多线程!

网络环境是物理层面的,且充满了各种不确定性。数据包在传输中可能会丢失或被过滤,IP 地址、空闲端口以及已分配的映射关系随时可能发生变化,设备也可能随时加入或离开网络。请务必考虑到这些因素,在检查和处理错误时要格外仔细;如果可能的话,请优雅地应对这些问题:提供清晰的错误提示界面(UI),并设置超时与重试机制。

端口映射随时可能发生变化(甚至被移除),同样地,网关的远程/外部 IP 地址也可能发生改变。你应该考虑定期重新查询外部 IP,并尝试更新/刷新端口映射(例如,每隔 5 分钟以及在发生网络故障时进行更新)。

并非所有的设备都支持 UPnP,而且有些用户会主动禁用 UPnP 功能。你需要对此做好应对准备(例如,在文档中说明并让用户手动转发端口,或者加入其他替代的 NAT 穿透方法,比如使用中继/镜像服务器、NAT 打洞、STUN/TURN 等)。

请考虑发生映射冲突时会发生什么。也许同一个网络下的多个用户想要同时玩你的游戏,或者也许有其他应用程序占用了同一个端口。请将端口设置为可配置的,并且最好能自动选择一个端口(在失败时尝试使用不同的端口进行重试)。

拓展阅读: 如果你想了解更多关于 UPnP(特别是互联网网关设备 IGD 和端口控制协议 PCP)的信息,维基百科 是一个不错的入门站点;相关规范可以在 开放连接基金会(Open Connectivity Foundation) 找到;而 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