Steamworks 文献库
ISteamNetworkingSockets 接口
网络 API 类似于伯克利套接字,但用于游戏。

  • 它是一个面向连接的 API (如 TCP,而非 UDP)。 当发送或接收消息时,使用连接句柄识别对等点。
  • 但与 TCP 不同,此 API 面向消息,而非面向流。 (消息间的边界由 API 维护。)
  • 可靠消息和不可靠消息均受支持。
  • 大消息被分成多个数据包,小消息则合并成较少的数据包。
  • 可靠的 ACK/重新组合/重新传输策略。
  • 强大的加密与验证功能。 当玩家连网时,如果 SteamID 通过了身份验证,您就可以确定访问该用户帐户的人已获得授权进行连接。 先侵入 VAC 安全进程后,才能进行窃听或篡改。 需要先访问目标电脑,才能冒充他人。
  • 支持通过 Valve 网络的中继连接。 这样可以防止 IP 地址遭泄露,保护您的玩家和游戏服务器免受攻击。
  • 还支持使用 IPv4 或 IPv6 在普通 UDP 上进行标准连接。

相关文件:
  • Steam 网络 不同网络 API 概览。
  • ISteamNetworkingUtils 用于测量 ping 和估计对等点之间 ping 时间的实用工具。
  • steamnetworkingtypes] 杂项类型与实用工具。
  • Steam 数据报中继为通过 Valve 骨干网中继您游戏流量的服务。 这可以使 IP 地址不被暴露,且在很多情况下,会提升 ping 时间和连接质量。
  • ISteamNetworkingMessages 为 UDP 样式接口。 它不会先建立连接,而是会通过指明每个发送调用的目的对等端来发送和接收信息。 后台仍使用同样的连接,因此您仍会得到相同的性能,不过会自动为您创建连接并在超时后闲置,而您对连接的控制没有之前那么强,也不会收到关于其状态的反馈。

此 API 的开源版本可在 github 获得。 您可以随意使用。 要使用 Valve 网络,您需要成为 Steam 合作伙伴,并使用 Steamworks SDK 中的版本。

ISteamNetworkingSockets 的成员函数通过全局访问器函数 SteamNetworkingSockets() 调用。

管理连接

CreateListenSocketIP

HSteamListenSocket CreateListenSocketIP( const SteamNetworkingIPAddr &localAddress, int nOptions, const SteamNetworkingConfigValue_t *pOptions );

通过普通 UDP(IPv4 或 IPv6)调用 ConnectByIPAddress,创建"服务器"套接字,以侦听要连接的客户端。

您必须选择一个要侦听的特定本地端口,并将其设置为本地地址的端口字段。

通常您会将地址的 IP 部分设置为零,(SteamNetworkingIPAddr::Clear())。 这意味着您将不会绑定至任何特定的本地接口(即和简单套接字代码中的 INADDR_ANY 一样的接口)。 此外,如果可能,套接字将绑定在“双协议栈”模式,这意味着它可以同时接受 IPv4 和 IPv6 客户端连接。 如果您非常希望绑定特定接口,则可将本地地址设置为适当的 IPv4 或 IPv6 IP。

如果您需要设置任何初始配置选项,请在此传入。 若要进一步了解为什么这么做要好于在创建后“立即”设置选项,请参见 SteamNetworkingConfigValue_t

当客户端尝试连接时,将会发布 SteamNetConnectionStatusChangedCallback_t。 连接将处于 k_ESteamNetworkingConnectionState_Connecting 状态。

ConnectByIPAddress

HSteamNetConnection ConnectByIPAddress( const SteamNetworkingIPAddr &address, int nOptions, const SteamNetworkingConfigValue_t *pOptions );

创建连接并通过 UDP 在给定 IPv4 或 IPv6 地址与“服务器”交谈。 远程主机必须在指定端口上对 ISteamnetworkingSockets::CreateListenSocketIP 进行相应的调用以进行侦听。

在我们开始连接时将会触发一个 SteamNetConnectionStatusChangedCallback_t 回调,在超时或连接成功时会触发另一个。

如果服务器没有配置任何身份,那么其网络地址将会是唯一使用的身份。 或者,网络主机可能会提供一个平台专属的身份,可能会有能验证身份的有效证书,也可能没有。 ( 这些细节将包含在 SteamNetConnectionStatusChangedCallback_t 中。) 由您的应用程序决定是否允许该连接。

如果您需要设置任何初始配置选项,请在此传入。 若要进一步了解为什么这么做要好于在创建后“立即”设置选项,请参见 SteamNetworkingConfigValue_t

CreateListenSocketP2P

HSteamListenSocket CreateListenSocketP2P( int nLocalVirtualPort, int nOptions, const SteamNetworkingConfigValue_t *pOptions );

CreateListenSocketIP 相似,但客户端会使用 ConnectP2P 进行连接。 连接将通过 Valve 网络中继。

nLocalVirtualPort 会指明客户端将如何通过 ConnectP2P 连接至此套接字。 应用程序仅使用一个侦听套接字是很常见的;在这种情况下,使用“0”。 如果您需要开启多个侦听套接字,并让客户端能够连接至其中一个,那么 nLocalVirtualPort 应当是一个较小的整数(<1000),且您创建的每个侦听套接字都有独一无二的一个。

如果您正在已知数据中心的专用服务器上侦听,就可以使用此函数而不是 CreateHostedDedicatedServerListenSocket进行侦听,这样客户端无需票证就可连接。 任何拥有该应用程序并登录 Steam 的用户都可以尝试连接到您的服务器。 此外,连接尝试可能需要客户端连接到 Steam,这是另一个可能会失败的组成部分。 使用票证时,一旦获得票证,即使客户端与 Steam 断开连接或 Steam 离线,也可以连接到您的服务器。

