Привет, я покажу, как написать 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()
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 (Отправляет изображения)
Автор: Luchnik22