游戏联网

了解如何在 DirectX 游戏中开发和整合网络功能。

概念一目了然

DirectX 游戏中可以使用各种网络功能,无论是简单的单机游戏还是大型多人游戏。 最简单的网络用途是将用户名和游戏分数存储在中央网络服务器上。

多人游戏中网络 API 是必需的,无论其使用基础结构(客户端-服务器模型或互联网对等模型)还是即兴游戏(本地对等游戏)。 对于基于服务器的多玩家游戏,中央游戏服务器通常处理大部分游戏作,客户端游戏应用用于输入、显示图形、播放音频和其他功能。 网络传输的速度和延迟是一种令人满意的游戏体验的问题。

对于对等游戏,每个玩家的应用程序负责处理输入和图形。 在大多数情况下,游戏玩家位于邻近位置,以便网络延迟应较低,但仍令人担忧。 如何发现对等方并建立连接成为问题。

对于单玩家游戏,中央 Web 服务器或服务通常用于存储用户名、游戏分数和其他杂项信息。 在这些游戏中,网络传输速度和延迟不太重要,因为这不会直接影响游戏操作。

网络条件可以随时更改,因此使用网络 API 的任何游戏都需要处理可能发生的网络异常。 若要了解有关处理网络异常的详细信息,请参阅 网络基础知识

防火墙和 Web 代理很常见,可能会影响使用网络功能的能力。 需要使用网络的游戏必须为正确处理防火墙和代理做好准备。

对于移动设备,重要的是监控可用的网络资源,并在流量计费的网络(如漫游或数据成本高昂的网络)上相应调整使用行为。

网络隔离是 Windows 使用的应用安全模型的一部分。 Windows 主动发现网络边界,并强制实施网络隔离的网络访问限制。 应用必须声明网络隔离功能才能定义网络访问的范围。 如果不声明这些功能,应用将无法访问网络资源。 若要详细了解 Windows 如何为应用强制实施 网络隔离,请参阅如何配置网络隔离功能

设计注意事项

可以在 DirectX 游戏中使用各种网络 API。 因此,选择正确的 API 非常重要。 Windows 支持各种网络 API,你的应用可以使用这些 API 通过 Internet 或专用网络与其他计算机和设备通信。 第一步是确定应用所需的网络功能。

这些是游戏更受欢迎的网络 API。

  • TCP 和套接字 - 提供可靠的连接。 将 TCP 用于不需要安全性的游戏操作。 TCP 允许服务器简单地扩展,因此通常用于使用架构(客户端-服务器或互联网对等)模型的游戏。 TCP 也可以用于即席(本地对等)游戏,通过 Wi-Fi Direct 和 Bluetooth 进行连接。 TCP 通常用于游戏对象移动、角色交互、文本聊天和其他作。 StreamSocket 类提供了可以用于 Microsoft Store 游戏的 TCP 套接字。 StreamSocket 类与 Windows::Networking::Sockets 命名空间中的相关类一起使用。
  • 使用 SSL 的 TCP 和套接字 - 提供防止窃听的可靠连接。 将 TCP 连接与 SSL 配合使用,以实现需要安全保护的游戏操作。 SSL 的加密和开销会增加延迟和性能方面的成本,因此仅在需要安全性时使用。 使用 SSL 的 TCP 通常用于登录、购买和交易资产、游戏角色创建和管理。 StreamSocket 类提供支持 SSL 的 TCP 套接字。
  • UDP 和套接字 - 提供不可靠的网络传输,开销较低。 UDP 用于需要低延迟且可以容忍某些数据包丢失的游戏操作。 这通常用于格斗游戏、射击游戏和追踪器、网络音频和语音聊天。 DatagramSocket 类提供了可用于 Microsoft Store 游戏的 UDP 套接字。 DatagramSocket 类与 Windows::Networking::Sockets 命名空间中的相关类一起使用。
  • HTTP 客户端 - 提供与 HTTP 服务器的可靠连接。 最常见的网络方案是访问网站以检索或存储信息。 一个简单的示例是一个使用网站来存储用户信息和游戏分数的游戏。 与 SSL 一起使用时,HTTP 客户端可用于登录、购买、交易资产、游戏角色创建和管理。 HttpClient 类提供新式 HTTP 客户端 API,用于Microsoft应用商店游戏。 HttpClient 类与 Windows::Web::Http 命名空间中的相关类一起使用。