如果您使用此方法,最好在应用初始化时调用 ISteamNetworkingUtils::InitRelayNetworkAccess

如果您需要设置任何初始配置选项,请在此传入。 若要进一步了解为什么这么做要好于在创建后“立即”设置选项,请参见 SteamNetworkingConfigValue_t

ConnectP2P

HSteamNetConnection ConnectP2P( const SteamNetworkingIdentity &identityRemote, int nRemoteVirtualPort, int nOptions, const SteamNetworkingConfigValue_t *pOptions );
开始连接通过平台专属识别符而识别的对等端。 这将会使用默认的对接服务,取决于平台和库配置。 (如,在 Steam 上,将会通过 Steam 后端。)流量会通过 Steam 数据报中继网络中继。

如果您使用此方法,最好在应用初始化时调用 ISteamNetworkingUtils::InitRelayNetworkAccess

如果您需要设置任何初始配置选项,请在此传入。 若要进一步了解为什么这么做要好于在创建后“立即”设置选项,请参见 SteamNetworkingConfigValue_t

AcceptConnection

EResult AcceptConnection( HSteamNetConnection hConn );

接收在侦听套接字上收到的传入连接。

当收到一个连接尝试时(或许是在交换了几个基本的握手数据包以阻止无关紧要的仿冒之后),将会在 k_ESteamNetworkingConnectionState_Connecting 状态中创建一个连接接口对象,并发布一个 SteamNetConnectionStatusChangedCallback_t 。 此时,您的应用程序必须接受或关闭该连接。 (不能忽略它。)依连接类型而定,接受连接会将其转变为已连接状态,或寻找路由状态。

您应该在 1-2 秒之内就采取行动,因为接受连接就会发送回复,通知客户端已连接。 如果您延迟行动,在客户端看来就和网络无响应一样,可能会将此连接尝试视为超时。 换句话说,客户端无法区分网络问题带来的延迟和应用程序带来的延迟。

这意味着如果您的应用程序在几秒过后还没有处理回调(如,在载入地图时),可能会有客户端在此期间尝试连接,并因超时而失败。

如果应用程序没有及时对连接做出响应,且我们停止接收来自客户端的通信,那么该连接尝试会
在本地超时,将该连接转为 k_ESteamNetworkingConnectionState_ProblemDetectedLocally 状态。 客户端也可能在连接被接受前将其关闭,视具体的事件序列而定,也可能会转变为 k_ESteamNetworkingConnectionState_ClosedByPeer。

返回:
  • k_EResultInvalidParam——如果句柄无效。
  • k_EResultInvalidState——如果连接未处于正确的状态。 (请记住连接状态可能会在通知发布至队列和被应用程序接收之间的这段时间内改变。)

关于连接配置选项的提醒: 如果您需要设置对通过特定侦听套接字接受的所有连接通用的任何配置选项,请考虑在侦听套接字上设置这些选项,因为此类选项是自动继承的。 如果您确实需要设置连接专属的选项,则在接受连接之前在连接上进行设置是安全的做法。

CloseConnection

bool CloseConnection( HSteamNetConnection hPeer, int nReason, const char *pszDebug, bool bEnableLinger );

与远程主机断开连接并使连接句柄无效。 连接上所有未读的数据都将被丢弃。

nReason 是应用程序定义的代码,它将在另一端接收,并(当可能时)在后端分析中记录。 该值应在限制范围内。 (请参阅 ESteamNetConnectionEnd 。)如果您不需要将任何信息传达给远程主机,也不希望分析能够区分“正常”连接终止和“非正常”的连接终止,则可以传入零,在这种情况下,将使用 k_ESteamNetConnectionEnd_App_Generic 的通用值。

pszDebug 是可选的人类可读的诊断字符串,它将由远程主机接收并(在可能时)在后端分析中记录。

您可以选择让套接字进入“徘徊”状态。这将会使连接尝试在实际关闭连接之前传递所有剩余的出站可靠消息。 否则,将丢弃所有未发送的可靠数据。 无论如何,一旦关闭连接,您将无法进一步查看连接状态或任何消息。 (在某些使用情况下,这是可以接受的。 如果不能接受,则需要等待关闭连接。)请记住,这是一个应用程序协议,而不是像 TCP 这样的内核协议。 因此,您需要确保应用程序保持运行足够长的时间,以便刷新数据。

如果连接已经结束(k_ESteamNetworkingConnectionState_ClosedByPeer 或 k_ESteamNetworkingConnectionState_ProblemDetectedLocally)并且您只是要释放连接对象,则将忽略 nReason,pszDebug 和 bEnableLinger。

CloseListenSocket

bool CloseListenSocket( HSteamListenSocket hSocket );

销毁一个侦听套接字。 侦听套接字上接受的所有连接都强行关闭。

CreateSocketPair

bool CreateSocketPair( HSteamNetConnection *pOutConnection1, HSteamNetConnection *pOutConnection2, bool bUseNetworkLoopback, const SteamNetworkingIdentity *pIdentity1, const SteamNetworkingIdentity *pIdentity2 );

创建一对彼此对话的连接,例如环回连接。 这对于测试非常有用,或者即使您正在运行本地“服务器”,您的客户端/服务器代码也可以同样地工作。

这两个连接将立即置于已连接状态,并且不会立即发布任何回调。 此后,如果您关闭任何一个连接,另一个连接都将收到回调,就像它们通过网络进行通信一样。 您必须同时关闭*两端*,才能完全清理资源!

默认情况下,会使用内部缓冲区完全绕过网络,将消息切成数据包、加密、复制有效负载等。 这意味着默认情况下,环回数据包将不会模拟延迟或丢失。 为 bUseNetworkLoopback 传入 true 会使套接字通过临时端口上的本地网络环回设备(127.0.0.1)发送数据包。 在这种情况下,支持伪延迟和丢失,并且将 CPU 时间拓延用以加密和解密。

