От клика до железа: хроника одного запроса. Часть 1

Введение

f2aee3c88ae00e44002564b776778bc5.png

Увлекались ли вы когда-нибудь задачей так сильно, что полностью выпадали из жизни? Я — да. Писал код, разбирался с нюансами, тестировал, переделывал, снова тестировал… В какой-то момент мой друг, давно не слышавший обо мне, решил узнать, куда я пропал. Мы созвонились, и я рассказал, чем занимаюсь. Он послушал, усмехнулся: «Как же хорошо, что я выбрал бэкенд-разработку».

На самом деле ничего сверхъестественного в этой задаче не было. Но и простой её тоже не назовёшь — архитектура сложилась под влиянием множества ограничений: браузер не может напрямую запускать exe-файл, бэкенд не имеет доступа к локальному оборудованию, а взаимодействие между всеми этими частями нужно было выстроить чётко и последовательно.

Всё это заставляет внимательнее смотреть на возможности, которые предоставляет среда браузера. Chrome-расширения работают в строго изолированном контексте, JavaScript не имеет доступа к файловой системе или системным вызовам, и для связи с нативным приложением приходится учитывать ряд особенностей: протокол обмена, формат сообщений, правила безопасности и другие детали, которые легко упустить, если не сталкивался с этим раньше.

В этой статье я расскажу, как построить такую связку с помощью механизма Native Messaging: от интерфейса в браузере до запуска локального exe. Разберём архитектуру, покажу, какие задачи решает этот подход, и напишем рабочий пример — расширение, которое сможет общаться с программой на C.

Так что устраивайтесь поудобнее и давайте разбираться.

Ниже — структура статьи, чтобы вы могли сразу перейти к интересующим вас разделам.

  • Мотивация

    • Почему именно этот подход?

      • Как это работает?

      • Выбор технологий

  • Что такое Native Messaging и как оно работает?

  • Ограничения протокола Native Messaging

    • Передача данных только через JSON

    • Фиксированный формат сообщений

    • Нет прямого управления процессами

    • Ограничения по безопасности

    • Ограничение на размер сообщения

  • Разработка Chrome-расширения

    • Что такое Message Passing API?

    • Файловая структура расширения

    • Практика

      • manifest.json

        • Что здесь важно?

      • background.js

      • content.js

        • Почему используется window.postMessage ()?

        • myExtensionApi.js

          • Как веб-страница будет использовать API?

    • Установка расширения

      • Закрепляем ID расширения с помощью key

      • Зачем нужен key?

      • Разработка тестовой веб-страницы

  • Разработка нативного приложения на C

    • Компиляция .exe с помощью GCC

  • Связываем расширение и приложение

    • Регистрация нативного приложения в системе

      • JSON-манифест (nmh-manifest.json)

      • Добавляем записи в реестр (install.bat, uninstall.bat)

  • Проверка работоспособности

  • Что дальше?

  • Ссылка на гитхаб-репозиторий

Мотивация

(Если вам не интересен контекст и вы хотите сразу перейти к практике, можете пропустить этот раздел.)

Прежде чем перейти к техническим деталям, давайте разберёмся в мотивации. Почему вообще возникла необходимость использовать Chrome-расширение и механизм Native Messaging?

Во-первых, это не первый случай, когда в проекте, над которым я работаю, применяют эту технологию. Ещё до моего прихода подобное решение уже использовалось для другого бизнес-кейса. Тогда потребовалось связать между собой несколько компонентов:

  • Бэкенд, который отвечает за управление процессом,

  • WebSocket, обеспечивающий коммуникацию,

  • Фронтенд, обрабатывающий сообщения от WebSocket,

  • Chrome-расширение, которое передаёт команды локальному приложению,

  • Нативное приложение, взаимодействующее с железом,

  • И само оборудование, с которым, собственно, всё и работает.

рис. 1 - Диаграмма последовательности реализованного решения (Альфа).
рис. 1 — Диаграмма последовательности реализованного решения (Альфа).

Этот механизм доказал свою надёжность: весь процесс мог длиться до 48 часов, при этом все его звенья работали чётко и синхронно.

Теперь о текущей задаче. В этот раз речь идёт об интеграции с программно-аппаратным комплексом, предназначенным для выполнения определённых операций с физическими носителями данных. Это не просто железка, а серьёзное оборудование, которое стоит недёшево.

Почему именно этот подход?

Казалось бы, логично было бы организовать всё через бэкенд: просто отправлять команды с сервера напрямую на оборудование. Однако это невозможно.

