Пишем прогу для мониторинга сети на C#.

Решил написать не сложную утили для мониторинга сети в фоновом режиме отслеживающую главные показатели и сохраняя их в логи. Все это будет работать под виндой, по этому будем изучать новый язык C#. По чему Си шарп, по тому что так проще для новичка. То есть меня )))

Задачи.

  • Пинга до заданого хоста.
  • Возможность подключиться по TCP.
  • Доступность роутера и т.п.

Так как программа будет работать в фоновом режиме, оформим ее как системный сервис (для упрощения и отладки сделал пока консольный вариант). Проверка не должна загружать канал, так что не будем флудить пингом и т.д. Будем отправлять несколько запросов раз в две — три минуты. Сохранять логи, загружать настройки будем в каком нибудь удобном формате: JSON и CSV.

Подготовка.

Я скачал Visual Studio с сайта Микрософт. Добавляем подержу .NET и C# при установке . Ну и создаем проект консольной программы для Windos на C#. Запускаем, вроде все работает.

Для нашей программы нам потребуются настройки (IP хоста, порт, задержка и т.д.).Сделаем файл настроек из которого будем загружать данные. Составим список настроек, определимся с форматом переменных для этих значение:

  • Хост и порт для которого мы будет проверять доступность HTTP. Так же зададим и ТАЙМ-АУТ для подключения.
  • Количество пакетов Ping и их тайм-аут. Задержка между отправлениями пакетов.
  • Хост который будем пинговать.
  • IP адрес роутера. Если роутер не доступен то зачем вообще делать все остальное )
  • Максимальный уровень packet loss, при котором подключение считается нормальным.
  • Выходной файл CSV, в который будут дописываться результаты.
  • Изюминка: выходной формат строки для CSV, если ты вдруг решишь отключить вывод ненужных столбцов или изменить порядок.
  • Возможность отключить логирование.

Сделаем все современненько, файл настроек будет в формате JSON — setting.json .

JSON (англ. JavaScript Object Notation) — текстовый формат обмена данными, основанный на JavaScript. Как и многие другие текстовые форматы, JSON легко читается людьми. Формат JSON был разработан Дугласом Крокфордом.

JSON-текст представляет собой (в закодированном виде) одну из двух структур:
Набор пар ключ: значение. В различных языках это реализовано как запись, структура, словарь, хеш-таблица, список с ключом или ассоциативный массив. Ключом может быть только строка (регистрозависимая: имена с буквами в разных регистрах считаются разными), значением — любая форма.
Упорядоченный набор значений. Во многих языках это реализовано как массив, вектор, список или последовательность.
{
  "http_test_host": "api.ipify.org",
  "http_test_port": "80",
  "http_timeout": "3000",
  "ping_count": "10",
  "ping_timeout": "3000",
  "ping_packet_delay": "0",
  "ping_hosts": [ "ya.ru", "1.1.1.1" ],
  "measure_delay": "60000",
  "cui_output": "true",
  "router_ip": "192.168.0.1",
  "nq_max_loss": "0,1",
  "out_file": "log.csv",
  "w_csv": "true",
  "out_format": "FTIME;STIME;IUP;HTTP;AVGRTT;ROUTERRTT;LOSS;RN" 
  }
// RN - маркер конца строки

Кодинг.

Первым делом объявим переменные для настроек :

static String HTTP_TEST_HOST; // HTTP сервер, соединение до которого будем тестировать
        static int HTTP_TEST_PORT; // Порт HTTP сервера
        static int HTTP_TIMEOUT; // Таймаут подключения
        static int PING_COUNT; // Количество пакетов пинга
        static int PING_DELAY; // Ожидание перед отправкой следующего пакета пинга
        static int PING_TIMEOUT; // Таймаут пинга
        static List<String> PING_HOSTS;  // Хосты, пинг до которых меряем
        static int MEASURE_DELAY; // Время между проверками
        static String ROUTER_IP; // IP роутера
        static double MAX_PKT_LOSS; // Максимально допустимый Packet loss
        static String OUT_FILE; // Выходной файл CSV
        static bool WRITE_CSV; // Писать ли CSV
        static String CSV_PATTERN; // Шаблон для записи в CSV
        // Промежуточные переменные
        static bool prev_inet_ok = true;
        static DateTime first_fail_time;
        static long total_time = 0;
        static int pkt_sent = 0;
        static int success_pkts = 0;
        static int exited_threads = 0;
        static Dictionary<string, int> measure_results = new Dictionary<string, int>();

Теперь нам надо загрузить настройки в переменные. Для работы с форматом JSON будем использовать библиотеку Json.NET .

Json.NET — популярная и простая библиотека для работы с JSON.

https://www.nuget.org/packages/Newtonsoft.Json/

Теперь загружаем данные из файла :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO; 
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Threading;
using System.Net.Sockets;
using System.Net.NetworkInformation;

