Direct Sockets

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

โดยทั่วไปแล้ว เว็บแอปพลิเคชันมาตรฐานจะจำกัดไว้สำหรับโปรโตคอลการสื่อสารที่เฉพาะเจาะจง เช่น HTTP และ API เช่น WebSocket และ WebRTC แม้ว่าฟีเจอร์เหล่านี้จะมีประสิทธิภาพ แต่ก็ได้รับการออกแบบมาให้มีข้อจำกัดที่เข้มงวดเพื่อป้องกันการละเมิด โดยจะสร้างการเชื่อมต่อ TCP หรือ UDP แบบดิบไม่ได้ ซึ่งจำกัดความสามารถของเว็บแอปในการสื่อสารกับระบบเดิมหรืออุปกรณ์ฮาร์ดแวร์ที่ใช้โปรโตคอลที่ไม่ใช่เว็บของตนเอง เช่น คุณอาจต้องการสร้างไคลเอ็นต์ SSH บนเว็บ เชื่อมต่อกับเครื่องพิมพ์ในเครื่อง หรือจัดการกลุ่มอุปกรณ์ IoT ในอดีต การดำเนินการนี้ต้องใช้ปลั๊กอินของเบราว์เซอร์ หรือแอปพลิเคชันตัวช่วยที่มาพร้อมเครื่อง

Direct Sockets API แก้ไขข้อจำกัดนี้โดยอนุญาตให้ Isolated Web App (IWA) สร้างการเชื่อมต่อ TCP และ UDP โดยตรงได้โดยไม่ต้องใช้รีเลย์เซิร์ฟเวอร์ IWA ช่วยให้เราเปิดเผย API นี้ได้อย่างปลอดภัยด้วยมาตรการรักษาความปลอดภัยเพิ่มเติม เช่น นโยบายรักษาความปลอดภัยเนื้อหา (CSP) ที่เข้มงวดและการแยกต้นทางแบบข้ามต้นทาง

กรณีการใช้งาน

คุณควรใช้ Direct Sockets แทน WebSocket มาตรฐานเมื่อใด

  • อุปกรณ์ IoT และอุปกรณ์อัจฉริยะ: สื่อสารกับฮาร์ดแวร์ที่ใช้ TCP/UDP แบบดิบ แทน HTTP
  • ระบบเดิม: การเชื่อมต่อกับเซิร์ฟเวอร์อีเมล (SMTP/IMAP) แชท IRC หรือเครื่องพิมพ์รุ่นเก่า
  • เดสก์ท็อประยะไกลและเทอร์มินัล: การติดตั้งใช้งานไคลเอ็นต์ SSH, Telnet หรือ RDP
  • ระบบ P2P: การใช้ตารางแฮชแบบกระจาย (DHT) หรือเครื่องมือการทำงานร่วมกันที่ยืดหยุ่น (เช่น IPFS)
  • การออกอากาศสื่อ: ใช้ประโยชน์จาก UDP เพื่อสตรีมเนื้อหาไปยังอุปกรณ์ปลายทางหลายรายการพร้อมกัน (การส่งแบบหลายผู้รับ) ซึ่งช่วยให้ใช้ Use Case ต่างๆ ได้ เช่น การเล่นวิดีโอที่ประสานกันในเครือข่ายคีออสค้าปลีก
  • ความสามารถของเซิร์ฟเวอร์และ Listener: การกำหนดค่า IWA ให้ทำหน้าที่เป็น ปลายทางที่รับสำหรับการเชื่อมต่อ TCP ขาเข้าหรือ Datagram UDP โดยใช้ TCPServerSocket หรือ UDPSocket ที่เชื่อมโยง

ข้อกำหนดเบื้องต้นสำหรับ Direct Sockets

คุณจะต้องตั้งค่า IWA ที่ใช้งานได้ก่อนจึงจะใช้ Direct Sockets ได้ จากนั้นคุณจะผสานรวม Direct Sockets เข้ากับหน้าเว็บได้

เพิ่มนโยบายสิทธิ์

หากต้องการใช้ Direct Sockets คุณต้องกำหนดค่าออบเจ็กต์ permissions_policy ในไฟล์ Manifest ของ IWA คุณต้องเพิ่มคีย์ direct-sockets เพื่อเปิดใช้ API อย่างชัดเจน นอกจากนี้ คุณต้องใส่แป้น cross-origin-isolated ด้วย คีย์นี้ไม่ได้เจาะจงสำหรับ Direct Sockets แต่จำเป็นสำหรับ IWA ทั้งหมดและ กำหนดว่าเอกสารจะเข้าถึง API ที่ต้องมีการแยกต้นทางข้ามได้หรือไม่

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

คีย์ของซ็อกเก็ตโดยตรงจะกำหนดว่าอนุญาตให้เรียกใช้ 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 ซึ่งจะปิดใช้อัลกอริทึมของ Nagle เพื่อหยุดไม่ให้ระบบจัดกลุ่มแพ็กเก็ตขนาดเล็ก ซึ่งอาจช่วยลดเวลาในการตอบสนอง หรือ sendBufferSize และ receiveBufferSize เพื่อจัดการอัตราการรับส่งข้อมูล

