Microsoft 游戏开发工具包中的 Windows 套接字简介

Microsoft 游戏开发工具包 (GDK) 支持使用 Windows 套接字 2 (Winsock) API。

Windows 套接字 2 (Winsock)使你能够创建支持高级 Internet、Intranet 和其他网络的应用程序,以跨网络传输应用程序数据,而不受网络协议的影响。

Microsoft 游戏开发工具包 (GDK) 游戏与 Winsock 交互的方式通常和 Win32 程序与 Winsock 交互的方式相同。

本主题将针对 Winsock 在 Microsoft 游戏开发工具包 (GDK) 游戏中使用,介绍细微差别和最佳做法。

设置

本部分介绍在使用 Winsock API 时需要包括哪些 .h 和 .lib 文件。

  • 在您的源文件中添加 #include <winhttp.h>
  • 对于电脑游戏,继续链接到 Winhttp.lib
  • 对于 Xbox 主机游戏,则必须链接到 XGamePlatform.lib,而不是直接链接到 Winhttp.lib

只有 WINAPI_PARTITION_GAMES API 系列下的 API 才能用于 Microsoft 游戏开发工具包 (GDK) 游戏。

网络初始化

在第一次调用 WSAStartup 之前,Microsoft 游戏开发工具包 (GDK) 游戏必须确保网络堆栈已准备就绪。 如果在游戏启动过程中调用 WSAStartup 过早,则 WSAStartup 或后续 Winsock 调用可能失败。 有关如何确定网络堆栈何时准备就绪的详细信息,请参阅网络初始化和连接性

暂停和继续

必须通过 RegisterAppStateChangeNotification 注册暂停和继续事件。 暂停时,应关闭所有套接字柄并调用 WSACleanup。 继续时,应再次等待网络初始化,然后调用 WSAStartup,并创建新的套接字。

Microsoft 游戏开发工具包 (GDK) 首选本地用户数据报协议 (UDP) 多人游戏端口 API

Microsoft 游戏开发工具包 (GDK) 的首选本地 UDP 多人游戏端 API 返回最佳动态选定端口,游戏应使用 Winsock 绑定到该端口,以便通过用户数据报协议 (UDP) 促进游戏内通信。 Microsoft 游戏开发工具包 (GDK) 平台确保此特定端口是最有可能适用于每个用户的特定网络环境的端口。 使用此特定端口可充分利用平台的客户支持和诊断流、提高标准化的网络地址转换 (NAT) 的兼容性,并提供标准化的通用即插即用 UPnP™ 认证设备功能,并且将数据包标识为对于服务质量 (QoS) 路由器和 ISP 算法是实时敏感的。

UDP 端口尤其与依赖于对等网络拓扑的游戏相关。 它是唯一一种允许不执行防火墙打洞入站 UDP 数据包就可以通过防火墙的端口。 在 Microsoft 游戏开发工具包 (GDK) 中依赖对等网络拓扑的游戏仍应提供它们自己的公共 IP 地址和端口发现,以及适用于具有适中或严格 NAT 类型的客户端的 NAT 穿越解决方案。 首选端口本身也提高了这些技术的成功率,但不能替代它们。

使用客户端服务器网络拓扑的游戏也可从使用 UDP 端口中受益。 疑难解答、UPnP™ 和数据包识别仍与在医院、酒店和大学宿舍中常见的强制网络门户和其他基于源的筛选方法相关。

我们强烈建议 Microsoft 游戏开发工具包 (GDK) 游戏将首选的本地 UDP 多人游戏端口用于其主要多人游戏和聊天网络流量。

套接字的安全性

Microsoft 游戏开发工具包 (GDK) 游戏负责确保使用 Winsock API 通过网络传输的所有数据都有相应的安全和加密保护。

BCryptWinCryptschannel 和其他标准 Windows API 提供推荐的加密基元。 Microsoft 游戏开发工具包 (GDK) 游戏应使用这些 API 为所有套接字流实现数据报传输层安全性 (DTLS) 和其他标准化的安全协议。

对于不希望实现其自身安全模型的游戏,Microsoft 游戏开发工具包 (GDK) 库 PlayFab Party 提供包括完整和集成的套接字安全情景在内的众多功能。

套接字内存注意事项

Microsoft 游戏开发工具包 (GDK) 游戏当前只有约 16MB 内存可用于所有使用 WinSock 和 WinHTTP 的网络堆栈,如果超过此值,系统可能变得不稳定。 内存消耗最大的来源之一,是在 WinSock kernel 模式下发送和接收用来存储传入和传出数据包的内存池 (数据包在通过传输线发送,或通过诸多 recv 变种之一传输到游戏的用户模式内存中之前,将存储于内存池中)。