var config = JsonConvert.DeserializeObject<Dictionary<String, Object>>(File.ReadAllText("setting.json"));

            HTTP_TEST_HOST = (String)config["http_test_host"];
            PING_HOSTS = ((JArray)config["ping_hosts"]).ToObject<List<String>>();
            ROUTER_IP = (String)config["router_ip"];
            HTTP_TEST_PORT = int.Parse((String)config["http_test_port"]);
            HTTP_TIMEOUT = int.Parse((String)config["http_timeout"]);
            PING_COUNT = int.Parse((String)config["ping_count"]);
            PING_TIMEOUT = int.Parse((String)config["ping_timeout"]);
            PING_DELAY = int.Parse((String)config["ping_packet_delay"]);
            MEASURE_DELAY = int.Parse((String)config["measure_delay"]);
            OUT_FILE = (String)config["out_file"];
            WRITE_CSV = bool.Parse((String)config["w_csv"]);
            CSV_PATTERN = (String)config["out_format"];
            MAX_PKT_LOSS = double.Parse((String)config["nq_max_loss"]);

            Console.WriteLine("Load config complit...");

Теперь мы создадим понятные заголовки для файла CSV и запишем их в файл.

String CSV_HEADER = CSV_PATTERN
                .Replace("FTIME", "Data")
                .Replace("IUP", "Internet up")
                .Replace("AVGRTT", "Average ping (ms)")
                .Replace("ROUTERRTT", "Ping to router (ms)")
                .Replace("LOSS", "Packet loss, %")
                .Replace("HTTP", "HTTP OK")
                .Replace("STIME", "Time");
            foreach (var host in PING_HOSTS)
            {
                CSV_HEADER = CSV_HEADER.Replace("RN", $"Ping to {host};RN");
            }
            CSV_HEADER = CSV_HEADER.Replace("RN", "\r\n");
            if (WRITE_CSV) // Если запись включена в настройках создать файл и записать заголовки.
            {
                // Если файла нету , создать и записать заголовки.
                if (!File.Exists(OUT_FILE)) File.WriteAllText(OUT_FILE, CSV_HEADER);
            }

Во время мониторинга программа будет собирать много данных, давайте выделим из в отдельную структура.

struct net_state
    {
        public bool inet_ok; // Флаг доступности сети
        public bool http_ok; // Флаг теста http
        public Dictionary<String, int> avg_rtts; // Словарь пинга до хостов
        public double packet_loss; // Потеря пакетов
        public DateTime measure_time; // Дата, время
        public int router_rtt; // 
    }

Давайте теперь напишем функцию записи данных в лог файл:

        static void Save_log(net_state snapshot)
        {
            if (WRITE_CSV) // Если запись логов включена
            {                
                String rtts = "";
                int avg_rtt = 0;
                foreach (var ci in PING_HOSTS)
                {
                    rtts += $"{snapshot.avg_rtts[ci]};";
                    avg_rtt += snapshot.avg_rtts[ci];
                }
                avg_rtt = avg_rtt / PING_HOSTS.Count;
                File.AppendAllText(OUT_FILE, CSV_PATTERN
                    .Replace("FTIME", snapshot.measure_time.ToShortDateString())
                    .Replace("IUP", snapshot.inet_ok.ToString())
                    .Replace("AVGRTT", avg_rtt.ToString())
                    .Replace("ROUTERRTT", snapshot.router_rtt.ToString())
                    .Replace("LOSS", snapshot.packet_loss.ToString())
                    .Replace("HTTP", snapshot.http_ok.ToString())
                    .Replace("STIME", snapshot.measure_time.ToShortTimeString())
                    .Replace("RN", $"{rtts}\r\n"));
            }
        }

Теперь пишем основную часть кода, сам мониторинг в отдельной функции. Проверяем доступность роутера,если не доступен то записываем лог и выходим. Потом проверяем HTTP соединение, затем пингуем заданные хосты.

