Pull to refresh

Пишем шустрый Remote — Desktop клиент на C# и XNA

Reading time 4 min
Views 51K
Привет, я покажу, как написать 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 (Отправляет изображения)

P.S. Перезалил исходный код на гитхаб github.com/Luchanso/remote-desktop
Tags:
Hubs:
+16
Comments 25
Comments Comments 25

Articles