如果您希望为任一连接分配特定的身份,则可以传入特定的身份。 否则,如果传入 nullptr,则相应的连接将采用通用的“本地主机”身份。 如果您使用真实的网络环回,则可能会转换为实际绑定的环回端口。 否则,端口为零。

发送和接收消息

SendMessageToConnection

EResult SendMessageToConnection( HSteamNetConnection hConn, const void *pData, uint32 cbData, int nSendFlags, int64 *pOutMessageNumber );
通过指定的连接向远程主机发送消息。

nSendFlags 会决定将提供的交付保证,以及应在何时缓冲数据等。 如:
  • k_nSteamNetworkingSend_Unreliable
  • k_nSteamNetworkingSend_Reliable
  • k_nSteamNetworkingSend_NoNagle
  • k_nSteamNetworkingSend_NoDelay

请注意,我们用于消息的语义与标准的“流式传输”套接字的语义并不完全相同。 (SOCK_STREAM)对于普通流式传输套接字,不会认为每个区块之间的边界是相关的,并且写入的数据块的大小不一定会与另一端的读取返回的区块的大小相匹配。 远程主机可能读取了部分区块,或者区块可能进行了合并。 但是,对于此处使用的消息语义,大小将匹配。 每个发送调用将与远程主机上成功读取的调用一对一匹配。 如果要将现有的面向流式传输的代码移植到可靠消息的语义上,则代码应以相同的方式工作,因为可靠消息的语义比流式传输语义更严格。 唯一的警告与性能有关:为保留消息大小每条消息会有消耗,因此,如果您的代码发送了许多小数据区块,性能就会受到影响。 任何基于流式传输套接字且不会写入过多小区块的代码都可以正常工作,无需进行任何更改。

如果发送成功,则 pOutMessageNumber 会是一个可选的指针,用于接收分配给该消息的消息号。

返回:
  • k_EResultInvalidParam:无效的连接句柄,或者单个消息太大。 (请参阅 k_cbMaxSteamNetworkingSocketsMessageSizeSend)
  • k_EResultInvalidState:连接处于无效状态
  • k_EResultNoConnection:连接已结束
  • k_EResultIgnored:您使用了 k_nSteamNetworkingSend_NoDelay,并且该消息已被丢弃,因为我们尚未准备好发送它。
  • k_EResultLimitExceeded:已在队列中的的数据太多,无法发送。 (请参阅 k_ESteamNetworkingConfig_SendBufferSize)

SendMessages

void SendMessages( int nMessages, SteamNetworkingMessage_t *const *pMessages, int64 *pOutMessageNumberOrResult );
发送一个或多个消息而不复制消息负载。 这是发送消息的最有效方法。 要使用此功能,必须首先使用 ISteamNetworkingUtils::AllocateMessage 分配消息对象。 (不要在堆栈上声明或分配自己的对象。)

您应该填写消息有效负载。 您可以让它为您分配缓冲区,然后填写有效负载,或者如果您已经分配了缓冲区,则只需将 m_pData 指向您的缓冲区并将回调函数设置为适当的函数,即可释放它。 请注意,如果您使用自己的缓冲区,则该缓冲区必须保持有效,直到执行回调为止。 还要注意,您的回调可以随时从任何线程调用(甚至可能在 SendMessages 返回之前!),因此它必须快速且保证线程安全。

您还必须填写:
  • m_conn - 将消息发送到的连接的句柄
  • m_nFlags - k_nSteamNetworkingSend_xxx 标志的位掩码。

所有其他字段当前均已保留,不应修改。

该库将拥有消息结构的所有权。 它们可能随时被修改或变得无效,因此在将它们传入该函数后,您不得读取它们。

pOutMessageNumberOrResult 是一个可选数组,如果发送成功,它将收到分配给每个消息的消息号。 如果发送失败,则将 EResult 负值放入数组。 例如,如果连接处于无效状态,数组将保留 -k_EResultInvalidState。 请参见 SendMessageToConnection,查看可能出现的故障的代码。

FlushMessagesOnConnection

EResult FlushMessagesOnConnection( HSteamNetConnection hConn );
刷新在 Nagle 计时器上等待的所有消息,并在下一次传输时机到来时将其发送(通常意味着现在)。

如果启用了 Nagle(默认情况下处于启用状态),则在调用 SendMessageToConnection 时,消息将被缓冲(直到被发送前的 Nagle 时间),以将小消息合并到同一数据包中。 (See k_ESteamNetworkingConfig_NagleTime)

返回:
  • k_EResultInvalidParam: invalid connection handle
  • k_EResultInvalidState:连接处于无效状态
  • k_EResultNoConnection:连接已结束
  • k_EResultIgnored:我们(尚未)连接,因此此操作无效。

ReceiveMessagesOnConnection

int ReceiveMessagesOnConnection( HSteamNetConnection hConn, SteamNetworkingMessage_t **ppOutMessages, int nMaxMessages );
如果有,从连接中获取下一条可用消息。 返回返回到数组的消息数,最多等于 nMaxMessages。
如果连接句柄无效,则返回 -1。 如果没有可用数据,则返回 0。

数组中返回的消息顺序是有用的。 可靠的消息将按照其发送顺序进行接收(并且大小相同——有关与流式传输套接字的细微差别,请参阅 SendMessageToConnection)。

不可靠的消息可能会被丢弃,或者乱序(相对于彼此或相对于可靠消息)传递。

如果返回了任何消息,则必须调用 SteamNetworkingMessage_t::Release() 来使每个消息在完成后释放资源。 可以让对象存活一段时间(将其放入某个队列等),并且您可以从任何线程调用 Release()。

有效轮询多个连接