static void Monit()
        {
            // Создаем экземпляр измерений.
            net_state snapshot = new net_state();
            snapshot.inet_ok = true;
            snapshot.measure_time = DateTime.Now;
            // Проверяем доступность роутера
            Ping ping = new Ping();
            var prr = ping.Send(ROUTER_IP, PING_TIMEOUT);
            // В CSV файле все поля должны быть заполнены. Если роутер не пингуется заполняем их параметром PING_TIMEOUT
            snapshot.router_rtt = prr.Status == IPStatus.Success ? (int)prr.RoundtripTime : PING_TIMEOUT;
            if (prr.Status != IPStatus.Success)
            {
                snapshot.avg_rtts = new Dictionary<string, int>();
                snapshot.http_ok = false;
                snapshot.inet_ok = false;
                snapshot.packet_loss = 1;
                foreach (var ci in PING_HOSTS)
                {
                    snapshot.avg_rtts.Add(ci, PING_TIMEOUT);
                }
                Console.WriteLine("Router was unreachable.");
                Save_log(snapshot);
                return;
            }
            snapshot.inet_ok = true;
            // Проверяем доступность HTTP
            try
            {
                snapshot.http_ok = true;
                TcpClient tc = new TcpClient();
                tc.BeginConnect(HTTP_TEST_HOST, HTTP_TEST_PORT, null, null);
                Thread.Sleep(HTTP_TIMEOUT);
                // Если подключиться не удалось
                if (!tc.Connected)
                {
                    snapshot.http_ok = false;
                }
                tc.Dispose();
            }
            catch { snapshot.http_ok = false; snapshot.inet_ok = false; }
            //Теперь пингуем заданные хосты
            exited_threads = 0;
            pkt_sent = 0;
            success_pkts = 0;
            total_time = 0;
            measure_results = new Dictionary<string, int>();
            // Перебираем все хосты и запускаем пинг в отдельном потоке.
            foreach (var ci in PING_HOSTS)
            {
                Thread thread = new Thread(new ParameterizedThreadStart(PingTest));
                thread.Start(ci);
            }
            while (exited_threads < PING_HOSTS.Count) continue;
            //Анализируем результаты пинга
            snapshot.avg_rtts = measure_results;
            snapshot.packet_loss = (double)(pkt_sent - success_pkts) / pkt_sent;
            snapshot.inet_ok = !(
                snapshot.http_ok == false ||
                ((double)total_time / success_pkts >= 0.75 * PING_TIMEOUT) ||
                snapshot.packet_loss >= MAX_PKT_LOSS ||
                snapshot.router_rtt == PING_TIMEOUT);
            Save_log(snapshot);
            if (prev_inet_ok && !snapshot.inet_ok)
            {
                //Интернет был , но теперь неудачу
                prev_inet_ok = false;
                first_fail_time = DateTime.Now;
            }
            else if (!prev_inet_ok && snapshot.inet_ok)
            {
                String t_s = new TimeSpan(DateTime.Now.Ticks - first_fail_time.Ticks).ToString(@"hh\:mm\:ss");
                prev_inet_ok = true;
            }

Так как хостов для пинга у нас может быть сколько угодно и количество пингов тоже. Выделим процес самого пинга в отдельную функцию PingTest()

        static void PingTest(Object arg)
        {
            String host = (String)arg;
            int pkts_lost_row = 0;
            int local_success = 0;
            long local_time = 0;
            Ping ping = new Ping();
            // Запускаем пинг заданное количество раз.
            for (int i = 0; i < PING_COUNT; i++)
            {
                // Если потеряно 3 пакеты, записываем результаты и выходим из цикла
                if (pkts_lost_row == 3)
                {
                    measure_results.Add(host, (int)(local_time / (local_success == 0 ? 1 : local_success)));
                    exited_threads++;
                    return;
                }
                try
                {
                    var result = ping.Send(host, PING_TIMEOUT);
                    // Если пинг прошел
                    if (result.Status == IPStatus.Success)
                    {
                        pkts_lost_row = 0;
                        local_success++;
                        // RoundtripTime Возвращает количество миллисекунд, затраченных на отправку Эхо-запроса
                        local_time += result.RoundtripTime;
                        total_time += result.RoundtripTime;
                        pkt_sent++;
                        success_pkts++;
                    }
                    switch (result.Status)
                    {
                        case IPStatus.Success: break; //Already handled 
                        case IPStatus.BadDestination:
                            measure_results.Add(host, -1);
                            exited_threads++;
                            return;
                        case IPStatus.DestinationHostUnreachable:
                        case IPStatus.DestinationNetworkUnreachable:
                        case IPStatus.DestinationUnreachable:
                            measure_results.Add(host, -1);
                            exited_threads++;
                            return;
                        case IPStatus.TimedOut:
                            pkts_lost_row++;
                            pkt_sent++;
                            break;
                        default:
                            measure_results.Add(host, -1);
                            exited_threads++;
                            return;
                    }
                }
                catch (Exception xc)
                {
                    exited_threads++;
                    measure_results.Add(host, -1);
                    return;
                }
            }
            measure_results.Add(host, (int)(local_time / (local_success == 0 ? 1 : local_success)));
            exited_threads++;
            return;
        }

Теперь осталось зациклить вызов Monit() и поставить задержу между тестами.

while (true)
            { 
                Monit();
                Thread.Sleep(MEASURE_DELAY);
            }

Вот такой отчет получился у меня.

Думаю тут будет совсем не сложно добавить любой функционал который вам нужен. Например отправку сообщение о сбое в Телеграм.

Скачать код целиком можно с нашего сайте.

В статье использовался материал журнала Хакер( https://xakep.ru).

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.