Standardowe aplikacje internetowe są zwykle ograniczone do określonych protokołów komunikacyjnych, takich jak HTTP, oraz interfejsów API, takich jak WebSocket i WebRTC. Chociaż są one zaawansowane, zostały zaprojektowane tak, aby były ściśle ograniczone i zapobiegały nadużyciom. Nie mogą one nawiązywać połączeń TCP ani UDP, co ogranicza możliwość komunikacji aplikacji internetowych ze starszymi systemami lub urządzeniami, które używają własnych protokołów innych niż internetowe. Możesz na przykład utworzyć klienta SSH opartego na przeglądarce, połączyć się z lokalną drukarką lub zarządzać flotą urządzeń IoT. Wcześniej wymagało to wtyczek do przeglądarki lub natywnych aplikacji pomocniczych.
Interfejs Direct Sockets API rozwiązuje to ograniczenie, umożliwiając odizolowanym aplikacjom internetowym nawiązywanie bezpośrednich połączeń TCP i UDP bez serwera przekaźnikowego. Dzięki dodatkowym środkom bezpieczeństwa, takim jak ścisły standard Content Security Policy (CSP) i izolacja między źródłami, ten interfejs API może być bezpiecznie udostępniany.
Przypadki użycia
Kiedy używać Direct Sockets zamiast standardowych protokołów WebSocket?
- Urządzenia IoT i urządzenia inteligentne: komunikacja ze sprzętem, który używa protokołów TCP/UDP zamiast HTTP.
- Starsze systemy: łączenie się ze starszymi serwerami poczty (SMTP/IMAP), serwerami czatu IRC lub drukarkami.
- Pulpit zdalny i terminale: implementacja klientów SSH, Telnet lub RDP.
- Systemy P2P: wdrażanie rozproszonych tabel mieszających (DHT) lub odpornych narzędzi do współpracy (takich jak IPFS).
- Transmisja multimediów: wykorzystanie protokołu UDP do strumieniowego przesyłania treści do wielu punktów końcowych jednocześnie (multicast), co umożliwia zastosowania takie jak skoordynowane odtwarzanie wideo w sieci kiosków detalicznych.
- Funkcje serwera i odbiornika: skonfiguruj IWA tak, aby działał jako punkt końcowy odbierający przychodzące połączenia TCP lub datagramy UDP za pomocą
TCPServerSocketlub powiązanychUDPSocket.
Wymagania wstępne dotyczące interfejsu Direct Sockets
Zanim zaczniesz korzystać z Direct Sockets, musisz skonfigurować działającą aplikację internetową. Następnie możesz zintegrować Direct Sockets ze swoimi stronami.
Dodawanie zasad dotyczących uprawnień
Aby używać interfejsu Direct Sockets, musisz skonfigurować obiekt permissions_policy w pliku manifestu izolowanej aplikacji internetowej. Aby wyraźnie włączyć interfejs API, musisz dodać klucz direct-sockets. Dodatkowo musisz użyć klawisza cross-origin-isolated. Ten klucz nie jest specyficzny dla interfejsu Direct Sockets, ale jest wymagany w przypadku wszystkich aplikacji IWA i określa, czy dokument może uzyskiwać dostęp do interfejsów API, które wymagają izolacji między źródłami.
{
"permissions_policy": {
"direct-sockets": ["self"],
"cross-origin-isolated": ["self"]
}
}
Klucz direct-sockets określa, czy połączenia z new TCPSocket(...), new
TCPServerSocket(...) lub new UDPSocket(...) są dozwolone. Jeśli ta zasada nie jest skonfigurowana, te konstruktory natychmiast odrzucają żądanie z błędem NotAllowedError.
Implementowanie TCPSocket
Aplikacje mogą prosić o połączenie TCP, tworząc instancję TCPSocket.
Otwieranie połączenia
Aby otworzyć połączenie, użyj operatora new i await otwartej obietnicy.
Konstruktor TCPSocket inicjuje połączenie przy użyciu określonych parametrów remoteAddress i 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;
Opcjonalny obiekt konfiguracji umożliwia precyzyjną kontrolę sieci. W tym konkretnym przypadku wartość keepAliveDelay jest ustawiona na 720000 milisekund, aby utrzymać połączenie w okresach bezczynności. Deweloperzy mogą tu też skonfigurować inne właściwości, takie jak noDelay, która wyłącza algorytm Nagle'a, aby zapobiec łączeniu małych pakietów przez system, co może zmniejszyć opóźnienie, lub sendBufferSize i receiveBufferSize, aby zarządzać przepustowością.
W ostatniej części powyższego fragmentu kodu oczekiwane jest otwarcie obietnicy, która zostanie spełniona dopiero po zakończeniu uzgadniania połączenia. Zwraca ona obiekt TCPSocketOpenInfo zawierający strumienie odczytu i zapisu wymagane do przesyłania danych.
Odczyt i zapis
Po otwarciu gniazda możesz wchodzić z nim w interakcje za pomocą standardowych interfejsów Streams API.
- Zapis: strumień zapisu akceptuje
BufferSource(np.ArrayBuffer). - Odczyt: strumień do odczytu zwraca
Uint8Arraydane.
// 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();
Zoptymalizowane czytanie dzięki BYOB
W przypadku aplikacji o wysokiej wydajności, w których zarządzanie przydzielaniem pamięci ma kluczowe znaczenie, interfejs API obsługuje odczyt „Bring Your Own Buffer” (BYOB). Zamiast zezwalać przeglądarce na przydzielanie nowego bufora dla każdego otrzymanego fragmentu danych, możesz przekazać do czytnika wstępnie przydzielony bufor. Zmniejsza to obciążenie związane z odśmiecaniem pamięci, ponieważ dane są zapisywane bezpośrednio w pamięci.
// 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();
Implementacja UDPSocket
Klasa UDPSocket umożliwia komunikację UDP. Działa w 2 różnych trybach w zależności od konfiguracji opcji.
Tryb połączony
W tym trybie gniazdo komunikuje się z jednym konkretnym miejscem docelowym. Jest to przydatne w przypadku standardowych zadań klient-serwer.
// Connect to a specific remote host
let udpSocket = new UDPSocket({
remoteAddress: 'example.com',
remotePort: 7 });
let { readable, writable } = await udpSocket.opened;
Tryb powiązany
W tym trybie gniazdo jest powiązane z lokalnym punktem końcowym IP. Może odbierać datagramy z dowolnych źródeł i wysyłać je do dowolnych miejsc docelowych. Jest to często używane w przypadku protokołów wykrywania lokalnego lub zachowań podobnych do serwera.
// 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;
Obsługa wiadomości UDP
W przeciwieństwie do strumienia bajtów TCP strumienie UDP działają na UDPMessageobiektach, które zawierają dane i informacje o zdalnym adresie. Poniższy kod pokazuje, jak obsługiwać operacje wejścia/wyjścia podczas korzystania z UDPSocket w „trybie powiązanym”.
// 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);
W przeciwieństwie do „trybu połączonego”, w którym gniazdo jest zablokowane dla konkretnego urządzenia, tryb powiązany umożliwia gniazdu komunikację z dowolnymi miejscami docelowymi. W związku z tym podczas zapisywania danych w strumieniu zapisu musisz przekazać obiekt UDPMessage, który wyraźnie określa remoteAddress i remotePort dla każdego pakietu, instruując gniazdo, gdzie dokładnie ma kierować dany datagram. Podobnie podczas odczytywania ze strumienia odczytu zwracana wartość zawiera nie tylko ładunek danych, ale także remoteAddress i remotePort nadawcy, co umożliwia aplikacji identyfikowanie źródła każdego przychodzącego pakietu.
Uwaga: gdy używasz UDPSocket w „trybie połączonym”, gniazdo jest skutecznie zablokowane dla konkretnego elementu równorzędnego, co upraszcza proces wejścia/wyjścia. W tym trybie właściwości remoteAddress i remotePort nie mają żadnego wpływu na zapisywanie, ponieważ miejsce docelowe jest już ustalone. Podobnie podczas odczytywania wiadomości te właściwości zwracają wartość null, ponieważ źródło jest gwarantowane jako połączony element równorzędny.
Obsługa multicastu
W przypadku zastosowań takich jak synchronizacja odtwarzania wideo na wielu kioskach lub implementacja lokalnego wykrywania urządzeń (np. mDNS) gniazda bezpośrednie obsługują UDP typu multicast. Umożliwia to wysyłanie wiadomości na adres „grupy” i odbieranie ich przez wszystkich subskrybentów w sieci, a nie przez jednego konkretnego użytkownika.
Uprawnienia do multicastu
Aby korzystać z funkcji multiemisji, musisz dodać do manifestu IWA konkretne uprawnienie direct-sockets-multicast. Różni się ono od standardowego uprawnienia do bezpośrednich gniazd i jest niezbędne, ponieważ multicast jest używany tylko w sieciach prywatnych.
{
"permissions_policy": {
"direct-sockets": ["self"],
"direct-sockets-multicast": ["self"],
"direct-sockets-private": ["self"],
"cross-origin-isolated": ["self"]
}
}
Wysyłanie datagramów multicast
Wysyłanie do grupy multiemisji jest bardzo podobne do standardowego „trybu połączonego” UDP, z dodatkowymi opcjami sterowania zachowaniem pakietów.
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...
Odbieranie datagramów multicast
Aby odbierać ruch multicastowy, musisz otworzyć gniazdo w „trybie powiązanym” (zwykle powiązane z 0.0.0.0 lub ::), a następnie dołączyć do określonej grupy za pomocą MulticastController.UDPSocket Możesz też użyć opcji multicastAllowAddressSharing (podobnej do SO_REUSEADDR w systemie Unix), która jest niezbędna w protokołach wykrywania urządzeń, w których wiele aplikacji na tym samym urządzeniu musi nasłuchiwać na tym samym porcie.
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');
Tworzenie serwera
Interfejs API obsługuje też TCPServerSocket do akceptowania przychodzących połączeń TCP, co umożliwia działanie IWA jako serwera lokalnego. Poniższy kod pokazuje, jak utworzyć serwer TCP za pomocą interfejsu 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
Tworząc instancję klasy z adresem '::', serwer wiąże się ze wszystkimi dostępnymi interfejsami sieciowymi IPv6, aby nasłuchiwać przychodzących prób. W przeciwieństwie do tradycyjnych interfejsów API po stronie serwera opartych na wywołaniach zwrotnych ten interfejs API wykorzystuje wzorzec interfejsu Streams API w internecie: połączenia przychodzące są dostarczane jako ReadableStream. Gdy wywołasz funkcję
reader.read(), aplikacja czeka na następne połączenie z kolejki i je akceptuje, co powoduje zwrócenie wartości będącej w pełni funkcjonalną instancją TCPSocket
gotową do dwukierunkowej komunikacji z tym konkretnym klientem.
Debugowanie gniazd bezpośrednich za pomocą Narzędzi deweloperskich w Chrome
Od wersji Chrome 138 możesz debugować ruch Direct Sockets bezpośrednio w panelu Sieć w Narzędziach deweloperskich w Chrome, co eliminuje potrzebę korzystania z zewnętrznych narzędzi do przechwytywania pakietów. To narzędzie umożliwia monitorowanie TCPSocketpołączeńUDPSocket i ruchu (w trybie powiązanym i połączonym) wraz ze standardowymi żądaniami HTTP.
Aby sprawdzić aktywność sieciową aplikacji:
- Otwórz panel Sieć w Narzędziach deweloperskich w Chrome.
- Znajdź i wybierz połączenie gniazda w tabeli żądań.
- Otwórz kartę Wiadomości, aby wyświetlić log wszystkich przesłanych i odebranych danych.

