Привет, я покажу, как написать Remote — Desktop клиент, используя C# + XNA

На написание этой статьи меня вдохновил вот этот топик
Я очень долго ждал второй части той статьи, но так и не дождался. Как утверждал автор, во второй статье должна была быть реализация передачи изображения по протоколу UDP на удалённый клиент. После я пытался сам реализовать второю часть статьи, но всегда выходило плохо. Из — за медленного рисования GDI — программа просто зависала на компьютере Core 2 Duo 2.66 GHz, Nvidia GeForce 9600 GT. Я использовал разные алгоритмы оптимизации, но это слабо помогало и тогда я решил использовать XNA.
Очень сильно хотелось выбрать протокол передачи TCP, с ним меньше проблем, но я выбрал UDP, потомучто все говорят, что для таких дел лучше его брать бла бла бла… Вам наверное интересно почему с UDP больше проблем? Ответ прост- UDP сообщение не может превысить размер в 65 507 байт, что очень не удобно. Наши пакеты составляют в среднем размер 130 000 байт (для экрана размером 1366x768), при попытке отправить такой пакет возникает ошибка, как показано ниже.

Решить эту проблему можно двумя путями:
1) Создать костыль
2) Создать структуру
1) Так как я ленивый, выбрал костыль. Костыль заключается в том, чтобы разбивать более большое сообщение на множество маленьких и в первом сообщении писать количество кусков которое будет отправляться. Костылём я назвал, потомучто, потеряв первое сообщение, программаполетит к чертям не сможет нормально склеить изображение (она не будет знать на сколько частей разбито изображение).
2) Можно разбивать экран на множество кусочков и запоминать их координаты. Всё это надо будет хранить в структуре, что очень удобно, кстати, этот алгоритм поможет в будущем сделать оптимизацию.
Начну с простого. С отправителя. Отправлять мы будем скриншоты нашего экрана на удалённый компьютер. Я написал функцию для загрузки данных и инициализации некоторых переменных.
Точкой запуска будет наша функция Run()
Сначала загружаются данные Load(), после происходит объявление переменных и зацикливание. В цикле мы получаем изображение экрана, конвертируем в массив байтов, используем мой костыль (разбиение сообщения на несколько под сообщений — CutMsg(bytes)), отправляем все пакеты.
В функции Load() ничего интересного не происходит.
Из файла ip.txt будут считываться две строки. Первая строка — IP адрес на который нужно отсылать данные. Вторая строка — Порт, на который будет происходить отсылка. Также там будет происходить получение длины и ширины экрана.
Функция конвертирования
И самое интересное — реализация костыля.
Я делю данные по блокам 65500 (число взял меньше, чтобы явно попасть) и записываю их в лист массивов байтов, после я возвращаю этот лист.
С получателем всё сложнее, я там использовал делегаты и события для асинхронной работы и утомлять вас кодом не хочу, что так напишу основное.
Асинхронное получение данных.
Снова видим зацикливание, далее получаем первый пакет, с него считываем первый байт (в этом байте записано количество будущих сообщений), если длина сообщения больше 10, то первый пакет мы явно потеряли, следовательно прибавим счётчик потерь, иначе получаем все сообщения — склеиваем в одно и вызываем событие GetData(byte []).
В GetData(byte[]) мы получаем Texture2D, конвертируя её из массива байтов.
Весь проект вы сможете скачать в конце статьи, что так не отчаивайтесь если что — то я не написал.
В итоге при одновременном запуске «отправителя» и «получателя» на своём компьютере происходит рекурсия и огромное количество потерь (30 — 90 потерь), при запуске «отправителя» на моём компьютере, а на компьютере родителей «получателя», потерь минимум (10 — 15 потерь). Оба компьютера (родителей и мой) соединены в одну Wi-Fi сеть с каналом 54 Мбит/с. Есть пинг (около 250 мс.) — напоминает по пингу TeamViewer. Если добавить оптимизацию и заменить костыль, то получится отличная программа для передачи изображения.
Рекурсия

Компьютер родителей (передача изображения с моего компьютера на их)

Как выглядит потеря

В следующей статье я доделаю программу, а точнее добавлю возможность удалённого управления и возможно ещё оптимизирую её.
Скачать проект
Скачать Receiver (Получает изображения)
Скачать Sender (Отправляет изображения)
P.S. Перезалил исходный код на гитхаб github.com/Luchanso/remote-desktop

На написание этой статьи меня вдохновил вот этот топик
Немного от себя
Я очень долго ждал второй части той статьи, но так и не дождался. Как утверждал автор, во второй статье должна была быть реализация передачи изображения по протоколу UDP на удалённый клиент. После я пытался сам реализовать второю часть статьи, но всегда выходило плохо. Из — за медленного рисования GDI — программа просто зависала на компьютере Core 2 Duo 2.66 GHz, Nvidia GeForce 9600 GT. Я использовал разные алгоритмы оптимизации, но это слабо помогало и тогда я решил использовать XNA.
Выбор протокола передачи
Очень сильно хотелось выбрать протокол передачи TCP, с ним меньше проблем, но я выбрал UDP, потомучто все говорят, что для таких дел лучше его брать бла бла бла… Вам наверное интересно почему с UDP больше проблем? Ответ прост- UDP сообщение не может превысить размер в 65 507 байт, что очень не удобно. Наши пакеты составляют в среднем размер 130 000 байт (для экрана размером 1366x768), при попытке отправить такой пакет возникает ошибка, как показано ниже.

