2016 年 7 月

第 31 卷,第 7 期

新型应用 - 在 UWP 中构建 Wi-Fi 扫描工具

作者 Frank La La

Wi-Fi 在最近十年左右开始通用。许多商店和咖啡馆提供免费 Wi-Fi 以方便顾客使用。实际上,所有酒店都会向来宾提供无线 Internet。在家中我们大多都具有无线网络。由于平板和移动设备很少有以太网插孔,所以 Wi-Fi 已成为我们现代生活中不可或缺的一部分。除此之外,我们对 Wi-Fi 几乎没有其他概念。

所以,问题出现了。我们身边存在的大量 Wi-Fi 网络如何? 具体数量是多少? 这些网络是否安全? 这些网络使用的是什么信道? 这些网络的名称是什么? 我们可以映射它们吗? 从 Wi-Fi 网络元数据中我们可以获取什么信息?

最近在遛狗的时候,我不经意间看到手机上的 Wi-Fi 网络连接屏幕,其中有一些幽默的网络名称。这激起了我的好奇心:其他人中还有多少人选择了设置一个幽默的网络名称,而非实用的。因此,我产生了这样一个想法:映射和扫描社区内及周边的无线网络。如果我可以将这个过程自动化,那么我甚至可以在工作往返途中扫描和映射无线网络了。理论上,我可以在 Raspberry Pi 上运行一个程序,使其定期进行无线扫描并将相关数据记录到 Web 服务。相对于不时地查看手机,这当然更实用一些。

事实证明,通用 Windows 平台 (UWP) 可以通过 Windows.Devices.WiFi 命名空间中的类来大量访问无线网络数据。正如大家所知,UWP 应用不仅可以在手机和 PC 上运行,也适用于运行Windows 10 IoT Core 的 Raspberry Pi 2。现在,我已具备了我的项目实施所需的全部条件。

在本专栏中,我将探讨使用 UWP 中直接内置的 API 扫描 Wi-Fi 网络的基础知识。

Windows.Devices.WiFi 命名空间

Windows.Devices.WiF 命名空间内部的类中包含扫描和探索可到达范围内无线适配器和无线网络所需的全部内容。在 Visual Studio 中新建 UWP 项目后,添加一个名为 WifiScanner 的新类,然后添加以下属性:

public WiFiAdapter WiFiAdapter { get; private set; }

由于在给定的系统上可能存在多个 Wi-Fi 适配器,因此必须要选择自己要使用的 Wi-Fi 适配器。如图 1 所示,通过InitializeFirstAdapter 方法获取系统中列举出的第一个适配器。

图 1 找到连接到系统的第一个 Wi-Fi 适配器并将其初始化

private async Task InitializeFirstAdapter()
{
  var access = await WiFiAdapter.RequestAccessAsync();
  if (access != WiFiAccessStatus.Allowed)
  {
    throw new Exception("WiFiAccessStatus not allowed");
  }
  else
  {
    var wifiAdapterResults =
      await DeviceInformation.FindAllAsync(WiFiAdapter.GetDeviceSelector());
  if (wifiAdapterResults.Count >= 1)
    {
      this.WiFiAdapter =
        await WiFiAdapter.FromIdAsync(wifiAdapterResults[0].Id);
    }
    else
    {
      throw new Exception("WiFi Adapter not found.");
    }
  }
}

添加 Wi-Fi 功能

你可能注意到访问 Wi-Fi 时会进行检查,并且如果 RequestAccessAsync 方法返回 False,则代码会提示异常。这是因为应用需要具有一种设备功能,来允许其扫描并连接到 Wi-Fi 网络。该功能未列于清单属性编辑器的“功能”选项卡中。若要添加此功能,右键单击 Package.appxmanager 文件,然后选择“查看代码”。

现在将看到 Package.appxmanager 文件的原始 XML。在“功能”节点中,添加以下代码:

<DeviceCapability Name="wifiControl" />

保存文件。现在你的应用已具备访问 Wi-Fi API 的权限。

探索无线网络

具有了用于识别 Wi-Fi 适配器的代码及相应的访问权限后,下一步就是对网络进行实际扫描。庆幸地是,进行实际扫描的代码相当简单;只需在 WifiAdapter 对象上调用 ScanAsync 方法即可。向 WifiScanner 类添加以下方法:

public async Task ScanForNetworks()
{
  if (this.WiFiAdapter != null)
  {
    await this.WiFiAdapter.ScanAsync();
  }
  }

ScanAsync 运行后,会填充 WifiAdapter 的 NetworkReport 属性。NetworkReport 是 WiFiNetworkReport 的一个实例,其中包含 AvailableNetworks,即 List<WiFiAvailableNetwork>。WiFiAvailableNework 对象含有无数与给定网络相关的数据点。你可以查看服务集标识符 (SSID)、信号强度、加密方法和接入点运行时间及其他数据点,查看以上所有内容都无需连接到网络。