在 DirectX 游戏中处理网络异常

当 DirectX 游戏中发生网络异常时,这表示存在重大问题或故障。 使用网络 API 时,可能会由于多种原因而发生异常。 通常,异常可能是由于远程主机或服务器网络连接或其他网络问题的变化造成的。

使用网络 API 时出现异常的一些原因包括:

  • 来自用户的主机名或 URI 的输入包含错误且无效。
  • 查找主机名或 URI 时,名称解析失败。
  • 网络连接丢失或更改。
  • 使用套接字或 HTTP 客户端 API 造成的网络连接失败。
  • 网络服务器或远程终结点错误。
  • 其他网络错误。

由于网络错误(例如连接丢失或变化、连接故障和服务器故障),异常可能随时发生。 这些错误会导致异常被引发。 如果应用未处理异常,则可能会导致整个应用被运行时终止。

调用大多数异步网络方法时,必须编写代码来处理异常。 有时,当发生异常时,可以重试网络方法作为解决问题的方法。 其他情况下,应用程序可能需要计划在没有网络连接的情况下继续使用之前缓存的数据。

通用 Windows 平台 (UWP) 应用通常会抛出单个异常。 异常处理程序可以检索有关异常原因的更多详细信息,以便更好地了解失败并做出适当的决策。

当 DirectX 游戏(即 UWP 应用)中发生异常时,可以检索错误原因的 HRESULT 值。 Winerror.h 包含一个大型列表,其中列出了可能的 HRESULT 值,包括网络错误。

网络 API 支持检索有关异常原因的此详细信息的不同方法。

  • 一种检索导致异常的错误的 HRESULT 值的方法。 可能的潜在 HRESULT 值的列表非常多且未指定。 使用任何网络 API 时,可以检索 HRESULT 值。
  • HRESULT 值转换为枚举值的帮助程序方法。 可能的枚举值列表已被指定,而且数量相对较小。 辅助方法可用于 Windows::Networking::Sockets中的套接字类。

Windows.Networking.Sockets 中的异常

如果传递的字符串不是有效的主机名(包含主机名中不允许的字符),则用于套接字的 HostName 类的构造函数可能会引发异常。 如果应用程序从用户的 HostName 获取用于游戏的对等连接输入,则构造函数应放在 try/catch 块中。 如果引发异常,应用可以通知用户并请求新的主机名。

添加代码来验证用户输入的主机名字符串

// Define some variables at the class level.
Windows::Networking::HostName^ remoteHost;

bool isHostnameFromUser = false;
bool isHostnameValid = false;

///...

// If the value of 'remoteHostname' is set by the user in a control as input 
// and is therefore untrusted input and could contain errors. 
// If we can't create a valid hostname, we notify the user in statusText 
// about the incorrect input.

String ^hostString = remoteHostname;

try 
{
    remoteHost = ref new Windows::Networking:Host(hostString);
    isHostnameValid = true;
}
catch (InvalidArgumentException ^ex)
{
    statusText->Text = "You entered a bad hostname, please re-enter a valid hostname.";
    return;
}

isHostnameFromUser = true;

// ... Continue with code to execute with a valid hostname.

Windows.Networking.Sockets 命名空间提供了用于在使用套接字时处理错误的方便的辅助方法和枚举。 这可用于在应用中以不同的方式处理特定网络异常。

DatagramSocketStreamSocketStreamSocketListener 操作上遇到的错误会导致引发异常。 异常的原因是一个错误值,表示为 HRESULT 值。 SocketError.GetStatus 方法用于将套接字操作中产生的网络错误转换为 SocketErrorStatus 枚举值。 大多数 SocketErrorStatus 枚举值对应于本机 Windows 套接字操作返回的错误。 应用可以根据异常的原因筛选特定的 SocketErrorStatus 枚举值来修改应用行为。

