网络基础知识

任何支持网络的应用都必须执行的操作

能力

若要使用网络,必须将适当的功能元素添加到应用清单。 如果在应用的清单中未指定任何网络功能,则应用将没有网络功能,并且任何连接到网络尝试都将失败。

以下是最常用的网络功能。

能力 Description
internetClient 提供对互联网以及机场、咖啡店等公共场所网络的出站访问。 大多数需要 Internet 访问的应用都应使用此功能。
internetClientServer 允许该应用通过互联网以及机场和咖啡店等公共场所的网络进行入站和出站网络访问。
privateNetworkClientServer 在用户受信任的位置(如家庭和工作)为应用提供入站和出站网络访问权限。

在某些情况下,应用可能需要其他功能。

能力 Description
enterpriseAuthentication 允许应用连接到需要域凭据的网络资源。 例如,从专用 Intranet 上的 SharePoint 服务器上检索数据的应用。 借助此功能,凭据可用于访问需要凭据的网络上的网络资源。 具有此功能的应用可以在网络上模拟你。 你不需要此功能,才能允许应用通过身份验证代理访问 Internet。

有关更多详细信息,请参阅受限功能企业功能方案的文档。
proximity 用于与靠近计算机的设备进行近场通信所必需。 近场邻近感应可用于在附近的设备上发送或连接应用程序。

此功能允许应用通过用户同意发送邀请或接受邀请来访问网络以连接到靠近的设备。
sharedUserCertificates 此功能允许应用访问软件和硬件证书,例如智能卡证书。 在运行时调用此功能时,用户必须采取措施,例如插入卡片或选择证书。

借助此功能,软件和硬件证书或智能卡用于应用中的标识。 雇主、银行或政府服务可以使用此功能进行识别。

当应用不在前台时进行通信

后台任务 提供有关在应用不在前台时执行工作的常规信息。 更具体地说,当它不是当前的前台应用,但数据仍通过网络发送给它时,你的代码必须采取特殊的步骤以接收通知。 出于此目的,Windows支持控制通道触发器。 有关详细信息,请参阅 ControlChannelTrigger。 一项较新的技术为某些场景提供了更好的功能和更低的开销,例如支持推送的流套接字:套接字代理和套接字活动触发器。

如果应用使用 DatagramSocketStreamSocket 或 StreamSocketListener,则应用可以将打开套接字的所有权转移到系统提供的套接字代理,然后离开前台,甚至终止。 在传输的套接字上建立连接或流量到达该套接字时,应用或其指定的后台任务将激活。 如果你的应用未运行,它将启动。 然后,套接字代理通过 SocketActivityTrigger 触发器通知你的应用,新的流量已到达。 你的应用从套接字代理回收套接字,并处理套接字上的流量。 这意味着应用在未主动处理网络流量时消耗的系统资源要少得多。

套接字代理旨在替换适用的控制通道触发器,因为它提供相同的功能,但限制更少,内存占用更少。 套接字代理可由非锁屏应用的应用使用,在手机上使用的方式与其他设备上相同。 当流量送达以便由套接字代理激活应用时,这些应用无需处于运行状态。 并且套接字代理支持在 TCP 套接字上侦听,而控制通道触发器不支持此操作。

选择网络触发器

在某些情况下,任一类型的触发器都适用。 选择要在应用中使用的触发器类型时,请考虑以下建议。

有关如何使用套接字代理的详细信息和示例,请参阅 后台的网络通信

安全连接

安全套接字层(SSL)和最近的传输层安全性(TLS)是旨在为网络通信提供身份验证和加密的加密协议。 这些协议旨在防止在发送和接收网络数据时窃听和篡改。 这些协议使用客户端-服务器模型进行协议交换。 这些协议还使用数字证书和证书颁发机构来验证服务器是否是它声称是谁。

创建安全套接字连接

可以将 StreamSocket 对象配置为使用 SSL/TLS 在客户端和服务器之间进行通信。 对 SSL/TLS 的这种支持仅限于使用 StreamSocket 对象作为 SSL/TLS 协商中的客户端。 当收到传入通信时,不能将 SSL/TLS 与 StreamSocketListener 创建的 StreamSocket 一起使用,因为作为服务器的 SSL/TLS 协商不是由 StreamSocket 类实现的。

可通过两种方法通过 SSL/TLS 保护 StreamSocket 连接:

  • ConnectAsync - 建立与网络服务的初始连接,并立即协商对所有通信使用 SSL/TLS。
  • UpgradeToSslAsync - 最初无需加密即可连接到网络服务。 应用可以发送或接收数据。 然后,升级连接以对所有进一步通信使用 SSL/TLS。