循环访问可用网络相当简单: 创建普通旧 CLR 对象 (POCO) 以包含 WiFiAvailableNetwork 对象中的部分数据,如以下代码所示:

foreach (var availableNetwork in report.AvailableNetworks)
{
  WiFiSignal wifiSignal = new WiFiSignal()
  {
    MacAddress = availableNetwork.Bssid,
    Ssid = availableNetwork.Ssid,
    SignalBars = availableNetwork.SignalBars,
    ChannelCenterFrequencyInKilohertz =
      availableNetwork.ChannelCenterFrequencyInKilohertz,
    NetworkKind = availableNetwork.NetworkKind.ToString(),
    PhysicalKind = availableNetwork.PhyKind.ToString()
  };
}

构建 UI

在最后的项目中,我计划运行此应用时不使用 UI,这样有助于在开发和故障排除过程中看到可到达范围内的网络及其相关的元数据。对目前可能还不具备 Raspberry Pi 但仍想跟进的开发人员也有帮助。如图 2 所示,该项目的 XAML 很直观,并且有多个文本框用于存储扫描输出。

图 2 UI 的 XAML

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
  <Grid.RowDefinitions>
    <RowDefinition Height="60"/>
      <RowDefinition Height="60"/>
      <RowDefinition Height="*"/>
  </Grid.RowDefinitions>
  <TextBlock FontSize="36" Grid.RowSpan="2" >WiFi Scanner</TextBlock>
  <StackPanel Name="spButtons" Grid.Row="1" Orientation="Horizontal">
    <Button Name="btnScan" Click="btnScan_Click" Grid.Row="1">Scan For
      Networks</Button>
  </StackPanel>
  <TextBox Name="txbReport" TextWrapping="Wrap" AcceptsReturn="True"
    Grid.Row="2"></TextBox>
  </Grid>
</Page>

捕捉位置数据

为了提供其他值,每次扫描无线网络时还要注意扫描的位置。这样后面就可能提供有趣的信息和数据的可视化。庆幸地是,向 UWP 应用添加位置很简单。但是,需要将“位置”功能添加到自己的应用。可以在“解决方案资源管理器”中双击 Package.appxmanifest 文件、单击“功能”选项卡并在“功能”列表中选中“位置”复选框来完成添加。

以下代码将使用 UWP 内置的 API 来检索位置:

Geolocator geolocator = new Geolocator();
Geoposition position = await geolocator.GetGeopositionAsync();

既然获取了位置,则需要保存位置信息。以下是 WiFiPointData 类,可以存储位置信息及在位置中发现的网络的相关信息:

public class WiFiPointData
{
  public DateTimeOffset TimeStamp { get; set; }
  public double Latitude { get; set; }
  public double Longitude { get; set; }
  public double Accuracy { get; set; }
  public List<WiFiSignal> WiFiSignals { get; set; }
  public WiFiPointData()
  {
    this.WiFiSignals = new List<WiFiSignal>();
  }
}

此时,务必注意倘若你的设备不具备 GPS 设施,则应用需要通过 Wi-Fi 连接到 Internet 来对位置进行解析。如果没有携带式 GPS 传感器,则需具备移动热点,并要确保你的笔记本或 Raspberry Pi 2 连接到了该热点。这也意味着报告位置的准确性会差些。有关创建位置感知 UWP 的最佳做法的详细信息,请参阅 Windows 开发人员中心的文章,即位于 bit.ly/1P0St0C 的“位置感知应用指南”。

反复扫描

对于扫描和映射持续驱动场景,应用需要定期扫描 Wi-Fi 网络。若要实现上述目的,需要使用 DispatchTimer 来定期扫描 Wi-Fi 网络。如果你不了解 DispatchTimer 的工作方式,请参阅位于 bit.ly/1WPMFcp 中的文档。

务必注意 Wi-Fi 扫描可能需要数秒的时间,根据系统不同会有所不同。以下代码设置 DispatchTimer 每 10 秒引发一个事件,此时间足以让最慢的系统完成扫描:

DispatcherTimer timer = new DispatcherTimer();
timer.Interval = new TimeSpan(0, 0, 10);
timer.Tick += Timer_Tick;
timer.Start();

每隔 10 秒,计时器会运行 Timer_Tick 中的代码。以下代码会扫描 Wi-Fi 网络,并将结果追加到 UI 中的文本框中:

private async void Timer_Tick(object sender, object e)
{
  StringBuilder networkInfo = await RunWifiScan();
  this.txbReport.Text = this.txbReport.Text + networkInfo.ToString();
}

报告扫描结果

正如前文所述,调用 ScanAsync 方法后,扫描结果将存储在 List<WiFiAvailableNetwork> 中。只需循环访问此列表即可获取这些结果。图 3 中的代码执行的就是以上操作,该代码会将结果置于 WiFiPointData 类的实例中。

图 3 循环访问扫描时发现的所有网络的代码

