Поиск устройств в сети с .NET (C#)
#.NET
#C#
#разработка
#сеть
#устройства
Чтобы найти иголку в стоге сена, достаточно сжечь сено
и провести магнитом над пеплом.
(с) Бернар Вербер
Поиск устройств в сети с помощью C# (.NET). Пинг, сканирование сети, ARP, широковещательные сообщения UDP. А также делаем пример приложений для RasberryPi и Android с возможностью поиска устройства со смартфона.
Содержание
- Сеть вокруг нас
- Пинг-понг
- Шах и ARP
- Крик через UDP
- Rasberry Pi + Android
- Прокаченная малинка
- MAUI в деле
- Летим дальше!
- Это интересно
Сеть вокруг нас
Информация витает вокруг нас. Компьютерные сети стали повседневной нормальностью. Не важно дома ли Вы, на работе или прогуливаетесь на улице - сеть будет рядом, вы постоянно подключены. Глобальная сеть, он же интернет, доступна отовсюду: со смартфонов и компьютеров, из дома или за городом, на земле или в самолете. А количество подключенных к сети устройств имеет просто невероятное количество. Вот он, современный мир. Не зря же существует такое понятие как интернет вещей.
Давайте вернемся от высоких рассуждений к более практичным вопросам. Такое распостранение компьютерных сетей и устройств создают потребность в решении задач разработки ПО для таких устройств, их удобной интеграции, поиска и так далее. В большинстве языков программирования и платформ разработки имеются встроенные средства для работы с сетью. Платформа .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 Вы можете видеть ниже:
Перед этим приложение сканирует все 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 Вы увидите примерно такой вывод:
У Вашего устройства может не быть модуля WIFi, тогда сканер сетей работать не будет и в большинстве случаев будут видны только логи о рассылке широковещательных сообщений UDP. Ничего страшного в этом нет, все-таки мы пришли сюда не по этой теме.
Итак, Rasberry Pi с опубликованным сервисом у нас готовы и подключены к сети. Теперь перейдем к мобильной разработке.
MAUI в деле
MAUI - это кросплатформенная среда для создания собственных мобильных и классических приложений на платформе .NET (C#). Для использования той же кодовой базы и библиотек, что и для серверного приложения, а также для простоты примера мы будем использовать именно MAUI, чтобы создать мобильное приложение.
В решении само приложение представлено в проекте YPermitin.ExternalDevices.YPED.
К проекту подключена библиотека DeviceDetector, которую мы уже использовали в сервисе на Rasberry Pi. На этот раз из этой библиотеки мы будем использовать метод StartSearch для поиска устройств в сети.
В исходном коде главной страницы приложения мы можем увидеть инициализацию объекта DeviceDetector при создании страницы. Вызов метода StartSearch выполняется при нажатии кнопки "Обнаружить устройства", после чего в течении 60 секунд приложение слушает сеть на наличие широковещательных сообщений UDP. При этом присылаемые данные должны подходить по формату.
Как только сообщение нужного формата получено, из него получается информация о найденном устройстве и выводится на экран. Вот как это выглядит.
При нажатии на элемент списка найденных устройств, приложение отправки HTTP-запрос к API найденного сервиса для получения базовой информации (вызов метода контроллера ServiceInfoController, о котором речь шла выше). Смотри событие DeviceItemTapped на главной странице.
Таким образом, мы создали мобильное Android-приложение, используя ту же самую кодовую базу на языке C#, что используется и в сервисе для Rasberry Pi. Наше клиентское приложение находит устройства в сети и может отправлять к нему запросы. Еще бонусом в приложении добавлена функция определиня внешнего IP-адреса в сети интернет через обращение к API api.tinydevtools.ru, но к теме статьи это мало относится.
Теперь обнаружить устройста в сети не составит труда!
Летим дальше!
Поиск устройств в сети не такая уж и редкая задача. Думаю, многие выполняли сопряжение со смартфоном гарнитуры или часов. Все это из той же области.
Сегодня мы рассмотрели поиск устройств через ping (т.е. через ICMP-пакеты), через протокол ARP, широковещательные сообщения UDP, а также рассмотрели пример приложений для Rasberry Pi и Android.
Но найти устройство это лишь начало, дальше нужно им управлять. А это уже другая история...
Спасибо, что дочитали! Хорошего вам дня и успехов в делах!