(この記事は Web Advent Calendar 2025 の記事【3つ目】です)
はじめに
この記事は、ブラウザで TCP・UDP の通信を直接扱うことができる API の「Direct Sockets API」に関する話です。今回、Direct Sockets API を使った通信を試してみます。
実際に試していく
「Direct Sockets API」を使うための下準備
「Direct Sockets API」を使うためには、通常の Webページで処理を実行するのではなく、以下の記事に書いた「Isolated Web Apps」を組み合わせる等の対応が必要です(※ 別の方法もあるようですが)。
●ブラウザで「Isolated Web Apps」を試す(Direct Sockets API を使うための下準備) - Qiita
https://qiita.com/youtoy/items/650fec647f348ec15862
2つの下準備
まずは、上記の記事で書いた「Isolated Web Apps の動作確認」のところまで進めます。そして、上記の記事で作っていたアプリを書きかえます。
それと TCP・UDP の通信相手は、以下の別の記事で書いた Node.js のコードを用います。
●Node.js の標準機能でシンプルな TCP と UDP - Qiita
https://qiita.com/youtoy/items/c429d0376a10bbd0b985
これらが準備できたら、Isolated Web Apps + Direct Sockets API を使った TCP・UDP の通信を試していきます。
「Direct Sockets API」を扱うための実装
Isolated Web Apps で Direct Sockets API を扱うための実装・設定を以下としました。
HTMLファイルの実装
以下は HTMLファイルです。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>IWA Direct Sockets テスト</title>
<style>
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
margin: 16px;
}
section {
border: 1px solid #ccc;
padding: 12px;
margin-bottom: 16px;
}
button {
padding: 4px 10px;
margin-bottom: 8px;
}
pre {
background: #f7f7f7;
padding: 8px;
min-height: 3em;
white-space: pre-wrap;
}
</style>
</head>
<body>
<h1>IWA Direct Sockets テスト</h1>
<p id="env"></p>
<section>
<h2>TCP クライアント (127.0.0.1:5000)</h2>
<button id="tcp-btn">TCP: 1回送信して返信を受け取る</button>
<pre id="tcp-log"></pre>
</section>
<section>
<h2>UDP クライアント (127.0.0.1:5001)</h2>
<button id="udp-btn">UDP: 1回送信して返信を受け取る</button>
<pre id="udp-log"></pre>
</section>
<script type="module" src="/app.js"></script>
</body>
</html>
ページを開いた時の見た目は、以下となります。
JavaScriptファイルの実装
以下は JavaScriptファイルの実装です。
const envEl = document.getElementById("env");
const tcpLog = document.getElementById("tcp-log");
const udpLog = document.getElementById("udp-log");
function logTcp(msg) {
tcpLog.textContent += msg + "\n";
}
function logUdp(msg) {
udpLog.textContent += msg + "\n";
}
envEl.textContent =
`TCPSocket: ${typeof TCPSocket !== "undefined"} / ` +
`UDPSocket: ${typeof UDPSocket !== "undefined"}`;
document.getElementById("tcp-btn").addEventListener("click", () => {
runTcpOnce().catch((err) => {
logTcp("エラー: " + err);
});
});
async function runTcpOnce() {
if (typeof TCPSocket === "undefined") {
logTcp("TCPSocket が利用できません(IWA 以外 or フラグ不足?)");
return;
}
const host = "127.0.0.1";
const port = 5001;
const message = "IWAからのTCP通信です\n";
const encoder = new TextEncoder();
const decoder = new TextDecoder();
logTcp(`接続中: ${host}:${port} ...`);
const socket = new TCPSocket(host, port);
let openedInfo;
try {
openedInfo = await socket.opened;
} catch (e) {
logTcp("接続失敗: " + e);
return;
}
const { readable, writable, localAddress, localPort } = openedInfo;
logTcp(`接続完了: local=${localAddress}:${localPort}`);
const writer = writable.getWriter();
await writer.ready;
await writer.write(encoder.encode(message));
writer.releaseLock();
logTcp(`送信: ${message.trimEnd()}`);
const reader = readable.getReader();
const { value, done } = await reader.read();
if (done) {
logTcp("受信なしのままストリーム終了");
} else {
const text = decoder.decode(value);
logTcp("受信: " + text.trimEnd());
}
reader.releaseLock();
await socket.close();
logTcp("ソケットをクローズしました");
}
document.getElementById("udp-btn").addEventListener("click", () => {
runUdpOnce().catch((err) => {
logUdp("エラー: " + err);
});
});
async function runUdpOnce() {
if (typeof UDPSocket === "undefined") {
logUdp("UDPSocket が利用できません(IWA 以外 or フラグ不足?)");
return;
}
const host = "127.0.0.1";
const port = 5002;
const message = "IWAからのUDP通信です\n";
const encoder = new TextEncoder();
const decoder = new TextDecoder();
logUdp(`UDP ソケット接続中 (connected モード): ${host}:${port} ...`);
const udpSocket = new UDPSocket({ remoteAddress: host, remotePort: port });
let openedInfo;
try {
openedInfo = await udpSocket.opened;
} catch (e) {
logUdp("UDP ソケットオープン失敗: " + e);
return;
}
const { readable, writable, localAddress, localPort } = openedInfo;
logUdp(`UDP ソケットオープン: local=${localAddress}:${localPort}`);
const writer = writable.getWriter();
await writer.ready;
await writer.write({
data: encoder.encode(message),
});
writer.releaseLock();
logUdp(`送信: ${message}`);
const reader = readable.getReader();
const { value, done } = await reader.read();
if (done || !value) {
logUdp("受信なしのままストリーム終了");
} else {
const { data } = value;
const text = decoder.decode(data);
logUdp("受信: " + text.trimEnd());
}
reader.releaseLock();
await udpSocket.close();
logUdp("UDP ソケットをクローズしました");
}
マニフェストファイルの内容
前回使った内容に関して、上記以外にマニフェストファイルの修正も行いました。内容は以下のとおりです。
{
"name": "IWA TCP/UDP",
"version": "1.0.0",
"start_url": "/",
"permissions_policy": {
"cross-origin-isolated": ["self"],
"direct-sockets": ["self"],
"direct-sockets-private": ["self"]
},
"icons": [
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
今回の修正をするにあたり、以下のリポジトリにあるファイルを参照しました。
●GoogleChromeLabs/telnet-client
https://github.com/GoogleChromeLabs/telnet-client?tab=readme-ov-file
参照したのは、以下のファイルです。
●telnet-client/assets/.well-known/manifest.webmanifest at main · GoogleChromeLabs/telnet-client
https://github.com/GoogleChromeLabs/telnet-client/blob/main/assets/.well-known/manifest.webmanifest
今回の Direct Sockets API を扱うために必要になるっぽい、以下の "permissions_policy" の部分を追加しました。
アプリのインストール・実行関連
前回の記事で試したアプリをアンインストールして、今回のアプリをインストールします。
それについて、 chrome://web-app-internals でアンインストールをしてから、今回のアプリをインストールしました。
以下が、インストールや起動、処理の実行に関する内容です。
インストール・起動コマンド
前回と同じコマンドで、アプリのインストールと起動を行います。具体的には、以下を実行します。
※ この時、あらかじめ前回の記事と同様に、ローカルサーバーを起動しておいてください
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
--user-data-dir=/tmp/iwa-profile \
--enable-features=IsolatedWebApps,IsolatedWebAppDevMode \
--install-isolated-web-app-from-url=http://127.0.0.1:5500/
一度インストールした後は、再度アプリを起動する時には以下のコマンドで OK です。
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
--user-data-dir=/tmp/iwa-profile \
--enable-features=IsolatedWebApps,IsolatedWebAppDevMode
処理の実行
ブラウザが開いたら、 chrome://apps/ のページへ移動して自作の Isolated Web Apps を開きます。
その後、Node.js で実装した通信相手のサーバーを起動した状態にして(※ 前に書いた記事内の TCP、UDP のどちらも対応できるものを準備しました)、その上で今回のアプリのページ内で用意したボタンを押してみます。
その結果、以下のように TCP と UDP のどちらの通信も行うことができました。


