Share:

Поиск устройств в сети с .NET (C#)

YPermitinв.NET

2024-06-28

#.NET

#C#

#разработка

#сеть

#устройства

Чтобы найти иголку в стоге сена, достаточно сжечь сено
и провести магнитом над пеплом.
(с) Бернар Вербер

Поиск устройств в сети с помощью C# (.NET). Пинг, сканирование сети, ARP, широковещательные сообщения UDP. А также делаем пример приложений для RasberryPi и Android с возможностью поиска устройства со смартфона.

Содержание

Сеть вокруг нас

Информация витает вокруг нас. Компьютерные сети стали повседневной нормальностью. Не важно дома ли Вы, на работе или прогуливаетесь на улице - сеть будет рядом, вы постоянно подключены. Глобальная сеть, он же интернет, доступна отовсюду: со смартфонов и компьютеров, из дома или за городом, на земле или в самолете. А количество подключенных к сети устройств имеет просто невероятное количество. Вот он, современный мир. Не зря же существует такое понятие как интернет вещей.

Давайте вернемся от высоких рассуждений к более практичным вопросам. Такое распостранение компьютерных сетей и устройств создают потребность в решении задач разработки ПО для таких устройств, их удобной интеграции, поиска и так далее. В большинстве языков программирования и платформ разработки имеются встроенные средства для работы с сетью. Платформа .NET и C# не являются исключениями.

Сегодня мы коснемся этой темы, но лишь в малой части. Мы будем искать устройства в сети от самых простых методов в виде ping'а, широковещательных UDP-сообщений, запроса информации с маршрутизатора и другое. Это ни в коем случае не всеобъемлющая информация. Это старт для написания собственных программ в части поиска устройств.

Завершим свою работу мы на приложениях для Rasberry Pi и Android. Со смартфона наше приложение сможет находить Rasberry Pi в сети и делать к нему запросы. Время пришло, начинаем!

Пинг-понг

Самый простой способ для поиска устройств в сети, который первым приодит на ум, это конечно же использование операции ping. По своей сути, ping - это средство для проверки доступности узла в сети через отправку к нему эхо-запроса ICMP. В .NET доступен класс Ping, который и позволяет делать отправку подобных запросов к узлам. Работа класса в целом аналогична одноименной утилите ping в Windows и *.nix.

Ниже представлен пример простейшей программы, которая "пингует" диапазон адресов с 192.168.88.1 по 192.168.88.255 и выводит список адресов, для которых успешно прошла проверка доступности.

using System.Net;
using System.Net.NetworkInformation;
string addressBase = "192.168.88."; // Базовая часть адреса
int startAddress = 1; // Начало диапазона сканирования адресов
int endAddress = 255; // Окончание диапазона сканирования адресов
List<IPAddress> detectedAddresses = new List<IPAddress>();
Ping ping = new Ping();
// Сканируем последовательно адресов на доступность
for (int addressPart = startAddress; addressPart <= endAddress; addressPart++)
{
string currentAddressAsString = $"{addressBase}{addressPart}";
IPAddress currentAddress = IPAddress.Parse(currentAddressAsString);
// Отправляем ICMP-запрос с таймаутом в 1 секунду
var pingResult = ping.Send(currentAddress, 1000);
Console.WriteLine($"[{DateTime.Now}] {currentAddress}: {pingResult.Status}");
if (pingResult.Status == IPStatus.Success)
{
detectedAddresses.Add(currentAddress);
}
}
// Выводим результат сканирования в виде списка доступных узлов
Console.WriteLine();
if (detectedAddresses.Count == 0)
{
Console.WriteLine("Не найдено активных устройств.");
}
else
{
Console.WriteLine("Найдены устройства на следующих адресах:");
foreach (var detectedAddress in detectedAddresses)
{
// Пытаемся определить имя хоста по его адресу
string hostName;
try
{
hostName = Dns.GetHostEntry(detectedAddress)?.HostName ?? "<Неизвестно>";
}
catch
{
hostName = "<Неизвестно>";
}
Console.WriteLine("- {0}: {1}", detectedAddress, hostName);
}
}
Console.WriteLine();
Console.WriteLine("Для выхода нажмите любую клавишу...");
Console.ReadKey();

В программе сначала мы отправляем ICMP-пакеты на каждый из указанных адресов с таймаутом ожидания ответа в 1000 мс. Все узлы, для которых мы успешно получили ответ, сохраняем в список обнаруженных адресов. На последнем шаге выводим результат в виде обнаруженных узлов. Также для каждого адреса пытаемся получить DNS-имя.

Пример вывода программы может быть следующим. Сначала в процессе сканирования выводим ход процесса пинга узлов:

[28.06.2024 11:16:07] 192.168.88.1: Success
[28.06.2024 11:16:08] 192.168.88.2: TimedOut
[28.06.2024 11:16:09] 192.168.88.3: TimedOut
[28.06.2024 11:16:10] 192.168.88.4: TimedOut
[28.06.2024 11:16:11] 192.168.88.5: TimedOut
[28.06.2024 11:16:12] 192.168.88.6: TimedOut
[28.06.2024 11:16:13] 192.168.88.7: TimedOut
[28.06.2024 11:16:14] 192.168.88.8: TimedOut
[28.06.2024 11:16:15] 192.168.88.9: TimedOut
[28.06.2024 11:16:16] 192.168.88.10: TimedOut
[28.06.2024 11:16:16] 192.168.88.11: Success
[28.06.2024 11:16:17] 192.168.88.12: TimedOut
[28.06.2024 11:16:18] 192.168.88.13: TimedOut
[28.06.2024 11:16:19] 192.168.88.14: TimedOut
[28.06.2024 11:16:19] 192.168.88.15: Success
...
А на финальном шаге уже получим отчет по результатам:
Найдены устройства на следующих адресах:
- 192.168.88.1: router.lan
- 192.168.88.11: yy-nix
- 192.168.88.15: <Неизвестно>
- 192.168.88.16: <Неизвестно>
- 192.168.88.24: <Неизвестно>
- 192.168.88.26: <Неизвестно>
- 192.168.88.27: <Неизвестно>
- 192.168.88.28: <Неизвестно>
Для выхода нажмите любую клавишу...

Особенностью работы класса Ping являются ограничения, при которых утилита успешно отправит и получит ICMP-пакеты. В некоторых случаях наличие прокси-серверов, включенного NAT или брэндмауэров и некоторых других особенностей конфигурации сети могут препятствовать работе проверки доступности узлов. Поэтому полностью доверять результатом не приходится.

Также данный подход проверяет лишь доступность узла в сети и не позволяет проверить наличие или доступность служб более высокого уровня, таких как веб-сервера или другое ПО, использующее передачу данных по сети.

Рассмотрим еще один способ поиска устройств в сети с помощью протокола определения адресов ARP.

Шах и ARP

ARP (Address Resolution Protocol) - это протокол определения адреса, позволяющий определить MAC-адреса других узлов по известным IP-адресам.

Ранее мы делали ping диапазона адресов сети, а с помощью протокола ARP мы можем получить больше информации об узлах сети. Для .NET нет штатной возможности работы с протоколом ARP, но в сообществе создана библоитека ArpLookup за авторством Georg Jung. Библиотека кроссплатформенная, что не может не радовать!

Хорошим примером использования данной библиотеки является приложение arp-scanner за авторством giuliocomi. Приложение позволяет идентифицировать доступные узлы в локальной сети.

Пример работы arp-scanner Вы можете видеть ниже:

ARP-SCANNER

Перед этим приложение сканирует все IP-адреса в указанном при запуске диапазоне.

Отличие данного подхода от простого ping'а в контексте обнаружения устройств заключается в том, что ICMP-пакеты часто блокируются брэндмауэром на узлах, в то время как ARP-запросы остаются доступными для использования. Поэтому шанс обнаружения активных устройств этим методом куда выше. Но не исключено, что администратор может и эти запросы заблокировать, тогда данный способ не сработает.

Напишем свою небольшую утилиту с использованием библиотеки ArpLookup, чтобы показать пример использования более наглядно.

using System.Net;
using System.Net.NetworkInformation;
using ArpLookup;
string addressBase = "192.168.88."; // Базовая часть адреса
int startAddress = 1; // Начало диапазона сканирования адресов
int endAddress = 255; // Окончание диапазона сканирования адресов
byte[] emptyPhysicalAddress = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
List<(IPAddress,PhysicalAddress)> detectedAddresses = new List<(IPAddress,PhysicalAddress)>();
// Сканируем последовательно адреса на доступность
for (int addressPart = startAddress; addressPart <= endAddress; addressPart++)
{
string currentAddressAsString = $"{addressBase}{addressPart}";
IPAddress currentAddress = IPAddress.Parse(currentAddressAsString);
// Отправляем ARP-запрос для получения MAC-адреса.
// Если MAC-адрес успешно получен, значит, узел в сети "живой"
PhysicalAddress? physicalAddress;
try
{
physicalAddress = Arp.Lookup(currentAddress);
// Если получен пустой MAC-адрес,
// то присваиваем явно неопределенное значение
if (physicalAddress == null
|| physicalAddress.GetAddressBytes().SequenceEqual(emptyPhysicalAddress)
|| physicalAddress == PhysicalAddress.None)
{
physicalAddress = null;
}
}
catch
{
physicalAddress = null;
}
// Отправляем ICMP-запрос с таймаутом в 1 секунду
if (physicalAddress != null)
{
detectedAddresses.Add(new ValueTuple<IPAddress, PhysicalAddress>(currentAddress,physicalAddress));
Console.WriteLine($"[{DateTime.Now}] {currentAddress}: {physicalAddress}");
}
else
{
Console.WriteLine($"[{DateTime.Now}] {currentAddress}: <Не доступен>");
}
}
// Выводим результат сканирования в виде списка доступных узлов
Console.WriteLine();
if (detectedAddresses.Count == 0)
{
Console.WriteLine("Не найдено активных устройств.");
}
else
{
Console.WriteLine("Найдены устройства на следующих адресах:");
foreach (var detectedAddress in detectedAddresses)
{
// Пытаемся определить имя хоста по его адресу
string hostName;
try
{
hostName = Dns.GetHostEntry(detectedAddress.Item1)?.HostName ?? "<Неизвестно>";
}
catch
{
hostName = "<Неизвестно>";
}
Console.WriteLine("- {0}: {1}: {2}", detectedAddress.Item1, detectedAddress.Item2, hostName);
}
}
Console.WriteLine();
Console.WriteLine("Для выхода нажмите любую клавишу...");
Console.ReadKey();

В целом программа очень похожа на версию из раздела про ping, которую мы создали в предыдущем разделе. Главное отличие здесь - это использование ArpLoockup вместо класса Ping, где мы отправляем запрос.

// Отправляем ARP-запрос для получения MAC-адреса.
physicalAddress = Arp.Lookup(currentAddress);

В процессе сканирования сети будет выводиться прогресс операции вида:

[28.06.2024 13:31:11] 192.168.88.1: 2CC81B4A9F8A
[28.06.2024 13:31:18] 192.168.88.2: <Не доступен>
[28.06.2024 13:31:19] 192.168.88.3: <Не доступен>
[28.06.2024 13:31:20] 192.168.88.4: <Не доступен>
[28.06.2024 13:31:21] 192.168.88.5: <Не доступен>
[28.06.2024 13:31:21] 192.168.88.6: <Не доступен>
[28.06.2024 13:31:22] 192.168.88.7: <Не доступен>
[28.06.2024 13:31:23] 192.168.88.8: <Не доступен>
[28.06.2024 13:31:24] 192.168.88.9: <Не доступен>
[28.06.2024 13:31:24] 192.168.88.10: <Не доступен>
[28.06.2024 13:31:24] 192.168.88.11: <Не доступен>
[28.06.2024 13:31:25] 192.168.88.12: <Не доступен>
[28.06.2024 13:31:26] 192.168.88.13: <Не доступен>
[28.06.2024 13:31:27] 192.168.88.14: <Не доступен>
[28.06.2024 13:31:27] 192.168.88.15: 2CC81B4A9F89
По окончанию работы утилиты будет выведен финальный отчет:
Найдены устройства на следующих адресах:
- 192.168.88.1: 2CC8********: router.lan
- 192.168.88.15: 2CC8********: <Неизвестно>
- 192.168.88.16: CAC9********: <Неизвестно>
- 192.168.88.24: B060********: <Неизвестно>
- 192.168.88.26: 7089********: <Неизвестно>
- 192.168.88.27: 82D1********: <Неизвестно>
- 192.168.88.28: B887********: <Неизвестно>
- 192.168.88.35: 000C********: fiastoolset.yy.corp
- 192.168.88.82: 18C0********: <Неизвестно>

Фактически мы получили тот же результат, что и при использовании класса Ping, но альтернативным путем.

Будем честными, искать устройства подобными методами можно, но только тогда, когда найти их нужно только одной стороне. Но что, если устройству нужно, чтобы его нашли?

Крик через UDP

Устройство может самостоятельно сигнализировать в локальной сети, что оно включено и готово к работе. Самый распостраненный вариант реализации такого поведения это широковещательные сообщения UDP.

Платформа .NET имеет собственный класс UdpClient для удобной работы с протоколом UDP, в том числе и для отправки и приема широковещательных сообщений.

Сделаем простое приложение для отправки и приема широковещательных сообщений UDP, причем в контексте одного узла и приложения. Это просто демонстрационный пример.

using System.Net;
using System.Net.Sockets;
using System.Text;
// Адрес группы для многоадресной рассылки
IPAddress broadcastAddress = IPAddress.Parse("239.255.255.255");
// Порт для отправки и прослушивания сообщений
int localPort = 25432;
// Запускаем в фоне задание для получения сообщений
Task.Run(ReceiveMessageAsync);
// Запускаем в фоне задание для отправки сообщений
Task.Run(SendMessageAsync);
// Отправка сообщений в группу
async Task SendMessageAsync()
{
// Создаем клиента для отпрвки
using var sender = new UdpClient();
// И точку подключения для отправки
var endpoint = new IPEndPoint(broadcastAddress, localPort);
while (true)
{
// Формируем сообщение
string messageContent = $"[{DateTime.Now}] Hello from UDP";
byte[] data = Encoding.UTF8.GetBytes(messageContent);
// Отправляем в группу
await sender.SendAsync(data, endpoint);
Console.WriteLine($"(Отправлено): {messageContent}");
// Ждем до следующей отправки
await Task.Delay(1000);
}
}
// Получение сообщений из группы
async Task ReceiveMessageAsync()
{
// Создаем клиента для получения сообщений
using var receiver = new UdpClient(localPort);
// Присоединяемся к группе многоадресной рассылки
receiver.JoinMulticastGroup(broadcastAddress);
// Отключаем получение собственных сообщений
receiver.MulticastLoopback = false;
while (true)
{
// Получаем сообщение при появлении
var result = await receiver.ReceiveAsync();
// Отображаем содержимое
string messageContent = Encoding.UTF8.GetString(result.Buffer);
Console.WriteLine($"(Получено): {messageContent}");
}
}
Console.WriteLine("Для выхода нажмите любую клавишу...");
Console.ReadKey();

Здесь мы имеем два асинхронных метода SendMessageAsync для отправки широковещательных сообщений в группу и ReceiveMessageAsync для их приема. Перед их запуском инициализируются два важных параметра для рассылки и приема сообщений:

  • localPort - порт, на который будут отправляться сообщения и, соответственно, прослушиваться сообщения для приема.
  • broadcastAddress - адрес группы многоадресной рассылки. Это служебный адрес, который должен указываться в диапазоне от 224.0.0.0 до 239.255.255.255. Если передать другой адрес, либо если маршрутизатор не поддерживает групповые рассылки, то будет получено исключение SocketException.

При отправке сообщений инициализируется UDP-клиент в виде объекта класса UdpClient с точкой подключения IPEndPoint с указанным портом и адресом группы многоадресной рассылки.

При получении сообщений инициализируется UDP-клиент также в виде объекта класса UdpClient, но в этот раз выполняется присоединение к группе многоадресной рассылки с помощью метода JoinMulticastGroup. Стоит отметить, что для отправки сообщений в группу нет необходимости выполнять присоединение к ней методом JoinMulticastGroup, достаточно того, чтобы в точке подключения адрес совпадал с адресом в точке подключения клиента, который эти сообщения отправляет.

Для удаления из группы нужно вызывать метод DropMulticastGroup, но в нашем примере в этом нет необходимости.

При запуске приложения мы получим примерно такой вывод:

(Отправлено): [28.06.2024 15:20:57] Hello from UDP
(Получено): [28.06.2024 15:20:57] Hello from UDP
(Отправлено): [28.06.2024 15:20:58] Hello from UDP
(Получено): [28.06.2024 15:20:58] Hello from UDP
(Отправлено): [28.06.2024 15:20:59] Hello from UDP
(Получено): [28.06.2024 15:20:59] Hello from UDP
(Отправлено): [28.06.2024 15:21:00] Hello from UDP
(Получено): [28.06.2024 15:21:00] Hello from UDP

Таким образом, для задач обнаружения устройств в сети, если одно устройство должно найти другое, широковещательные сообщения UDP являются практически идеальным средством.

Тем более в теле сообщения можно отправлять любую необходимую информацию, чтобы получатель мог корректно идентифицировать устройство и сделать нужные действия. Ниже мы рассмотрим использование данного подхода на примере создания сервиса для RasberryPi и приложения для Android, которое будет этот сервис находить в сети. Итак, поехали дальше!

Rasberry Pi + Android

Итак, у нас будет расширенный пример двух приложений. Первое - сервис на ASP.NET Core под *.nix (хотя и на Windows тоже будет работать), который мы запустим на устройстве Rasberry Pi. А также будет клиентское приложение для Android на базеMAUI, которое будет выполнять поиск устройства в той же сети, что и смартфон, и отправлять на него некоторые запросы.

Мы остановимся лишь на основных моментах, пока будем создавать эти приложения. Полный пример и вся кодовая база будет доступна в репозитории YPermitin.ExternalDevices на GitHub. Чтобы пример остался "как есть", без изменений с течением времени, пример был сохранен в отдельной веткеpost-detect-devices-with-csharp.

Переходим к делу, сначала создадим сервис для "малинки".

Прокаченная малинка

На Rasberry Pi мы заблоговременно установили Ubuntu. Этот процесс здесь расписывать смысла нет, т.к. вопрос выходит за рамки темы статьи. Вы можете узнать всю нужную информацию на официальном сайте. Фактически, вместо Rasberry Pi может быть любая машина с Ubuntu, в том числе обычный ПК.

Созданный сервис на ASP.NET Core мы публикуем как любой другой сервис на *.nix. Процесс публикации мы также рассматривать не будем. При необходимости весь процесс установки и настройки с подробным разбором всех шагов Вы можете можете узнать в статье "Развертывание ASP.NET Core приложений на Ubuntu Linux".

В решении сам сервис представлен в виде проекта YPermitin.ExternalDevices.ManagementService, который является REST-сервисом на базе ASP.NET Core и использует платформу .NET 8 версии. В контексте нашего примера нас интересуют следующие части этого приложения:

  • NetworkDiscoveryHostedService - это фоновая задача, которая раз в 10 секунд отправляет широковещательное сообщение UDP, чтобы клиентские приложения могли его обнаружить.

    Для этого используется метод SendBroadcastMessage из библиотеки DeviceDetector, реализующей рассылку и прием многоадресных сообщений UDP, о которых шла речь в раздеах выше. Если Вы посмотрите исходный код библиотеки DeviceDetector, то увидите ту же самую логику создания и инициализации класса UdpClient, в том числе присоединение его к группе методом JoinMulticastGroup.

    Единственным большим отличием будет метод приема сообщений, который реализован через StartSearch. В нем мы запускаем операцию приема через метод ReceiveAsync, а затем ожидаем получения сообщений с указанным текстовым содержимым. При получении каждого сообщения срабатывает событие onDeviceDetected, оповещающее вызывающий код о найденном устройстве.

    Весь описанный процесс выполняется до тех пор, пока не будет отменен явно через токен отмены операции (передается в качестве аргумента метода), или по достижении таймаута из аргумента timeoutMs.

    В текущем сервисе мы используем только метод отправки сообщений SendBroadcastMessage, а метод обнаружения сообщенийStartSearch будет использоваться в клиентском приложении для Android. О нем речь будет ниже.

  • ServiceInfoController - контроллер для реализации метода REST API с целью возвращения базовой информации о сервисе, а именно:
    • Hostname - имя хоста сервиса.
    • HostDateUTC - текущая дата по UTC.

    Здесь нет чего-то особенного. Фактически метод используется лишь для проверки связи с сервисом со стороны клиентского приложения.

Остальные части сервиса явно не относятся к теме статьи. Например, там есть часть, отвечающая за проверку списка WiFi-сетей, доступных для устройства. Или интеграция с DBus для *.nix систем. Плюс некоторые другие вещи.

При успешном запуске сервиса на Rassbery Pi Вы увидите примерно такой вывод:

Запуск сервиса на Rasberry Pi

У Вашего устройства может не быть модуля WIFi, тогда сканер сетей работать не будет и в большинстве случаев будут видны только логи о рассылке широковещательных сообщений UDP. Ничего страшного в этом нет, все-таки мы пришли сюда не по этой теме.

Rasberry Pi с WiFi модулем

Итак, Rasberry Pi с опубликованным сервисом у нас готовы и подключены к сети. Теперь перейдем к мобильной разработке.

MAUI в деле

MAUI - это кросплатформенная среда для создания собственных мобильных и классических приложений на платформе .NET (C#). Для использования той же кодовой базы и библиотек, что и для серверного приложения, а также для простоты примера мы будем использовать именно MAUI, чтобы создать мобильное приложение.

В решении само приложение представлено в проекте YPermitin.ExternalDevices.YPED.

К проекту подключена библиотека DeviceDetector, которую мы уже использовали в сервисе на Rasberry Pi. На этот раз из этой библиотеки мы будем использовать метод StartSearch для поиска устройств в сети.

В исходном коде главной страницы приложения мы можем увидеть инициализацию объекта DeviceDetector при создании страницы. Вызов метода StartSearch выполняется при нажатии кнопки "Обнаружить устройства", после чего в течении 60 секунд приложение слушает сеть на наличие широковещательных сообщений UDP. При этом присылаемые данные должны подходить по формату.

Как только сообщение нужного формата получено, из него получается информация о найденном устройстве и выводится на экран. Вот как это выглядит.

Android. Поиск устройств

При нажатии на элемент списка найденных устройств, приложение отправки HTTP-запрос к API найденного сервиса для получения базовой информации (вызов метода контроллера ServiceInfoController, о котором речь шла выше). Смотри событие DeviceItemTapped на главной странице.

Android. Запрос к устройству

Таким образом, мы создали мобильное Android-приложение, используя ту же самую кодовую базу на языке C#, что используется и в сервисе для Rasberry Pi. Наше клиентское приложение находит устройства в сети и может отправлять к нему запросы. Еще бонусом в приложении добавлена функция определиня внешнего IP-адреса в сети интернет через обращение к API api.tinydevtools.ru, но к теме статьи это мало относится.

Android. Мой IP

Теперь обнаружить устройста в сети не составит труда!

Летим дальше!

Поиск устройств в сети не такая уж и редкая задача. Думаю, многие выполняли сопряжение со смартфоном гарнитуры или часов. Все это из той же области.

Сегодня мы рассмотрели поиск устройств через ping (т.е. через ICMP-пакеты), через протокол ARP, широковещательные сообщения UDP, а также рассмотрели пример приложений для Rasberry Pi и Android.

Но найти устройство это лишь начало, дальше нужно им управлять. А это уже другая история...

Спасибо, что дочитали! Хорошего вам дня и успехов в делах!

Это интересно

Y

YPermitin

.NET, TSQL, DevOps, 1C:Enterprise

Developer, just developer.

Поделиться

Другие статьи

Расширение для SQL Server. Быстро и просто. SQLCLR снова в деле
Расширение для SQL Server. Быстро и просто. SQLCLR снова в деле
Решение проблем с модулями VMware в Ubuntu 22.04
Решение проблем с модулями VMware в Ubuntu 22.04
Берем процессы под контроль в .NET
Берем процессы под контроль в .NET

Все статьи от автора: YPermitin

Copyright © 2024 Убежище инженера