Ten widok zawiera przeglądarkę szesnastkową, która umożliwia sprawdzanie nieprzetworzonego binarnego ładunku wiadomości TCP i UDP, dzięki czemu możesz mieć pewność, że implementacja protokołu jest idealna pod względem bajtów.
Prezentacja
IWA Kitchen Sink to aplikacja z wieloma kartami, z których każda prezentuje inny interfejs API IWA, np. Direct Sockets czy Controlled Frame.
Klient telnetu demo zawiera odizolowaną aplikację internetową, która umożliwia użytkownikowi łączenie się z serwerem TCP/IP za pomocą interaktywnego terminala. Innymi słowy, klienta Telnet.
Podsumowanie
Direct Sockets API wypełnia lukę w krytycznej funkcjonalności, umożliwiając aplikacjom internetowym obsługę protokołów sieciowych w formie surowej, co wcześniej było niemożliwe bez natywnych otoczek. To coś więcej niż zwykłe połączenie z klientem. Dzięki TCPServerSocket aplikacje mogą nasłuchiwać połączeń przychodzących, a UDPSocket oferuje elastyczne tryby komunikacji peer-to-peer i wykrywania sieci lokalnych.
Udostępniając te podstawowe funkcje TCP i UDP za pomocą nowoczesnego interfejsu Streams API, możesz teraz tworzyć w JavaScript w pełni funkcjonalne implementacje starszych protokołów, takich jak SSH, RDP czy niestandardowe standardy IoT. Ten interfejs API przyznaje dostęp do sieci na niskim poziomie, dlatego ma istotne znaczenie dla bezpieczeństwa. Dlatego jest on ograniczony do izolowanych aplikacji internetowych (IWA), co zapewnia, że takie uprawnienia są przyznawane tylko zaufanym, wyraźnie zainstalowanym aplikacjom, które egzekwują ścisłe zasady bezpieczeństwa. Ta równowaga pozwala tworzyć zaawansowane aplikacje zorientowane na urządzenia, zachowując jednocześnie bezpieczeństwo, którego użytkownicy oczekują od platformy internetowej.