Проблема в том, что бэкенд и конечное устройство находятся в разных сегментах сети.

  • Изменить сетевую архитектуру нельзя — корпоративная политика безопасности запрещает любые изменения, в том числе открытие дополнительных портов или обходные решения через VPN.

  • Бэкенд не может напрямую взаимодействовать с оборудованием, потому что оно физически недоступно из его сегмента сети.

Поэтому пришлось искать альтернативный путь. Решение? Использовать Chrome-расширение как посредника.

Как это работает?

  • Пользователь в веб-интерфейсе нажимает кнопку.

  • Происходит HTTP-запрос на бэкенд, который создаёт сессию и отправляет её ID в ответе.

  • Фронтенд получает этот ID и передаёт его в Chrome-расширение.

  • Chrome-расширение запускает нативное приложение и передаёт ему ID сессии.

  • Нативное приложение взаимодействует с оборудованием через DLL (для Windows) или .so (Shared Object) для Unix-подобных систем, подключаемые с помощью JNA.

  • Результаты по цепочке передаются обратно на бэкенд.

рис. 2 - Диаграмма последовательности нового бизнес-кейса (Бета).
рис. 2 — Диаграмма последовательности нового бизнес-кейса (Бета).

Выбор технологий

Так как для работы с оборудованием предоставляется набор динамических библиотек (DLL для Windows, .so для Linux/macOS), взаимодействовать с ним можно через C, C++ или C#. В комплекте есть заголовочные файлы и описание API, так что технически всё выглядит достаточно просто: вызываешь нужные методы — получаешь результат.

Для основной реализации нативного приложения был выбран Java. Основная причина — кроссплатформенность. Java позволяет запускать приложение на разных операционных системах без необходимости перекомпилировать код для каждой платформы.

Однако, поскольку в первой части статьи основная цель — объяснить базовые принципы Native Messaging, начнём с более простого примера и напишем простое приложение на C.

Выбор архитектуры был обусловлен не желанием усложнить процесс, а объективными ограничениями инфраструктуры. Chrome-расширение в связке с Native Messaging — это не костыль, а проверенное решение, которое уже однажды показало свою надёжность. Теперь пора разбираться, как это работает.

Что такое Native Messaging и как оно работает?

Native Messaging — это механизм взаимодействия Chrome-расширения с локальным нативным приложением. Его ключевая особенность — использование стандартного ввода/вывода (stdin/stdout) для передачи данных, без открытия сетевых соединений или работы с файловой системой.

Этот механизм предоставляет гибкость, но накладывает ряд ограничений, о которых стоит знать.

Ограничения протокола Native Messaging

1. Передача данных только через JSON

Native Messaging поддерживает только текстовые сообщения в формате JSON. Двоичные данные передавать нельзя, их нужно либо кодировать (например, в Base64), либо сохранять в файл и передавать путь.

2. Фиксированный формат сообщений

Каждое сообщение должно начинаться с 4-байтового заголовка, указывающего его длину, а затем следовать JSON-данные. Это требование жёсткое, и несоблюдение формата приведёт к отказу в обработке сообщения.

Рис. 3 - Структура сообщения Native Messaging.
Рис. 3 — Структура сообщения Native Messaging.

3. Нет прямого управления процессами

Chrome не управляет жизненным циклом нативного приложения. Оно должно самостоятельно завершаться после обработки запроса, если не используется долговременное соединение (сохранение потока открытым).

4. Ограничения по безопасности

  • Нативное приложение не может быть частью расширения — его нужно устанавливать отдельно.

  • Расширение может взаимодействовать только с зарегистрированными нативными приложениями, указанными в манифесте.

  • Нативное приложение не может запустить Chrome-расширение — только наоборот.

5. Ограничение на размер сообщения

  • Сообщение от расширения к нативному приложению не должно превышать 1 МБ.

  • Сообщение от нативного приложения обратно ограничено 4 МБ.

  • Если необходимо передавать большие объёмы данных, можно использовать:

    • Файлы — данные записываются на диск, а расширение получает ссылку на них.

    • Чанки — большие сообщения разбиваются на части и отправляются последовательно, а затем собираются на принимающей стороне.

Рис.4 - Передача данных чанками.
Рис. 4 — Передача данных чанками.

Разработка Chrome-расширения

Прежде чем перейти к коду, разберёмся в условиях, в которых будет работать наше расширение.

Обычно взаимодействие с Chrome-расширением происходит через popup.html — всплывающее окно, которое открывается при нажатии на иконку расширения. Однако в нашем случае взаимодействие будет осуществляться из внешней веб-страницы.