SocketProtectionLevel 指定应用想要建立或升级连接的所需套接字保护级别。 但是,在连接的两个终结点之间的协商过程中,确定已建立连接的最终保护级别。 如果另一终结点请求较低级别,则结果可以是比指定的保护级别低。

异步操作成功完成后,可以通过 StreamSocketInformation.ProtectionLevel 属性检索 ConnectAsyncUpgradeToSslAsync 调用中使用的请求保护级别。 但是,这并不反映连接正在使用的实际保护级别。

注释

代码不应隐式依赖于使用特定保护级别,或者假定默认使用给定的安全级别。 安全环境不断变化,协议和默认保护级别会随时间而变化,以避免使用具有已知弱点的协议。 默认值可能因单独的计算机配置而异,也可能取决于已安装的软件以及已应用了哪些修补程序。 如果应用依赖于特定安全级别的使用,则必须显式指定该级别,然后检查以确保它在已建立的连接上实际使用。

使用 ConnectAsync

ConnectAsync 可用于与网络服务建立初始连接,然后立即协商为所有通信使用 SSL/TLS。 有两种 ConnectAsync 方法支持传递 protectionLevel 参数:

如果在调用上述任一 ConnectAsync 方法时,将 protectionLevel 参数设置为 Windows.Networking.Sockets.SocketProtectionLevel.Ssl,则 StreamSocket 将被建立为使用 SSL/TLS 进行加密。 此值需要加密,并且绝不允许使用 NULL 密码。

使用这些 ConnectAsync 方法之一时,通常遵循的顺序是相同的。

以下示例创建 StreamSocket 并尝试建立与网络服务的连接,并立即协商使用 SSL/TLS。 如果协商成功,客户端与网络服务器之间通过 StreamSocket 进行的所有网络通信都将被加密。

using Windows.Networking;
using Windows.Networking.Sockets;

    // Define some variables and set values
    StreamSocket clientSocket = new StreamSocket();
     
    HostName serverHost = new HostName("www.contoso.com");
    string serverServiceName = "https";
    
    // For simplicity, the sample omits implementation of the
    // NotifyUser method used to display status and error messages 
    
    // Try to connect to contoso using HTTPS (port 443)
    try {

        // Call ConnectAsync method with SSL
        await clientSocket.ConnectAsync(serverHost, serverServiceName, SocketProtectionLevel.Ssl);

        NotifyUser("Connected");
    }
    catch (Exception exception) {
        // If this is an unknown status it means that the error is fatal and retry will likely fail.
        if (SocketError.GetStatus(exception.HResult) == SocketErrorStatus.Unknown) {
            throw;
        }
        
        NotifyUser("Connect failed with error: " + exception.Message);
        // Could retry the connection, but for this simple example
        // just close the socket.
        
        clientSocket.Dispose();
        clientSocket = null; 
    }
           
    // Add code to send and receive data using the clientSocket
    // and then close the clientSocket
#include <winrt/Windows.Networking.Sockets.h>

using namespace winrt;
...
    // Define some variables, and set values.
    Windows::Networking::Sockets::StreamSocket clientSocket;

    Windows::Networking::HostName serverHost{ L"www.contoso.com" };
    winrt::hstring serverServiceName{ L"https" };

    // For simplicity, the sample omits implementation of the
    // NotifyUser method used to display status and error messages.

    // Try to connect to the server using HTTPS and SSL (port 443).
    try
    {
        co_await clientSocket.ConnectAsync(serverHost, serverServiceName, Windows::Networking::Sockets::SocketProtectionLevel::Tls12);
        NotifyUser(L"Connected");
    }
    catch (winrt::hresult_error const& exception)
    {
        NotifyUser(L"Connect failed with error: " + exception.message());
        clientSocket = nullptr;
    }
    // Add code to send and receive data using the clientSocket,
    // then set the clientSocket to nullptr when done to close it.
using Windows::Networking;
using Windows::Networking::Sockets;

    // Define some variables and set values
    StreamSocket^ clientSocket = ref new StreamSocket();
 
    HostName^ serverHost = ref new HostName("www.contoso.com");
    String^ serverServiceName = "https";

    // For simplicity, the sample omits implementation of the
    // NotifyUser method used to display status and error messages 

    // Try to connect to the server using HTTPS and SSL (port 443)
    task<void>(clientSocket->ConnectAsync(serverHost, serverServiceName, SocketProtectionLevel::Ssl)).then([this] (task<void> previousTask) {
        try
        {
            // Try getting all exceptions from the continuation chain above this point.
            previousTask.Get();
            NotifyUser("Connected");
        }
        catch (Exception^ exception)
        {
            NotifyUser("Connect failed with error: " + exception->Message);
            
            clientSocket->Close();
            clientSocket = null;
        }
    });
    // Add code to send and receive data using the clientSocket
    // Then close the clientSocket when done

