Direct Sockets

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

標準網頁應用程式通常會限制使用特定通訊協定 (例如 HTTP) 和 API (例如 WebSocketWebRTC)。雖然這些功能很強大,但設計上會嚴格限制,以防遭到濫用。無法建立原始 TCP 或 UDP 連線,因此網路應用程式與使用自有非網路通訊協定的舊版系統或硬體裝置通訊時,會受到限制。舉例來說,您可能想建構以網頁為基礎的 SSH 用戶端、連線至本機印表機,或管理 IoT 裝置群。過去,這需要瀏覽器外掛程式或原生輔助應用程式。

Direct Sockets API 可讓隔離網頁應用程式 (IWA) 建立直接的 TCP 和 UDP 連線,無需中繼伺服器,解決這項限制。由於 IWA 採用嚴格的內容安全政策 (CSP) 和跨來源隔離等額外安全措施,因此可以安全地公開這個 API。

用途

在什麼情況下,您應優先使用 Direct Sockets 而非標準 WebSocket?

  • 物聯網和智慧型裝置:與使用原始 TCP/UDP (而非 HTTP) 的硬體通訊。
  • 舊版系統:連線至舊版郵件伺服器 (SMTP/IMAP)、IRC 聊天伺服器或印表機。
  • 遠端桌面和終端機:實作 SSH、Telnet 或 RDP 用戶端。
  • P2P 系統:實作分散式雜湊表 (DHT) 或彈性協作工具 (例如 IPFS)。
  • 媒體廣播:利用 UDP 同時將內容串流至多個端點 (多點播送),實現各種用途,例如在零售資訊站網路中協調播放影片。
  • 伺服器和接聽程式功能:設定 IWA,使其透過 TCPServerSocket 或繫結 UDPSocket,做為傳入 TCP 連線或 UDP 資料封包的接收端點。

Direct Sockets 的必要條件

使用 Direct Sockets 前,請先設定可正常運作的 IWA。接著,您就可以將 Direct Sockets 整合至網頁。

新增權限政策

如要使用直接通訊端,您必須在 IWA 資訊清單中設定 permissions_policy 物件。您必須新增 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

應用程式可以建立 TCPSocket 例項,要求建立 TCP 連線。

開啟連線

如要開啟連線,請使用 new 運算子和 await 開啟的 Promise。

TCPSocket 建構函式會使用指定的 remoteAddressremotePort 啟動連線。

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 演算法,避免系統批次處理小型封包,進而減少延遲;或是 sendBufferSizereceiveBufferSize,用來管理輸送量。

在上述程式碼片段的最後一部分,程式碼會等待開啟的 Promise,只有在交握完成後才會解析,並傳回 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 支援「自備緩衝區」(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 物件,明確指定每個封包的 remoteAddressremotePort,指示插座將特定資料包路由傳送至何處。同樣地,從可讀取的串流讀取時,傳回的值不僅包含資料酬載,也包含傳送端的 remoteAddressremotePort,讓應用程式能夠識別每個傳入封包的來源。

注意:在「連線模式」中使用 UDPSocket 時,插槽會有效鎖定特定對等互連,簡化 I/O 程序。在這個模式中,寫入時 remoteAddressremotePort 屬性實際上是無運算,因為目的地已固定。同樣地,讀取訊息時,這些屬性會傳回空值,因為來源保證是連線的對等互連裝置。

支援多點傳播

對於在多個資訊亭同步播放影片,或實作本機裝置探索 (例如 mDNS) 等用途,Direct Sockets 支援多點播送 UDP。這樣一來,訊息就能傳送至「群組」地址,並由網路上所有訂閱者接收,而非單一特定對等互連裝置。

多點傳播權限

如要使用多播功能,請務必在 IWA 資訊清單中新增特定 direct-sockets-multicast 權限。這與標準的直接通訊端權限不同,而且由於多點播送僅用於私人網路,因此必須使用這項權限。

{
  "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 選項 (類似於 Unix 上的 SO_REUSEADDR),這對於裝置探索通訊協定來說至關重要,因為同一裝置上的多個應用程式需要監聽相同連接埠。

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 成為本機伺服器。以下程式碼說明如何使用 TCPServerSocket 介面建立 TCP 伺服器。

// 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 開始,您可以在 Chrome 開發人員工具的「網路」面板中直接偵錯 Direct Sockets 流量,不必使用外部封包監聽器。這項工具可讓您監控 TCPSocket 連線和 UDPSocket 流量 (包括繫結和連線模式),以及標準 HTTP 要求。

如要檢查應用程式的網路活動,請按照下列步驟操作:

  1. 在 Chrome 開發人員工具中開啟「網路」面板。
  2. 在要求表格中找出並選取通訊端連線。
  3. 開啟「訊息」分頁,即可查看所有傳輸及接收資料的記錄。

開發人員工具「訊息」分頁中的資料。

這個檢視畫面提供十六進位檢視器,可讓您檢查 TCP 和 UDP 訊息的原始二進位酬載,確保通訊協定實作完全符合位元組規格。

示範

IWA Kitchen Sink 應用程式有多個分頁,每個分頁都展示不同的 IWA API,例如 Direct Sockets、Controlled Frame 等。

或者,Telnet 用戶端範例包含獨立網頁應用程式,可讓使用者透過互動式終端機連線至 TCP/IP 伺服器。也就是 Telnet 用戶端。

結論

Direct Sockets API 可讓網路應用程式處理原始網路通訊協定,填補了重要的功能缺口,因為先前若沒有原生包裝函式,就無法支援這類通訊協定。這項功能不僅提供簡單的用戶端連線,應用程式還能透過 TCPServerSocket 監聽連入連線,而 UDPSocket 則提供彈性模式,適用於對等通訊和本機網路探索。

透過新式 Streams API 公開這些原始 TCP 和 UDP 功能後,您現在可以直接在 JavaScript 中,建構舊版通訊協定的完整實作項目,例如 SSH、RDP 或自訂 IoT 標準。由於這項 API 會授予低階網路存取權,因此會對安全性造成重大影響。因此,這項功能僅限隔離網頁應用程式 (IWA) 使用,確保只有強制執行嚴格安全政策的信任應用程式,才能獲得這項權限。在這樣的平衡狀態下,您能建構強大的裝置專屬應用程式,同時維持使用者對網路平台安全性的期望。

資源