Это накладывает несколько особенностей:

  • Веб-страница не может напрямую взаимодействовать с расширением, поэтому потребуется использовать content script или механизм chrome.runtime.connect.

  • Chrome-расширение должно получить разрешение на доступ к странице в манифесте.

  • Для передачи данных между веб-страницей, content script и background script потребуется Message Passing API.

Рис. 5 - Взаимодействие внешней веб-страницы с Chrome-расширением
Рис. 5 — Взаимодействие внешней веб-страницы с Chrome-расширением

Что такое Message Passing API?

Рис.6 - Иллюстрация работы компонентов Chrome-расширения
Рис. 6 — Иллюстрация работы компонентов Chrome-расширения

«Представьте три комнаты, в каждой из которых происходит что-то своё: в одной работает веб-страница, в другой — content script, а в третьей — background script. Дверей между ними нет, и они не могут просто взять и передать друг другу файлы или команды. Но есть телефонная связь — это и есть Message Passing API.»

Чтобы эти комнаты могли взаимодействовать, используется телефонная система:

  • Content Script может звонить в другие комнаты и передавать информацию, но не может сам инициировать важные процессы.

  • Background Script получает звонки и может выполнить команду, но сам не знает, когда его вызовут.

  • Injected Script (myExtensionApi.js) — это как секретарь, который набирает номер нужной комнаты и передаёт сообщения.

Без Message Passing API эти комнаты существовали бы отдельно, не имея возможности координировать свою работу.

Файловая структура расширения

Теперь, когда понятно, как части расширения общаются друг с другом, разберёмся с его файловой структурой:

Рис. 7- Файловая структура расширения
Рис. 7- Файловая структура расширения
  • background.js — фоновый скрипт расширения.

    • Запускается в фоне и работает как Service Worker.

    • Обрабатывает сообщения от content script и native messaging host.

    • Может выполнять долгосрочные задачи, но не работает постоянно — Chrome выгружает его из памяти, когда он не используется.

  • content.js — контентный скрипт.

    • Встраивается в веб-страницу и взаимодействует с её DOM.

    • Передаёт команды и данные между веб-страницей и background.js.

    • Не может напрямую использовать Chrome API, но может отправлять сообщения в background.js.

  • myExtensionApi.js — API-файл, доступный для веб-страницы.

    • Подключается к странице как обычный JS-скрипт.

    • Создаёт объект window.myExtension, через который веб-страница может вызывать функции расширения.

    • Передаёт запросы в content.js, который затем пересылает их в background.js.

  • manifest.json — основной конфигурационный файл расширения.

    • Определяет права и доступ к API Chrome.

    • Указывает, какие файлы являются background script и content script.

    • Описывает web_accessible_resources, чтобы myExtensionApi.js мог быть загружен на веб-странице.

  • icons/ — папка с иконками расширения.

    • Содержит icon.png и, возможно, другие размеры (icon16.png, icon48.png, icon128.png).

    • Используется для отображения в панели расширений Chrome и в настройках.

Практика

manifest.json

Начнём с manifest.json. Это файл, без которого Chrome-расширение просто не запустится. Он описывает всё:

  • Какое поведение у расширения

  • Какие файлы в него входят

  • Какие разрешения ему нужны

Можно сказать, что это паспорт нашего расширения. Давай взглянем на его содержимое:

{
  "name": "My Chrome Extension",
  "version": "1.0",
  "manifest_version": 3,
  "description": "Расширение для взаимодействия с нативным приложением через Native Messaging.",
  "permissions": [
    "nativeMessaging"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches": [""],
      "js": ["content.js"],
      "run_at": "document_start"
    }
  ],
  "web_accessible_resources": [
    {
      "resources": ["myExtensionApi.js"],
      "matches": [""]
    }
  ],
  "host_permissions": [
    ""
  ],
  "icons": {
    "16": "icons/icon.png",
    "48": "icons/icon.png",
    "128": "icons/icon.png"
  }
}

Что здесь важно?

1. «permissions»: [«nativeMessaging»]

Это ключевой момент. Chrome по умолчанию запрещает расширениям общаться с нативными приложениями. Чтобы это разрешить, в манифесте надо прописать «nativeMessaging».

2. «background»: { «service_worker»: «background.js» }

В Manifest V3 фоновый скрипт больше не работает постоянно. Вместо него используется Service Worker, который просыпается только по событию. Это сделано ради производительности, но накладывает ограничения:

  • Он засыпает, если не активен.

  • В нём нельзя использовать setTimeout и setInterval (нужно alarms).

3. «content_scripts»

