 处理器构建原始 TCP 服务)
workerd TCP 入口实战基于 connect() 处理器构建原始 TCP 服务【免费下载链接】workerdThe JavaScript / Wasm runtime that powers Cloudflare Workers项目地址: https://gitcode.com/GitHub_Trending/wo/workerdworkerd即 Cloudflare Workers 的开源运行时仓库根目录见 README.md不仅支持 HTTP 入口还允许开发者通过配置tcp类型的 Socket 与 Worker 暴露的connect()处理器直接接收并处理原始 TCP 连接。本文以仓库中的 tcp-ingress 示例 为骨架完整讲解其配置结构、connect()处理器的编写方式、运行与验证步骤并结合源码剖析 workerd 在收到 TCP 连接后的底层处理流程。读完本文你将能够在本地构建的 workerd 上运行一个可用的 TCP 服务并理解入站 Socket 与出站 Socket 的对应关系。示例功能概述tcp-ingress示例实现了一个最简单的 TCP 服务器当客户端连接到端口8081时Worker 的connect()处理器会把连接的输入流直接管道pipe到输出流即原样回显客户端发送的数据echo 服务。与此同时示例还在端口8080上暴露了一个普通的 HTTP 入口用于返回ok响应证明同一个 Worker 可以同时承载 HTTP 与 TCP 两类服务。入口协议地址行为httpHTTP*:8080命中fetch()处理器返回oktcp原始 TCP*:8081命中connect()处理器输入流回显运行前置条件与快速启动示例的官方使用方式见 samples/tcp-ingress/README.md要求先通过 Bazel 构建出 workerd 可执行文件再以serve子命令加载示例配置./bazel-bin/src/workerd/server/workerd serve samples/tcp-ingress/config.capnp --experimental其中./bazel-bin/src/workerd/server/workerd是 Bazel 构建后生成的 workerd 主程序serve子命令用于以配置文件方式启动本地运行时samples/tcp-ingress/config.capnp是本次要加载的配置--experimental启用实验性特性该示例依赖experimental兼容性标志。服务启动后用ncnetcat向 TCP 端口8081发送一行文本即可验证回显行为echo Hello World! | nc localhost 8081由于connect()处理器将输入流管道到输出流客户端会立即收到自己发送的Hello World!。同时也可以用curl localhost:8080验证 HTTP 入口返回ok。配置解析一个 Worker 同时监听 HTTP 与 TCP示例的完整配置位于 samples/tcp-ingress/config.capnp全文如下using Workerd import /workerd/workerd.capnp; const tcpIngressExample :Workerd.Config ( services [ (name main, worker .worker), ], sockets [ ( name http, address *:8080, http (), service main ), ( name tcp, address *:8081, tcp (), service main ) ] ); const worker :Workerd.Worker ( modules [ (name worker, esModule embed worker.js) ], compatibilityFlags [nodejs_compat_v2, experimental], compatibilityDate 2026-03-01, );services定义 Worker 服务services列表中的每一项都声明一个可被入口引用的服务。这里定义了名为main的服务其worker字段指向下方worker常量。注意service main这种写法利用了 Capn Proto 的字符串简写由于ServiceDesignator.name是第一个字段直接写字符串等价于service (name main)见 workerd.capnp 中的说明。sockets声明监听入口sockets列表声明运行时需要监听的所有入口每个元素对应Workerd.Socket结构定义见 workerd.capnp核心字段包括nameSocket 的唯一名称可用于命令行覆盖地址address监听地址格式遵循 KJ 的parseAddress()见 workerd.capnp常见写法有*:8081监听所有本地 IPv4/IPv6 接口的 8081 端口1.2.3.4:80监听指定 IPv4 地址和端口[1234:5678::abcd]:80监听指定 IPv6 地址和端口unix:/path/to/socket监听 Unix 域套接字example.com:80先做 DNS 解析再监听多地址时全部监听联合体union中的一个协议选项http ()、https含HttpOptions与TlsOptions或tcp ()service把该入口的请求转发给哪个服务。示例中httpSocket 用http ()声明为 HTTP 服务tcpSocket 用tcp ()声明为原始 TCP 服务两者都指向main服务因此同一个 Worker 实例同时处理两类协议。值得注意的细节是从 workerd.capnp 的结构定义看tcp分组中还包含tlsOptions 6 :TlsOptions字段但从 worker-entrypoint.c 的TODO(soon): Implement basic TLS support for connect handler.注释以及 global-scope.c 中 TLS support is not implemented so far 的说明来看connect()处理路径的 TLS 支持目前尚未完整实现示例中直接使用tcp ()是最稳妥的用法。命令行覆盖地址Socket 的name字段还有一个实用价值可以在启动命令中用--socket-addr nameaddr或--socket-fd namefd覆盖配置中声明的监听地址见 workerd.capnp。例如把 TCP 入口临时改到其他端口./bazel-bin/src/workerd/server/workerd serve samples/tcp-ingress/config.capnp \ --experimental --socket-addr tcp*:9001worker模块与兼容性标志worker常量通过esModule embed worker.js将 worker.js 内嵌为 ES 模块并声明了nodejs_compat_v2与experimental两个兼容性标志以及兼容日期2026-03-01。其中experimental与启动命令中的--experimental相呼应说明该示例依赖尚未稳定化的实验特性。Worker 实现导出connect()处理器示例的 Worker 代码位于 worker.jsexport default { async fetch(req) { return new Response(ok); }, async connect(socket) { // pipe the input stream to the output await socket.readable.pipeTo(socket.writable); } };要点如下fetch()处理器处理 HTTP 入口的请求这里固定返回ok。connect(socket)处理器workerd 的 TCP 入口处理器。每当有客户端在端口8081建立连接时workerd 会把该连接包装成一个Socket对象传给connect()。回显逻辑await socket.readable.pipeTo(socket.writable)使用标准 Web Streams API把连接的readable可读流来自客户端的数据直接管道到writable可写流发回客户端的数据。await会一直等到管道结束——即客户端关闭写方向半关闭或连接终止后处理器才返回。connect()处理器是 ES Modules 语法下 Worker 的默认导出对象中的一个命名方法如果 Worker 没有导出connect()workerd 在收到 TCP 连接时会记录一条警告 Received a connect event but we lack a handler. Did you remember to export a connect() function? 并抛出错误见 global-scope.c。底层原理workerd 如何派发 TCP 连接从源码看TCP 入站连接的派发路径与 HTTP 请求不同走的是ServiceWorkerGlobalScope::connect()实现见 global-scope.c核心流程可以概括为入口检查JSG_REQUIRE_NONNULL(exportedHandler, ...)要求 Worker 必须是 ES Modules 语法注释明确指出 Connect ingress is not currently supported with Service Workers syntax.即老式 Service Worker 语法无法使用connect()入口。接受连接调用response.accept(200, OK, headers)向连接方确认接受随后创建newNeuterableIoStream(connection)包装底层连接。这里用可失效neuterable流来管理生命周期connect()返回给调用方的 Promise 完成时底层connection会被销毁而 JS 侧的Socket可能通过ctx.waitUntil()存活更久因此必须用deferredNeuter在合适时机使流失效避免悬挂引用。包装 JS Socket通过setupSocket()把底层流包装成 JS 可见的Socket对象。该路径以allowHalfOpen true创建见 global-scope.c这意味着对端半关闭只关闭写方向不会立即关闭整条连接——这也正好解释了示例中客户端发送完数据后仍能收到回显的行为。调用 JS 处理器创建名为connect_handler的 trace span然后handler(js, jsSocket, env, ctx)调用用户导出的connect()函数并通过ioContext.awaitJs(...)等待其 Promise 完成见 global-scope.c。另外入站 Socket 的地址信息如对端地址在派发路径中会被填充到 Socket 的localAddress等字段供 JS 侧读取相关说明见 sockets.h。对照入站connect()与出站connect()的关系tcp-ingress展示的是入站TCP连接进入 Worker而仓库中的另一个示例 samples/tcp/gopher.js 则展示了出站TCPWorker 主动发起连接import { connect } from cloudflare:sockets; // ... const socket useProxy ? env.proxy.connect(gopherAddr) : connect(gopherAddr);出站场景从cloudflare:sockets模块导入connect()函数建立 TCP 客户端连接随后同样使用socket.writable写入数据、从socket.readable读取响应。两者共享同一套Socket对象模型和 Web Streams 读写语义——入站方向把连接的readable/writable交给connect(socket)处理器出站方向则由connect(host)返回一个可读写的 Socket。理解这一点后可以很容易地把tcp-ingress扩展为真正的 TCP 代理在connect(socket)里用cloudflare:sockets的connect()建立到后端的目标连接再把两个方向的数据互相管道。常见问题与排查建议connect()未被调用服务启动后端口无响应确认 Worker 是 ES Modules 语法且默认导出对象中定义了connect方法Workers 语法的 Worker 不支持 Connect ingress见 global-scope.c。无法回显或连接立即关闭检查allowHalfOpen语义——对端如果直接关闭连接而未等待回显管道会随之结束。示例中用echo ... | nc测试时echo写完即关闭写方向此时依赖 half-open 语义保证回显仍能送达。端口被占用使用--socket-addr tcp*:新端口覆盖监听地址无需修改配置文件。TLS 需求tcpSocket 结构虽预留了tlsOptions字段但当前connect()处理路径的 TLS 支持尚未落地请以官方后续更新为准相关 TODO 见 worker-entrypoint.c。延伸阅读tcp-ingress 配置 与 Worker 源码本文讲解的完整示例workerd.capnpSocket结构定义与address支持的全部格式global-scope.cconnect()处理器的底层派发实现samples/tcp/gopher.js出站 TCP 连接示例含代理模式samples/repl-server/README.md仓库中另一个基于 TCP/Socket 交互的实验性示例可对比不同用法。【免费下载链接】workerdThe JavaScript / Wasm runtime that powers Cloudflare Workers项目地址: https://gitcode.com/GitHub_Trending/wo/workerd创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考