PunchSpain mourns after latest train tragedy kills 69ESPNEvery Premier League team ranked by bang-for-buck: What if they spent like Man City?Bollywood HungamaBhumi Pednekar reunites with Akshay Kumar for Priyadarshan’s next comedyInquirerMMDA suspends coding scheme on Monday, Nov. 2UN NewsAfghanistan opium production jumps fivefold as farmers find ways round Taliban banDaily MaverickEl Salvador to send over 2,000 soldiers to Haiti for rescue while elections postponedEl ComercioEstados Unidos revela su hoja de ruta para Venezuela: reformas, vacancia de Delcy Rodríguez y eleccionesZDF heuteAktuelle Pressemitteilungen des ZDFFotogramas"Perfecta para ver en familia en Halloween": La película con la que vuelve a los cines el personaje más entrañable de la animación 'stop motion'Business AMDuitse elitesoldaten van KSK bereiden zich voor op mogelijke oorlog met RuslandSoompiSon Suk Ku And Nagayama Eita’s Opposing Detective Styles Complicate Their Partnership In “The Blue Road”Egypt IndependentHeal the victim, heal the world – how Sri Sri Ravi Shankar reclaims lives in war zones, prisons, and rural communities
The Daily Newsstand · Free, Always
Thursday, October 29, 2026

Как написать свой AnyDesk на C++ и не сойти с ума: битва с OLE, GDI+ и нейросетями

Translate

Каждый, кто работает с удаленными машинами, знает эту боль. Ты подключаешься к серверу, у которого три монитора, а AnyDesk или TeamViewer показывают тебе один общий экран. Или того хуже — заставляют переключаться между ними. Работать с базами данных или версткой в таких условиях — это пытка.

Так родилась идея написать свою программу для удаленного доступа. Без тяжелых Electron.js оберток, весом в 200 МБ, а нативное приложение на Win32 API, которое весит меньше мегабайта и показывает каждый монитор в отдельном окне.

В этой статье я расскажу, как мы (я и нейросети) строили OmniDesk с нуля, какие костыли пришлось изобретать для буфера обмена и почему TCP_NODELAY — это спасение.

Архитектура: Два канала — два порта

Сначала проект был консольным. Прописываешь IP, запускаешь, смотришь. Но для работы через NAT и обхода блокировок потребовался облачный сервер (Хаб).

Главная архитектурная ошибка, которую я допустил в начале, — попытка запихнуть управление и видео в один TCP-сокет. Когда пошли тяжелые кадры, управление мышью начинало жутко лагать. Пришлось разделить потоки на два порта: 8888 (только команды мыши, клавиатуры и буфер) и 8889 (тяжелый видеопоток). Позже я узнал, что можно было просто открыть два сокета на одном порту, но изоляция каналов сработала безотказно.

Видео на честных 60 FPS

Проблема с захватом экрана оказалась не в самом захвате, а в синхронизации потоков. Программа всегда должна работать как Сервер (ждать подключения), а при необходимости становиться Клиентом. Из-за этого на старте сессии постоянно возникали рассинхроны. Клиент ждал видеопоток, а сервер ждал токен начала трансляции. В итоге окно удаленного монитора не появлялось.

Решили через жесткий формат пакетов. Внутри видео-канала мы определили строгую структуру: сначала шлем MAGIC (маркер), затем ID монитора, затем размер кадра.

Вот как выглядит захват и сжатие экрана (кусок из Server.h):

// Захват экрана и сжатие в JPEG через GDI+
static bool CaptureScreenToJpegBuffer(RECT rect, std::vector<char>& out, const CLSID& jpegClsid) {
    // ... захват через BitBlt ...
    Gdiplus::Bitmap bitmap(bmp, NULL);
    IStream* stream = nullptr;
    if (CreateStreamOnHGlobal(NULL, TRUE, &stream) == S_OK) {
        Gdiplus::EncoderParameters params;
        params.Count = 1;
        params.Parameter[0].Guid = Gdiplus::EncoderQuality;
        params.Parameter[0].Type = Gdiplus::EncoderParameterValueTypeLong;
        params.Parameter[0].NumberOfValues = 1;
        ULONG quality = 85; // Баланс размера и качества
        params.Parameter[0].Value = &quality;
        
        // Сжимаем прямо в память
        if (bitmap.Save(stream, &jpegClsid, &params) == Gdiplus::Ok) {
            // ... извлекаем байты в буфер ...
        }
    }
    return ok;
}