Вот тут интересно:

  • «matches»: [»»] означает, что content.js будет загружаться на всех сайтах.

  • «run_at»: «document_start» позволяет загружать скрипт до того, как страница полностью загрузится.

4. «web_accessible_resources»

Chrome не разрешает веб-страницам просто так загружать файлы расширения.
Эта строка позволяет использовать myExtensionApi.js, чтобы веб-страница могла взаимодействовать с расширением.

5. «icons»

Не так важно для работы, но полезно для интерфейса. Без иконки расширение выглядит… как кусок кода без лица.

background.js

Фоновый скрипт отвечает за обработку запросов от content script и передачу данных нативному приложению через Native Messaging API.

В данном случае используется разовое соединение через chrome.runtime.sendNativeMessage (), когда расширению нужно выполнить один запрос и получить ответ без поддержания постоянного соединения.

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
    if (request.action === "sendToNative") {
        chrome.runtime.sendNativeMessage(
            "my.native.app",
            request.data,
            (response) => {
                if (chrome.runtime.lastError) {
                    console.error("Ошибка при общении с нативным приложением:", chrome.runtime.lastError.message);
                    sendResponse({ error: chrome.runtime.lastError.message });
                } else {
                    console.log("Ответ от нативного приложения:", response);
                    sendResponse(response);
                }
            }
        );
        return true; // Указываем, что sendResponse будет вызван асинхронно
    }
});

Как это работает?

1. chrome.runtime.onMessage.addListener ()

Слушает сообщения от content.js и передаёт их в нативное приложение.

2. chrome.runtime.sendNativeMessage ()

Отправляет разовое сообщение без установления постоянного соединения. Получает ответ и передаёт его обратно в content.js.

3. Обработка ошибок

Если что-то пошло не так, Chrome передаст chrome.runtime.lastError, который мы логируем.

4. return true;

Это необходимо, чтобы sendResponse () работал асинхронно.

Теперь background.js готов. Следующий шаг — content.js, который будет передавать команды в background script.

content.js

Контентный скрипт загружается в контексте веб-страницы, но работает изолированно. Он не может напрямую обращаться к API Chrome, но может:

  • Слушать команды от веб-страницы

  • Передавать их в background.js

  • Получать ответ и отправлять его обратно на страницу

window.addEventListener("message", (event) => {
    if (event.source !== window  !event.data  event.data.type !== "FROM_PAGE") {
        return;
    }

    console.log("Получен запрос от веб-страницы:", event.data);

    // Отправляем сообщение в background.js
    chrome.runtime.sendMessage(
        { action: "sendToNative", data: event.data.payload },
        (response) => {
            console.log("Ответ от background.js:", response);
            // Отправляем ответ обратно на веб-страницу
            window.postMessage({ type: "FROM_EXTENSION", payload: response }, "*");
        }
    );
});

Как это работает?

1. Слушаем события от веб-страницы

window.addEventListener («message», …) перехватывает сообщения, отправленные через window.postMessage (). Проверяем, что сообщение пришло от той же страницы и содержит type: «FROM_PAGE».

2. Передаём сообщение в background.js

Используем chrome.runtime.sendMessage (), отправляя объект { action: «sendToNative», data: event.data.payload }.

3. Ждём ответ и отправляем обратно на страницу

Когда background.js получает ответ от нативного приложения, мы его логируем и передаём обратно на веб-страницу через window.postMessage ().

Почему используется window.postMessage ()?

Контентный скрипт не может напрямую взаимодействовать с объектами страницы (например, window.myExtension). Единственный способ передавать данные между ними — это window.postMessage (), который создаёт своего рода «туннель» между разными контекстами JavaScript.

Теперь content.js готов. Следующий шаг — myExtensionApi.js, который упростит взаимодействие веб-страницы с расширением.

myExtensionApi.js

Этот файл загружается на веб-странице и создаёт объект window.myExtension, предоставляющий удобные методы для общения с расширением. Вместо того чтобы веб-страница напрямую отправляла сообщения через window.postMessage (), мы инкапсулируем логику в отдельный API.

(() => {
    if (window.myExtension) {
        console.warn("myExtension уже определён.");
        return;
    }

    window.myExtension = {
        sendMessageToNative: (message) => {
            return new Promise((resolve, reject) => {
                const responseHandler = (event) => {
                    if (event.source !== window  !event.data  event.data.type !== "FROM_EXTENSION") {
                        return;
                    }
                    window.removeEventListener("message", responseHandler);

                    if (event.data.payload && event.data.payload.error) {
                        reject(new Error(event.data.payload.error));
                    } else {
                        resolve(event.data.payload);
                    }
                };

                window.addEventListener("message", responseHandler);

                window.postMessage({ type: "FROM_PAGE", payload: message }, "*");

                // Таймаут на случай, если ответа нет
                setTimeout(() => {
                    window.removeEventListener("message", responseHandler);
                    reject(new Error("Время ожидания ответа от расширения истекло"));
                }, 5000);
            });
        }
    };

    console.log("myExtension API загружен.");
})();

