Стандартные веб-приложения обычно ограничены определенными протоколами связи, такими как HTTP, и API, такими как WebSocket и WebRTC . Хотя они обладают большими возможностями, их конструкция предусматривает строгие ограничения для предотвращения злоупотреблений. Они не могут устанавливать прямые TCP или UDP-соединения, что ограничивает возможности веб-приложений взаимодействовать с устаревшими системами или аппаратными устройствами, использующими собственные протоколы, отличные от веб-протоколов. Например, вам может потребоваться создать веб-клиент SSH, подключиться к локальному принтеру или управлять парком IoT-устройств. Исторически для этого требовались плагины для браузеров или нативные вспомогательные приложения.
API Direct Sockets устраняет это ограничение, позволяя изолированным веб-приложениям (IWA) устанавливать прямые TCP и UDP соединения без ретрансляционного сервера. Благодаря дополнительным мерам безопасности, таким как строгая политика безопасности контента (CSP) и междоменная изоляция, этот API может быть безопасно предоставлен в IWA.
Варианты использования
В каких случаях следует использовать Direct Sockets вместо стандартных WebSocket?
- Интернет вещей и интеллектуальные устройства: взаимодействие с оборудованием, использующим протокол TCP/UDP вместо HTTP.
- Устаревшие системы: подключение к старым почтовым серверам (SMTP/IMAP), IRC-чатам или принтерам.
- Удаленный рабочий стол и терминалы: реализация клиентов SSH, Telnet или RDP.
- P2P-системы: внедрение распределенных хеш-таблиц (DHT) или отказоустойчивых инструментов для совместной работы (например, IPFS).
- Медиавещание: использование протокола UDP для одновременной потоковой передачи контента на несколько конечных точек (многоадресная рассылка), что позволяет реализовать такие сценарии, как скоординированное воспроизведение видео в сети розничных киосков.
- Возможности сервера и слушателя: Настройка IWA для работы в качестве принимающей конечной точки для входящих TCP-соединений или UDP-дейтаграмм с использованием
TCPServerSocketили привязанногоUDPSocket.
Предварительные условия для прямого подключения
Перед использованием Direct Sockets необходимо настроить работающий IWA . После этого вы сможете интегрировать Direct Sockets в свои страницы.
Добавить политику разрешений
Для использования Direct Sockets необходимо настроить объект permissions_policy в манифесте IWA. Необходимо добавить ключ direct-sockets , чтобы явно включить API. Кроме того, необходимо включить ключ cross-origin-isolated . Этот ключ не является специфическим для Direct Sockets, но требуется для всех IWA и определяет, может ли документ получать доступ к API, требующим изоляции между источниками.
{
"permissions_policy": {
"direct-sockets": ["self"],
"cross-origin-isolated": ["self"]
}
}
Ключ direct-sockets определяет, разрешены ли вызовы new TCPSocket(...) , new TCPServerSocket(...) или new UDPSocket(...) . Если это правило не задано, эти конструкторы немедленно отклонятся с NotAllowedError .
Реализуйте TCPSocket.
Приложения могут запрашивать TCP-соединение, создавая экземпляр TCPSocket .
Открыть соединение
Для открытия соединения используйте оператор new и await подтверждения открытого промиса.
Конструктор TCPSocket инициирует соединение, используя указанные remoteAddress и remotePort .
const remoteAddress = 'example.com';
const remotePort = 7;
// Configure options like keepAlive or buffering
const options = {
keepAlive: true,
keepAliveDelay: 720000
};
let tcpSocket = new TCPSocket(remoteAddress, remotePort, options);
// Wait for the connection to be established
let { readable, writable } = await tcpSocket.opened;
Дополнительный объект конфигурации позволяет осуществлять точный контроль сети; в данном конкретном случае keepAliveDelay устанавливается на 720000 миллисекунд для поддержания соединения в периоды бездействия. Разработчики также могут настроить здесь другие свойства, такие как noDelay , который отключает алгоритм Нейгла, предотвращая пакетную обработку небольших пакетов — что потенциально снижает задержку, — или sendBufferSize и receiveBufferSize для управления пропускной способностью.
В последней части приведенного выше фрагмента кода ожидается выполнение открытого промиса, который разрешается только после завершения рукопожатия, возвращая объект TCPSocketOpenInfo содержащий потоки для чтения и записи, необходимые для передачи данных.
Читать и писать
После открытия сокета взаимодействуйте с ним, используя стандартные интерфейсы API Streams .
- Запись: Записываемый поток принимает объект
BufferSource(аналогичныйArrayBuffer). - Чтение: Из читаемого потока выдаются данные
Uint8Array.
// Writing data
const writer = writable.getWriter();
const encoder = new TextEncoder();
await writer.write(encoder.encode("Hello Server"));
// Call when done
writer.releaseLock();
// Reading data
const reader = readable.getReader();
const { value, done } = await reader.read();
if (!done) {
const decoder = new TextDecoder();
console.log("Received:", decoder.decode(value));
}
// Call when done
reader.releaseLock();
Оптимизированное чтение с возможностью приносить свои напитки.
Для высокопроизводительных приложений, где управление выделением памяти имеет решающее значение, API поддерживает чтение с использованием собственного буфера (BYOB). Вместо того чтобы позволять браузеру выделять новый буфер для каждого полученного фрагмента данных, вы можете передать предварительно выделенный буфер считывателю. Это снижает накладные расходы на сборку мусора, поскольку данные записываются непосредственно в имеющуюся память.
// 1. Get a BYOB reader explicitly
const reader = readable.getReader({ mode: 'byob' });
// 2. Allocate a reusable buffer (e.g., 4KB)
let buffer = new Uint8Array(4096);
// 3. Read directly into the existing buffer
const { value, done } = await reader.read(buffer);
if (!done) {
// 'value' is a view of the data written directly into your buffer
console.log("Bytes received:", value.byteLength);
}
reader.releaseLock();
Реализуйте UDPSocket.
Класс UDPSocket позволяет осуществлять UDP-связь. Он работает в двух различных режимах в зависимости от настроек параметров.
Подключенный режим
В этом режиме сокет взаимодействует с одним конкретным адресатом. Это полезно для стандартных задач взаимодействия клиент-сервер.
// Connect to a specific remote host
let udpSocket = new UDPSocket({
remoteAddress: 'example.com',
remotePort: 7 });
let { readable, writable } = await udpSocket.opened;
Ограниченный режим
В этом режиме сокет привязывается к локальной IP-адресу. Он может принимать дейтаграммы из произвольных источников и отправлять их в произвольные пункты назначения. Это часто используется для локальных протоколов обнаружения или для работы в режиме, аналогичном серверному.
// Bind to all interfaces (IPv6)
let udpSocket = new UDPSocket({
localAddress: '::'
// omitting localPort lets the OS pick one
});
// localPort will tell you the OS-selected port.
let { readable, writable, localPort } = await udpSocket.opened;
Обработка UDP-сообщений
В отличие от TCP-потока байтов, UDP-потоки работают с объектами UDPMessage , которые содержат данные и информацию об удаленном адресе. Следующий код демонстрирует, как обрабатывать операции ввода/вывода при использовании UDPSocket в «привязанном режиме».
// Writing (Bound Mode requires specifying destination)
const writer = writable.getWriter();
await writer.write({
data: new TextEncoder().encode("Ping"),
remoteAddress: '192.168.1.50',
remotePort: 8080
});
// Reading
const reader = readable.getReader();
const { value } = await reader.read();
// value contains: { data, remoteAddress, remotePort }
console.log(`Received from ${value.remoteAddress}:`, value.data);
В отличие от «подключенного режима», где сокет привязан к конкретному узлу, связанный режим позволяет сокету взаимодействовать с произвольными адресатами. Следовательно, при записи данных в записываемый поток необходимо передать объект UDPMessage , который явно указывает remoteAddress и remotePort для каждого пакета, точно указывая сокету, куда следует направить эту конкретную дейтаграмму. Аналогично, при чтении из читаемого потока возвращаемое значение включает не только полезную нагрузку данных, но и remoteAddress и remotePort отправителя, что позволяет вашему приложению идентифицировать источник каждого входящего пакета.
Примечание: При использовании UDPSocket в «подключенном режиме» сокет фактически привязывается к конкретному узлу, что упрощает процесс ввода-вывода. В этом режиме свойства remoteAddress и remotePort фактически ничего не делают при записи, поскольку место назначения уже определено. Аналогично, при чтении сообщений эти свойства будут возвращать null, поскольку гарантируется, что источником является подключенный узел.
Поддержка многоадресной рассылки
Для таких сценариев использования, как синхронизация воспроизведения видео на нескольких киосках или реализация локального обнаружения устройств (например, mDNS), Direct Sockets поддерживает многоадресную рассылку UDP. Это позволяет отправлять сообщения на «групповой» адрес и получать их от всех подписчиков в сети, а не от одного конкретного участника.
Разрешения на многоадресную рассылку
Для использования возможностей многоадресной рассылки необходимо добавить в манифест IWA специальное разрешение direct-sockets-multicast . Это отличается от стандартного разрешения direct-sockets и необходимо, поскольку многоадресная рассылка используется только в частных сетях.
{
"permissions_policy": {
"direct-sockets": ["self"],
"direct-sockets-multicast": ["self"],
"direct-sockets-private": ["self"],
"cross-origin-isolated": ["self"]
}
}
Отправка многоадресных датаграмм
Отправка пакетов в многоадресную группу очень похожа на стандартный UDP-режим "подключения", с добавлением специальных опций для управления поведением пакетов.
const MULTICAST_GROUP = '239.0.0.1';
const PORT = 12345;
const socket = new UDPSocket({
remoteAddress: MULTICAST_GROUP,
remotePort: PORT,
// Time To Live: How many router hops the packet can survive (default: 1)
multicastTimeToLive: 5,
// Loopback: Whether to receive your own packets (default: true)
multicastLoopback: true
});
const { writable } = await socket.opened;
// Write to the stream as usual...
Приём многоадресных датаграмм
Для приема многоадресного трафика необходимо открыть UDPSocket в «привязанном режиме» (обычно привязываясь к 0.0.0.0 или :: :) и затем присоединиться к определенной группе с помощью MulticastController . Также можно использовать параметр multicastAllowAddressSharing (аналогичный SO_REUSEADDR в Unix), который необходим для протоколов обнаружения устройств, где нескольким приложениям на одном устройстве необходимо прослушивать один и тот же порт.
const socket = new UDPSocket({
localAddress: '0.0.0.0', // Listen on all interfaces
localPort: 12345,
multicastAllowAddressSharing: true // Allow multiple applications to bind to the same address / port pair.
});
// The open info contains the MulticastController
const { readable, multicastController } = await socket.opened;
// Join the group to start receiving packets
await multicastController.joinGroup('239.0.0.1');
const reader = readable.getReader();
// Read the stream...
const { value } = await reader.read();
console.log(`Received multicast from ${value.remoteAddress}`);
// When finished, you can leave the group (this is an optional, but recommended practice)
await multicastController.leaveGroup('239.0.0.1');
Создать сервер
API также поддерживает TCPServerSocket для приема входящих TCP-соединений, что фактически позволяет вашему IWA выступать в качестве локального сервера. Следующий код демонстрирует, как установить TCP-сервер с использованием интерфейса TCPServerSocket .
// Listen on all interfaces (IPv6)
let tcpServerSocket = new TCPServerSocket('::');
// Accept connections via the readable stream
let { readable } = await tcpServerSocket.opened;
let reader = readable.getReader();
// Wait for a client to connect
let { value: clientSocket } = await reader.read();
// 'clientSocket' is a standard TCPSocket you can now read/write to
Создав экземпляр класса с адресом '::' , сервер привязывается ко всем доступным сетевым интерфейсам IPv6 для прослушивания входящих попыток. В отличие от традиционных серверных API на основе обратных вызовов, этот API использует шаблон Streams API веб-приложений: входящие соединения доставляются в виде ReadableStream . При вызове reader.read() приложение ожидает и принимает следующее соединение из очереди, получая значение, представляющее собой полностью функциональный экземпляр TCPSocket , готовый к двусторонней связи с конкретным клиентом.
Отладка прямых сокетов с помощью инструментов разработчика Chrome.
Начиная с Chrome 138, вы можете отлаживать трафик Direct Sockets непосредственно в панели «Сеть» в инструментах разработчика Chrome, что избавляет от необходимости использовать внешние анализаторы пакетов. Этот инструмент позволяет отслеживать соединения TCPSocket , а также трафик UDPSocket (как в связанном, так и в подключенном режимах) наряду со стандартными HTTP-запросами.
Чтобы проверить сетевую активность вашего приложения:
- Откройте панель «Сеть» в инструментах разработчика Chrome.
- Найдите и выберите сокетное соединение в таблице запросов.
- Откройте вкладку «Сообщения» , чтобы просмотреть журнал всех переданных и полученных данных.

