• 聊一个即将推出的跨JavaScript运行时的Socket API
  • 发布于 2个月前
  • 150 热度
    0 评论
  • 王晶
  • 0 粉丝 41 篇博客
  •   
今天和大家来一起聊一个即将推出的跨 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 。
用户评论