对于参数验证错误,应用还可以使用异常 HRESULT,了解有关导致异常的错误的更多详细信息。 在 Winerror.h 头文件中列出了可能的 HRESULT 值。 对于大多数参数验证错误,HRESULT 返回的是 E_INVALIDARG

添加代码以在尝试建立流套接字连接时处理异常

using namespace Windows::Networking;
using namespace Windows::Networking::Sockets;
    
    // Define some more variables at the class level.

    bool isSocketConnected = false
    bool retrySocketConnect = false;

    // The number of times we have tried to connect the socket.
    unsigned int retryConnectCount = 0;

    // The maximum number of times to retry a connect operation.
    unsigned int maxRetryConnectCount = 5; 
    ///...

    // We pass in a valid remoteHost and serviceName parameter.
    // The hostname can contain a name or an IP address.
    // The servicename can contain a string or a TCP port number.

    StreamSocket ^ socket = ref new StreamSocket();
    SocketErrorStatus errorStatus; 
    HResult hr;

    // Save the socket, so any subsequent steps can use it.
    CoreApplication::Properties->Insert("clientSocket", socket);

    // Connect to the remote server. 
    create_task(socket->ConnectAsync(
            remoteHost,
            serviceName,
            SocketProtectionLevel::PlainSocket)).then([this] (task<void> previousTask)
    {
        try
        {
            // Try getting all exceptions from the continuation chain above this point.
            previousTask.get();

            isSocketConnected = true;
            // Mark the socket as connected. We do not really care about the value of the property, but the mere 
            // existence of  it means that we are connected.
            CoreApplication::Properties->Insert("connected", nullptr);
        }
        catch (Exception^ ex)
        {
            hr = ex.HResult;
            errorStatus = SocketStatus::GetStatus(hr); 
            if (errorStatus != Unknown)
            {
                                                                switch (errorStatus) 
                   {
                    case HostNotFound:
                        // If the hostname is from the user, this may indicate a bad input.
                        // Set a flag to ask the user to re-enter the hostname.
                        isHostnameValid = false;
                        return;
                        break;
                    case ConnectionRefused:
                        // The server might be temporarily busy.
                        retrySocketConnect = true;
                        return;
                        break; 
                    case NetworkIsUnreachable: 
                        // This could be a connectivity issue.
                        retrySocketConnect = true;
                        break;
                    case UnreachableHost: 
                        // This could be a connectivity issue.
                        retrySocketConnect = true;
                        break;
                    case NetworkIsDown: 
                        // This could be a connectivity issue.
                        retrySocketConnect = true;
                        break;
                    // Handle other errors. 
                    default: 
                        // The connection failed and no options are available.
                        // Try to use cached data if it is available. 
                        // You may want to tell the user that the connect failed.
                        break;
                }
                }
                else 
                {
                    // Received an Hresult that is not mapped to an enum.
                    // This could be a connectivity issue.
                    retrySocketConnect = true;
                }
            }
        });
    }

Windows.Web.Http 中的异常

Windows::Web::Http::Http::HttpClient 一起使用的 Windows::Foundation::Uri 类的构造函数在传递的字符串不是有效的 URI(包含 URI 中不允许的字符)时可能会引发异常。 在C++中,没有方法尝试将字符串解析为 URI。 如果应用从用户获取 Windows::Foundation::Uri 的输入,则构造函数应在 try/catch 块中。 如果引发异常,应用可以通知用户并请求新的 URI。

你的应用还应检查 URI 中的方案是否为 HTTP 或 HTTPS,因为这些方案是 Windows::Web::Http::HttpClient 支持的唯一方案。