轮询组是可以有效轮询的一组连接。 (在此 API 中,“轮询”连接意味着检索所有暂挂的消息。 实际上,我们没有像 BSD 套接字那样可以“轮询”连接状态的 API。)

CreatePollGroup

HSteamNetPollGroup CreatePollGroup()
创建一个新的轮询组。

使用 DestroyPollGroup 完成操作后,应该销毁轮询组。

DestroyPollGroup

bool DestroyPollGroup( HSteamNetPollGroup hPollGroup )
销毁使用 CreatePollGroup 创建的轮询组。

如果轮询组中有任何连接,它们会被从组中移除,并进入不属于任何轮询组的状态。 如果传入了无效的轮询组句柄,则返回 false。

SetConnectionPollGroup

bool SetConnectionPollGroup( HSteamNetConnection hConn, HSteamNetPollGroup hPollGroup );

将连接分配给轮询组。 请注意,一个连接仅能属于一个轮询组。 将连接添加到某个轮询组会将其从所在的任何其他轮询组中隐式移除。

您可以传入 k_HSteamNetPollGroup_Invalid,将连接从其当前轮询组中移除,同时不会将其添加到新的轮询组中。

如果连接上有当前暂挂的已接收消息,则会尝试将它们添加到轮询组的消息队列中,其顺序大致与如果连接在收到消息时已成为轮询组的一部分时所应用的顺序相同。

如果连接句柄无效,或者轮询组句柄无效(而不是 k_HSteamNetPollGroup_Invalid),则返回 false。

ReceiveMessagesOnPollGroup

int ReceiveMessagesOnPollGroup( HSteamNetPollGroup hPollGroup, SteamNetworkingMessage_t **ppOutMessages, int nMaxMessages );

ReceiveMessagesOnConnection 相同,但是将返回轮询组中任何连接上可用的下一条消息。 检查 SteamNetworkingMessage_t::m_conn 以了解是哪个连接。 (SteamNetworkingMessage_t::m_nConnUserData 也可能有用。)

消息在不同连接之间的传递顺序通常与完成消息的最后一个数据包的顺序相匹配。 但并不能保证一直如此,尤其是对于正好在连接被分配至一个轮询组时接收到的数据包而言。

消息在相同连接上的传递顺序已得到很好的定义,并且有着与 ReceiveMessagesOnConnection 中所提及的相同的保证。 (但是消息未按连接分组,因此它们不一定会连续出现在列表中;它们可能与其他连接的消息交错出现。)

连接信息

GetConnectionInfo

bool GetConnectionInfo( HSteamNetConnection hConn, SteamNetConnectionInfo_t *pInfo );
返回有关连接的高级状态的基本信息。
如果连接句柄无效,则返回 false。

GetConnectionRealTimeStatus

EResult GetConnectionRealTimeStatus( HSteamNetConnection hConn, SteamNetConnectionRealTimeStatus_t *pStatus, int nLanes, SteamNetConnectionRealTimeLaneStatus_t *pLanes );

返回有关实时连接状态和每个通道的队列状态的一小组信息。

如果不需要该信息,pStatus 可为 NULL。 (例如,您只对通道信息感兴趣。)

在输入时,nLanes 会指定 pLanes 数组的长度。 如果您不希望接收任何通道数据,则该值可为 0。 该值可以小于配置的通道总数。

pLanes 指向将接收某通道专用的信息的数组。 如果不需要,可为 NULL。

返回值:
  • k_EResultNoConnection - 连接句柄无效,或连接已关闭。
  • k_EResultInvalidParam - nLanes 状况不佳。

GetDetailedConnectionStatus

int GetDetailedConnectionStatus( HSteamNetConnection hConn, char *pszBuf, int cbBuf );
以诊断文本格式返回非常详细的连接统计数据。 对于转储到日志等很有用。 此信息的格式可能会更改。

返回:
  • -1:失败(连接句柄无效)
  • 0:好的,您的缓冲区已填满并已“\0”-终止
  • 0:您的缓冲区为 nullptr,或者因其太小而导致文本被截断。 请使用至少 N 个字节的缓冲区再试一次。

SetConnectionUserData

bool SetConnectionUserData( HSteamNetConnection hPeer, int64 nUserData );

设置连接用户数据。 数据在以下位置使用:

您要在连接创建时将该参数设为原子参数吗? 请参阅 k_ESteamNetworkingConfig_ConnectionUserData。

警告:使用回调结构中提供的值时要*非常小心*。 回调已位于队列中,您将在回调中收到的值是回调在队列中时有效的用户数据。 如果您不明白这点,
就可能会出现微妙的竞争条件!

如果此连接的任何传入消息已在队列中,用户数据字段就会更新,以便当您接收消息时(例如使用 ReceiveMessagesOnConnection),它们将始终有最新的用户数据。 因此,回调可能发生的棘手竞争条件不会在检索消息时出现。

如果句柄无效,则返回 false。

GetConnectionUserData

int64 GetConnectionUserData( HSteamNetConnection hPeer );
获取连接用户数据。 如果句柄无效或尚未在连接上设置任何用户数据,则返回 -1。

SetConnectionName

void SetConnectionName( HSteamNetConnection hPeer, const char *pszName );
设置连接的名称,主要用于调试

GetConnectionName

bool GetConnectionName( HSteamNetConnection hPeer, char *pszName, int nMaxLen );
将连接名称提取到缓冲区中,至少为 nMaxLen 个字节。 如果句柄无效,则返回 false。

ConfigureConnectionLanes

EResult ConfigureConnectionLanes( HSteamNetConnection hConn, int nNumLanes, const int *pLanePriorities, const uint16 *pLaneWeights );

在连接上配置多个出站消息流(“通道”),并控制它们之间的队头阻塞。 给定通道内的消息始终按照其队列顺序发送,但来自不同通道的消息可能会乱序发送。 每个通道都有其自己的消息编号序列。 每个通道上发送的第一条消息将被分配编号 1。

