Решил написать не сложную утили для мониторинга сети в фоновом режиме отслеживающую главные показатели и сохраняя их в логи. Все это будет работать под виндой, по этому будем изучать новый язык 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).