使用 UpgradeToSslAsync

代码使用 UpgradeToSslAsync 时,它会首先建立与网络服务的连接,而无需加密。 应用可以发送或接收某些数据,然后升级连接以使用 SSL/TLS 进行所有进一步通信。

UpgradeToSslAsync 方法采用两个参数。 protectionLevel 参数指示所需的保护级别。 validationHostName 参数是远程网络目标的主机名,用于升级到 SSL 时进行验证。 通常,validationHostName 会是应用最初用来建立连接时所使用的同一主机名。 如果 protectionLevel 参数设置为Windows。System.Socket.SocketProtectionLevel.Ssl 在调用 UpgradeToSslAsync 时,StreamSocket 必须使用 SSL/TLS 对套接字上的进一步通信进行加密。 此值需要加密,并且绝不允许使用 NULL 密码。

用于 UpgradeToSslAsync 方法的正常序列如下所示:

以下示例创建 StreamSocket,尝试建立与网络服务的连接,发送一些初始数据,然后协商使用 SSL/TLS。 如果协商成功,则客户端与网络服务器之间使用 StreamSocket 的所有网络通信都将加密。

using Windows.Networking;
using Windows.Networking.Sockets;
using Windows.Storage.Streams;

    // Define some variables and set values
    StreamSocket clientSocket = new StreamSocket();
 
    HostName serverHost = new HostName("www.contoso.com");
    string serverServiceName = "http";

    // For simplicity, the sample omits implementation of the
    // NotifyUser method used to display status and error messages 

    // Try to connect to contoso using HTTP (port 80)
    try {
        // Call ConnectAsync method with a plain socket
        await clientSocket.ConnectAsync(serverHost, serverServiceName, SocketProtectionLevel.PlainSocket);

        NotifyUser("Connected");

    }
    catch (Exception exception) {
        // If this is an unknown status it means that the error is fatal and retry will likely fail.
        if (SocketError.GetStatus(exception.HResult) == SocketErrorStatus.Unknown) {
            throw;
        }

        NotifyUser("Connect failed with error: " + exception.Message, NotifyType.ErrorMessage);
        // Could retry the connection, but for this simple example
        // just close the socket.

        clientSocket.Dispose();
        clientSocket = null; 
        return;
    }

    // Now try to send some data
    DataWriter writer = new DataWriter(clientSocket.OutputStream);
    string hello = "Hello, World! ☺ ";
    Int32 len = (int) writer.MeasureString(hello); // Gets the UTF-8 string length.
    writer.WriteInt32(len);
    writer.WriteString(hello);
    NotifyUser("Client: sending hello");

    try {
        // Call StoreAsync method to store the hello message
        await writer.StoreAsync();

        NotifyUser("Client: sent data");

        writer.DetachStream(); // Detach stream, if not, DataWriter destructor will close it.
    }
    catch (Exception exception) {
        NotifyUser("Store failed with error: " + exception.Message);
        // Could retry the store, but for this simple example
            // just close the socket.

            clientSocket.Dispose();
            clientSocket = null; 
            return;
    }

    // Now upgrade the client to use SSL
    try {
        // Try to upgrade to SSL
        await clientSocket.UpgradeToSslAsync(SocketProtectionLevel.Ssl, serverHost);

        NotifyUser("Client: upgrade to SSL completed");
           
        // Add code to send and receive data 
        // The close clientSocket when done
    }
    catch (Exception exception) {
        // If this is an unknown status it means that the error is fatal and retry will likely fail.
        if (SocketError.GetStatus(exception.HResult) == SocketErrorStatus.Unknown) {
            throw;
        }

        NotifyUser("Upgrade to SSL failed with error: " + exception.Message);

        clientSocket.Dispose();
        clientSocket = null; 
        return;
    }
#include <winrt/Windows.Networking.Sockets.h>
#include <winrt/Windows.Storage.Streams.h>

