网络通信
- 1. Socket通信
-
- (1) 基于UDP协议的通信
- (2) 通过TCP协议传输数据
- 2. HTTP协议通信
- 3. 后台传输
- 4. 推送通知
- 5. 访问RSS资源
- 6. 扫描Wi-Fi网络
1. Socket通信
在Windows.Networking.Sockets命名空间下提供了支持Socket通信相关的类型。有趣的是,这些类型的命名中并没有出现如TCP、UDP等关键词,官方似乎有意避开这些传统的命名方式,而是按照各通信协议的功能来命名。可参考如下:
- DatagramSocket ——用UDP协议的Socket网络通信
- StreamSocket —— 通过流方式接收/发送网络数据,实际上是基于TCP协议的Socket通信。在服务器端,可以使用StreamSocketListener来监听连接请求
- MessageWebSocket与StreamWebSocket —— 使用WebSocket相关技术进行网络通信
(1) 基于UDP协议的通信
DatagramSocket类封装了基于UDP数据报相关的网络通信功能。由于UDP协议是无连接协议,资源消耗较少,处理效率高,经常被用于传输要求不太严格的数据,如聊天信息、网络视频等。
在服务器端,DatagramSocket类的使用步骤如下:
- 创建DatagramSocket实例。
- 处理MessageReceived事件,当收到新的消息时会引发该事件。
- 调用BindEndpointAsync方法绑定本地终结点,包括地址和端口;如果希望在本地任何地址上都监听到数据,可以调用BindServiceNameAsync方法,该方法仅仅绑定本地端口。
而在客户端,直接实例化一个DatagramSocket对象后,就可以直接通过GetOutputStreamAsync方法获取一个输出流对象以向远程主机发送数据(直接将数据写入该流即可),在调用时应指定远程主机的地址(IP地址或主机名)和接收端口。
接下来通过一个示例来演示UDP协议通信的应用方法。本示例既可以作为监听服务器,也可以作为客户端使用,因此可以在同一台机器上进行测试。本示例实现客户端将文本消息发送到服务器,服务器将显示收到的文本消息。
在页面布局中,可以使用Pivot控件来添加两个选项卡,一个用于服务器,另一个用于客户端。
<Pivot>
<PivotItem Header="服务器">
<Grid RowSpacing="10">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel>
<TextBlock Text="在客户端输入以下IP地址:" FontSize="20"/>
<TextBlock x:Name="tbIp" FontSize="36" IsTextSelectionEnabled="True"/>
</StackPanel>
<ListView x:Name="lvMsg" Grid.Row="1">
<ListView.Header>
<TextBlock Foreground="LightPink" Text="收到的消息列表" FontSize="20"/>
</ListView.Header>
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Margin="3,12">
<TextBlock FontSize="20" Foreground="Yellow">
来自
<Run Text="{Binding Path=FromIP}"/>
的消息:
</TextBlock>
<TextBlock TextWrapping="Wrap" FontSize="24" Text="{Binding Path=Message}"/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</PivotItem>
<PivotItem Header="客户端">
<StackPanel Spacing="10">
<TextBox x:Name="txtServer" Header="服务器IP: "/>
<TextBox x:Name="txtMessage" Header="消息内容: " TextWrapping="Wrap" Height="160"/>
<Button HorizontalAlignment="Center" Content="发送" Tapped="Button_Tapped"/>
</StackPanel>
</PivotItem>
</Pivot>
为了在测试应用程序时能够轻松得知服务器的IP地址,可以获取本地网络的显示名称,并显示在界面上。在页面重写的OnNavigatedTo方法中加入以下代码:
protected async override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
//显示服务器的IP
var hosts = NetworkInformation.GetHostNames();
HostName svName = hosts.FirstOrDefault(h => h.Type == HostNameType.Ipv4 &&
h.IPInformation.NetworkAdapter.IanaInterfaceType == 6);
if (svName != null)
{
tbIp.Text = svName.DisplayName;
}
//...
}
GetHostNames方法会返回多个主机名,随后通过FirstOrDefault扩展方法将设备中的以太网卡连接筛选出来。h.Type == HostNameType.Ipv4表示只选出v4版本的IP地址;NetworkAdapter.IanaInterfaceType属性类型是一个整数值,此处数值6表示以太网卡,如果希望使用本地无线网卡进行通信,可以使用数值71进行筛选。
服务器所使用的监听端口号将通过一个常量值来固定:
/// <summary>
/// 用于接收数据的端口
/// </summary>
const string SERVICE_PORT = "795";
在类级别中声明必要的变量,主要是两个用于通信的Socket对象,一个用于服务器,另一个用于客户端。
/// <summary>
/// 用于服务器的Socket
/// </summary>
DatagramSocket svrSocket = null;
/// <summary>
/// 用于客户端的Socket
/// </summary>
在重写的OnNavigatedTo方法中添加服务器监听实现:
protected async override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
//显示服务器的IP
var hosts = NetworkInformation.GetHostNames();
HostName svName = hosts.FirstOrDefault(h => h.Type == HostNameType.Ipv4 &&
h.IPInformation.NetworkAdapter.IanaInterfaceType == 6);
if (svName != null)
{
tbIp.Text = svName.DisplayName;
}
svrSocket = new DatagramSocket();
// 添加接收消息事件处理
svrSocket.MessageReceived += SerSocket_Received;
// 绑定到本地端口
await svrSocket.BindServiceNameAsync(SERVICE_PORT);
clientSocket = new DatagramSocket();
}
处理DatagramSocket对象的MessageReceived事件,显示收到的消息。
async void SerSocket_Received(DatagramSocket sender,DatagramSocketMessageReceivedEventArgs args)
{
// 获取相关的DataReader对象
DataReader reader = args.GetDataReader();
// 读取消息内容
uint len = reader.UnconsumedBufferLength;
string msg = reader.ReadString(len);
// 远程主机
string remoteHost = args.RemoteAddress.DisplayName;
reader.Dispose();
// 显示接收到的消息
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal,
()=>
{
lvMsg.Items.Add(new { FromIP = remoteHost, Message = msg});
});
}
下面代码实现客户端向服务器发送消息:
private async void Button_Tapped(object sender, TappedRoutedEventArgs e)
{
// 获取输出流
using (var outStream = await clientSocket.GetOutputStreamAsync(new HostName(txtServer.Text),SERVICE_PORT))
{
using (var writer = new DataWriter(outStream))
{
// 写入流
writer.WriteString(txtMessage.Text);
//提交写入的内容
await writer.StoreAsync();
}
}
}
在获取到输出流后,可以使用DataWriter类来向流对象写入数据,使用该类所封装的WriteString方法可以直接写入字符串内容。默认情况下是使用UTF-8编码格式,一般不需要修改。
要注意的是,在写完数据后,要调用StoreAsync方法,被写入的数据才会提交到输出流中。
示例运行效果:UDP
使用UDP的另一篇博客,可参考:添加链接描述
(2) 通过TCP协议传输数据
与UDP不同,TCP协议是基于连接的,即在通信之前,客户端需要连接服务器。这意味着TCP对数据的次序与完整性要求更为严格,确保数据准确无误地达到目标终端。例如,文件传输就应当使用TCP协议来完成,因为少一个字节或者多一个字节都有可能损坏文件。
StreamSocket类封装了基于TCP协议的Socket通信功能,在客户端,通常遵循以下顺序来使用StreamSocket类:
- 实例化StreamSocket对象。
- 调用ConnectAsync方法连接服务器。
- 通过OutputStream属性返回的输出流就可以发送数据;而InputStream属性则返回输入流对象,用于接收数据。
- 当不再使用Socket时,调用Dispose方法释放其占用的相关资源。
在服务器中,则需要一个StreamSocketListener实例,绑定本地主机或某个端口,以监听客户端的连接请求。当有客户端发出连接请求时,会引发ConnectionReceived事件,从事件参数中可以获取一个与客户端进行通信的StreamSocket实例。
下面示例将演示如何使用基于TCP协议的Socket通信。
本例将服务器与客户端合并在一个应用程序中,应用程序既可以充当服务器角色,也可以用作客户端。在客户端选择一个图像文件并输入一些文本,应用程序会将图像与文本一起发送到服务器。随即服务器会显示收到的数据。
页面布局XAML如下:
<Pivot>
<PivotItem Header="服务器">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel Spacing="10">
<TextBlock Text="服务器IP地址:" Height="30"/>
<TextBlock x:Name="tbSvIP" FontSize="24" IsTextSelectionEnabled="True" Height="30"/>
</StackPanel>
<ListBox x:Name="lbItems" Grid.Row="1">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Image Width="50" Height="50" Stretch="UniformToFill" Source="{Binding Path=Image}"/>
<TextBlock Grid.Column="1" TextWrapping="Wrap" FontSize="18" Text="{Binding Path=Text}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</PivotItem>
<PivotItem Header="客户端">
<StackPanel Spacing="10">
<TextBox x:Name="txtServerIp" Header="服务器地址:"/>
<Button Content="选择图像..." Tapped="OnPickImageFile"/>
<TextBox x:Name="txtContent" Header="说明文本:" Height="120"/>
<Button Content="发送" HorizontalAlignment="Stretch" Tapped="OnSend"/>
</StackPanel>
</PivotItem>
</Pivot>
在后台代码中声明StreamSocketListener实例,并处理ConnectionReceived事件:
protected async override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
//显示服务器的IP
var hosts = NetworkInformation.GetHostNames();
HostName svName = hosts.FirstOrDefault(h => h.Type == HostNameType.Ipv4 &&
h.IPInformation.NetworkAdapter.IanaInterfaceType == 6);
if (svName != null)
{
tbSvIP.Text = svName.DisplayName;
}
//实例化StreamSocketListener对象
listener = new StreamSocketListener();
//添加ConnectionReceived事件处理程序
listener.ConnectionReceived += Listener_ConnectionReceived;
//绑定本地端口
await listener.BindServiceNameAsync(LISTEN_PORT);
}
BindServiceNameAsync方法将监听器绑定到一个本地端口,只要发送到该端口的客户端连接都会被接收。
当不再需要StreamSocketListener对象时,可以通过以下代码将其释放:
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
listener?.Dispose();
listener = null;
}
以下代码处理ConnectionReceived事件:
async void Listener_ConnectionReceived(StreamSocketListener sender, StreamSocketListenerConnectionReceivedEventArgs args)
{
string text = string.Empty;
IRandomAccessStream imgStream = new InMemoryRandomAccessStream();
//处理接收到的连接
using (var socket = args.Socket)
{
using (DataReader reader = new DataReader(socket.InputStream))
{
try
{
//读出第一个整数,表示图片文件长度
await reader.LoadAsync(sizeof(uint));
uint len = reader.ReadUInt32();
await reader.LoadAsync(len);
IBuffer buffer = reader.ReadBuffer(len);
//写入内存流
await imgStream.WriteAsync(buffer);
await reader.LoadAsync(sizeof(uint));
//读出字符串的长度
len = reader.ReadUInt32();
//读出字符串内容
if (len > 0)
{
await reader.LoadAsync(len);
text = reader.ReadString(len);
}
}
catch (Exception ex)
{
await new MessageDialog(ex.Message).ShowAsync();
}
}
//显示接收到的内容
await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
{
BitmapImage bmp = new BitmapImage();
bmp.DecodePixelWidth = 50;
imgStream.Seek(0);
await bmp.SetSourceAsync(imgStream);
lbItems.Items.Add(new { Image = bmp, Text = text });
});
}
}
使用DataReader类读取数据时要先用LoadAsync方法从关联的输入流中加载一部分数据,然后再读取。数据的读取过程为:先读出图像文件的大小,然后再根据这个大小值读出图像文件;随后再读出字符串的长度,最后根据长度将字符串读出。
下面代码用于发送数据:
IBuffer bufferImg;
private async void OnPickImageFile(object sender, TappedRoutedEventArgs e)
{
FileOpenPicker picker = new FileOpenPicker();
picker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
picker.FileTypeFilter.Add(".jpg");
var imgFile = await picker.PickSingleFileAsync();
if (imgFile != null)
{
bufferImg = await FileIO.ReadBufferAsync(imgFile);
return;
}
bufferImg = null;
}
private async void OnSend(object sender, TappedRoutedEventArgs e)
{
if (txtServerIp.Text.Length == 0)
{
return;
}
if (bufferImg is null)
{
await new MessageDialog("请选择图像。").ShowAsync();
return;
}
var btn = sender as Button;
btn.IsEnabled = false;
using (StreamSocket socket = new StreamSocket())
{
try
{
//发起连接
await socket.ConnectAsync(new HostName(txtServerIp.Text),LISTEN_PORT);
//准备发送数据
using (DataWriter writer = new DataWriter(socket.OutputStream))
{
//首先写入图像数据
uint len = bufferImg.Length;
writer.WriteUInt32(len);
writer.WriteBuffer(bufferImg);
//接着写入文本
if (txtContent.Text.Length == 0)
{
writer.WriteUInt32(0);
}
else
{
len = writer.MeasureString(txtContent.Text);
writer.WriteUInt32(len);
writer.WriteString(txtContent.Text);
}
await writer.StoreAsync();
}
}
catch (Exception ex)
{
await new MessageDialog(ex.Message).ShowAsync();
}
}
btn.IsEnabled = true;
}
发送数据时,可以通过DataWriter对象把内容写入输出流。过程是:先写入图像的文件长度,随后写入图像文件的内容;紧接着,写入要发送文本的长度,最后才写入文本内容。这样做是为让服务器在接收数据时能够准确地对数据进行分割。因为数据都是以字节的形式进行传输的,图像文件的内容和文本内容在发送时会被合并到一起,如果不事先写入每段数据的大小的话,在服务器接收到数据后就无法判断哪些字节是属于图像文件,哪些字节是属于文本的。
示例运行效果:TCP
2. HTTP协议通信
在网络通信技术中,除了使用Socket外,还可以通过HTT协议直接访问Web服务器上的内容,如从服务器获取内容或者提交内容到服务器等。
在Windows.Web.Http命名空间下,公开了一系列支持各种HTTP访问的类型。其中,主要的组件是HttpClient类,使用该类可以向指定URI发出HTTP请求,并获取从服务器返回的数据。无论是采取何种请求方式(比较常用的是GET和POST),HTTP访问过程都是遵循“一问一答”模式。客户端应用程序向指定的URI发出请求可以理解为向服务器“提出问题”,HTTP服务器在收到请求后会进行处理,最后会向客户端发出一条回复消息,可以理解为服务器“回答问题”。
对于要发送到服务器的数据,可以通过以下几种类型来封装(这些类型都实现IHttpContent接口):
- HttpStringContent : 直接将字符串作为内容发送。
- HttpBufferContent : 将缓冲区(IBuffer)内的数据发送到服务器。
- HttpStreamContent : 数据以流的形式发送,如发送文件。
- HttpFromUrlEncodedContent : 发送application/x-www-from-urlencoded MIME格式的数据,通常以POST方式提交。
- HttpMultipartContent 或 HttpMultipartFromDataContent : 主要用于包装由多个部分组成的内容(multipart/* MIME或者multipart/from-data MIME格式)。例如发表一条含有图片的新微博。
下面示例将演示使用HttpClient类以GET方式请求网络上的图像文件,并将获取到的图像显示在页面上。
页面布局XAML如下:
<StackPanel Spacing="5">
<TextBox x:Name="txtUri" Header="请输入图像URI:"/>
<Button Content="获取图像" Tapped="OnGetImage"/>
<Image x:Name="img" Height="300" Stretch="Uniform"/>
</StackPanel>
在后台代码中,处理Button的Tapped事件处理程序,从Web服务器上获取网络图像。
private async void OnGetImage(object sender, TappedRoutedEventArgs e)
{
using (HttpClient client = new HttpClient())
{
try
{
//直接向服务器发送GET请求
HttpResponseMessage response = await client.GetAsync(new Uri(txtUri.Text));
//如果请求成功
if (response != null && response.StatusCode == HttpStatusCode.Ok)
{
//显示获得的图像
BitmapImage bmp = new BitmapImage();
bmp.DecodePixelWidth = 400;
using (IRandomAccessStream stream = new InMemoryRandomAccessStream())
{
var str = response.Content.WriteToStreamAsync(stream);
stream.Seek(0);//将读取位置设置为流的开始处
await bmp.SetSourceAsync(stream);
}
img.Source = bmp;
}
}
catch (Exception)
{
}
}
}
运行应用程序后,在文本框中输入一个网路图像的URI,然后单击下方的按钮,应用程序会下载并显示图像。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIiclRnblN2XjlGcjAzNfRHLGZkRGZkRfJ3bs92YsYTMfVmepNHL3tGVNRTQ65EeRpHW4Z0MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL0cDNyEDNwgTM5IjMxAjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
3. 后台传输
后台传输一般用于上传或者下载体积较大的数据,如下载或上传文件。要传输较小的数据,直接运用Socket或者HTTP传输即可。即使应用程序已退出,传输仍可以在后台进行(除非显示取消传输或者出现错误)。而当应用程序被卸载时会连同与之关联的后台任务一并删除。
后台传输分为两类:上传和下载。相关的类型位于Windows.Networking.BackgroundTransfer命名空间下。
对于上传操作,可以参考以下过程来实现:
- 实例化BackgroundUploader对象。
- 调用CreateUpload或者CreateUploadAsync方法创建一个UploadOperation对象实例,并从相应方法返回。这两种方法在调用时通常需要指定目标URI(要上传到的服务器地址)及要上传的文件。如果希望上传多个文件,可以调用带有可以传递BackgroundTransferContentPart类型列表的重载版本。
- 调用上一步中获得的UploadOperation对象的StartAsync方法就可以开始传输了。如果该传输任务已经在运行,则调用AttachAsync方法重新附加到当前应用程序中即可。
要在后台下载内容,则可以参考以下过程:
- 实例化BackgroundDownloader对象。
- 调用CreateDownload或者CreateDownloadAsync方法创建DownloadOperation实例。
- 调用DownloadOperation实例的StartAsync方法开始传输;如果任务已经在运行,可以调用AttachAsync方法重新附加到当前应用程序。
接下来,将通过一个示例来演示后台传输的使用方法。本例将实现从客户端将文件上传到服务器程序。为了便于测试,服务器通过一个Windows控制台应用程序(.Net Framework)承载WCF服务来实现。
首先,实现服务器应用程序。服务协定定义如下:
[ServiceContract]
public interface IService
{
[OperationContract,WebInvoke(UriTemplate="/upload")]
void UploadFile(Stream content);
}
UploadFile方法必须声明为具有一个Stream类型参数的服务方法,从客户端上传的文件内容会传递给该参数,在服务类的实现代码中,将从该参数中读取客户端上传的文件内容。
下面代码实现服务协定:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
public class Service : IService
{
public async void UploadFile(Stream content)
{
string fileName = string.Empty;
IncomingWebRequestContext request = WebOperationContext.Current.IncomingRequest;
//从标头获取文件名
fileName = request.Headers["fileName"];
if (string.IsNullOrEmpty(fileName))
{
Guid g = Guid.NewGuid();
fileName = g.ToString();
}
//开始接收文件
try
{
//获取用户文档库位置
string docPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
string newFilePath = Path.Combine(docPath, fileName);
//如果文件存在,将其删除
if (File.Exists(newFilePath))
{
File.Delete(newFilePath);
}
using (FileStream outputStream = File.OpenWrite(newFilePath))
{
//从客户端上传的流中将数据复制到文件流中
await content.CopyToAsync(outputStream);
}
Console.WriteLine($"在{DateTime.Now.ToLongTimeString()}成功接收文件。");
//向客户端发送回应消息
//WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HttpStatusCode.OK;
}
catch (Exception ex)
{
//处理错误
Console.WriteLine(ex.Message);
//WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HttpStatusCode.InternalServerError;
//WebOperationContext.Current.OutgoingResponse.StatusDescription = ex.Message;
}
}
}
文件名通过一个名为fileName的HTTP标头来传递,该标头是自定义的,因此在客户端开始上传文件时,需要指定该HTTP标头。数据的读取过程比较简单,就是直接将参数传递进来的流中的内容复制到文件流即可,文件最终保存到"文档"目录下。
服务类编写完成后,在Main入口点处启动WCF服务以接收客户端上传的数据。
class Program
{
static void Main(string[] args)
{
Uri localUri = new Uri("<服务基址>");
//开始运行WCF服务
WebServiceHost host = new WebServiceHost(typeof(Service), localUri);
//配置缓冲区的最大值
WebHttpBinding binding = new WebHttpBinding();
binding.MaxReceivedMessageSize = 500 * 1024 * 1024;
host.AddServiceEndpoint(typeof(IService), binding, "");
host.Opened += (a, b) => Console.WriteLine($"服务已启动。\n上传地址:{localUri}upload");
host.Closed += (a, b) => Console.WriteLine("closed服务已关闭。");
try
{
//打开服务
host.Open();
Console.WriteLine(host.State);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Task.Delay(30000).Wait(); //30s后关闭服务
host.Close();
Console.ReadLine();
}
}
应该将上面的<服务基址>替换为主机的真实地址,如192.168.1.12:8800,其中8800是端口号。在打开服务之前,需要修改WebHttpBinding类的MaxReceivedMessageSize属性,以字节为单位,可以依实际情况设置,因为默认的值太小,体积较大的文件无法接收,因此必须手动更改该属性的值。
下面将实现客户端。
页面布局XAML如下:
<StackPanel Spacing="10">
<TextBox x:Name="txtUploadUri" Header="请输入上传URI: "/>
<Button x:Name="btnUpload" Content="选择文件并开始上传" Tapped="btnUpload_Tapped"/>
<ProgressBar x:Name="pb" Maximum="100" Minimum="0" Foreground="Yellow" Height="15"/>
<TextBlock x:Name="tbMsg" FontSize="20" TextWrapping="Wrap"/>
</StackPanel>
当导航进入页面后,应先检查一下是否有后台传输任务正在进行,如果有,则加入到当前程序中。
protected async override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
var uploadTasks = await BackgroundUploader.GetCurrentUploadsAsync();
if (uploadTasks.Count > 0)
{
UploadOperation uploadOpt = uploadTasks[0];
await SetUpload(uploadOpt,false);
}
}
调用BackgroundUploader类的GetCurrentUploadsAsync静态方法,可以获取当前正在执行的传输列表。
当按钮被按下后,会通过文件选择器让用户选择一个文件,然后开始上传。
private async void btnUpload_Tapped(object sender, TappedRoutedEventArgs e)
{
FileOpenPicker picker = new FileOpenPicker();
picker.SuggestedStartLocation = PickerLocationId.ComputerFolder;
picker.FileTypeFilter.Add("*");//表示所有文件类型
var file = await picker.PickSingleFileAsync();
if (file != null)
{
BackgroundUploader uploader = new BackgroundUploader();
//设置文件名
uploader.SetRequestHeader("fileName", file.Name);
uploader.Method = "POST";
//创建上传任务
UploadOperation uploadOpt = uploader.CreateUpload(new Uri(txtUploadUri.Text.Trim()), file);
await SetUpload(uploadOpt,true);
}
}
以上多段代码都调用了SetUpload方法,该方法为自定义方法,由于开始传输任务与附加传输任务的操作相似,故而统一封装到SetUpload方法中。代码如下:
/// <summary>
/// 执行后台上传操作
/// </summary>
/// <param name="opr">操作后台传输任务的对象</param>
/// <param name="starting">是否为新的上传任务</param>
async Task SetUpload(UploadOperation opr,bool starting)
{
//当上传进度更新时能收到报告
IProgress<UploadOperation> progressReporter = new Progress<UploadOperation>(OnProgressHandler);
//启动或附加任务
try
{
if (starting)
{
await opr.StartAsync().AsTask(progressReporter);
}
else
{
await opr.AttachAsync().AsTask(progressReporter);
}
}
catch (Exception ex)
{
var state = BackgroundTransferError.GetStatus(ex.HResult);
ShowMessage($"错误:{state}");
}
}
AsTask方法可以将异步操作转换为基于Task的操作方式,并通过Progress类来接收进度更新。该Progress实例绑定一个OnProgressHandler方法以响应传输进度更新,实现代码如下:
void OnProgressHandler(UploadOperation p)
{
BackgroundUploadProgress progress = p.Progress;
switch (progress.Status)
{
case BackgroundTransferStatus.Canceled:
ShowMessage("任务已取消。");
pb.Value = 0d;
break;
case BackgroundTransferStatus.Completed:
ShowMessage("任务已完成。");
pb.Value = 0d;
break;
case BackgroundTransferStatus.Error:
ShowMessage("发生错误。");
pb.Value = 0d;
break;
case BackgroundTransferStatus.Running:
double ps = progress.BytesSent * 100 / progress.TotalBytesToSend;
pb.Value = ps;
ShowMessage($"已上传{progress.BytesSent}字节,共{progress.TotalBytesToSend}字节。");
break;
default:
break;
}
}
BackgroundUploadProgress结构封装与处理进度相关的成员,例如已经上传了多少个字节、总共要上传多少个字节及当前状态等。使用这些成员可以实时显示上传的进度。
最后是一个辅助方法,显示提示消息:
void ShowMessage(string msg)
{
tbMsg.Text = msg;
}
以管理员身份启动VS 2019(WCF项目需要管理员权限才可正常运行),运行示例程序(服务器端与客户端都要运行),在客户端上输入服务器地址,然后选择一个文件,开始上传。
示例运行效果:后台传输
4. 推送通知
传统的联网通知是设备客户端定期访问网络来查询是否有新的信息提供给用户,但这中方案消耗网络资源较多,而且,如果用户使用按流量计费的上网方式,势必会耗掉许多不必要的网络流量。因此,推送通知就伴随云技术而诞生,应用开发者首先将通知内容发送到微软公司的云服务器上(称为推送服务器),再由推送服务器将消息转发到用户的设备上,即使用户暂时没有联网,推送服务器会将消息保留一段时间。
推送通知使客户端程序不再通过主动查询的方式进行更新,而是主动等待消息的到来,这种方案可以大大节省网络资源的开销,同时不会浪费过多的数据流量。
推送服务器是通过为每个应用程序分配一个唯一的URI来确定将通知转发给目标用户的,称为推送通道。因此,客户端应用程序在运行后,需要向推送服务器申请一个URI,并且还应该将该URI告诉开发者,一般可以通过各种网络技术将URI发送到开发者服务器。在得到通道URI后,开发者只需将通知的内容(通知内容实质上是XML文档)以HTTP POST方式发送到该通道URI,推送服务器就能识别出是发送给哪个客户端的了。
在客户端应用开发中,与推送通知相关的API都被放到Windows.Networking.PushNotifications命名空间下。调用PushNotificationChannelManager类的CreatePushNotificationChannelForApplicationAsync静态方法可以返回一个PushNotificationChannel对象实例,该实例包含推动通道的URI,当应用程序不希望接收推送通知时,可以调用Close方法关闭通道。
接下来,将通过一个完整的示例来演示实现推送通知的过程。
(1) 创建客户端应用程序,页面布局XAML如下:
<StackPanel>
<TextBlock TextWrapping="Wrap" FontSize="20">
以下开关默认处于关闭状态,当开关打开时,会向推送服务器申请通道URI,并显示在VS2019的"输出"窗口中。
<LineBreak/>
如果不希望接收推送通知,使以下控件处于关闭状态即可。
</TextBlock>
<ToggleSwitch IsOn="False" Toggled="ToggleSwitch_Toggled"/>
</StackPanel>
处理ToggleSwitch控件的Toggled事件,当开关控件的状态发生改变后做出响应。
PushNotificationChannel channel = null;
private async void ToggleSwitch_Toggled(object sender, RoutedEventArgs e)
{
ToggleSwitch tgswitch = sender as ToggleSwitch;
if (tgswitch.IsOn)
{
//申请通道
channel = await PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync();
//输出URI
Debug.WriteLine($"推送通道:{channel.Uri}");
}
else
{
//关闭通道
channel?.Close();
channel = null;
}
}
当开关控件处于打开状态时,申请通道URI;否则关闭通道。当通道被关闭后,应用程序就不能接收推送通知了。
(2) 只有将当前应用程序项目与商店对应产品进行关联,才可以申请到有效的通道URI。所以需要使用开发者账号登录Windows开发人员中心,进入"仪表板",新建产品,由于这只是测试demo,不用发布到商店,故不用上传软件包。之后在VS 2019中,将当前项目与商店关联。
(3) 选中下图中标记的选项,会导航进相关页面,得到自动分配的sid和秘钥,后面用于发送通知的客户端兑换访问令牌时会用到该sid和秘钥。这个秘钥只需开发者自己知道即可,不要对他人公开。如果该秘钥不慎泄露,可以重新获取,否则一些不法分子可能会利用该秘钥来冒充开发者发送垃圾广告或虚假信息。
(4) 接下来还要开发发送推送通知的示例客户端。本例将通过一个UWP应用程序来完成。
页面布局XAML如下:
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock>获取Token</TextBlock>
<Grid BorderThickness="1" BorderBrush="GhostWhite" Margin="20">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBox x:Name="tbSid" AcceptsReturn="True" Header="SID:" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="10"/>
<PasswordBox x:Name="tbKey" PasswordChar="*" Header="客户端密钥:" Grid.Row="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="10"/>
<Button x:Name="tbnToken" Content="获取访问令牌" Grid.Column="1" VerticalAlignment="Center"
HorizontalAlignment="Stretch" Margin="10" Tapped="tbToken_Tapped"/>
<TextBox x:Name="tbToken" TextWrapping="Wrap" IsReadOnly="True" Grid.Row="1" Grid.Column="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="10"/>
</Grid>
<TextBlock Grid.Row="1">发送通知</TextBlock>
<Grid Grid.Row="1" Margin="20" BorderThickness="1" BorderBrush="GhostWhite">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock VerticalAlignment="Center">通道URI:</TextBlock>
<TextBox x:Name="tbUri" Grid.Column="1" HorizontalAlignment="Stretch"/>
</Grid>
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Button x:Name="bntSent" Content="发送通知" Margin="10" HorizontalAlignment="Stretch"
Tapped="bntSent_Tapped"/>
<TextBox x:Name="tbHeader" Grid.Row="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Margin="10" IsReadOnly="True"/>
<TextBox x:Name="tbContent" Grid.RowSpan="2" Grid.Column="1" Header="通知内容"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="10"
AcceptsReturn="True"/>
</Grid>
</Grid>
</Grid>
(5) 以下代码实现向推送服务器索取访问令牌。
AccessTokenData tokendt = null;
async Task<AccessTokenData> RequestTokenAsync(string sid, string pk)
{
//验证身份并获取令牌的URI
Uri reqUri = new Uri("https://login.live.com/accesstoken.srf");
try
{
using (HttpClient client = new HttpClient())
{
//POST的内容必须为application/x-www-from-urlencoded格式
Dictionary<string, string> formdata = new Dictionary<string, string>
{
{ "grant_type","client_credentials"},
{ "client_id",sid},
{ "client_secret",pk},
{ "scope","notify.windows.com"}
};
FormUrlEncodedContent content = new FormUrlEncodedContent(formdata);
//发送请求
HttpResponseMessage response = null;
response = await client.PostAsync(reqUri, content);
if (response != null && response.StatusCode == System.Net.HttpStatusCode.OK)
{
//反序列化服务器回应的数据
using (Stream stream = await response.Content.ReadAsStreamAsync())
{
DataContractJsonSerializer sz = new DataContractJsonSerializer(typeof(AccessTokenData));
tokendt = sz.ReadObject(stream) as AccessTokenData;
}
}
else
{
await new MessageDialog("发送失败").ShowAsync();
}
}
}
catch (Exception ex)
{
await new MessageDialog(ex.Message).ShowAsync();
}
return tokendt;
}
名为tbToken的Button控件的事件处理程序:
private async void tbToken_Tapped(object sender, TappedRoutedEventArgs e)
{
if (string.IsNullOrEmpty(tbSid.Text) || string.IsNullOrEmpty(tbKey.Password))
{
return;
}
var token = await RequestTokenAsync(tbSid.Text, tbKey.Password);
tbToken.Text = token.AccessToken;
}
其中AccessTokenData是一个自定义类,后面反序列化推送服务器返回的json对象时会用到。
申请Token的URI为添加链接描述,在向服务器提交的字段中,client_id字段指的是应用在应用商店中的SID值,client_secret字段对应的是客户端秘钥,提交方式必须为POST。
向推送服务器发送通知只需要用到access_token对应的值即可。
处理请求返回的JSON对象,最简单的方法是声明一个可序列化的类,并将成员的命名与JSON对象的字段匹配,最后使用JSON反序列化的方式直接读出。
[DataContract]
public class AccessTokenData
{
[DataMember(Name ="access_token")]
public string AccessToken { get; set; }
[DataMember(Name ="token_type")]
public string TokenType { get; set; }
}
得到访问令牌后,就可以使用它来发送通知了。
private async void bntSent_Tapped(object sender, TappedRoutedEventArgs e)
{
HttpWebRequest req = WebRequest.CreateHttp(tbUri.Text.Trim());
req.Method = "POST";
//添加验证头
req.Headers.Add("Authorization", $"bearer {tokendt.AccessToken}");
req.ContentType = "text/xml";
//该HTTP标头表示通知类型为Toast
req.Headers.Add("X-WNS-Type", "wns/toast");
//写入通知内容
byte[] data = Encoding.UTF8.GetBytes(tbContent.Text);
using (Stream stream = req.GetRequestStream())
{
await stream.WriteAsync(data, 0, data.Length);
}
try
{
//发送请求
HttpWebResponse response = req.GetResponse() as HttpWebResponse;
//输出响应头
StringBuilder strbd = new StringBuilder();
foreach (var hd in response.Headers.AllKeys)
{
strbd.AppendLine($"{hd}: {req.Headers.Get(hd)}");
}
tbHeader.Text = strbd.ToString();
if (response.StatusCode == HttpStatusCode.OK)
{
await new MessageDialog("发送成功").ShowAsync();
}
else
{
await new MessageDialog("发送失败").ShowAsync();
}
}
catch (Exception ex)
{
tbHeader.Text = ex.Message;
}
}
本示例仅仅演示了Toast通知的发送。其他类型通知的发送方法相似,只是表示通知内容的XML文档不同而已。各类通知的XML模板都可以在"Windows开发人员中心"的参考文档中找到。
示例运行效果视频:WNS(Toast)通知
5. 访问RSS资源
RSS(Really Simple Syndication)被广泛用于信息共享和网站内容同步发布等多种网络资源方案中,它基于XML标准,可以在多种平台和终端之间传递。比较常见的RSS资源有新闻频道、博客、软件更新等。
Windows.Web.Syndication命名空间提供了支持访问并分析RSS资源的API。使用这些公开的类型,可以轻松地检索RSS内容,开发者不需要手动去分析描述RSS资源的XML文档。
SyndicationClient类支持对RSS服务器的访问,在创建对象后,调用RetrieveFeedAsync方法可以从指定URI处获取RSS文档,返回类型为SyndicationFeed。该类对RSS文档的结构进行封装,并可以从它的Items属性得到一个SyndicationItem列表。SyndicationItem表示一条数据记录。
下面示例将演示如何访问RSS资源。
应用程序的页面布局XAML如下:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="输入URI:" VerticalAlignment="Center"/>
<TextBox x:Name="txtUri" Grid.Column="1"/>
<Button Content="获取" Margin="12,0,0,0" Grid.Column="2" Tapped="OnGetData"/>
</Grid>
<Grid Grid.Row="1" Margin="0,18,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ListView x:Name="lvItems" IsItemClickEnabled="True" ItemClick="lvItems_ItemClick">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Foreground="Purple" Text="{Binding Title.Text}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<WebView x:Name="wv" Grid.Column="1"/>
</Grid>
</Grid>
在后台代码中,首先处理"获取"按钮的Tapped事件,从指定URI处请求RSS资源。
private async void OnGetData(object sender, TappedRoutedEventArgs e)
{
SyndicationClient client = new SyndicationClient();
//开始请求RSS资源
try
{
SyndicationFeed feed = await client.RetrieveFeedAsync(new Uri(txtUri.Text.Trim()));
if (feed != null)
{
await new MessageDialog(feed.Items.Count.ToString()).ShowAsync();
lvItems.ItemsSource = feed.Items;
}
}
catch (Exception ex)
{
await new MessageDialog(ex.Message).ShowAsync();
}
}
直接将SyndicationFeed对象的Items属性赋值给ListView控件的ItemsSource属性,可以在ListView控件中显示RSS文档的所有内容列表。
在前面的XAML代码中,ListView控件自定义了子项的数据模板:
数据模板中包含一个TextBlock控件,其Text属性使用数据绑定来赋值。由于ItemsSource属性引用的是SyndicationFeed对象的Items列表,即每一个ListView子项所对应的数据对象为单个SyndicationItem实例,而SyndicationItem类的Title属性又是一个实现了ISyndicationText接口的类型实例。再从Title.Text属性中就能得到当前数据记录的标题内容。因此,Binding扩展标记的Path路径就是Title.Text。
处理ListView控件的ItemClick事件(必须将IsItemClickEnabled属性设置为ture才能引发该事件),当列表中的某个项被单击时触发事件,并在WebView控件中显示数据记录的详细内容。代码如下:
private void lvItems_ItemClick(object sender, ItemClickEventArgs e)
{
SyndicationItem feedItem = e.ClickedItem as SyndicationItem;
if (feedItem != null)
{
//如果Content属性为null,则获取Summary属性的内容
if (feedItem.Content is null)
{
wv.NavigateToString(feedItem.Summary.Text);
}
else
{
wv.NavigateToString(feedItem.Content.Text);
}
}
}
运行应用程序示例,然后在文本框中输入一个RSS地址(如某个新闻频道的RSS地址),然后单击"获取"按钮,当数据获取成功后,会在列表控件中显示数据记录列表。单击某个项就可以查看其具体内容,如下图所示。
6. 扫描Wi-Fi网络
使用WiFiAdapter类可以在应用程序中扫描附近的无线网络。要对Wi-Fi网络进行扫描,首先应当获取当前设备上可用的适配器(无线网卡),获取方法有两种:最简单的方法是直接调用WiFiAdapter类的FindAllAdaptersAsync方法,它是一个静态方法,可以直接访问,调用完成后会返回一个WiFiAdapter列表,其中每一个WiFiAdapter实例映射到一张无线网卡上。通常设备只配置一张无线网卡,因此多数情况下该列表中只有一个元素,但一台设备也可能存在配置多张无线网卡的情况,为了针对不同的情况,FindAllAdaptersAsync方法以列表形式返回结果。另一种获取无线适配器的方法是调用GetDeviceSelector方法,得到一个用于设备筛选的AQS字符串(高级查询字符串),然后调用 Windows.Devices.Enumeration.DeviceInformation.FindAllAsync方法来执行筛选,从而得到一个设备信息列表(由DeviceInformation类型组成的集合),最后再通过WiFiAdapter.FromIdAsync方法从设备的ID产生WiFiAdapter实例。
有了WiFiAdapter实例后,就可以调用ScanAsync方法开始对附近的无线网络进行扫描。扫描结束后,会把扫描的结果存放在NetworkReport属性中,该属性引用一个WiFiNetworkReport类型的对象,访问该对象的AvailableNetworks属性就能得到一个由WiFiAvailableNetwork对象组成的列表。WiFiAvailableNetwork类封装了与单个有效无线网络相关的信息,比如SSID、网络类型、信号强度等。
WiFiAdapter类除了可以扫描无线网络外,还可以对已经扫描出来的网络进行连接。要连接指定无线网络,可以调用ConnectAsync方法,将表示目标网络的WiFiAvailableNetwork对象作为参数传入即可;如果要断开已经连接的网络,调用Disconnect方法。
下面将通过一个示例来演示如何扫描Wi-Fi网络。具体步骤如下:
(1) 新建应用程序项目。
(2) 应用程序的页面布局XAML如下:
<Grid Margin="15">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<ComboBox x:Name="cmbAdts" Header="无线网卡" Margin="1,6" DisplayMemberPath="Name"
SelectionChanged="cmbAdts_SelectionChanged"/>
<ListView x:Name="lvNetworks" Grid.Row="1" Margin="2" SelectionChanged="lvNetworks_SelectionChanged"
SelectionMode="Single">
<ListView.ItemTemplate>
<DataTemplate>
<Grid Margin="1,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<TextBlock Text="SSID: " FontSize="20"/>
<TextBlock Grid.Column="1" FontSize="20" FontWeight="Bold" Text="{Binding Ssid}"/>
<TextBlock Grid.Row="1" Text="信号强度: "/>
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding SignalBars}"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
ComboBox控件用来显示当前设备上可用的无线网卡列表,用户可以从中选择一张网卡,然后在这张网卡上对无线网络进行扫描,扫描的结果将显示在ListView控件中。
(3) 自定义一个WiFiAdapterItem类,用来封装与无线网卡相关的信息。
public class WifiAdapterItem
{
WiFiAdapter mWifiadt;
public WifiAdapterItem(WiFiAdapter adt) => mWifiadt = adt;
public WiFiAdapter Adapter => mWifiadt;
public string Name => mWifiadt.NetworkAdapter.NetworkAdapterId.ToString();
}
在该类中,公开了一个Adapter属性,其他的调用代码可以通过这个属性来获得关联的WiFiAdapter实例的引用。
(4) 处理应用程序页面的Loaded事件,代码如下:
private async void Page_Loaded(object sender, RoutedEventArgs e)
{
//首先判断一下应用程序是否有访问无线适配器的权限
var res = await WiFiAdapter.RequestAccessAsync();
if (res != WiFiAccessStatus.Allowed)
{
await new MessageDialog("操作被拒绝。").ShowAsync();
return;
}
//获取无线网卡列表
var wifiAdts = await WiFiAdapter.FindAllAdaptersAsync();
var itemList = new List<WifiAdapterItem>();
foreach (var adt in wifiAdts)
{
var item = new WifiAdapterItem(adt);
itemList.Add(item);
}
//将列表绑定到ComboBox控件上
cmbAdts.ItemsSource = itemList;
if (itemList.Count > 0)
{
cmbAdts.SelectedIndex = 0;
}
}
无法保证应用程序都具备访问无线网卡的权限,因此,在获取适配器列表之前,应该先用RequestAccessAsync方法检查一下,应用程序是否有访问权,只要方法返回的是WiFiAccessStatus.Allowed以外的值,都说明应用程序没有访问权限。
(5) 处理ComboBox控件的SelectionChanged事件,当下拉列表框中的选择项发生更改后,立即使用被选中的无线适配器进行无线网络扫描。
WiFiAdapter adapter = null;
private async void cmbAdts_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var itemInfo = cmbAdts.SelectedItem as WifiAdapterItem;
adapter = itemInfo.Adapter;
await adapter.ScanAsync();
IReadOnlyList<WiFiAvailableNetwork> nwItems = adapter.NetworkReport?.AvailableNetworks;
if (nwItems != null && nwItems.Count > 0)
{
lvNetworks.ItemsSource = nwItems;
}
}
注意ScanAsync方法是支持异步等待的方法,在使用时,一定要用await运算符来等待扫描完成,否则,NetworkReport中的AvailableNetworks列表里面可能不存在任何数据,获取空白的列表没有实际意义。
(6) 要让应用程序具有访问无线网络适配器的权限,必须配置清单文件。打开清单文件,在Capabilities元素下面加入以下配置:
<Capabilities>
<Capability Name="internetClient" />
<DeviceCapability Name="wifiControl"/>
</Capabilities>
加入wifiControl声明后,应用程序才能访问Wi-Fi网络相关的API。
示例的运行结果如下图所示。