foreach (var availableNetwork in report.AvailableNetworks)
{
  WiFiSignal wifiSignal = new WiFiSignal()
  {
    MacAddress = availableNetwork.Bssid,
    Ssid = availableNetwork.Ssid,
    SignalBars = availableNetwork.SignalBars,
    NetworkKind = availableNetwork.NetworkKind.ToString(),
    PhysicalKind = availableNetwork.PhyKind.ToString(),
    Encryption = availableNetwork.SecuritySettings.NetworkEncryptionType.ToString()
  };
  wifiPoint.WiFiSignals.Add(wifiSignal);
  }

为了使 UI 在仍能提供大量数据分析的同时不失简洁,可以将 WiFiPointData 转换为逗号分隔值 (CSV) 格式并对 UI 文本框中的文本进行设置。CSV 是一种相对简单的格式,并且可以导入到 Excel 和 Power BI 进行分析。转换 WiFiPointData 的代码如图 4 所示。

图 4 转换 WiFiPointData

private StringBuilder CreateCsvReport(WiFiPointData wifiPoint)
{
  StringBuilder networkInfo = new StringBuilder();
  networkInfo.AppendLine("MAC,SSID,SignalBars,Type,Lat,Long,Accuracy,Encryption");
  foreach (var wifiSignal in wifiPoint.WiFiSignals)
  {
    networkInfo.Append($"{wifiSignal.MacAddress},");
    networkInfo.Append($"{wifiSignal.Ssid},");
    networkInfo.Append($"{wifiSignal.SignalBars},");
    networkInfo.Append($"{wifiSignal.NetworkKind},");
    networkInfo.Append($"{wifiPoint.Latitude},");
    networkInfo.Append($"{wifiPoint.Longitude},");
    networkInfo.Append($"{wifiPoint.Accuracy},");
    networkInfo.Append($"{wifiSignal.Encryption}");
    networkInfo.AppendLine();
  }
  return networkInfo;
}

可视化数据

自然,我已迫不及待地设置我的云服务来显示数据并将其可视化。相应地,我获取了应用生成的 CSV 数据,并将其复制并粘贴到一个文本文件中。然后我确保使用 .CSV 扩展名保存此文件。下一步,我将数据导入到 Power BI Desktop 中。Power BI Desktop 可从 powerbi.microsoft.com 免费下载,可轻松实现数据的可视化和研究。

若要从应用导入数据,请在 Power Bi Desktop 初始屏幕上单击“获取数据”。

在下一个屏幕中,选择 CSV,然后单击“连接”。在“文件选取器”对话框中,选择包含从应用中复制和粘贴的数据的 CSV 文件。

将其加载后,在屏幕的右侧会看到字段列表。本文并不提供 Power BI Desktop 的详细教程,如图 5 所示,生成显示 Wi-Fi 网络的位置、相应的 SSID 和使用的加密协议的可视化并不需要过多技术。

对 Wi-Fi 扫描应用收集的数据的 Power BI 可视化
图 5 对 Wi-Fi 扫描应用收集的数据的 Power BI 可视化

令人惊讶的是,大约三分之一的网络没有任何加密。其中有些是各种企业设置的来宾网络,有些则不是。

实际应用

虽然最初只打算用来衡量邻居的技术水平和幽默程度,但此项目具有一些相当有趣的实际用途。可以巧妙地应用轻松自动映射 Wi-Fi 信号强度和位置这一功能。如果每个城市总线都配备了运行此应用的 IoT 设备,这个城市会发生什么? 各个城市可以测量 Wi-Fi 网络的普及程度,然后将该数据与临近城市的传入数据相关联。然后执政官员可以根据此数据做出明智的决策。如果社区向城镇或某个区域提供了公共 Wi-Fi,那么无需再额外支付遣送技术员的费用即可实时测量信号强度。各个城市还可以确定不安全的网络在哪些地区普及,然后建立针对性的警惕计划来增强社区的网络安全。

在较小的区域范围内,设置自己的网络后,即可使用快速扫描 Wi-Fi 网络元数据的功能。许多路由器都支持用户修改广播信道。名为“Wi-Fi Analyzer”(bit.ly/25ovZ0Q) 的这款应用是一个非常典型的示例,它可以显示附近无线网络的强度和频率等信息。在一个新位置设置 Wi-Fi 网络时此应用很有用。

总结

从 UI 中复制并粘贴的文本数据无法调整。而且,如果要在不能提供任何显示的 IoT 设备中运行此应用,则应用需向云发送不含任何 UI 的数据。在下个月的专栏中,你将了解到如何设置云服务来存储所有这些数据。此外,你将了解到如何在运行 Windows IoT Core 的 Raspberry Pi 2 中部署解决方案。


Frank La Vigne是 Microsoft 技术与公民参与团队的技术推广者。他帮助用户充分利用技术,从而创建更美好的社区。他定期在 FranksWorld.com 上发布博客,且拥有一个称为“Frank’s World TV”(youtube.com/FranksWorldTV) 的 YouTube 频道。

衷心感谢以下技术专家对本文的审阅: Rachel Appel、Robert Bernstein 和 Jose Luis Manners