Этот режим просмотра предоставляет шестнадцатеричный просмотрщик, позволяющий проверять необработанные двоичные данные ваших сообщений TCP и UDP, гарантируя, что ваша реализация протокола является байтово корректной.
Демо
Приложение IWA Kitchen Sink имеет несколько вкладок, каждая из которых демонстрирует различные API IWA, такие как Direct Sockets, Controlled Frame и другие.
В качестве альтернативы, демонстрационный пример клиента Telnet содержит изолированное веб-приложение, которое позволяет пользователю подключаться к TCP/IP-серверу через интерактивный терминал. Другими словами, это клиент Telnet.
Заключение
API Direct Sockets устраняет критически важный функциональный пробел, позволяя веб-приложениям обрабатывать необработанные сетевые протоколы, которые ранее было невозможно поддерживать без нативных оболочек. Он выходит за рамки простого подключения клиентов; с помощью TCPServerSocket приложения могут прослушивать входящие соединения, а UDPSocket предлагает гибкие режимы как для одноранговой связи, так и для обнаружения локальной сети.
Благодаря предоставлению доступа к этим базовым возможностям TCP и UDP через современный API Streams, теперь можно создавать полнофункциональные реализации устаревших протоколов — таких как SSH, RDP или собственные стандарты IoT — непосредственно на JavaScript. Поскольку этот API предоставляет низкоуровневый сетевой доступ, он имеет существенные последствия для безопасности. Поэтому он ограничен изолированными веб-приложениями (IWA) , гарантируя, что такие возможности предоставляются только доверенным, явно установленным приложениям, которые соблюдают строгие политики безопасности. Такой баланс позволяет создавать мощные, ориентированные на устройства приложения, сохраняя при этом безопасность, которую пользователи ожидают от веб-платформы.