Direct Sockets

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

Standart web uygulamaları genellikle HTTP gibi belirli iletişim protokolleri ve WebSocket ile WebRTC gibi API'lerle sınırlıdır. Bu özellikler güçlü olsa da kötüye kullanımı önlemek için sıkı bir şekilde sınırlandırılmıştır. Bu uygulamalar, ham TCP veya UDP bağlantıları oluşturamaz. Bu durum, web uygulamalarının kendi web dışı protokollerini kullanan eski sistemler ya da donanım cihazlarla iletişim kurma olanağını sınırlar. Örneğin, web tabanlı bir SSH istemcisi oluşturmak, yerel bir yazıcıya bağlanmak veya bir grup IoT cihazını yönetmek isteyebilirsiniz. Geçmişte bu işlem için tarayıcı eklentileri veya yerel yardımcı uygulamalar gerekiyordu.

Direct Sockets API, yalıtılmış web uygulamalarının (IWA) bir aktarım sunucusu olmadan doğrudan TCP ve UDP bağlantıları kurmasına olanak tanıyarak bu sınırlamayı giderir. İGP'ler, sıkı İçerik Güvenliği Politikası (İGP) ve kaynaklar arası izolasyon gibi ek güvenlik önlemleri sayesinde bu API'yi güvenli bir şekilde kullanıma sunabilir.

Kullanım alanları

Standart WebSocket'ler yerine Direct Sockets'i ne zaman kullanmalısınız?

  • IoT ve akıllı cihazlar: HTTP yerine ham TCP/UDP kullanan donanımlarla iletişim kurma.
  • Eski sistemler: Daha eski posta sunucularına (SMTP/IMAP), IRC sohbet sunucularına veya yazıcılara bağlanma.
  • Uzak masaüstü ve terminaller: SSH, Telnet veya RDP istemcilerini uygulama.
  • P2P sistemleri: Dağıtılmış karma tablolar (DHT) veya esnek işbirliği araçları (ör. IPFS) uygulama.
  • Medya yayını: İçeriği aynı anda birden fazla uç noktaya aktarmak için UDP'den yararlanma (çoklu yayın), perakende kioskları ağında koordineli video oynatma gibi kullanım alanlarını etkinleştirme.
  • Sunucu ve dinleyici özellikleri: TCPServerSocket veya bağlı UDPSocket kullanarak gelen TCP bağlantıları ya da UDP datagramları için IWA'yı alıcı uç nokta olarak yapılandırma.

Direct Sockets'ın ön koşulları

Direct Sockets'i kullanmadan önce işlevsel bir IWA ayarlamanız gerekir. Ardından Direct Sockets'i sayfalarınıza entegre edebilirsiniz.

İzin politikası ekleme

Direct Sockets'ı kullanmak için IWA manifestinizde permissions_policy nesnesini yapılandırmanız gerekir. API'yi açıkça etkinleştirmek için direct-sockets anahtarını eklemeniz gerekir. Ayrıca cross-origin-isolated tuşunu da eklemeniz gerekir. Bu anahtar, Direct Sockets'e özgü değildir ancak tüm IWA'lar için gereklidir ve belgenin, kaynaklar arası izolasyon gerektiren API'lere erişip erişemeyeceğini belirler.

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

direct-sockets anahtarı, new TCPSocket(...), new TCPServerSocket(...) veya new UDPSocket(...)'ye yapılan çağrıların izin verilip verilmeyeceğini belirler. Bu politika ayarlanmazsa bu oluşturucular NotAllowedError ile hemen reddeder.

TCPSocket'i uygulama

Uygulamalar, TCPSocket örneği oluşturarak TCP bağlantısı isteyebilir.

Bağlantı açma

Bir bağlantıyı açmak için new operatörünü ve await açılan sözü kullanın.

TCPSocket oluşturucusu, belirtilen remoteAddress ve remotePort kullanılarak bağlantıyı başlatır.

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;

İsteğe bağlı yapılandırma nesnesi, ayrıntılı ağ kontrolüne olanak tanır. Bu özel durumda, bağlantının işlem yapılmadığı dönemlerde korunması için keepAliveDelay 720.000 milisaniye olarak ayarlanır. Geliştiriciler burada başka özellikleri de yapılandırabilir. Örneğin, noDelay özelliği, sistemi küçük paketleri gruplandırmayı durdurarak (gecikmeyi azaltma olasılığı) Nagle algoritmasını devre dışı bırakır. sendBufferSize ve receiveBufferSize özellikleri ise işleme hızını yönetmek için kullanılır.

