网络基础知识

对于任何已启用网络的应用,必须执行的操作。

能力

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

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

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

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

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

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

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

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

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

为应用提供后台任务支持 包含有关在应用不在前台时使用后台任务执行工作的常规信息。 更具体地说,当代码不是当前前台应用且数据通过网络到达时,必须采取特殊步骤来通知它。 在 Windows 8 中,你已将控制通道触发器用于此目的,并且 Windows 10 中仍支持它们。 有关使用控制通道触发器的完整信息可在 此处获得。 Windows 10 中的新技术为某些场景(例如推送支持的流套接字)提供更佳的功能,并降低开销,包括套接字代理和套接字活动触发器。

如果你的应用使用 DatagramSocketStreamSocketStreamSocketListener,则应用可以将开放套接字的所有权转让给系统提供的套接字代理,然后离开前台,甚至终止。 当在已转移的套接字上建立连接或有流量到达该套接字时,你的应用程序或其指定的后台任务将被激活。 如果应用没有在运行,它就会被启动。 然后,套接字代理通过 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 = new ref StreamSocket();
 
    HostName^ serverHost = new ref 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 与应用最初建立连接的主机名相同。 如果在调用 UpgradeToSslAsync时,protectionLevel 参数设置为 Windows.System.Socket.SocketProtectionLevel.Ssl,则 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 = new ref StreamSocket();
 
    Hostname^ serverHost = new ref 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 = new ref 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(clientSocket.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 连接

与传统的套接字连接一样,使用 UWP 应用的 StreamWebSocketMessageWebSocket 功能时,WebSocket 连接还可以使用传输层安全性(TLS)/安全套接字层(SSL)进行加密。 在大多数情况下,需要使用安全的 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 的映射:

WebSocket 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 值的方法。
  • Windows 10 中的 网络 API 改进