今天和大家来一起聊一个即将推出的跨 JavaScript 运行时的 Socket API 。
什么是 TCP 套接字
TCP(传输控制协议)是互联网的基础网络协议。它是用于发出 HTTP 请求(在 HTTP/3 之前,使用 QUIC )、通过 SMTP 发送电子邮件、使用数据库特定协议(如 MySQL )和许多其他应用程序层协议查询数据库的底层协议。
TCP Scoket 是一种编程接口,代表两个都同意通过 TCP “通话”的应用程序之间的双向通信连接。一个应用程序启动与正在侦听入站 TCP 连接的另一个应用程序的出站 TCP 连接。通过协商三次握手建立连接,握手完成后就可以双向发送数据。
Scoket 是单个 TCP 连接的编程接口 - 它有一个可读和可写的数据 "流",只要连接保持打开,应用程序就可以持续读写数据。
Socket 兼容性
对于 Workers,我们的目标是尽可能支持跨浏览器和非浏览器环境支持的标准 API,以便尽可能多的 NPM 包无需更改即可在 Workers 上运行,并且包作者不必编写特定于运行时的代码。但对于 TCP Scoket,迄今为止,JavaScript 运行时还没有用于创建和使用 TCP 或 UDP Scoket 的标准 API。
Node.js 提供了 net 和 tls API,但这些 API 是在 10 多年前 Node.js 项目的早期设计的,并且仍然基于回调。使用起来太麻烦了,并且它们也不适合 Serverless 平台或 Web 浏览器的方式公开配置。
var server = net.createServer();
// 堆代码 duidaima.com
server.listen(9000, () => {
console.log('opened server on port: ', 9000);
});
server.on("connection", (socket) => {
console.log("new client connection is made");
});
另外 Deno 也实现了一套不同的 API — Deno.connect。
const conn1 = await Deno.connect({ port: 80 });
const conn2 = await Deno.connect({ hostname: "192.0.2.1", port: 80 });
const conn3 = await Deno.connect({ hostname: "[2001:db8::1]", port: 80 });
const conn4 = await Deno.connect({ hostname: "golang.org", port: 80, transport: "tcp" });
尽管 WICG 提案确实也是存在的,但 Web 浏览器不提供原始 TCP 套接字 API ,并且它与 Node.js 和 Deno 也都不同。
connect() — 一个通用的 Socket API
基于这样的背景,Cloudflare 和 Vercel 的工程师发布了一套通用的 Socket API 规范:
Sockets API 的草案规范定义了以下 API:
dictionary SocketAddress {
DOMString hostname;
unsigned short port;
};
typedef (DOMString or SocketAddress) AnySocketAddress;
enum SecureTransportKind { "off", "on", "starttls" };
[Exposed=*]
dictionary SocketOptions {
SecureTransportKind secureTransport = "off";
boolean allowHalfOpen = false;
};
[Exposed=*]
interface Connect {
Socket connect(AnySocketAddress address, optional SocketOptions opts);
};
interface Socket {
readonly attribute ReadableStream readable;
readonly attribute WritableStream writable;
readonly attribute Promise<undefined> closed;
Promise<undefined> close();
Socket startTls();
};
提案的 API 是基于 Promise 的,并尽可能的复用了现有的标准。例如 ReadableStream 和 WritableStream 用于 socket 的读写端。这使得我们可以很轻松地将数据从 TCP Socket 传输到接受 ReadableStream 作为输入的任何其他库或现有代码,或者通过 WritableStream 写入 TCP Socket。
API 的入口点是 connect() 函数,它接受一个包含主机名和端口(以冒号分隔)的字符串,或者一个具有离散主机名和端口字段的对象。它返回一个代表套接字连接的 Socket 对象。该对象的实例公开用于处理连接的属性和方法。
通过调用 Socket 对象上的 startTls() 方法,我们可以在纯文本或 TLS 模式下建立连接,也可以在特殊的 "starttls" 模式下建立连接,该模式允许 Socket 在进行一段时间的纯文本数据传输后轻松升级为 TLS。一旦 Socket 升级为使用 TLS,就无需创建新的 Socket ,也无需转而使用一套单独的应用程序接口。
下面是个简单的例子:
import { connect } from "@arrowood.dev/socket"
const options = { secureTransport: "starttls" };
const socket = connect("address:port", options);
const secureSocket = socket.startTls();
// The socket is immediately writable
// Relies on web standard WritableStream
const writer = secureSocket.writable.getWriter();
const encoder = new TextEncoder();
const encoded = encoder.encode("hello");
await writer.write(encoded);
下面是使用 node:net 和 node:tls API 的等效代码:
import tls from 'node:tls'
const socket = new net.Socket(HOST, PORT);
socket.once('connect', () => {
const options = { socket };
const secureSocket = tls.connect(options, () => {
// The socket can only be written to once the
// connection is established.
// Polymorphic API, uses Node.js streams
secureSocket.write('hello');
}
})
在库中使用 connect() 的 Node.js 实现
为了让开源库维护者更容易采用 connect() API,目前在 Node.js 中也发布了 connect() 的实现,这样我们可以让库在不同的 JavaScript 运行时工作,而无需维护任何特定于运行时的代码。
npm install --save @arrowood.dev/socket
import { connect } from "@arrowood.dev/socket"
目前 Wintercg/proposal-sockets-api 将作为规范草案进行发布,再经过一段时间的反馈,这个 API 将在不久的将来成为 Node.js 的内置 API 。