Как это работает?

1. Проверяем, не загружен ли API дважды

Если window.myExtension уже существует, ничего не делаем, чтобы избежать конфликтов.

2. Определяем метод sendMessageToNative (message)

При вызове он отправляет сообщение через window.postMessage () в content.js.

Ожидает ответа, используя Promise.

3. Добавляем обработчик ответа

window.addEventListener («message», …) ловит сообщения от content.js.

Когда приходит type: «FROM_EXTENSION», мы передаём данные в resolve ().

4. Добавляем защиту от зависания

Если за 5 секунд не приходит ответа, reject () завершает Promise с ошибкой.

Как веб-страница будет использовать API?

После подключения myExtensionApi.js, на веб-странице можно отправлять команды так:

window.myExtension.sendMessageToNative({ command: "ping" })
    .then(response => console.log("Ответ от нативного приложения:", response))
    .catch(error => console.error("Ошибка:", error));

Этот API делает взаимодействие прозрачным и удобным:

  • Веб-страница ничего не знает о window.postMessage () и chrome.runtime.sendMessage ().

  • Работа с расширением происходит через window.myExtension, без прямого взаимодействия с Chrome API.

Теперь расширение готово и мы можем его установить в браузер, а также написать тестовую веб-страницу index.html.

Установка расширения

Chrome позволяет загружать расширения без публикации в интернет-магазине. Для этого:

1. Открываем chrome://extensions/

0e415073cfffff60fb476f75bfbcded5.png

2. Включаем режим разработчика (Developer mode) в правом верхнем углу.

279e77499700a0dd24fdec4672d46b76.png

3. Нажимаем Load unpacked (Загрузить распакованное).

4. Выбираем папку с нашим расширением (extension).

ccc7900aa310551404389e2cf626ad49.png

После загрузки расширение появится в списке. Теперь его можно включить/выключить, просмотреть его ID и открыть консоль background.js.

Закрепляем ID расширения с помощью key

По умолчанию Chrome генерирует случайный ID расширения при каждом его установке. Если мы не хотим, чтобы он менялся, можно зафиксировать ID, добавив поле «key» в manifest.json.

В консоли background.js выполните команду:

chrome.management.getSelf((info) => console.log(info.id));
7e95ce0aff326343a67c6cf0c9d41ff6.png

Скопируйте ID и вставьте его в manifest.json в поле «key»:

{
    "name": "My Chrome Extension",
    "version": "1.0",
    "manifest_version": 3,
    "description": "Расширение для взаимодействия с нативным приложением через Native Messaging.",
    "permissions": [
      "nativeMessaging"
    ],
    "background": {
      "service_worker": "background.js"
    },
    "content_scripts": [
      {
        "matches": [""],
        "js": ["content.js"],
        "run_at": "document_start"
      }
    ],
    "web_accessible_resources": [
      {
        "resources": ["myExtensionApi.js"],
        "matches": [""]
      }
    ],
    "host_permissions": [
      ""
    ],
    "icons": {
      "16": "icons/icon.png",
      "48": "icons/icon.png",
      "128": "icons/icon.png"
    },
    "key": "cfmbjaocnfillcmjbimhmmknfmnafafj" // Добавленное поле
  }

Зачем нужен key?

  • Фиксированный ID расширения — это важно, если расширение взаимодействует с другими сервисами, которые проверяют его ID.

  • Сохранение разрешений — Chrome не сбросит разрешения при каждом обновлении, если ID остаётся неизменным.

Теперь расширение можно загружать повторно, и его ID останется неизменным.

Разработка тестовой веб-страницы

Что будет на странице?

  • Подключим myExtensionApi.js, чтобы веб-страница могла взаимодействовать с расширением.

  • Добавим одну кнопку, которая отправляет тестовую команду в нативное приложение через window.myExtension.sendMessageToNative ().

  • Выведем ответ от нативного приложения на страницу.




    
    
    Тест расширения
    
    



    

Тест Chrome-расширения

       

Ожидание ответа...

   

Почему myExtensionApi.js подключается так?

Это важно, потому что:

  • Обычное подключение через 

    Рейтинг@Mail.ru