ในส่วนสุดท้ายของข้อมูลโค้ดก่อนหน้า โค้ดจะรอพรอมิสที่เปิดอยู่ ซึ่งจะได้รับการแก้ไขเมื่อการแฮนด์เชคเสร็จสมบูรณ์เท่านั้น โดยจะแสดงผลออบเจ็กต์ TCPSocketOpenInfo ที่มีสตรีมที่อ่านได้และเขียนได้ซึ่งจำเป็น สำหรับการส่งข้อมูล

อ่านและเขียน

เมื่อเปิดซ็อกเก็ตแล้ว ให้โต้ตอบกับซ็อกเก็ตโดยใช้อินเทอร์เฟซ Streams API มาตรฐาน

  • การเขียน: สตรีมที่เขียนได้จะยอมรับ 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();

การอ่านที่เพิ่มประสิทธิภาพด้วย BYOB

สำหรับแอปพลิเคชันที่มีประสิทธิภาพสูงซึ่งการจัดการการจัดสรรหน่วยความจำมีความสำคัญอย่างยิ่ง API รองรับการอ่าน "Bring Your Own Buffer" (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 ได้ โดยจะทำงานใน 2 โหมดที่แตกต่างกัน ขึ้นอยู่กับวิธีที่คุณกำหนดค่าตัวเลือก

โหมดที่เชื่อมต่อ

ในโหมดนี้ ซ็อกเก็ตจะสื่อสารกับปลายทางที่เฉพาะเจาะจงเพียงแห่งเดียว ซึ่งจะเป็นประโยชน์สำหรับงานไคลเอ็นต์-เซิร์ฟเวอร์มาตรฐาน

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

สตรีม UDP จะจัดการกับUDPMessageออบเจ็กต์ ซึ่ง มีข้อมูลและข้อมูลที่อยู่ระยะไกล ซึ่งแตกต่างจากสตรีมไบต์ TCP โค้ดต่อไปนี้แสดง วิธีจัดการการดำเนินการอินพุต/เอาต์พุตเมื่อใช้ 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 อย่างชัดเจนสำหรับแต่ละแพ็กเก็ต เพื่อสั่งให้ซ็อกเก็ตกำหนดเส้นทางของ Datagram นั้นๆ อย่างแม่นยำ ในทำนองเดียวกัน เมื่ออ่านจากสตรีมที่อ่านได้ ค่าที่ส่งกลับมาจะรวมถึงเพย์โหลดข้อมูล และ remoteAddress และ remotePort ของผู้ส่งด้วย ซึ่งจะช่วยให้ แอปพลิเคชันระบุแหล่งที่มาของทุกแพ็กเก็ตขาเข้าได้

หมายเหตุ: เมื่อใช้ UDPSocket ใน "โหมดที่เชื่อมต่อ" ซ็อกเก็ตจะถูกล็อกกับเพียร์ที่เฉพาะเจาะจง ซึ่งช่วยลดความซับซ้อนของกระบวนการ I/O ในโหมดนี้ พร็อพเพอร์ตี้ remoteAddress และ remotePort จะไม่มีผลเมื่อเขียน เนื่องจากปลายทางได้รับการแก้ไขแล้ว ในทำนองเดียวกัน เมื่ออ่านข้อความ พร็อพเพอร์ตี้เหล่านี้จะแสดงค่าเป็น Null เนื่องจากระบบรับประกันว่าแหล่งที่มาจะเป็นเพียร์ที่เชื่อมต่ออยู่

การรองรับมัลติแคสต์

สำหรับกรณีการใช้งาน เช่น การซิงค์การเล่นวิดีโอในคีออสก์หลายเครื่องหรือ การติดตั้งใช้งานการค้นหาอุปกรณ์ในพื้นที่ (เช่น mDNS) Direct Sockets รองรับ Multicast UDP ซึ่งจะช่วยให้ส่งข้อความไปยังอีเมล "กลุ่ม" และรับข้อความ โดยสมาชิกทุกคนในเครือข่ายได้ แทนที่จะส่งไปยังเพียร์รายเดียว

สิทธิ์แบบมัลติแคสต์

หากต้องการใช้ความสามารถในการส่งแบบหลายผู้รับ คุณต้องเพิ่มสิทธิ์ direct-sockets-multicastที่เฉพาะเจาะจงลงในไฟล์ Manifest ของ IWA สิทธิ์นี้แตกต่าง จากสิทธิ์ซ็อกเก็ตโดยตรงมาตรฐาน และมีความจำเป็นเนื่องจากมัลติแคสต์ ใช้ในเครือข่ายส่วนตัวเท่านั้น

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

ส่ง Datagram แบบมัลติแคสต์

การส่งไปยังกลุ่มมัลติแคสต์จะคล้ายกับ "โหมดที่เชื่อมต่อ" ของ 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...

รับ Datagram แบบมัลติแคสต์

หากต้องการรับการรับส่งข้อมูลแบบมัลติแคสต์ คุณต้องเปิด 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 นี้ใช้รูปแบบ Streams API ของเว็บ ซึ่งแตกต่างจาก API ของเซิร์ฟเวอร์แบบเรียกกลับแบบเดิม โดยการเชื่อมต่อขาเข้าจะแสดงเป็น ReadableStream เมื่อคุณเรียกใช้ reader.read() แอปพลิเคชันจะรอและยอมรับการเชื่อมต่อถัดไปจาก คิว โดยจะแปลงเป็นค่าที่เป็นอินสแตนซ์ TCPSocket ที่ทำงานได้อย่างสมบูรณ์ พร้อมสำหรับการสื่อสารแบบสองทางกับไคลเอ็นต์นั้นๆ

ดีบัก Direct Sockets ด้วยเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome

ตั้งแต่ Chrome 138 เป็นต้นไป คุณจะแก้ไขข้อบกพร่องของการรับส่งข้อมูลของ Direct Sockets ได้โดยตรงภายในแผงเครือข่ายในเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome ซึ่งช่วยให้ไม่ต้องใช้เครื่องมือดมแพ็กเก็ตภายนอก เครื่องมือนี้ช่วยให้คุณตรวจสอบการเชื่อมต่อ TCPSocket รวมถึงการรับส่งข้อมูล UDPSocket (ทั้งในโหมดที่เชื่อมต่อและโหมดที่เชื่อมต่อ) ควบคู่ไปกับคำขอ HTTP มาตรฐาน

วิธีตรวจสอบกิจกรรมเครือข่ายของแอป

  1. เปิดแผงเครือข่ายในเครื่องมือสำหรับนักพัฒนาเว็บใน Chrome
  2. ค้นหาและเลือกการเชื่อมต่อซ็อกเก็ตในตารางคำขอ
  3. เปิดแท็บข้อความเพื่อดูบันทึกข้อมูลทั้งหมดที่ส่งและรับ

ข้อมูลในแท็บข้อความในเครื่องมือสำหรับนักพัฒนาเว็บ

มุมมองนี้มีโปรแกรมดูเลขฐานสิบหก ซึ่งช่วยให้คุณตรวจสอบเพย์โหลดไบนารีดิบ ของข้อความ TCP และ UDP เพื่อให้มั่นใจว่าการใช้โปรโตคอลของคุณ ถูกต้องทุกไบต์

สาธิต

IWA Kitchen Sink มีแอปที่มี หลายแท็บ ซึ่งแต่ละแท็บจะสาธิต IWA API ที่แตกต่างกัน เช่น Direct Sockets, Controlled Frame และอื่นๆ

หรือไคลเอ็นต์ Telnet demo มี Isolated Web App ที่อนุญาตให้ผู้ใช้เชื่อมต่อกับเซิร์ฟเวอร์ TCP/IP ผ่าน เทอร์มินัลแบบอินเทอร์แอกทีฟ กล่าวคือ ไคลเอ็นต์ Telnet

บทสรุป

Direct Sockets API ช่วยปิดช่องโหว่ด้านฟังก์ชันการทำงานที่สำคัญด้วยการช่วยให้เว็บแอปพลิเคชันจัดการโปรโตคอลเครือข่ายดิบที่ก่อนหน้านี้ไม่สามารถรองรับได้หากไม่มี Wrapper ที่มาพร้อมเครื่อง โดยไม่ได้จำกัดอยู่แค่การเชื่อมต่อไคลเอ็นต์แบบง่ายๆ แต่TCPServerSocketยังช่วยให้แอปพลิเคชันรับฟังการเชื่อมต่อขาเข้าได้ ในขณะที่UDPSocketมีโหมดที่ยืดหยุ่นสำหรับการสื่อสารแบบเพียร์ทูเพียร์และการค้นหาเครือข่ายในพื้นที่

การเปิดเผยความสามารถ TCP และ UDP ดิบเหล่านี้ผ่าน Streams API ที่ทันสมัย ช่วยให้คุณสร้างการใช้งานโปรโตคอลเดิมที่มีฟีเจอร์ครบถ้วน เช่น SSH, RDP หรือมาตรฐาน IoT ที่กำหนดเอง ได้โดยตรงใน JavaScript เนื่องจาก API นี้ให้สิทธิ์เข้าถึงเครือข่ายระดับต่ำ จึงมีนัยด้านความปลอดภัยที่สำคัญ ดังนั้นจึงจำกัดไว้สำหรับ Isolated Web App (IWA) เพื่อให้มั่นใจว่าสิทธิ์ดังกล่าว จะมอบให้เฉพาะแอปพลิเคชันที่เชื่อถือได้ซึ่งติดตั้งอย่างชัดเจนและบังคับใช้ นโยบายการรักษาความปลอดภัยที่เข้มงวดเท่านั้น ความสมดุลนี้ช่วยให้คุณสร้างแอปพลิเคชันที่มีประสิทธิภาพซึ่งเน้นอุปกรณ์เป็นหลัก พร้อมทั้งรักษาความปลอดภัยที่ผู้ใช้คาดหวังจากแพลตฟอร์มเว็บ

แหล่งข้อมูล