Yukarıdaki snippet'in son bölümünde kod, açılan sözün çözülmesini bekler. Bu söz yalnızca el sıkışma tamamlandığında çözülür ve veri iletimi için gereken okunabilir ve yazılabilir akışları içeren bir TCPSocketOpenInfo nesnesi döndürür.

Okuma ve yazma

Soket açıldıktan sonra standart Streams API arayüzlerini kullanarak soketle etkileşimde bulunun.

  • Yazma: Yazılabilir akış, BufferSource (ör. ArrayBuffer) kabul eder.
  • Okuma: Okunabilir akış Uint8Array veri sağlar.
// 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();

BYOB ile optimize edilmiş okuma

Bellek ayırmanın yönetilmesinin kritik olduğu yüksek performanslı uygulamalar için API, "Kendi Arabelleğinizi Getirin" (BYOB) okumayı destekler. Tarayıcının alınan her veri parçası için yeni bir arabellek ayırmasına izin vermek yerine, okuyucuya önceden ayrılmış bir arabellek iletebilirsiniz. Bu, verileri doğrudan mevcut belleğinize yazarak çöp toplama ek yükünü azaltır.

// 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'i uygulama

UDPSocket sınıfı, UDP iletişimi sağlar. Seçenekleri nasıl yapılandırdığınıza bağlı olarak iki farklı modda çalışır.

Bağlı mod

Bu modda soket, tek bir belirli hedefle iletişim kurar. Bu, standart istemci-sunucu görevleri için kullanışlıdır.

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

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

Sınırlı mod

Bu modda soket, yerel bir IP uç noktasına bağlanır. Rastgele kaynaklardan gelen datagramları alıp rastgele hedeflere gönderebilir. Bu genellikle yerel keşif protokolleri veya sunucu benzeri davranışlar için kullanılır.

// 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 mesajlarını işleme

TCP bayt akışının aksine, UDP akışları verileri ve uzak adres bilgilerini içeren UDPMessage nesneleriyle ilgilenir. Aşağıdaki kod, "bağlı modda" bir UDPSocket kullanırken giriş/çıkış işlemlerinin nasıl yapılacağını gösterir.

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

Soketin belirli bir eşe kilitlendiği "bağlı mod"un aksine, bağlı mod soketin rastgele hedeflerle iletişim kurmasına olanak tanır. Dolayısıyla, yazılabilir akışa veri yazarken her paket için remoteAddress ve remotePort değerlerini açıkça belirten bir UDPMessage nesnesi iletmeniz gerekir. Bu, sokete söz konusu datagramın tam olarak nereye yönlendirileceğini bildirir. Benzer şekilde, okunabilir akıştan okuma yaparken döndürülen değer yalnızca veri yükünü değil, gönderenin remoteAddress ve remotePort değerlerini de içerir. Bu sayede uygulamanız, gelen her paketin kaynağını belirleyebilir.

Not: UDPSocket "bağlı modda" kullanılırken soket, G/Ç sürecini basitleştirerek belirli bir eşe etkili bir şekilde kilitlenir. Bu modda, hedef zaten sabit olduğundan yazma işlemi sırasında remoteAddress ve remotePort özellikleri etkisizdir. Benzer şekilde, iletiler okunurken kaynak bağlı eş olduğundan bu özellikler null değerini döndürür.

Çok noktaya yayın desteği

Video oynatmayı birden fazla kiosk arasında senkronize etme veya yerel cihaz keşfini (ör. mDNS) uygulama gibi kullanım alanlarında Direct Sockets, çoklu yayın UDP'sini destekler. Bu sayede iletiler, belirli bir tek eşe değil, ağdaki tüm abonelere gönderilip alınabilir.

Çok noktaya yayın izinleri

Çoklu yayın özelliklerini kullanmak için IWA manifest dosyanıza belirli direct-sockets-multicast iznini eklemeniz gerekir. Bu, standart doğrudan yuva izninden farklıdır ve yalnızca özel ağlarda çoklu yayın kullanıldığı için gereklidir.

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

Çoklu yayın datagramları gönderme

Çoklu yayın grubuna gönderme, paket davranışını kontrol etmek için belirli seçeneklerin eklenmesiyle standart UDP "bağlı moduna" çok benzer.

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

Çoklu yayın datagramlarını alma