using namespace winrt;
using namespace Windows::Storage::Streams;
...
    // Define some variables, and set values.
    Windows::Networking::Sockets::StreamSocket clientSocket;

    Windows::Networking::HostName serverHost{ L"www.contoso.com" };
    winrt::hstring serverServiceName{ L"https" };

    // For simplicity, the sample omits implementation of the
    // NotifyUser method used to display status and error messages. 

    // Try to connect to the server using HTTP (port 80).
    try
    {
        co_await clientSocket.ConnectAsync(serverHost, serverServiceName, Windows::Networking::Sockets::SocketProtectionLevel::PlainSocket);
        NotifyUser(L"Connected");
    }
    catch (winrt::hresult_error const& exception)
    {
        NotifyUser(L"Connect failed with error: " + exception.message());
        clientSocket = nullptr;
    }

    // Now, try to send some data.
    DataWriter writer{ clientSocket.OutputStream() };
    winrt::hstring hello{ L"Hello, World! ☺ " };
    uint32_t len{ writer.MeasureString(hello) }; // Gets the size of the string, in bytes.
    writer.WriteInt32(len);
    writer.WriteString(hello);
    NotifyUser(L"Client: sending hello");

    try
    {
        co_await writer.StoreAsync();
        NotifyUser(L"Client: sent hello");

        writer.DetachStream(); // Detach the stream when you want to continue using it; otherwise, the DataWriter destructor closes it.
    }
    catch (winrt::hresult_error const& exception)
    {
        NotifyUser(L"Store failed with error: " + exception.message());
        // We could retry the store operation. But, for this simple example, just close the socket by setting it to nullptr.
        clientSocket = nullptr;
        co_return;
    }

    // Now, upgrade the client to use SSL.
    try
    {
        co_await clientSocket.UpgradeToSslAsync(Windows::Networking::Sockets::SocketProtectionLevel::Tls12, serverHost);
        NotifyUser(L"Client: upgrade to SSL completed");

        // Add code to send and receive data using the clientSocket,
        // then set the clientSocket to nullptr when done to close it.
    }
    catch (winrt::hresult_error const& exception)
    {
        // If this is an unknown status, then the error is fatal and retry will likely fail.
        Windows::Networking::Sockets::SocketErrorStatus socketErrorStatus{ Windows::Networking::Sockets::SocketError::GetStatus(exception.to_abi()) };
        if (socketErrorStatus == Windows::Networking::Sockets::SocketErrorStatus::Unknown)
        {
            throw;
        }

        NotifyUser(L"Upgrade to SSL failed with error: " + exception.message());
        // We could retry the store operation. But for this simple example, just close the socket by setting it to nullptr.
        clientSocket = nullptr;
        co_return;
    }
using Windows::Networking;
using Windows::Networking::Sockets;
using Windows::Storage::Streams;

    // Define some variables and set values
    StreamSocket^ clientSocket = ref new StreamSocket();
 
    HostName^ serverHost = ref new HostName("www.contoso.com");
    String^ serverServiceName = "http";

    // For simplicity, the sample omits implementation of the
    // NotifyUser method used to display status and error messages 

    // Try to connect to contoso using HTTP (port 80)
    task<void>(clientSocket->ConnectAsync(serverHost, serverServiceName, SocketProtectionLevel::PlainSocket)).then([this] (task<void> previousTask) {
        try
        {
            // Try getting all exceptions from the continuation chain above this point.
            previousTask.Get();
            NotifyUser("Connected");
        }
        catch (Exception^ exception)
        {
            NotifyUser("Connect failed with error: " + exception->Message);
 
            clientSocket->Close();
            clientSocket = null;
        }
    });
       
    // Now try to send some data
    DataWriter^ writer = ref new DataWriter(clientSocket->OutputStream);
    String^ hello = "Hello, World! ☺ ";
    Int32 len = (int) writer->MeasureString(hello); // Gets the UTF-8 string length.
    writer->WriteInt32(len);
    writer->WriteString(hello);
    NotifyUser("Client: sending hello");

    task<void>(writer->StoreAsync()).then([this] (task<void> previousTask) {
        try {
            // Try getting all exceptions from the continuation chain above this point.
            previousTask.Get();

            NotifyUser("Client: sent hello");

            writer->DetachStream(); // Detach stream, if not, DataWriter destructor will close it.
       }
       catch (Exception^ exception) {
               NotifyUser("Store failed with error: " + exception->Message);
               // Could retry the store, but for this simple example
               // just close the socket.
 
               clientSocket->Close();
               clientSocket = null;
               return;
       }
    });

    // Now upgrade the client to use SSL
    task<void>(clientSocket->UpgradeToSslAsync(SocketProtectionLevel::Ssl, serverHost)).then([this] (task<void> previousTask) {
        try {
            // Try getting all exceptions from the continuation chain above this point.
            previousTask.Get();

           NotifyUser("Client: upgrade to SSL completed");
           
           // Add code to send and receive data 
           // Then close clientSocket when done
        }
        catch (Exception^ exception) {
            // If this is an unknown status it means that the error is fatal and retry will likely fail.
            if (SocketError.GetStatus(exception.HResult) == SocketErrorStatus.Unknown) {
                throw;
            }

            NotifyUser("Upgrade to SSL failed with error: " + exception.Message);

            clientSocket->Close();
            clientSocket = null; 
            return;
        }
    });