特别是在高带宽情况下,游戏有责任将数据以低延迟(至少等于远程对等或服务器推送的速度)转移到自己的用户模式缓冲区中。 有多种更加复杂并且保险的方式可以实现这一点,但以下所有建议的基础目标是在数据到达时始终有用户模式缓冲区可以立即传输数据,以使内核无需分配更多的内存。 最终效果是,将待处理数据的内存使用从非常有限的的内核池更改为直接由你管理的更大的游戏内存池。

使用 WinSock 的更高级别 API 通常有机制来控制其套接字的内核内存使用情况。 WinHTTPXCurl 均允许通过各自阅读通知机制控制其内核内存使用情况,而其他 API (如 XSAPIPlayFab Party) 则可以通过以下技术来最小化其内核内存使用。

Berkley (BSD) 套接字

若要坚持阻止 Berkley 套接字 API,则需要增加调用 recv 的频率。 建议使用一个带有紧密循环的专用线程,以便为需要在其他位置处理的收到的数据创建队列。 理想情况下,应始终在 recv 调用中阻止该线程;在 recv 调用外花费的时间都可能导致再次调用 recv 时增加内核内存使用。 如果尝试在重新调用 recv 之前处理数据,游戏通常会落后,且只要继续高速接收数据,内核内存使用就会一直增加。 我们还建议指定至少 8,000 的缓冲区,以与内核缓冲区大小边界一致,并帮助解决中断和延迟发送模式。

WinSock 重叠 I/O

WinSock 重叠 I/O 允许异步保留用户模式接收缓冲区挂起。 该设置还允许内核直接使用内存缓冲区,并避免额外内存副本(与 Berkley 套接字模式不同),假定始终有一个缓冲区挂起,内核完全不会分配给接收的数据。 此外,通过重叠 I/O,可以将 SO_RCVBUF 和 SO_SNDBUF 设置为 0 这个特殊值,这通常可避免分配任何发送/接收内核内存。

此方法是在几乎所有情况下管理套接字使用的内存的最好方法。

已注册的 I/O

已注册 I/O 是一个复杂的网络 API,提供最低延迟,并保证没有内核内存被用于发送和接收操作。 它使你能够设置内核直接使用的多个接收/发送缓冲区,以确保始终有用于传入数据的缓冲区。

UDP 最大传输单元大小

尽管 Microsoft 游戏开发工具包 (GDK) 中存在理论最大有效负载,但实际上,特定连接的最大值取决于网络连接类型。 该网络连接类型在游戏正在运行时可能会有变化。 您应该设计您的网络代码采用每个数据包最大 1,384 字节的 UDP 有效负载,而不是试图确定实际最大传输单元 (MTU) 并对游戏运行期间 MTU 中的更改作出响应。 在所有网络配置下都可放心地使用此值,以避免传输中的碎片。 无论套接字类型是 IPv4 还是 IPv6,我们都建议配置。

有效负载大于 1,384 字节的传输通常要求 IP 级别的数据包碎片。 互联网服务提供商以及用户的家用路由器和设备对于 IP 数据包碎片的支持并不理想。 在这些网络配置中,IP 数据包碎片不会导致 Winsock API 失败。 结果将改为显示内容包丢失。 要避免 IP 级别数据包碎片,请将 1,384 字节用作安全的最大数据包有效负载。

为了确保你的游戏避免碎片,并协助你的游戏满足有关最低多人游戏网络要求的关联的 Xbox 要求,你应该将套接字选项 IP_DONTFRAGMENTIP_USER_MTU 应用于你的游戏打开的每个套接字。 只有当 XGameRuntimeIsFeatureAvailable(XGameRuntimeFeature::XNetworking) API 返回 true 时,这些标志才适用于 Microsoft 游戏开发工具包 (GDK) 游戏。 如果该 XNetworking功能不可用,则 setsockopt 调用将失败。 下面是说明如何设置这两个套接字选项的示例。


HRESULT
ApplyFragmentationSocketOptions(
    SOCKET s
)
{
    int value = 1;
    int error = setsockopt(s, IPPROTO_IP, IP_DONTFRAGMENT, (char *)&value, sizeof(value));
    if (error == SOCKET_ERROR)
    {
        return HRESULT_FROM_WIN32(WSAGetLastError());
    }

    value = 1384;
    error = setsockopt(s, IPPROTO_IP, IP_USER_MTU, (char *)&value, sizeof(value));
    if (error == SOCKET_ERROR)
    {
        return HRESULT_FROM_WIN32(WSAGetLastError());
    }

    return S_OK;
}

另请参阅