添加代码以验证用户 URI 的字符串

    // Define some variables at the class level.
    Windows::Foundation::Uri^ resourceUri;

    bool isUriFromUser = false;
    bool isUriValid = false;

    ///...

    // If the value of 'inputUri' is set by the user in a control as input 
    // and is therefore untrusted input and could contain errors. 
    // If we can't create a valid hostname, we notify the user in statusText 
    // about the incorrect input.

    String ^uriString = inputUri;

    try 
    {
        isUriValid = false;
        resourceUri = ref new Windows::Foundation:Uri(uriString);

        if (resourceUri->SchemeName != "http" && resourceUri->SchemeName != "https")
        {
            statusText->Text = "Only 'http' and 'https' schemes supported. Please re-enter URI";
            return;
        }
        isUriValid = true;
    }
    catch (InvalidArgumentException ^ex)
    {
        statusText->Text = "You entered a bad URI, please re-enter Uri to continue.";
        return;
    }

    isUriFromUser = true;


    // ... Continue with code to execute with a valid URI.

Windows::Web::Http 命名空间缺少便利函数。 因此,使用此命名空间中使用 HttpClient 和其他类的应用需要使用 HRESULT 值。

在使用 C++ 的应用中, Platform::Exception 表示在发生异常时应用执行期间出错。 Platform::Exception::HResult 属性返回分配给特定异常的 HRESULT Platform::Exception::Message 属性返回与 HRESULT 值关联的系统提供的字符串。 在 Winerror.h 头文件中列出了可能的 HRESULT 值。 应用可以根据异常的原因筛选特定的 HRESULT 值以修改应用行为。

对于大多数参数验证错误,HRESULT 返回的是 E_INVALIDARG。 对于某些非法方法调用,返回的 HRESULTE_ILLEGAL_METHOD_CALL

添加代码以在尝试使用 HttpClient 连接到 HTTP 服务器时处理异常

using namespace Windows::Foundation;
using namespace Windows::Web::Http;
    
    // Define some more variables at the class level.

    bool isHttpClientConnected = false
    bool retryHttpClient = false;

    // The number of times we have tried to connect the socket
    unsigned int retryConnectCount = 0;

    // The maximum number of times to retry a connect operation.
    unsigned int maxRetryConnectCount = 5; 
    ///...

    // We pass in a valid resourceUri parameter.
    // The URI must contain a scheme and a name or an IP address.

    HttpClient ^ httpClient = ref new HttpClient();
    HResult hr;

    // Save the httpClient, so any subsequent steps can use it.
    CoreApplication::Properties->Insert("httpClient", httpClient);

    // Send a GET request to the HTTP server. 
    create_task(httpClient->GetAsync(resourceUri)).then([this] (task<void> previousTask)
    {
        try
        {
            // Try getting all exceptions from the continuation chain above this point.
            previousTask.get();

            isHttpClientConnected = true;
            // Mark the HttClient as connected. We do not really care about the value of the property, but the mere 
            // existence of  it means that we are connected.
            CoreApplication::Properties->Insert("connected", nullptr);
        }
        catch (Exception^ ex)
        {
            hr = ex.HResult;
                                                switch (errorStatus) 
               {
                case WININET_E_NAME_NOT_RESOLVED:
                    // If the Uri is from the user, this may indicate a bad input.
                    // Set a flag to ask user to re-enter the Uri.
                    isUriValid = false;
                    return;
                    break;
                case WININET_E_CANNOT_CONNECT:
                    // The server might be temporarily busy.
                    retryHttpClientConnect = true;
                    return;
                    break; 
                case WININET_E_CONNECTION_ABORTED: 
                    // This could be a connectivity issue.
                    retryHttpClientConnect = true;
                    break;
                case WININET_E_CONNECTION_RESET: 
                    // This could be a connectivity issue.
                    retryHttpClientConnect = true;
                    break;
                case INET_E_RESOURCE_NOT_FOUND: 
                    // The server cannot locate the resource specified in the uri.
                    // If the Uri is from user, this may indicate a bad input.
                    // Set a flag to ask the user to re-enter the Uri
                    isUriValid = false;
                    return;
                    break;
                // Handle other errors. 
                default: 
                    // The connection failed and no options are available.
                    // Try to use cached data if it is available. 
                    // You may want to tell the user that the connect failed.
                    break;
            }
            else 
            {
                // Received an Hresult that is not mapped to an enum.
                // This could be a connectivity issue.
                retrySocketConnect = true;
            }
        }
    });

其他资源

参考文献

示例应用