Решить эту проблему можно двумя путями:
1) Создать костыль
2) Создать структуру
1) Так как я ленивый, выбрал костыль. Костыль заключается в том, чтобы разбивать более большое сообщение на множество маленьких и в первом сообщении писать количество кусков которое будет отправляться. Костылём я назвал, потомучто, потеряв первое сообщение, программа
2) Можно разбивать экран на множество кусочков и запоминать их координаты. Всё это надо будет хранить в структуре, что очень удобно, кстати, этот алгоритм поможет в будущем сделать оптимизацию.
Практика
Начну с простого. С отправителя. Отправлять мы будем скриншоты нашего экрана на удалённый компьютер. Я написал функцию для загрузки данных и инициализации некоторых переменных.
Точкой запуска будет наша функция Run()
public void Run() { Load(); // Загружаем данные и получаем размер экрана udpClient = new UdpClient(); Bitmap BackGround = new Bitmap(width, height); Graphics graphics = Graphics.FromImage(BackGround); while (true) { // Получаем снимок экрана graphics.CopyFromScreen(0, 0, 0, 0, new Size(width, height)); // Получаем изображение в виде массива байтов byte [] bytes = ConvertToByte(BackGround); List<byte[]> lst = CutMsg(bytes); for (int i = 0; i < lst.Count; i++) { // Отправляем картинку клиенту udpClient.Send(lst[i], lst[i].Length, ipEndPoint); } } }
Сначала загружаются данные Load(), после происходит объявление переменных и зацикливание. В цикле мы получаем изображение экрана, конвертируем в массив байтов, используем мой костыль (разбиение сообщения на несколько под сообщений — CutMsg(bytes)), отправляем все пакеты.
В функции Load() ничего интересного не происходит.
Из файла ip.txt будут считываться две строки. Первая строка — IP адрес на который нужно отсылать данные. Вторая строка — Порт, на который будет происходить отсылка. Также там будет происходить получение длины и ширины экрана.
Функция конвертирования
private byte [] ConvertToByte(Bitmap bmp) { MemoryStream memoryStream = new MemoryStream(); // Конвертируем в массив байтов с сжатием Jpeg bmp.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Jpeg); return memoryStream.ToArray(); }
И самое интересное — реализация костыля.
private List<byte[]> CutMsg(byte[] bt) { int Lenght = bt.Length; byte[] temp; List<byte[]> msg = new List<byte[]>(); MemoryStream memoryStream = new MemoryStream(); // Записываем в первые 2 байта количество пакетов memoryStream.Write( BitConverter.GetBytes((short)((Lenght / 65500) + 1)), 0, 2); // Далее записываем первый пакет memoryStream.Write(bt, 0, bt.Length); memoryStream.Position = 0; // Пока все пакеты не разделили - делим КЭП while (Lenght > 0) { temp = new byte[65500]; memoryStream.Read(temp, 0, 65500); msg.Add(temp); Lenght -= 65500; } return msg; }
Я делю данные по блокам 65500 (число взял меньше, чтобы явно попасть) и записываю их в лист массивов байтов, после я возвращаю этот лист.
Код получателя
С получателем всё сложнее, я там использовал делегаты и события для асинхронной работы и утомлять вас кодом не хочу, что так напишу основное.
Асинхронное получение данных.
int countErorr = 0; private void AsyncReceiver() { IPEndPoint ep = new IPEndPoint(IPAddress.Loopback, 0); while (true) { try { MemoryStream memoryStream = new MemoryStream(); byte[] bytes = udpClient.Receive(ref ep); memoryStream.Write(bytes, 2, bytes.Length - 2); int countMsg = bytes[0] - 1; if (countMsg > 10) throw new Exception("Потеря первого пакета"); for (int i = 0; i < countMsg; i++) { byte[] bt = udpClient.Receive(ref ep); memoryStream.Write(bt, 0, bt.Length); } GetData(memoryStream.ToArray()); memoryStream.Close(); } catch { countErorr++; } } }
Снова видим зацикливание, далее получаем первый пакет, с него считываем первый байт (в этом байте записано количество будущих сообщений), если длина сообщения больше 10, то первый пакет мы явно потеряли, следовательно прибавим счётчик потерь, иначе получаем все сообщения — склеиваем в одно и вызываем событие GetData(byte []).
В GetData(byte[]) мы получаем Texture2D, конвертируя её из массива байтов.
private void Receive_GetData(byte[] Date) { BackGround = ConvertToTexture2D(Date); } private Texture2D ConvertToTexture2D(byte[] bytes) { MemoryStream memoryStream = new MemoryStream(bytes); System.Drawing.Bitmap bmp = (System.Drawing.Bitmap)System.Drawing.Bitmap.FromStream(memoryStream); // Конвертируем картинку в .png, потомучто Texture2D ест только его memoryStream = new MemoryStream(); bmp.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png); // Получаем из потока Texture2D return Texture2D.FromStream(GraphicsDevice, memoryStream); }
Весь проект вы сможете скачать в конце статьи, что так не отчаивайтесь если что — то я не написал.
Итоги и вывод
В итоге при одновременном запуске «отправителя» и «получателя» на своём компьютере происходит рекурсия и огромное количество потерь (30 — 90 потерь), при запуске «отправителя» на моём компьютере, а на компьютере родителей «получателя», потерь минимум (10 — 15 потерь). Оба компьютера (родителей и мой) соединены в одну Wi-Fi сеть с каналом 54 Мбит/с. Есть пинг (около 250 мс.) — напоминает по пингу TeamViewer. Если добавить оптимизацию и заменить костыль, то получится отличная программа для передачи изображения.
Рекурсия

Компьютер родителей (передача изображения с моего компьютера на их)

Как выглядит потеря

В следующей статье я доделаю программу, а точнее добавлю возможность удалённого управления и возможно ещё оптимизирую её.
Скачать проект
Скачать Receiver (Получает изображения)
Скачать Sender (Отправляет изображения)
P.S. Перезалил исходный код на гитхаб github.com/Luchanso/remote-desktop
