0. Предисловие
Все началось с очередного звонка пользователя, который с гордостью сообщил сообщил: — „Всё сломалось“, и с моих „потуг“ удаленно найти PC, на котором работает данный пользователь..
Решение планировало быть простым до безумия и собираться на коленке. Так-как большинство наших сотрудников работают под "виндой" и все рабочие станции входят в домен, был задан вектор поиска решения. Изначально, планировалось написать небольшой скрипт. В его задачу входило собрать базовую информацию о системе и сотруднике, который за этой системой работает. Набор информации минимальный. А именно: логин, название рабочей станции и ее ip. Результат работы сохраняем на сервере, а сам скрипт "вешаем" на пользователя через GPO.
В такой реализации были существенные недостатки в виде:
- получить информацию можно было бы только зайдя на сервер (его сетевую папку где хранился файл), что не всегда удобно
- поддерживать файл в актуальном состоянии
- получать данные в реальном времени
После раздумий пришло решение: использовать бота в Telegram. Прибегнув к небольшой ловкости рук, скрипт был переписан в небольшую программку для отправки информации в чат, за место "скучной" записи в файл на сервере. (+ были добавлены еще некоторые параметры которые оправлялись боту)
P.S. Данные приведенные на изображение отцензурированы для сохранения коммерческой тайны.
Но и такой подход решил только проблему с доступностью информации, одновременно сохраняя остальные минусы старого подхода.
Нужно было что-то менять. Было решено написать полноценное клиент-серверное приложение.
Концепция проста. Пишем сервер который будет обслуживать входящие соединения от клиента и отсылать ему запрашиваемую информацию.
1. Пишем сервер
Для начала выбираем протокол для "общения". Выбор не велик — UDP/TCP. Я решил в пользу TCP. Преимущества очевидны:
- обеспечивает надежную связь
- обмен данными в рамках одной сессии
Начнем с создания класса пользователя
public class User
{
public string Name { get; set; }
public string PC { get; set; }
public string IP { get; set; }
public string Version { get; set; }
public byte[] Screen { get; set; }
}
Изначально он имел только 3 свойства. Но в процессе разработки, код сервера вида изменялся. Появлялся новый функционал. Версия стала необходима для совместимости клиента и сервера. Брать версию из сборки я не стал, решив что она избыточна. Так-же появилась возможность делать скрин экрана пользователя.
Конструктор:
public User(string name, string pc, string ip, string version)
{
this.Name = name;
this.PC = pc;
this.IP = ip;
this.Version = version;
}
Нам не всегда нужно передавать снимок экрана. Поэтому создаем перегрузку конструктора:
public User(string name, string pc, string ip, string version, byte[] screen)
{
this.Name = name;
this.PC = pc;
this.IP = ip;
this.Version = version;
this.Screen = screen;
}
Забегая немного в перед, скажу что изначально данные передавались через BinaryWriter "построчно" и без приведения к общему типу данных. Что было очень неудобно при добавление новых функций в приложение. Переписывание функции отправки данных привело к добавлению возможности их сериализации. Теперь объект User можно было представить в трех видах:
- Binary
- JSON
- XML
[Serializable, DataContract]
public class User
{
[DataMember]
public string Name { get; set; }
[DataMember]
public string PC { get; set; }
[DataMember]
public string IP { get; set; }
[DataMember]
public string Version { get; set; }
[DataMember]
public byte[] Screen { get; set; }
public User(string name, string pc, string ip, string version)
{
this.Name = name;
this.PC = pc;
this.IP = ip;
this.Version = version;
}
public User(string name, string pc, string ip, string version, byte[] screen)
{
this.Name = name;
this.PC = pc;
this.IP = ip;
this.Version = version;
this.Screen = screen;
}
public byte[] GetBinary()
{
BinaryFormatter formatter = new BinaryFormatter();
using (MemoryStream stream = new MemoryStream())
{
formatter.Serialize(stream, this);
return stream.ToArray();
}
}
public byte[] GetXML()
{
XmlSerializer formatter = new XmlSerializer(typeof(User));
using (MemoryStream stream = new MemoryStream())
{
formatter.Serialize(stream, this);
return stream.ToArray();
}
}
public byte[] GetJSON()
{
DataContractJsonSerializer jsonFormatter = new DataContractJsonSerializer(typeof(User));
using (MemoryStream stream = new MemoryStream())
{
jsonFormatter.WriteObject(stream, this);
return stream.ToArray();
}
}
}
Что-бы иметь возможность десериализации бинарного объекта User, пришлось вынести его в отдельную библиотеку и использовать в программе уже через нее.
Также хочу обратить внимание на поток который мы получаем на выходе. Массив байтов возвращается через метод ToArray. Его минус — он создает копию стрима в памяти. Но это не критично, в отличие от использования метода GetBuffer, который возвращает нам не чистый массив данных, а полностью весь поток (смысл в том что память выделенная под поток может быть заполнена не полностью), в результате мы получаем увеличения массива. К сожалению этот нюанс я увидел не сразу. А только при детальном анализе данных.
За обработку наших соединений отвечает класс ClientObject
public class ClientObject
{
public TcpClient client;
[Flags]
enum Commands : byte
{
GetInfoBin = 0x0a,
GetInfoJSON = 0x0b,
GetInfoXML = 0x0c,
GetScreen = 0x14,
GetUpdate = 0x15,
GetTest = 0xff
}
public ClientObject(TcpClient tcpClient)
{
client = tcpClient;
}
protected void Sender(TcpClient client, byte[] data)
{
try
{
Logger.add("Sender OK 0xFF");
BinaryWriter writer = new BinaryWriter(client.GetStream());
writer.Write(data);
writer.Flush();
writer.Close();
}
catch (Exception e)
{
Logger.add(e.Message + "0xFF");
}
}
protected byte[] _Info ()
{
return new User.User(Environment.UserName, Environment.MachineName, GetIp(),
Settings.Version, _Screen()).GetBinary();
}
protected byte[] _Info(string type)
{
User.User tmp = new User.User(Environment.UserName, Environment.MachineName, GetIp(),
Settings.Version);
switch (type)
{
case "bin": return tmp.GetBinary();
case "json": return tmp.GetJSON();
case "xml": return tmp.GetXML();
}
return (new byte[1] { 0x00 });
}
protected byte[] _Screen()
{
Bitmap bm = new Bitmap(Screen.PrimaryScreen.Bounds.Width,
Screen.PrimaryScreen.Bounds.Height);
Graphics gr = Graphics.FromImage(bm as Image);
gr.CopyFromScreen(0, 0, 0, 0, bm.Size);
using (MemoryStream stream = new MemoryStream())
{
bm.Save(stream, ImageFormat.Jpeg);
return stream.ToArray();
}
}
protected byte[] _Test()
{
return Encoding.UTF8.GetBytes("Test send from server");
}
public void CmdUpdate(Process process)
{
Logger.add("Command from server: Update");
try
{
string fileName = "Update.exe", myStringWebResource = null;
WebClient myWebClient = new WebClient();
myStringWebResource = Settings.UrlUpdate + fileName;
myWebClient.DownloadFile(myStringWebResource, fileName);
Process.Start("Update.exe", process.Id.ToString());
}
catch (Exception e)
{
Logger.add(e.Message);
}
finally
{
Logger.add("Command end");
}
}
public void _Process()
{
try
{
BinaryReader reader = new BinaryReader(this.client.GetStream());
byte cmd = reader.ReadByte();
Logger.add(cmd.ToString());
switch ((Commands)cmd)
{
case Commands.GetInfoBin: Sender(this.client, _Info("bin")); break;
case Commands.GetInfoJSON: Sender(this.client, _Info("json")); break;
case Commands.GetInfoXML: Sender(this.client, _Info("xml")); break;
case Commands.GetScreen: Sender(this.client, _Screen()); break;
case Commands.GetUpdate: CmdUpdate(Process.GetCurrentProcess()); break;
case Commands.GetTest: Sender(this.client, _Test()); break;
default: Logger.add("Incorrect server command "); break;
}
reader.Close();
}
catch (Exception e)
{
Logger.add(e.Message + " 0x2F");
}
finally
{
Logger.add("Client close connect");
this.client.Close();
MemoryManagement.FlushMemory();
}
}
static string GetIp()
{
IPHostEntry host = Dns.GetHostEntry(Dns.GetHostName());
return host.AddressList.FirstOrDefault(ip => ip.AddressFamily ==
AddressFamily.InterNetwork).ToString();
}
}
В нем описываются все команды которые поступают от клиента. Команды реализованы очень просто. Подразумевалось что клиентом может выступать любое устройство, программа или сервер обработки. Поэтому характер ответа задается получением одного байта:
[Flags]
enum Commands : byte
{
GetInfoBin = 0x0a,
GetInfoJSON = 0x0b,
GetInfoXML = 0x0c,
GetScreen = 0x14,
GetUpdate = 0x15,
GetTest = 0xff
}
Можно быстро добавить новый функционал или построить сложную логику поведения, которая будет определяться все-го 1 байтом используя битовую маску. Для удобства все байты приведены к читаемым командам.
За оправку данных отвечает метод Sender который принимает на вход объект TcpClient и набор данных в виде массива байтов.
protected void Sender(TcpClient client, byte[] data)
{
try
{
Logger.add("Sender OK");
BinaryWriter writer = new BinaryWriter(client.GetStream());
writer.Write(data);
writer.Flush();
writer.Close();
}
catch (Exception e)
{
Logger.add(e.Message);
}
}
Тоже все довольно сдержанно. Создаем BinaryWriter из потока от TcpClient пишем в него массив байт, отчищаем и закрываем.
За создание объекта User, отвечает метод ._Info который имеет перегрузку
protected byte[] _Info ()
{
return new User.User(Environment.UserName, Environment.MachineName, GetIp(),
Settings.Version, _Screen()).GetBinary();
}
protected byte[] _Info(string type)
{
User.User tmp = new User.User(Environment.UserName, Environment.MachineName, GetIp(),
Settings.Version);
switch (type)
{
case "bin": return tmp.GetBinary();
case "json": return tmp.GetJSON();
case "xml": return tmp.GetXML();
}
return (new byte[1] { 0x00 });
}
Инициализируем новый экземпляр User, заполняем конструктор и сразу вызываем метод .GetBinary для получения сериализованных данных. Перегрузка понадобиться нам, если мы хотим явно указать какой тип данных мы хотим получить.
Метод ._Screen, отвечает за создание скриншота рабочего стола.
Из интересного. Здесь можно выделить метод CmdUpdate. Он принимает на вход
текущей процесс:
CmdUpdate(Process.GetCurrentProcess());
Данный метод реализует обновления нашего сервера по команде клиента. Внутри него создается объект WebClient, который скачивает программу помощник с сервера/сайта указанного источника, необходимую для обновления самого сервера. После чего запускает ее и передает в качестве входного параметра, ID текущего процесса:
string fileName = "Update.exe", myStringWebResource = null;
WebClient myWebClient = new WebClient();
myStringWebResource = Settings.UrlUpdate + fileName;
myWebClient.DownloadFile(myStringWebResource, fileName);
Process.Start("Update.exe", process.Id.ToString());
Точкой входа, у обработчика выступает ._Process. Он создает BinaryReader и считывает из него байт команды. В зависимости от полученного байта, выполняется та или иная операция. В конце мы завершаем работу клиента и отчищаем память.
Получать obj TcpClient, мы будем с помощью TcpListener в вечном цикле, используя .AcceptTcpClient. Полученный объект клиента, мы передаем в наш обработчик. Запуская его в новом потоке, для избежания блокировки main thread
static TcpListener listener;
try
{
listener = new TcpListener(IPAddress.Parse("127.0.0.1"), Settings.Port);
listener.Start();
Logger.add("Listener start");
while (true)
{
TcpClient client = listener.AcceptTcpClient();
ClientObject clientObject = new ClientObject(client);
Task clientTask = new Task(clientObject._Process);
clientTask.Start();
MemoryManagement.FlushMemory();
}
}
catch (Exception ex)
{
Logger.add(ex.Message);
}
finally
{
Logger.add("End listener");
if (listener != null)
{
listener.Stop();
Logger.add("Listener STOP");
}
}
Еще сервер имеет пару вспомогательных классов: Logger и Settings
static public class Settings
{
static public string Version { get; set; }
static public string Key { set; get; }
static public string UrlUpdate { get; set; }
static public int Port { get; set; }
static public bool Log { get; set; }
static public void Init(string version, string key, string urlUpdate, int port, bool log)
{
Version = version;
Key = key;
UrlUpdate = urlUpdate;
Port = port;
Log = log;
}
}
В дальнейшем планируется возможность сохранения и считывание настроек из файла.
Класс Logger, позволяет нам сохранять в файл события которые возникли во время выполнения программы. Есть возможность, через настройки отключить запись логов.
static class Logger
{
static Stack<string> log_massiv = new Stack<string>();
static string logFile = "log.txt";
static public void add(string str)
{
log_massiv.Push(time() + " - " + str);
write(log_massiv, logFile, Settings.Log);
}
private static void write(Stack<string> strs, string file, bool log)
{
if (log)
{
File.AppendAllLines(file, strs);
log_massiv.Clear();
}
}
private static string time()
{
return
DateTime.Now.Day + "." +
DateTime.Now.Month + "." +
DateTime.Now.Year + " " +
DateTime.Now.Hour + ":" +
DateTime.Now.Minute + ":" +
DateTime.Now.Second;
}
}
2. Client
Будет, но чуть позже.
Комментарии (15)
LoadRunner
18.05.2017 08:57У меня совет автору для второй части — для скриншота активного окна лучше использовать Alt+PrintScreen. Порой коммерческой тайной может быть даже панель задач со всеми значками.
Zam_dev
18.05.2017 10:54Зачем плодить конструкторы…
public User(string name, string pc, string ip, string version)
+ public User(string name, string pc, string ip, string version, byte[] screen)
= public User(string name, string pc, string ip, string version, byte[] screen=null)
k0rsh
18.05.2017 13:28Знакомое тело сервера. У Metanit'а такой пример был, кажется, для консольного чата.
Но речь о другом.
Почему, например, не сделать блок через using?
protected void Sender(TcpClient client, byte[] data) { try { Logger.add("Sender OK"); using(BinaryWriter writer = new BinaryWriter(client.GetStream())) { writer.Write(data); writer.Flush(); } } catch (Exception e) { Logger.add(e.Message); } }
И еще вопрос — не увидел блокировок в логгере. Как он себя поведет, когда несколько потоков его дернут одновременно?ruhex0
18.05.2017 13:38Да, вы правы. Сходство есть. До это-го не разу не работал с сетью в c#, пришлось учиться на лету.
Можно через using. Так будет правильно и надежно.
Проблему с одновременным вызовом не решал. Еще есть моменты которые нужно исправить. Но и логгер без внимания не оставлю )k0rsh
18.05.2017 13:53Ну и, кстати, о логгере. Сдается мне, что передавать стек в метод write через ссылку strs и в этом же методе очищать его непосредственно как log_massiv.Clear() — это не очень правильная практика.
Кроме того, не мешало бы завернуть File.AppendAllLines в блок try/catch на всякий случай.
k0rsh
18.05.2017 14:39В глаза бросается злоупотребление ключевым словом this — его имеет смысл применять тут в том случае, если в области видимости метода имя переменной совпадает с именем переменной в области видимости класса. Т.е.:
class MyClass { protected int num; public void NumMethod(int num) { this.num = num; } }
Еще позволю себе совет: в именовании переменных и методов лучше сразу стараться себя приучить делать это dotnet way :) Никаких подчеркиваний в именах переменных/методов, за исключением приватных полей класса:
private ILogger _logger = null; protected Stack<string> logMassiv = new Stack<string>(); protected int variableName = 0; public void MyMetod(int variableOne, int variableTwo);
ну и т.д. :))
aquamakc
18.05.2017 13:49Но и логгер без внимания не оставлю
Делал реализацию своего логера через потокобезопасную очередь. Суть такова:
Логгер — синглтон, с единственным публичным методом а-ля «addLogRecord».
В обработчике метода происходит добавление записи в очередь и запуск отдельного потока, если он не запущен.
Сам процесс логирования осуществляется в отдельном потоке, который работает, если очередь не пустая.
Этот поток забирает записи из очереди, производит их вывод (например, в файл) и умирает, если записей больше нет.
Кстати, это очень плохой подход:
DateTime.Now.Day + "." + DateTime.Now.Month + "." + DateTime.Now.Year + " " + DateTime.Now.Hour + ":" + DateTime.Now.Minute + ":" + DateTime.Now.Second;
Лучше так:
DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss");
k0rsh
18.05.2017 14:03Сам процесс логирования осуществляется в отдельном потоке, который работает, если очередь не пустая.
Этот поток забирает записи из очереди, производит их вывод (например, в файл) и умирает, если записей больше нет.
Для таких целей можно попробовать постоянно живущий BackgroundWorker и блокировку очереди сообщений посредством ReaderWriterLock. А чтобы не убивать каждый раз процесс, можно построить на базе AutoResetEvent флажок, который поднимается при записи сообщения в очередь и поднятия которого ждет фоновый процесс через WaitOne().aquamakc
18.05.2017 14:06Конечно можно. Вариантов много. Суть не меняется — вынос процесса логирования в отдельный поток. С передачей в него динамической коллекции записей.
Кстати, если используется потокобезопасная очередь ConcurrentQueue, то никаких внешних блокировок не надо делать.
aquamakc
18.05.2017 14:11Возвращаясь к логам, я бы порекомендовал ввести систему ранжирования сообщений. DEBUG, INFO, ALERT, ERROR и т.д. Это позволит задавать, как разные стили вывода сообщений, так и фильтровать избыточные. Ну в общем насколько хватит фантазии.
IL_Agent
Почему же не wcf, webapi или asp.net core наконец? :)
ruhex0
Предвидел такой вопрос. Изначально задумывалось написать за вечер и залить на пеки пользователей. Но все, переросло в нечто большее =) Wcf в перспективе.