1. Chrome 131 enables Direct Socket API by default
在WebRTC大佬群潜水看到一个讨论极其热烈的话题,Chrome 131 要开放裸TCP/UDP能力,之前没有注意见这个API,这次就研究了一下。
转发的图片:
由说明中可以看到:
-
运行环境是 IWAs Isolated Web Apps ,并非普通的浏览器网页,
-
有一个体验Demo中的README,Chrome 110(2023 年 2 月)就集成了该功能,只是需要加flags打开
2. IWA vs PWA
由于之前没有接触过IWA,有请豆包回答一下二者的区别
IWA(Isolated Web App,隔离型网络应用)和 PWA(Progressive Web App,渐进式网络应用)有以下一些区别:
一、概念与目标
IWA:
主要强调应用程序的隔离性,旨在为用户提供更安全、独立的网络应用体验。
通常通过限制应用对系统资源的访问以及与其他应用的交互,来降低安全风险。
例如,IWA 可以建立直接的 TCP 和 UDP 通信,同时保证这种通信不会对其他应用或系统造成干扰。
PWA:
目标是提供类似原生应用的体验,同时利用网络的优势,无需用户从应用商店下载安装。
强调离线可用性、快速加载、推送通知等功能,以提高用户的便利性和参与度。
二、功能特性
网络连接:
IWA:可以建立直接的 TCP 和 UDP 通信,并监听和接受传入的连接。这种能力使其适用于一些特定的网络应用场景,如需要直接与网络设备或系统进行通信的应用。
PWA:主要依赖于浏览器的网络连接能力,虽然可以通过缓存和离线存储来提供一定程度的离线可用性,但在直接的网络通信方面没有 IWA 那么强大的功能。
安全性:
IWA:由于强调隔离性,通常具有更高的安全级别。它们运行在相对独立的环境中,减少了与其他应用或系统组件的交互,从而降低了被攻击的风险。
PWA:安全性主要依赖于浏览器的安全机制。虽然浏览器也提供了一定的安全保障,但由于 PWA 与浏览器的紧密集成,可能面临一些与浏览器相关的安全风险。
安装与使用:
IWA:安装和使用方式可能因操作系统和具体实现而异。一般来说,用户可能需要通过特定的渠道或方式来安装和启动 IWA。
PWA:可以通过浏览器访问网页,然后将其添加到主屏幕,就像安装原生应用一样。用户可以直接从主屏幕启动 PWA,无需经过传统的应用商店安装过程。
三、应用场景
IWA:
适用于对安全性要求较高的网络应用,如企业级应用、金融交易应用等。
对于需要直接与网络设备进行通信的应用,如物联网应用、网络管理工具等,IWA 的直接 TCP 和 UDP 通信能力非常有用。
PWA:
适合各种类型的网络应用,尤其是那些需要在不同设备上提供一致体验的应用,如新闻网站、博客、电子商务网站等。
对于希望提高用户参与度和留存率的应用,PWA 的推送通知、离线可用性等功能可以提供很大的帮助。
3. 体验TCP/UDP Socket
3.1 TCPSocket
启动项目:
直接按telnet-client README说明运行该项目:
-
npm install
-
npm start -> 启动localhost:4321端口
这个时候打开是会报错的,因为不是IWAs环境
使用命令行启动浏览器,注意端口号是4321,README有问题,在 chrome://apps 里找到 telnet App 打开
google-chrome-unstable --enable-features=IsolatedWebApps,IsolatedWebAppDevMode \
--install-isolated-web-app-from-url=http://localhost:4321
打开后是这样的独立窗口,点击[Launch echo server],会出现右边的echo窗口
client 窗口
echo 窗口
看了下代码,这个应用有2个页面
-
index.html 代表客户端
-
echo.html TCPServer 模拟服务端,只有一个简单的回显功能
Demo体验:
- Host填 localhost
- Port填 echo 窗口那个port
- 点击[connect]
- 连接成功后,就可以在client窗口黑框里打字,显示的内容其实是从tcp流里读到的内容,如果打开[local echo],你会看到输入1个字母,显示2个字母
API介绍:
MDN还没有收录该API,不过可以在Direct Sockets API查看,和TCP相关的类有2个:
-
TCPSocket
-
TCPServerSocket
用法说明
const socket = new TCPSocket(host, port);
const { readable, writable } = await socket.opened;
const reader = readable.getReader();
const writer = writable.getWriter();
读数据,需要写一个死循环,一直从reader里面读,如果read返回done,就break跳出循环
for (;;) {
const {value, done} = await reader.read();
if (value) {
// do sth..
}
if (done) {
break;
}
}
// 死循环的另一写法是
while (true) {
// 同上
}
写数据,写比较简单,只是注意读写的数据都是 Uint8Array 类型的
writer.write(message);
TCPServerSocket的用法和TCPSocket非常类似,在创建实例后就能拿要TCP监听的port,读写流程就和client一致了。
3.2 UDPSocket
这个 telnet App 只提供了TCP的使用(可能是找不到UDP Server做测试,注意没有UDPServerSocket),此时我们使用一个开放的STUN服务器stun.l.google.com(STUN是基于UDP传输协议的应用层协议)来测试一下。
TS类型准备:
ts还没有收录该API的定义,所以需要在项目direct-sockets.d.ts里手动添加一下,这些内容是gemini识别IDL定义直接生成的,如果有需要可以直接拿走:
declare enum SocketDnsQueryType {
"ipv4",
"ipv6"
}
interface UDPSocketOptions {
remoteAddress?: string;
remotePort?: number;
localAddress?: string;
localPort?: number;
sendBufferSize?: number;
receiveBufferSize?: number;
dnsQueryType?: SocketDnsQueryType;
ipv6Only?: boolean;
}
interface UDPMessage {
data: BufferSource;
remoteAddress: string;
remotePort: number;
dnsQueryType: SocketDnsQueryType;
}
interface UDPSocketOpenInfo {
readable: ReadableStream;
writable: WritableStream;
remoteAddress: string;
remotePort: number;
localAddress: string;
localPort: number;
}
declare class UDPSocket {
constructor(options?: UDPSocketOptions);
readonly opened: Promise<UDPSocketOpenInfo>;
readonly closed: Promise<undefined>;
close(): Promise<undefined>;
}
发送与接收STUN协议:
STUN协议 的一个功能是拿到我们本机NAT外的公网地址,更具体的用法可以看STUN的RFC datatracker.ietf.org/doc/html/rf…
读写流程上其实和TCPSocket一样,这里就是验证下有无生效,UDP发送的内容是从wireshark上爪的一个STUN包,你也可以自己按照RFC自己构造。
async function testUdpSocket() {
const udpSocket = new UDPSocket({
remoteAddress: 'stun.l.google.com',
remotePort: 19302,
});
const {readable, writable} = await udpSocket.opened;
const reader = readable.getReader();
const writer = writable.getWriter();
receiveUdpMessage(reader);
sendUdpMessage(writer);
}
async function sendUdpMessage(writer: WritableStreamDefaultWriter<any>) {
const data = new Uint8Array([
0x00,
0x01,
0x00,
0x00,
0x21,
0x12,
0xa4,
0x42,
0x61,
0x74,
0x64,
0x44,
0x6c,
0x45,
0x42,
0x36,
0x58,
0x6a,
0x63,
0x4f]);
const message = {
data,
};
await writer.ready;
await writer.write(message);
writer.releaseLock();
}
async function receiveUdpMessage(reader: ReadableStreamDefaultReader<any>) {
try {
for (;;) {
const {value, done} = await reader.read();
if (value) {
console.log('receive value: ', value);
}
if (done) {
break;
}
}
reader.releaseLock();
} catch (e) {
console.error(e);
term.writeln(`<ERROR: ${e.message}>`);
}
}
同样我们打开wireshark抓包,看看google服务器有没有给我们正常返回,发送,可以看到STUN的内容和我们代码内容一致
接收wireshark的STUN包内容和我们代码打印的内容一致(数据1和数据2)