Çoklu yayın trafiği almak için UDPSocket "bağlı modda" açmanız (genellikle 0.0.0.0 veya ::'ye bağlanır) ve ardından MulticastController kullanarak belirli bir gruba katılmanız gerekir. Aynı cihazdaki birden fazla uygulamanın aynı bağlantı noktasını dinlemesi gereken cihaz keşfi protokolleri için gerekli olan multicastAllowAddressSharing seçeneğini de (Unix'teki SO_REUSEADDR seçeneğine benzer) kullanabilirsiniz.

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

Sunucu oluşturma

API, gelen TCP bağlantılarını kabul etmek için TCPServerSocket'i de destekler. Bu sayede IWA'nız yerel bir sunucu gibi davranabilir. Aşağıdaki kodda, TCPServerSocket arayüzü kullanılarak nasıl TCP sunucusu oluşturulacağı gösterilmektedir.

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

Sınıfı '::' adresiyle oluşturarak sunucu, gelen denemeleri dinlemek için kullanılabilir tüm IPv6 ağ arayüzlerine bağlanır. Geleneksel geri çağırmaya dayalı sunucu API'lerinin aksine bu API, web'in Streams API kalıbını kullanır: Gelen bağlantılar ReadableStream olarak teslim edilir. reader.read() işlevini çağırdığınızda uygulama, kuyruktan gelen bir sonraki bağlantıyı bekler ve kabul eder. Bu bağlantı, söz konusu istemciyle iki yönlü iletişime hazır, tam işlevsel bir TCPSocket örneğiyle sonuçlanır.

Chrome Geliştirici Araçları ile doğrudan soketlerde hata ayıklama

Chrome 138'den itibaren, harici paket yakalayıcılara gerek kalmadan doğrudan Chrome Geliştirici Araçları'ndaki panelinde doğrudan soket trafiğinde hata ayıklayabilirsiniz. Bu araç, standart HTTP isteklerinizin yanı sıra TCPSocket bağlantıları ve UDPSocket trafiği (hem bağlı hem de bağlantılı modlarda) izlemenize olanak tanır.

Uygulamanızın ağ etkinliğini incelemek için:

  1. Chrome Geliştirici Araçları'nda panelini açın.
  2. İstek tablosunda soket bağlantısını bulup seçin.
  3. Gönderilen ve alınan tüm verilerin günlüğünü görüntülemek için Mesajlar sekmesini açın.

Geliştirici Araçları'ndaki Mesajlar sekmesinde yer alan veriler.

Bu görünüm, TCP ve UDP mesajlarınızın ham ikili yükünü incelemenize olanak tanıyan bir Hex Viewer sağlar. Böylece protokol uygulamanızın bayt düzeyinde mükemmel olmasını sağlayabilirsiniz.

Demo

IWA Kitchen Sink, her biri Direct Sockets, Controlled Frame gibi farklı bir IWA API'sini gösteren birden fazla sekmeye sahip bir uygulamaya sahiptir.

Alternatif olarak, telnet istemcisi demosu, kullanıcının etkileşimli bir terminal aracılığıyla bir TCP/IP sunucusuna bağlanmasına olanak tanıyan yalıtılmış bir web uygulaması içerir. Diğer bir deyişle, Telnet istemcisi.

Sonuç

Direct Sockets API, web uygulamalarının daha önce yerel sarmalayıcılar olmadan desteklenmesi mümkün olmayan ham ağ protokollerini işlemesini sağlayarak kritik bir işlevsellik açığını kapatır. Bu, basit istemci bağlantısının ötesine geçer. TCPServerSocket ile uygulamalar gelen bağlantıları dinleyebilirken UDPSocket, hem eşler arası iletişim hem de yerel ağ keşfi için esnek modlar sunar.

Bu ham TCP ve UDP özelliklerini modern Streams API aracılığıyla kullanıma sunarak artık SSH, RDP veya özel IoT standartları gibi eski protokollerin tam özellikli uygulamalarını doğrudan JavaScript'te oluşturabilirsiniz. Bu API, düşük düzeyde ağ erişimi sağladığından önemli güvenlik etkileri vardır. Bu nedenle, bu özellik yalıtılmış web uygulamaları (IWA'lar) ile sınırlıdır. Bu sayede, bu tür bir güç yalnızca katı güvenlik politikalarını uygulayan, güvenilir ve açıkça yüklenmiş uygulamalara verilir. Bu denge sayesinde, web platformundan beklenen güvenlik düzeyini korurken güçlü ve cihaza özel uygulamalar oluşturabilirsiniz.

Kaynaklar