А чтобы избежать задержек, мы наглухо отключили алгоритм Нагла (TCP_NODELAY). Без этого Winsock копит мелкие пакеты, чтобы отправить их разом, что добавляет задержку в 40-200 мс.

Самая большая боль: Буфер обмена и OLE COM

Текст копировать легко. А вот файлы и папки — это ад. Если гнать файл по сети в момент нажатия Ctrl+C, проводник зависнет намертво. А если копируешь папку на 10 гигабайт — ты просто убьешь сеть.

Пришлось лезть в дебри Win32 OLE COM. Windows использует интерфейсы IDataObject и IStream для виртуальных файлов. Идея простая: мы даем системе объект, который притворяется файлом. Когда пользователь жмет Ctrl+V, проводник сам запрашивает у нас данные порциями.

Реализация IStream::Read — это сердце магии. Мы не грузим весь файл в память, а запрашиваем чанки по сети:

STDMETHODIMP Read(void* pv, ULONG cb, ULONG* pcbRead) {
    // Формируем запрос: "Сервер, дай мне кусок файла с такого-то смещения"
    OleChunkRequest request{};
    request.opcode = 4;
    request.fileIndex = m_FileIndex;
    request.byteOffset = m_BytesRead;
    request.bytesRequested = static_cast<uint32_t>(bytesToFetch);

    // Шлем запрос
    if (::send(g_ActiveClipboardSocket, (const char*)&request, sizeof(OleChunkRequest), 0) <= 0) {
        return E_FAIL;
    }

    // Ждем ответ — бинарный кусок файла
    if (!RecvAll(g_ActiveClipboardSocket, (char*)pv, bytesToFetch)) {
        return E_FAIL;
    }

    OmniCrypto::ProcessData((char*)pv, bytesToFetch, MSG_FILE_CHUNK);
    m_BytesRead += bytesToFetch;

    if (pcbRead != nullptr) *pcbRead = bytesToFetch;
    return S_OK;
}

Это работает потрясающе. Вы копируете папку с 1000 файлов на удаленной машине, вставляете у себя — и Windows сама подтягивает данные по мере необходимости, не забивая диск и оперативку.

ИИ писал код, а я восстанавливал бэкапы

Отдельная боль — работа с нейросетями. Я не программист в классическом смысле, поэтому 90% кода генерировал Google ИИ (тот, что в поиске по кнопке «Режим ИИ»).

Сначала это казалось магией. Но когда дело дошло до сложной многопоточности (а в удаленном доступе всё многопоточно), начался ад. Нейросеть постоянно путалась в мьютексах. Программа могла спокойно работать несколько часов, а потом вылетать с Access Violation в случайном месте. Кучу раз я откатывал проект из бэкапов, потому что ИИ предлагал «оптимизировать» код, ломал синхронизацию, и всё переставало работать.

Настоящий прорыв случился, когда я перестал просить «напиши мне идеальный код», и начал просить «объясни, почему тут возникает дедлок».

Мультимониторность через один сокет

Сейчас все мониторы передаются через один видеосокет. Сервер просто перебирает их в цикле, делает скриншоты, сжимает и шлет пакеты, в которых указан ID монитора. Клиент читает пакет, смотрит ID и понимает, в какое окно рисовать этот кадр.

// Клиент читает поток и разбирает его
if (!RecvAll(sock, (char*)&magic, sizeof(magic))) break;
if (magic != 0x4F4D4E49) break; // Проверка маркера

int mId = 0; 
uint64_t size = 0;
if (!RecvAll(sock, (char*)&mId, sizeof(mId))) break;     // Какой монитор?
if (!RecvAll(sock, (char*)&size, sizeof(size))) break;   // Размер картинки?

// Если ID нет в памяти — создаем окно, если есть — рисуем кадр

Итог

В итоге получилась программа весом меньше мегабайта, которая:

  • Работает нативно под Windows (никакой Electron.js).

  • Шифрует трафик алгоритмом ChaCha20.

  • Поддерживает копирование файлов «на лету» через OLE COM.

  • Показывает каждый монитор в отдельном окне.

Исходники я пока держу закрытыми (в планах коммерческая лицензия), но если вам интересны какие-то конкретные архитектурные решения — добро пожаловать в комментарии. С радостью расскажу, как ловили баги с CreateStreamOnHGlobal или как заставили GDI+ работать без утечек памяти!

А как вы решаете проблему удаленной работы с многомониторными станциями? Или до сих пор мучаетесь с переключением экранов в AnyDesk?

Если кто-то хочет потестировать программу или посмотреть на демо — пишите в личку, скину ссылку.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.