每个通道都有一个“优先级”。 仅当所有较高优先级通道都为空时,才会处理优先级低的通道。 此处仅考虑优先级值的顺序,而非其大小。 较高的数值优先于较低的数值。

每个通道还配有一个权重,该权重控制该通道相对于同一优先级的其他通道消耗的带宽的大致比例。 (这是假设通道保持繁忙的情况。 空闲通道不会累积“积分”以在消息排队后使用。)该值仅是相对于具有同一优先级的其他通道的比例。 对于具有不同优先级的通道,将以严格的优先级顺序为准,它们彼此之间的权重也互不相关。 也就是说,如果某通道具有唯一的优先级值,则该通道的权重值为何是无所谓的。

示例:3 个通道,优先级分别为 { 0, 10, 10 },权重为 { (NA), 20, 5 }。 第一个通道上发送的消息将始终先发送,其他两个通道中的消息将在之后发送。 因为没有其他优先级为 0 的通道,因此第一个通道的权重值是无关紧要的。 第二个和第三个通道将共享带宽,使用带宽的比例大约为 4:1。 (权重 { NA, 4, 1 } 是等效的。)

注意:
  • 在撰写本文时,某些代码的性能成本与通道数呈线性关系,因此请将通道数保持在绝对最小值。 3 个左右就可以了; >8 就已经很多了。 Steam 上的最大通道数为 255,这是一个非常大的数字,不推荐使用!
  • 通道优先级值可以是任意整数。 它们的绝对值并不重要,只有顺序才重要。
  • 权重必须为正,并且由于实现细节的原因,仅限 16 位值。 绝对大小并不重要,重要的是比例。
  • 在非 0 的通道索引上发送消息会对线路造成少量开销,因此为了获得最高的线路效率,不论通道 0 的优先级或权重为何,都应是“最常用”的通道。
  • 默认情况下,一个连接只有一个通道。 使用 nNumLanes=1 调用此函数虽然可以,但毫无意义,因为在这种情况下优先级和权重值都不重要。
  • 您可以随时重新配置连接通道,但不可减少通道数量。
  • 重新配置通道可能会使带宽共享平衡重新启动。 通常,您将在连接快要开始时(也许是在交换了几条消息之后)调用此函数一次。
  • 若要为所有通道分配相同的优先级,您可以使用 pLanePriorities=NULL。
  • 如果您希望具有相同优先级的所有通道平等地共享带宽(或者如果没有两个通道具有相同的优先级值,因此优先级值不重要),可以使用 pLaneWeights=NULL。
  • 优先级和权重会决定消息在网络上发送的顺序。 但不保证消息的接收顺序也是如此! 由于数据包丢失、交付失序以及数据包序列化的微妙细节,消息的接受可能会稍有失序! *唯一*能够提供有力保证的是,*同一通道*上*可靠的*消息将按照发送的顺序交付。
  • 每台主机都为自己发送的数据包配置通道;一个方向的消息通道与反方向的通道完全无关。

返回值:
  • k_EResultNoConnection - hConn 错误
  • k_EResultInvalidParam - 通道数无效、权重错误或您尝试减少通道数量
  • k_eResultinValidState - 连接已失效等

另见:
  • SteamNetworkingMessage_t::m_idxLane

杂项

GetListenSocketAddress

bool GetListenSocketAddress( HSteamListenSocket hSocket, SteamNetworkingIPAddr *address );

返回使用 CreateListenSocketIP 创建的侦听套接字所绑定到的本地 IP 和端口。

如果将套接字绑定到特定接口,则可能会返回特定的 IPv6 / IPv4 地址。
IPv6地址 :::0 表示“任何 IPv4 或 IPv6”
IPv6地址 ::: ffff:0000:0000 表示“任何 IPv4”

注意:这不是您查找客户端可以连接到的公用 IP 的方式。
请尝试 ISteamGameServer::GetPublicIP

如果句柄无效,或者该信息对于该类型的侦听套接字不可用,则返回 false。

GetIdentity

bool GetIdentity( SteamNetworkingIdentity *pIdentity );
获取分配给此接口的身份。

例如,在 Steam 上,这是用户的 SteamID,而对于游戏服务器接口而言,这是分配给游戏服务器的 SteamID。 如果我们尚不知道自己的身份,则返回 false 并将结果设置为无效的身份。 (例如 GameServer 尚未登录。 在 Steam上,即使用户未登录 Steam,也会知道其 SteamID。)

InitAuthentication

ESteamNetworkingAvailability InitAuthentication();

表明我们准备好参与经过验证的通信的意愿。 如果我们目前还没有准备好,那么要采取步骤,以获取必要的证书。 (这包括给我们的证书,以及对等端身份验证所需的所有 CA 证书。)

如果您知道自己将要建立经过验证的连接,则可以在程序初始化时进行调用,这样在尝试进行这些连接时我们将立即准备就绪。 (请注意,除使用 k_ESteamNetworkingConfig_IP_AllowWithoutAuth 禁用身份验证的普通 UDP 连接外,基本上所有连接都需要身份验证。)如果您不调用此函数,我们将等到需要利用这些资源的功能触发。

如果发生故障,您也可以调用此函数强制重试。 若尝试失败,我们将不会自动重试。 在这方面,尝试并失败之后的系统行为与第一次尝试之前的行为相同:尝试通过验证的通信或调用此函数将调用系统以尝试获取必要的资源。

您可以使用 GetAuthenticationStatus 或侦听 SteamNetAuthenticationStatus_t 来监视状态。

返回将从 GetAuthenticationStatus 返回的当前值。

GetAuthenticationStatus

ESteamNetworkingAvailability GetAuthenticationStatus( SteamNetAuthenticationStatus_t *pDetails );

