Directe stopcontacten

Demián Renzulli
Demián Renzulli
Andrew Rayskiy
Andrew Rayskiy
Vlad Krot
Vlad Krot

Standaard webapplicaties zijn doorgaans beperkt tot specifieke communicatieprotocollen zoals HTTP en API's zoals WebSocket en WebRTC . Hoewel deze krachtig zijn, zijn ze ontworpen om strikt beperkt te zijn om misbruik te voorkomen. Ze kunnen geen directe TCP- of UDP-verbindingen tot stand brengen, wat de mogelijkheden van webapps beperkt om te communiceren met oudere systemen of hardwareapparaten die hun eigen, niet-webprotocollen gebruiken. U wilt bijvoorbeeld een webgebaseerde SSH-client bouwen, verbinding maken met een lokale printer of een reeks IoT-apparaten beheren. Historisch gezien vereiste dit browserplug-ins of native hulpprogramma's.

De Direct Sockets API lost deze beperking op door Isolated Web Apps (IWA's) in staat te stellen directe TCP- en UDP-verbindingen tot stand te brengen zonder een relay-server. Dankzij extra beveiligingsmaatregelen, zoals een strikt Content Security Policy (CSP) en cross-origin-isolatie, kan deze API met IWA's veilig worden aangeboden.

Gebruiksvoorbeelden

Wanneer moet je Direct Sockets gebruiken in plaats van standaard WebSockets?

  • IoT en slimme apparaten: Communicatie met hardware die gebruikmaakt van ruwe TCP/UDP in plaats van HTTP.
  • Verouderde systemen: Verbinding maken met oudere mailservers (SMTP/IMAP), IRC-chatservers of printers.
  • Externe bureaubladtoegang en terminals: Implementatie van SSH-, Telnet- of RDP-clients.
  • P2P-systemen: Implementatie van gedistribueerde hashtabellen (DHT) of robuuste samenwerkingstools (zoals IPFS).
  • Mediabroadcasting: Gebruikmaken van UDP om content naar meerdere eindpunten tegelijk te streamen (multicasting), waardoor toepassingen zoals gecoördineerde videoweergave over een netwerk van winkelkiosken mogelijk worden.
  • Server- en luistermogelijkheden: De IWA configureren als ontvangend eindpunt voor inkomende TCP-verbindingen of UDP-datagrammen met behulp van TCPServerSocket of gebonden UDPSocket .

Voorwaarden voor directe aansluitingen

Voordat u Direct Sockets kunt gebruiken, moet u een functionele IWA instellen . Daarna kunt u Direct Sockets in uw pagina's integreren.

Voeg een machtigingsbeleid toe

Om Direct Sockets te gebruiken, moet u het object permissions_policy in uw IWA-manifest configureren. U moet de sleutel direct-sockets toevoegen om de API expliciet in te schakelen. Daarnaast moet u de sleutel cross-origin-isolated toevoegen. Deze sleutel is niet specifiek voor Direct Sockets, maar is vereist voor alle IWA's en bepaalt of het document toegang heeft tot API's die cross-origin-isolatie vereisen.

{
  "permissions_policy": {
    "direct-sockets": ["self"],
    "cross-origin-isolated": ["self"]
  }
}

De sleutel `direct-sockets` bepaalt of aanroepen naar new TCPSocket(...) , new TCPServerSocket(...) of new UDPSocket(...) zijn toegestaan. Als dit beleid niet is ingesteld, zullen deze constructors onmiddellijk een NotAllowedError retourneren.

Implementeer TCPSocket

Applicaties kunnen een TCP-verbinding aanvragen door een TCPSocket instantie te creëren.

Een verbinding openen

Om een ​​verbinding tot stand te brengen, gebruikt u de new operator en await tot de verbinding is geopend.

De TCPSocket constructor initieert de verbinding met behulp van het opgegeven remoteAddress en 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;

Het optionele configuratieobject maakt nauwkeurige netwerkcontrole mogelijk; in dit specifieke geval is keepAliveDelay ingesteld op 720.000 milliseconden om de verbinding te behouden tijdens perioden van inactiviteit. Ontwikkelaars kunnen hier ook andere eigenschappen configureren, zoals noDelay , waarmee het Nagle-algoritme wordt uitgeschakeld om te voorkomen dat het systeem kleine pakketten bundelt – wat mogelijk de latentie vermindert – of sendBufferSize en receiveBufferSize om de doorvoer te beheren.

In het laatste deel van het voorgaande codefragment wacht de code op de geopende promise, die pas wordt opgelost zodra de handshake is voltooid en een TCPSocketOpenInfo object retourneert dat de leesbare en schrijfbare streams bevat die nodig zijn voor gegevensoverdracht.

Lezen en schrijven

Zodra de socket geopend is, kunt u ermee communiceren via de standaard API-interfaces van Streams .

  • Schrijven: De beschrijfbare stream accepteert een BufferSource (zoals een ArrayBuffer ).
  • Lezen: De leesbare stream levert Uint8Array -gegevens op.
// 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();

Geoptimaliseerd lezen met BYOB

Voor veeleisende applicaties waarbij het beheren van geheugenallocatie cruciaal is, ondersteunt de API "Bring Your Own Buffer" (BYOB) lezen. In plaats van de browser een nieuwe buffer te laten toewijzen voor elk ontvangen stukje data, kunt u een vooraf toegewezen buffer aan de lezer doorgeven. Dit vermindert de overhead van garbage collection doordat de data direct in uw bestaande geheugen wordt geschreven.

// 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();

Implementeer UDPSocket

De UDPSocket klasse maakt UDP-communicatie mogelijk. Afhankelijk van de geconfigureerde opties werkt deze in twee verschillende modi.

Verbonden modus

In deze modus communiceert de socket met één specifieke bestemming. Dit is handig voor standaard client-servertaken.

// Connect to a specific remote host
let udpSocket = new UDPSocket({
    remoteAddress: 'example.com',
    remotePort: 7 });

let { readable, writable } = await udpSocket.opened;

Gebonden modus

In deze modus is de socket gekoppeld aan een lokaal IP-adres. De socket kan datagrammen ontvangen van willekeurige bronnen en verzenden naar willekeurige bestemmingen. Dit wordt vaak gebruikt voor lokale ontdekkingsprotocollen of serverachtig gedrag.

// 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-berichten verwerken

In tegenstelling tot TCP-bytestreams, werken UDP-streams met UDPMessage objecten, die de data en de informatie over het externe adres bevatten. De volgende code laat zien hoe invoer-/uitvoerbewerkingen kunnen worden afgehandeld bij gebruik van een UDPSocket in "gebonden modus".

// 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);

In tegenstelling tot de "verbonden modus", waarbij de socket is vergrendeld aan een specifieke tegenpartij, maakt de gebonden modus het mogelijk dat de socket communiceert met willekeurige bestemmingen. Daarom moet u bij het schrijven van gegevens naar de beschrijfbare stream een UDPMessage object doorgeven dat expliciet het remoteAddress en remotePort voor elk pakket specificeert. Dit geeft de socket precies aan waar het betreffende datagram naartoe moet worden gerouteerd. Evenzo bevat de geretourneerde waarde bij het lezen van de leesbare stream niet alleen de gegevenspayload, maar ook het remoteAddress en remotePort van de afzender, waardoor uw applicatie de herkomst van elk binnenkomend pakket kan identificeren.

Opmerking: Bij gebruik van UDPSocket in de "verbonden modus" is de socket in feite vergrendeld aan een specifieke peer, wat het I/O-proces vereenvoudigt. In deze modus hebben de eigenschappen remoteAddress en remotePort in feite geen effect bij het schrijven, omdat de bestemming al vastligt. Evenzo zullen deze eigenschappen bij het lezen van berichten null retourneren, aangezien de bron gegarandeerd de verbonden peer is.

Multicast-ondersteuning

Voor toepassingen zoals het synchroniseren van videoweergave over meerdere kiosken of het implementeren van lokale apparaatdetectie (bijvoorbeeld mDNS), ondersteunt Direct Sockets Multicast UDP. Hierdoor kunnen berichten naar een "groepsadres" worden verzonden en door alle abonnees op het netwerk worden ontvangen, in plaats van door één specifieke peer.

Multicast-machtigingen

Om multicastmogelijkheden te gebruiken, moet u de specifieke machtiging direct-sockets-multicast toevoegen aan uw IWA-manifest. Deze machtiging verschilt van de standaardmachtiging direct-sockets en is noodzakelijk omdat multicast alleen in privénetwerken wordt gebruikt.

{
  "permissions_policy": {
    "direct-sockets": ["self"],
    "direct-sockets-multicast": ["self"],
    "direct-sockets-private": ["self"],
    "cross-origin-isolated": ["self"]
  }
}

Multicast-datagrammen verzenden

Verzenden naar een multicastgroep is vergelijkbaar met de standaard UDP-modus "verbonden", met als extra optie het regelen van het gedrag van de pakketten.

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...

Multicast-datagrammen ontvangen

Om multicastverkeer te ontvangen, moet u een UDPSocket openen in "gebonden modus" (meestal gebonden aan 0.0.0.0 of :: :) en vervolgens deelnemen aan een specifieke groep met behulp van de MulticastController . U kunt ook de optie multicastAllowAddressSharing gebruiken (vergelijkbaar met SO_REUSEADDR op Unix), wat essentieel is voor protocollen voor apparaatdetectie waarbij meerdere applicaties op hetzelfde apparaat naar dezelfde poort moeten luisteren.

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');

Maak een server aan

De API ondersteunt ook TCPServerSocket voor het accepteren van inkomende TCP-verbindingen, waardoor uw IWA in feite als een lokale server kan fungeren. De volgende code illustreert hoe u een TCP-server kunt opzetten met behulp van de TCPServerSocket -interface.

// 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

Door de klasse te instantiëren met het adres '::' , bindt de server zich aan alle beschikbare IPv6-netwerkinterfaces om te luisteren naar inkomende pogingen. In tegenstelling tot traditionele, op callbacks gebaseerde server-API's, maakt deze API gebruik van het Streams API-patroon van het web: inkomende verbindingen worden geleverd als een ReadableStream . Wanneer u reader.read() aanroept, wacht de applicatie op de volgende verbinding uit de wachtrij en accepteert deze. De resulterende verbinding is een volledig functionele TCPSocket instantie die klaar is voor tweewegcommunicatie met die specifieke client.

Debug Direct Sockets met Chrome DevTools

Vanaf Chrome 138 kunt u Direct Sockets-verkeer rechtstreeks debuggen in het netwerkpaneel van Chrome DevTools, waardoor externe packet sniffers overbodig worden. Met deze tooling kunt u TCPSocket verbindingen en UDPSocket verkeer (zowel in gebonden als verbonden modus) monitoren, naast uw standaard HTTP-verzoeken.

Om de netwerkactiviteit van uw app te controleren:

  1. Open het netwerkpaneel in Chrome DevTools.
  2. Zoek en selecteer de socketverbinding in de aanvraagtabel.
  3. Open het tabblad Berichten om een ​​logboek van alle verzonden en ontvangen gegevens te bekijken.

De gegevens in het tabblad Berichten in DevTools.

Deze weergave biedt een hexadecimale viewer waarmee u de onbewerkte binaire gegevens van uw TCP- en UDP-berichten kunt inspecteren, zodat u er zeker van bent dat uw protocolimplementatie byte-perfect is.

Demo

IWA Kitchen Sink heeft een app met meerdere tabbladen, die elk een andere IWA API demonstreren, zoals Direct Sockets, Controlled Frame en meer.

Als alternatief bevat de demo van de telnet-client een geïsoleerde webapplicatie waarmee de gebruiker via een interactieve terminal verbinding kan maken met een TCP/IP-server. Met andere woorden, een telnet-client.

Conclusie

De Direct Sockets API dicht een cruciale functionaliteitskloof door webapplicaties in staat te stellen ruwe netwerkprotocollen te verwerken die voorheen zonder native wrappers niet ondersteund konden worden. Het gaat verder dan eenvoudige clientconnectiviteit; met TCPServerSocket kunnen applicaties luisteren naar inkomende verbindingen, terwijl UDPSocket flexibele modi biedt voor zowel peer-to-peer-communicatie als lokale netwerkdetectie.

Door deze onbewerkte TCP- en UDP-mogelijkheden beschikbaar te maken via de moderne Streams API, kunt u nu volwaardige implementaties van oudere protocollen – zoals SSH, RDP of aangepaste IoT-standaarden – rechtstreeks in JavaScript bouwen. Omdat deze API toegang tot het netwerk op laag niveau verleent, brengt dit aanzienlijke beveiligingsrisico's met zich mee. Daarom is de toegang beperkt tot geïsoleerde webapplicaties (IWA's) , zodat dergelijke bevoegdheden alleen worden verleend aan vertrouwde, expliciet geïnstalleerde applicaties die strikte beveiligingsregels hanteren. Deze balans stelt u in staat krachtige, apparaatgerichte applicaties te bouwen met behoud van de veiligheid die gebruikers van het webplatform verwachten.

Bronnen