创建安全的 WebSocket 连接

与传统套接字连接一样,使用 Windows 应用的 StreamWebSocket 和 MessageWebSocket 功能时,还可以使用传输层安全性(TLS)/安全套接字层(SSL)加密 WebSocket 连接。 在大多数情况下,需要使用安全的 WebSocket 连接。 这会增加连接成功的可能性,因为许多代理将拒绝未加密的 WebSocket 连接。

有关如何创建或升级到网络服务的安全套接字连接的示例,请参阅 如何使用 TLS/SSL 保护 WebSocket 连接

除了 TLS/SSL 加密之外,服务器可能还需要 Sec-WebSocket-Protocol 标头值才能完成初始握手。 此值由 StreamWebSocketInformation.ProtocolMessageWebSocketInformation.Protocol 属性表示,指示连接的协议版本,并使服务器能够正确解释打开的握手和之后交换的数据。 使用此协议信息,如果服务器无法以安全的方式解释传入数据,则可以关闭连接。

如果来自客户端的初始请求不包含此值,或者提供与服务器预期的值不匹配的值,则预期值将从服务器发送到 WebSocket 握手错误的客户端。

身份验证

如何在通过网络进行连接时提供身份验证凭据。

使用 StreamSocket 类提供客户端证书

Windows。Networking.Sockets.StreamSocket 类支持使用 SSL/TLS 对应用正在通信的服务器进行身份验证。 在某些情况下,应用还需要使用 TLS 客户端证书向服务器进行身份验证。 在Windows 10中,可以在 StreamSocket.Control 对象上提供客户端证书(必须在 TLS 握手启动之前设置)。 如果服务器请求客户端证书,Windows将使用提供的证书进行响应。

下面是一个代码片段,演示如何实现此代码片段:

var socket = new StreamSocket();
Windows.Security.Cryptography.Certificates.Certificate certificate = await GetClientCert();
socket.Control.ClientCertificate = certificate;
await socket.ConnectAsync(destination, SocketProtectionLevel.Tls12);

向 Web 服务提供身份验证凭据

使应用能够与安全 Web 服务交互的网络 API 提供了自己的方法来初始化客户端,或者使用服务器和代理身份验证凭据设置请求标头。 每个方法均使用 PasswordCredential 对象进行设置,该对象指示使用这些凭据的用户名、密码和资源。 下表提供了这些 API 的映射:

WebSockets MessageWebSocketControl.ServerCredential
MessageWebSocketControl.ProxyCredential
StreamWebSocketControl.ServerCredential
StreamWebSocketControl.ProxyCredential
后台传输 BackgroundDownloader.ServerCredential
BackgroundDownloader.ProxyCredential
BackgroundUploader.ServerCredential
BackgroundUploader.ProxyCredential
联合 SyndicationClient(PasswordCredential)
SyndicationClient.ServerCredential
SyndicationClient.ProxyCredential
AtomPub AtomPubClient(PasswordCredential)
AtomPubClient.ServerCredential
AtomPubClient.ProxyCredential

处理网络异常

在大多数编程领域,异常表示程序存在一些缺陷导致的重大问题或失败。 在网络编程中,有一个额外的例外来源:网络本身和网络通信的性质。 网络通信本质上是不可靠的,容易出现意外故障。 对于应用使用网络的每个方式,必须维护一些状态信息;应用代码必须通过更新该状态信息并启动适当的逻辑来处理网络异常,以便应用重新建立或重试通信失败。

当通用Windows应用引发异常时,异常处理程序可以检索有关异常原因的更详细信息,以便更好地了解失败并做出适当的决策。

每个语言投影都支持一种方法来访问此更详细的信息。 在通用 Windows 应用中,异常会显示为 HRESULT 值。 Winerror.h 包含文件中列出了一长串可能的 HRESULT 值,其中包括网络错误。

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

  • 某些 API 提供了一种帮助程序方法,该方法将 HRESULT 值从异常转换为枚举值。
  • 其他 API 提供检索实际 HRESULT 值的方法。