询问我们是否准备好参加经过身份验证的通信。 每当此状态更改时,都会发布 SteamNetAuthenticationStatus_t 回调,但是您可以随时使用此函数进行查询。

返回 SteamNetAuthenticationStatus_t::m_eAvail 的值。 如果只需要此高级状态,您可以为 pDetails 传入 NULL。 若需要更多详细信息,请传入非 NULL 来接收它们。

使用 Steam 数据报中继的游戏服务器

当您的游戏服务器位于 Valve 已知的数据中心且游戏流量正在利用 Steam 数据报中继网络时,将使用以下函数。 若要获取更多信息,请参见 Steam 数据报中继

ReceivedRelayAuthTicket

bool ReceivedRelayAuthTicket( const void *pvTicket, int cbTicket, SteamDatagramRelayAuthTicket *pOutParsedTicket );

当您从后端/匹配系统收到票证时,请调用此函数。 将票证放入永久性缓存中,并可选择返回已解析的票证。

请参见 stamdatagram_gamecoordinator.h,获取更多详细信息。

FindRelayAuthTicketForServer

int FindRelayAuthTicketForServer( const SteamNetworkingIdentity &identityGameServer, int nRemoteVirtualPort, SteamDatagramRelayAuthTicket *pOutParsedTicket );

搜索缓存以查找要与指定虚拟端口上的服务器对话的票证。 若找到,则返回票证到期之前的秒数,也可以返回完整的破解票证。 若没有票证,则返回 0。

通常,在调用 ConnectToHostedDedicatedServer 连接到服务器之前,这仅用于确认您拥有票证。

ConnectToHostedDedicatedServer

HSteamNetConnection ConnectToHostedDedicatedServer( const SteamNetworkingIdentity &identityTarget, int nRemoteVirtualPort, int nOptions, const SteamNetworkingConfigValue_t *pOptions );

客户端调用,以在指定的虚拟端口上连接到 Valve 数据中心中托管的服务器。 您必须已将此服务器的票证放入缓存中,否则此连接尝试将失败! 如果您并非自己签发自己的票证,则要在自动票证模式下通过 SDR 连接到专用服务器,请使用 ConnectP2P。 (服务器必须配置为通过使用 CreateListenSocketP2P 侦听来允许这种类型的连接。)

您可能想知道为什么票证要存储在缓存中,而不是简单地作为参数传入。 原因是即使客户端计算机失去与 Steam 或中央后端的连接,或者应用重新启动或崩溃等,也都可以使与游戏服务器的重新连接非常可靠。

如果您使用此方法,最好在应用初始化时调用 ISteamNetworkingUtils::InitRelayNetworkAccess

如果您需要设置任何初始配置选项,请在此传入。 若要进一步了解为什么这么做要好于在创建后“立即”设置选项,请参见 SteamNetworkingConfigValue_t

GetHostedDedicatedServerPort

uint16 GetHostedDedicatedServerPort();
返回 SDR_LISTEN_PORT 环境变量的值。 这是您的服务器将侦听的 UDP 服务器。 将在生产环境中自动为您配置。

在开发过程中,您需要自行设置。 请参见 Steam 数据报中继,了解有关如何配置开发环境的详细信息。

GetHostedDedicatedServerPOPID

SteamNetworkingPOPID GetHostedDedicatedServerPOPID();
如果未设置 SDR_LISTEN_PORT,则返回 0。 否则,返回服务器所在的数据中心。 在非生产环境中,它将为 k_SteamDatagramPOPID_dev。

GetHostedDedicatedServerAddress

EResult GetHostedDedicatedServerAddress( SteamDatagramHostedAddress *pRouting );
返回有关托管服务器的信息。 其中包含服务器的 PoPID,以及中继点可以用来将流量发送到您服务器的不透明路由信息。

您将需要将此信息发送到后端,并将其放入票证中,以便中继站知道如何将流量从客户端转发到您的服务器。 请参见SteamDatagramRelayAuthTicket,以获取更多信息。

另外,请注意,路由信息包含在 SteamDatagramGameCoordinatorServerLogin 中,因此如果可能的话,最好使用 SteamNetworkingSockets::GetGameCoordinatorServerLogin 将此信息发送到您的游戏协调器服务,并同时安全登录。

成功退出时,返回 k_EResultOK

退出失败:
  • 返回非 k_EResultOK 的内容。
  • k_EResultInvalidState:我们未对侦听 SDR 进行配置(未设置 SDR_LISTEN_SOCKET)。
  • k_EResultPending:我们(目前)没有所需的身份验证信息。 (请参阅 GetAuthenticationStatus。)如果您使用环境变量来预取网络配置,则此数据应始终立即可用。
  • 将在 m_data 中放置一个非本地化的诊断调试消息,其中描述了失败的原因。

注意:返回的 blob 未加密。 将其发送到您的后端,但不要直接与客户端共享。

CreateHostedDedicatedServerListenSocket

HSteamListenSocket CreateHostedDedicatedServerListenSocket( int nLocalVirtualPort, int nOptions, const SteamNetworkingConfigValue_t *pOptions );
在指定的虚拟端口上创建一个侦听套接字。 使用的物理 UDP 端口将由 SDR_LISTEN_PORT 环境变量决定。 如果未配置 UDP 端口,则此调用将失败。

请注意,此调用必须通过 SteamGameServerNetworkingSockets() 接口进行。

当您使用票证生成器库来签发自己的票证时,应使用此函数。 连接到此虚拟端口上的服务器的客户端将需要拥有票证,且必须使用 ConnectToHostedDedicatedServer 进行连接。

如果您需要设置任何初始配置选项,请在此传入。 若要进一步了解为什么这么做要好于在创建后“立即”设置选项,请参见 SteamNetworkingConfigValue_t

GetGameCoordinatorServerLogin

EResult GetGameCoordinatorServerLogin( SteamDatagramGameCoordinatorServerLogin *pLoginInfo, int *pcbSignedBlob, void *pBlob );

使用 SteamDatagram_ParseHostedServerLogin 生成可用于安全登录后端的身份验证 blob。 (请参见 steamdatagram_gamecoordinator.h)

调用函数之前:
  • 在 pLoginInfo 中填充应用数据(m_cbAppData 和 m_appData)。 您可以让所有其他字段保持未初始化状态。
  • *pcbSignedBlob 包含 pBlob 缓冲区的大小。 (它至少应为 k_cbMaxSteamDatagramGameCoordinatorServerLoginSerialized。)

退出成功:
  • 返回 k_EResultOK
  • pLoginInfo 的所有剩余字段将被填写。
  • *pcbSignedBlob 包含已放入 pBlob 中的序列化 blob 的大小。

退出失败:
  • 返回除 k_EResultOK 之外的其他内容。
  • k_EResultNotLoggedOn:您(尚)未登录
  • 请参阅 ISteamNetwrokingSockets::GetHostedDedicatedServerAddress,了解更多潜在的失败返回值。
  • 将在 pBlob 中放置一个非本地化的诊断调试消息,其中描述了失败的原因。

这可以通过使用发给此服务器的证书对 SteamDatagramGameCoordinatorServerLogin 的内容进行签名来实现。 在开发环境中,如果没有证书也可以。 (您将需要在 SteamDatagram_ParseHostedServerLogin 中启用不安全的开发人员登录。)
否则,您将需要签名证书。

注意:此处返回的路由 blob 未加密。 将其发送到您的后端,但不要直接与客户端共享。

FakeIP 系统


“FakeIP”本质上是一个临时的任意标识符,只是恰好是一个有效的 IPv4 地址。 这一系统的目的是便于与使用 IPv4 地址识别主机的现有代码集成。 FakeIP 地址实际上永远不会用于在互联网上发送或接收任何数据包,它仅仅是一个标识符。

FakeIP 地址的设计目的是(希望)尽可能透明地传递现有代码,同时尽量不与相同代码中可能用于网络(包括互联网和局域网)的“真实”地址发生冲突。 在撰写本文时,其范围为 169.254.0.0/16,端口号始终 >1024。 不过,将来是可能会发生变化的! 使用这些地址时不要太想当然,否则您的代码在未来可能会出现问题。 特别是,您应该使用诸如 ISteamNetworkingUtils::IsFakeIP 之类的函数来确定 IP 地址是否为本系统所使用的“假”地址。

BeginAsyncRequestFakeIP

bool BeginAsyncRequestFakeIP( int nNumPorts );

开始异步分配假 IPv4 地址,其他对等端可以使用该地址通过 P2P 与我们联系。 针对每个给定的 appid,此函数返回的 IP 地址是全局唯一的。

nNumPorts 是您希望预留的端口数量。 正如侦听多个 UDP 端口可以用于不同类型的流量一样,预留端口数量也是很有用的。 由于分配是在全局命名空间进行的,因此您可以请求的最大端口数有相对严格的限制。 (在撰写本文时,限制为 4。)*不能*保证端口分配有任何特定的顺序或关系! 尽管它们在实践中经常是连续的,也*不要*这样假设。

如果请求已在进行中,返回 false;如果启动了新请求,返回 true。 请求完成后,就会发布 SteamNetworkingFakeIPResult_t。

游戏服务器*必须*在初始化 SDK 之后、开始登录之前调用此函数。 Steam 需要事先知道将会使用 FakeIP。 无论您的公共 IP 通常出现在哪里(例如服务器浏览器),都会被 FakeIP 和索引 0 处的假端口取代。 请求实际上会排队至登录完成,因此您不能等到分配完成后再登录。 除了可以在本地检测到的微小故障(例如参数无效)外,SteamNetworkingFakeipresult_T 回调(无论成功还是失败)都要等到我们登录后才会发布。 此外,我们会假设 FakeIP 分配对于您的应用程序正常运行至关重要,因此在进行*几次*重试之前,不会报告故障。 此过程可能会持续几分钟。 *强烈*建议将失败视为严重错误。

要使用面向连接的(TCP 样式)API 进行通信,请执行以下操作:
  • 服务器使用 CreateListenSocketP2PFakeIP 创建侦听套接字;
  • 客户端使用 ConnectByIPAddress 进行连接,传入 FakeIP 地址;
  • 该连接的行为多类似于 P2P 连接。 在我们了解真实身份之前,SteamNetConnectionInfo_t 中出现的身份将是 FakeIP 身份。 然后才会出现真实的身份。 如果 SteamNetConnectionInfo_t::m_addrRemote 有效,则它将是 NAT 打洞连接的真实 IPv4 地址。 否则,即为无效。

若要使用来自(UDP 样式)API 的临时 sendto/recv 进行通信,请使用 CreateFakeUDPPort

GetFakeIP

void GetFakeIP( int idxFirstPort, SteamNetworkingFakeIPResult_t *pInfo );

返回有关我们已分配的 FakeIP 和端口的信息(若有)。 idxFirstPort 当前已保留,且必须为 0。 务必要检查 SteamNetworkingFakeIPResult_t::m_eResult。

CreateListenSocketP2PFakeIP

HSteamListenSocket CreateListenSocketP2PFakeIP( int idxFakePort, int nOptions, const SteamNetworkingConfigValue_t *pOptions );

创建一个侦听套接字,以侦听发送到 FakeIP 的 P2P 连接。 对等端可以通过调用 ConnectByIPAddress 发起与此侦听套接字的连接。

idxFakePort 指的是所请求的假端口的*索引*,而不是实际的端口号。 例如,传入 0 以指代预留中的第一个端口。 您必须仅在调用 BeginAsyncRequestFakeIP 后调用此函数。 不过,您无需等待请求完成即可创建侦听套接字。

GetRemoteFakeIPForConnection

EResult GetRemoteFakeIPForConnection( HSteamNetConnection hConn, SteamNetworkingIPAddr *pOutAddr );

如果连接是使用“FakeIP”系统启动的,那么我们可以获得远程主机的 IP 地址。 如果远程主机在建立连接时有全局 FakeIP,则此函数将返回该全局 IP。 否则,将从本地 FakeIP 地址空间分配本地唯一的 FakeIP,并将其返回。

本地 FakeIP 分配尝试以一致的方式分配地址。 如果与同一个远程主机建立了多个连接,它们*可能*会返回相同的 FakeIP。 但由于命名空间有限,因此无法保证这一点。

失败时,返回:
  • k_EResultInvalidParam: invalid connection handle
  • k_EResultIPNotFound:此连接不是使用 FakeIP 系统建立的

CreateFakeUDPPort

ISteamNetworkingFakeUDPPort *CreateFakeUDPPort( int idxFakeServerPort );

获取一个可以像 UDP 端口一样使用的接口,来向 FakeIP 地址发送数据报,或从其接收数据报。 这样做是为了能够轻松移植现有的基于 UDP 的代码以利用 SDR。

idxFakeServerPort 指的是使用 BeginAsyncRequestFakeIP 分配的端口的*索引*,用于创建“服务器”端口。 您可以在分配完成之前调用此函数。 但是,在分配成功之前,所有发送数据包的尝试均会失败。 当对等端收到从该接口发送的数据包时,发送数据包的地址将是全局唯一的 FakeIP。 如果多次调用此函数并传入相同的(非负数)假端口索引,则将返回相同的对象,且该对象不被计入引用。

要创建“客户端”端口(例如,相当于临时 UDP 端口的端口),请传入 -1。 在这种情况下,每次调用都会返回一个不同的对象。 当对等端收到从此接口发送的数据包时,对等端将从自己本地控制的命名空间分配一个 FakeIP。

回调结构

SteamNetConnectionStatusChangedCallback_t

每当创建、销毁或更改状态时,都会发布此回调。 m_info 字段将包含更改发生和回调发布时的连接的完整描述。 具体来说,m_info.m_eState 将具有新的连接状态。

通常,您将需要侦听此回调以了解下列事项发生的时间:
  • 新连接到达侦听套接字。将设置 m_info.m_hListenSocket,m_eOldState = k_ESteamNetworkingConnectionState_None, and m_info.m_eState = k_ESteamNetworkingConnectionState_Connecting。 请参阅 AcceptConnection
  • 您发起的连接已被远程主机接受。m_eOldState = k_ESteamNetworkingConnectionState_Connecting, and m_info.m_eState = k_ESteamNetworkingConnectionState_Connected。 某些连接可能会首先转换为 k_ESteamNetworkingConnectionState_FindingRoute。
  • 连接已被远程主机主动拒绝或关闭。m_eOldState = k_ESteamNetworkingConnectionState_Connecting k_ESteamNetworkingConnectionState_Connected, and m_info.m_eState = k_ESteamNetworkingConnectionState_ClosedByPeer。 m_info.m_eEndReason 和 m_info.m_szEndDebug 会有更多详细信息。 注意:收到此回调后,您仍必须使用 CloseConnection 销毁连接以释放本地资源。 (在这种情况下,由于连接已关闭,因此不会使用传入函数的详细信息。)
  • 检测到连接问题,并且本地主机已将其关闭。最常见的故障是超时,但是其他配置或身份验证故障也可能导致此问题。 m_eOldState = k_ESteamNetworkingConnectionState_Connecting or k_ESteamNetworkingConnectionState_Connected, and m_info.m_eState = k_ESteamNetworkingConnectionState_ProblemDetectedLocally。 m_info.m_eEndReason 和 m_info.m_szEndDebug 会有更多详细信息。 注意:收到此回调后,您仍必须使用 CloseConnection 销毁连接以释放本地资源。 (在这种情况下,由于连接已关闭,因此不会使用传入函数的详细信息。)

请记住,回调已发布到队列中,并且网络连接可以随时更改。 在处理此回调时,连接状态可能已经更改。

还要注意,当您通过自己的 API 调用创建和销毁连接时,将发布回调。

struct SteamNetConnectionStatusChangedCallback_t { /// 连接句柄 HSteamNetConnection m_hConn; /// 完整的连接信息 SteamNetConnectionInfo_t m_info; /// 前一个状态。 (当前状态在 m_info.m_eState 中) ESteamNetworkingConnectionState m_eOldState; };

SteamNetAuthenticationStatus_t


用于描述我们参与经过身份验证的加密通信的就绪状态的结构。 为此,我们需要:
  • 可能与此应用相关的受信任 CA 证书的列表。
  • CA 颁发的有效证书。

每当我们的就绪状态更改时,都会发布此回调。

struct SteamNetAuthenticationStatus_t { /// 状态 ESteamNetworkingAvailability m_eAvail; /// 未本地化的英语状态。 仅用于诊断/调试 /// 目的。 char m_debugMsg[]; };

自定义 P2P 信令

信令 是指通过低带宽和高延迟的受信任通道发送的对接消息,对等方会使用该消息来协调 P2P 连接。 Steam 为您提供信令服务。 但是,在某些情况下,您可能希望自己制作信令。 (例如,当您的一个或两个对等端不在 Steam 上时。)您可以使用 SteamNetworkingSockets 通过自己的信令进行 P2P 连接。 您可以使用 ICE 来穿过 NAT 并使用标准的 STUN/TURN 服务器;而视您的情况而定,SDR 中继网络和 Valve 主干网络可能不可用。 如果您需要有关这些 API 的更多信息,请告诉我们。