首页
/ 深入解析 Engine.IO Client:Socket.IO 底层的跨平台双向通信客户端怎么用

深入解析 Engine.IO Client:Socket.IO 底层的跨平台双向通信客户端怎么用

2026-09-04 09:45:13作者:晏闻田Solitary

engine.io-client 是 Socket.IO 生态中的传输层客户端,负责在浏览器与 Node.js 环境之间建立基于 HTTP 长轮询、WebSocket、WebTransport 的双向通信链路。它既是 Socket.IO 客户端 的通信底座,也可作为独立库直接连接 Engine.IO 服务器。本文基于仓库中 engine.io-client 的 README 展开,结合 lib/socket.tslib/transport.ts 等源码实现,完整覆盖它的各种接入方式、全部连接选项、事件与二进制数据机制,读完后你可以独立在浏览器、Node.js 或服务端 Worker 环境中接入 Engine.IO 协议,并理解其传输升级、心跳保活等底层原理。

一、定位:它在 Socket.IO 体系中承担什么角色

engine.io-clientEngine.IO 的客户端实现,提供“基于传输层(transport-based)的跨浏览器、跨设备双向通信能力”,向上支撑整个 Socket.IO 协议栈。其通信协议规范可在仓库的 Engine.IO 协议 v4 文档 中查阅,对应的测试套件位于 docs/engine.io-protocol/v4-test-suite

入口文件 的导出结构可以看出它的能力边界:

  • Socket:核心客户端类,同时导出 SocketOptionsSocketWithoutUpgradeSocketWithUpgrade 三个类型化变体,支持按需引入以便 tree-shaking;
  • Transport / TransportError:抽象传输层基类与错误类型;
  • 各传输实现:FetchXHR(浏览器/Node 两个版本)、WS(浏览器/Node 两个版本)、WebTransport,均可单独导出;
  • installTimerFunctionsparsenextTick 等工具函数。

版本与依赖信息见 package.json:当前版本 6.6.6,运行时依赖包括 ws(Node 端 WebSocket 实现)、xmlhttprequest-ssl(Node 端 XHR 兼容层)、engine.io-parser(报文编解码)与 @socket.io/component-emitter(事件发射器,即仓库内的 socket.io-component-emitter)。构建产物同时提供 CJS(build/cjs)与 ESM(build/esm)两种模块形态,独立构建文件位于 dist/ 目录(engine.io.jsengine.io.min.jsengine.io.esm.min.js),这正是 README 中“Standalone”用法所引用的 engine.io.js

二、接入方式:从 Standalone 到 Node.js

2.1 Standalone(独立脚本标签)

仓库构建出的 engine.io.js 会以全局变量 eio(即 Socket 构造函数)暴露。浏览器中最直接的用法:

<script src="/path/to/engine.io.js"></script>
<script>
  // eio = Socket
  const socket = eio('ws://localhost');
  socket.on('open', () => {
    socket.on('message', (data) => {});
    socket.on('close', () => {});
  });
</script>

这个全局函数对应源码中的 browser-entrypoint.ts,实现就一行:export default (uri, opts) => new Socket(uri, opts),即把 URI 与选项透传给 Socket 构造函数。

2.2 通过打包工具(browserify / 现代 bundler)

Engine.IO 是一个 CommonJS 模块,可以通过 require 引入并用打包工具(如 browserify)构建:

  1. 安装客户端包:

    npm install engine.io-client
    
  2. 编写应用代码:

    const { Socket } = require('engine.io-client');
    const socket = new Socket('ws://localhost');
    socket.on('open', () => {
      socket.on('message', (data) => {});
      socket.on('close', () => {});
    });
    
  3. 构建应用 bundle:

    browserify app.js > bundle.js
    
  4. 在页面中引入:

    <script src="/path/to/bundle.js"></script>
    

需要注意的是,package.jsonexports 字段同时声明了 importrequire 入口,因此使用 Rollup、Webpack、Vite 等现代打包器时也可以直接写 ESM 导入:import { Socket } from "engine.io-client"。如果只需要特定传输(例如只用 WebSocket),可以只导入 SocketWithUpgradeWebSocket,源码注释中给出了这样的示例(见 socket.ts L972-L981),这正是为了配合 tree-shaking 而设计的类拆分。

2.3 Node.js 中的基本用法

engine.io-client 加入 package.json 后:

const { Socket } = require('engine.io-client');
const socket = new Socket('ws://localhost');
socket.on('open', () => {
  socket.on('message', (data) => {});
  socket.on('close', () => {});
});

Node 环境下与浏览器的差异由 package.jsonbrowser 字段和按环境区分的模块切换解决:Node 端轮询传输使用 polling-xhr.node.ts、WebSocket 使用 websocket.node.ts(基于 ws 库),并可通过 USE_BUILTIN_WS=1 测试脚本切换为内置 WebSocket 实现(test:node-builtin-ws),也可通过 USE_FETCH=1 切换 Fetch 轮询实现。

2.4 使用 TLS 客户端证书(Node.js)

Node 环境支持在选项里手动指定证书信息:

const opts = {
  key: fs.readFileSync('test/fixtures/client.key'),
  cert: fs.readFileSync('test/fixtures/client.crt'),
  ca: fs.readFileSync('test/fixtures/ca.crt')
};

const { Socket } = require('engine.io-client');
const socket = new Socket('ws://localhost', opts);
socket.on('open', () => {
  socket.on('message', (data) => {});
  socket.on('close', () => {});
});

对应的选项还包括 pfx(含证书的私钥与 CA)、passphrase(私钥口令)、ciphers(加密套件列表)与 rejectUnauthorized(是否校验服务器证书)。这些选项均可在 socket.ts 的 SocketOptions 接口 中看到完整类型定义,注释明确标注了“Can be used in Node.js client environment to manually specify certificate information”,即它们只对 Node 客户端有意义。

2.5 额外请求头 extraHeaders 及其浏览器限制

Node 端可以为每次请求(xhr-polling 与 websockets)附加自定义请求头,用于握手鉴权或配合特殊代理:

const opts = {
  extraHeaders: {
    'X-Custom-Header-For-My-Project': 'my-secret-access-token',
    'Cookie': 'user_session=NI2JlCKF90aE0sJZD9ZzujtdsUqNYSBYxzlTsvdSUe35ZzdtVRGqYFr0kdGxbfc5gUOkR9RGp20GVKza; path=/; expires=Tue, 07-Apr-2015 18:18:08 GMT; secure; HttpOnly'
  }
};

const { Socket } = require('engine.io-client');
const socket = new Socket('ws://localhost', opts);
socket.on('open', () => {
  socket.on('message', (data) => {});
  socket.on('close', () => {});
});

但浏览器中的 WebSocket 对象不支持附加请求头,此时需要改用 transportOptionsextraHeaders 挂到 polling 传输上。README 给出了三种组合的对照(原文示例):

// WILL NOT WORK in the browser
const socket = new Socket('http://localhost', {
  extraHeaders: {
    'X-Custom-Header-For-My-Project': 'will not be sent'
  }
});
// WILL NOT WORK
const socket = new Socket('http://localhost', {
  transports: ['websocket'], // polling is disabled
  transportOptions: {
    polling: {
      extraHeaders: {
        'X-Custom-Header-For-My-Project': 'will not be sent'
      }
    }
  }
});
// WILL WORK
const socket = new Socket('http://localhost', {
  transports: ['polling', 'websocket'],
  transportOptions: {
    polling: {
      extraHeaders: {
        'X-Custom-Header-For-My-Project': 'will be used'
      }
    }
  }
});

这组示例背后的机制在 socket.ts 的 createTransport 方法 中得到印证:创建传输时,选项按“通用选项 → 公共查询参数 → this.opts.transportOptions[name]”的顺序合并,即同名传输级别的选项会覆盖全局选项。因此第三个示例能生效,前提是 transports 数组里保留了 polling 且客户端真的会以 polling 完成连接(若被 websocket 先行升级,则升级请求本身仍不带这些头——README 也明确提醒了这一点)。

三、完整连接选项速查

以下选项表完整继承自 README 的 API 章节,默认值部分以 socket.ts 构造函数中的默认值合并逻辑 为准交叉核对:

选项 类型 默认值 说明
agent http.Agent false(仅 Node) 使用的 http.Agent
upgrade Boolean true 是否尝试从长轮询升级到更优传输
forceBase64 Boolean false 即使 XHR2 responseType 可用(polling)或标准支持二进制(WebSocket),也强制 base64 编码
withCredentials Boolean false 跨域 XHR 轮询请求是否携带凭证(cookies、授权头、TLS 客户端证书等)
timestampRequests Boolean false 是否在每个传输请求上附加时间戳;注意 IE/Android 上始终附加时间戳
timestampParam String 't' 时间戳参数名
path String '/engine.io' 连接路径;源码中会去除尾部斜杠,并按 addTrailingSlash(默认 true)决定是否补回 /
transports Array ['polling', 'websocket', 'webtransport'] 依次尝试的传输列表;Engine 总是优先直接尝试第一个,前提是特性探测通过
transportOptions Object {} 以传输名为索引的选项,覆盖该传输的通用选项
rememberUpgrade Boolean false 若上一次 WebSocket 连接成功过,则本次连接跳过常规升级流程、直接尝试 WebSocket;传输错误后的重连仍走常规升级流程。官方建议在 SSL/TLS 环境或确定网络不拦截 WebSocket 时开启
pfx / key / passphrase / cert / ca / ciphers / rejectUnauthorized SocketOptions rejectUnauthorized: true Node 环境专用的 TLS 证书配置
perMessageDeflate Object | Boolean { threshold: 1024 } WebSocket permessage-deflate 扩展参数(语义同 ws 模块),设为 false 可关闭;threshold 表示仅当数据字节数超过该值(默认 1024)才压缩,浏览器端忽略此项
extraHeaders Object 每次请求附加的自定义头(xhr-polling 与 websockets),仅限 Node.js 环境
localAddress String 使用的本地 IP 地址
autoUnref Boolean false 创建时是否对底层定时器/套接字调用 unref(),使其成为唯一活动句柄时允许进程退出(仅 Node.js)
useNativeTimers Boolean false 是否始终使用原生 setTimeout 等函数,使得在安装了 mock 时钟(如 @sinonjs/fake-timers)时客户端仍能正常重连
closeOnBeforeunload Boolean false 浏览器 beforeunload 事件触发时是否静默关闭连接(WebSocket-only 选项)
protocols Array [] WebSocket 子协议列表(WebSocket-only 选项)
requestTimeout Number 0 xhr-polling 请求超时毫秒数(Polling-only 选项)

另外,源码中还存在一个 README 未列出的选项 tryAllTransportssocket.ts L115-L126):默认 false 时,若首选 HTTP 长轮询连接失败会直接中止;设为 true 时客户端会依次尝试 polling → WebSocket → WebTransport。这一行为由 _onError 中的回退逻辑 实现:错误发生且处于 opening 状态时,this.transports.shift() 后重新调用 _open()

URI 解析与选项优先级也有明确规则:构造函数接受 uri 或“仅选项对象”两种形式(见 socket.ts L1166-L1186),传入 URI 字符串时会解析出 hostnamesecurehttps/wss 协议判定)、portquery,并作为默认值与显式选项合并——显式传入的选项优先。

四、二进制数据的发送与接收

README 给出的浏览器端二进制收发示例:

<script src="/path/to/engine.io.js"></script>
<script>
  const socket = eio('ws://localhost/');
  socket.binaryType = 'blob';
  socket.on('open', () => {
    socket.send(new Int8Array(5));
    socket.on('message', (blob) => {});
    socket.on('close', () => {});
  });
</script>

README 的 Features 章节对二进制行为的完整描述如下:

  • 浏览器端 message 事件收到 ArrayBufferBlob(由 binaryType 决定);Node 端为 BufferArrayBufferbinaryType 可设为 bufferarraybuffer,Blob 仅在浏览器且受支持时使用);
  • 当 XHR2 或 WebSockets 可用时二进制数据直接透传;否则二进制会被编码为 base64 字符串,待二进制类型受支持时再解码;
  • 对不支持 ArrayBuffer 的浏览器,message 事件收到 { base64: true, data: dataAsBase64String } 对象。

源码层面,这些规则对应两处实现:

  1. Transport 构造函数this.supportsBinary = !opts.forceBase64——即 forceBase64 直接决定该传输是否启用二进制通路;
  2. binaryType 属性默认值来自运行环境:Node 端为 "nodebuffer"(见 globals.node.tsdefaultBinaryType),浏览器端则根据平台特性选择 arraybufferblob

解码发生在 Transport.onData:调用 engine.io-parserdecodePacket(data, this.socket.binaryType),把原始负载还原为带类型的 Packet 后再交给 Socket 分发——这正是 README 中“Received as ... in Node / in browser”差异的来源。

五、Socket 类:属性、事件与方法

5.1 属性

  • protocol(Number):协议修订号,直接来自 engine.io-parserprotocol 常量(socket.ts L365);
  • binaryType(String):浏览器可设 'arraybuffer''blob',Node 可设 bufferarraybuffer;Blob 仅在浏览器且受支持时可用。

5.2 事件

  • open:连接成功建立时触发;
  • message:收到服务器数据时触发。参数为 String | ArrayBuffer:UTF-8 编码数据或含二进制数据的 ArrayBuffer;
  • close:断连时触发。遵循 WebSocket API 规范,即使 open 从未发生(例如连接错误或调用了 close()),该事件也可能触发。从源码 _onClose 可见,close 事件携带 reason 与可选的 description(如 "transport close" + "network connection lost"),并会在发射后清空 writeBuffer
  • error:发生错误时触发;
  • flush:一次缓冲区刷新完成时触发;
  • drain:传输层 drain 事件之后、writeBuffer 为空时触发;
  • upgradeError:向某传输升级过程中出错时触发;
  • upgrade:升级成功、新传输被设置后触发;
  • ping:收到 ping 包时触发;
  • pong:pong 包完成刷新写出(即真正写入网络)时触发。

ping/pong 的触发细节可从 _onPacket 看到:收到 ping 包后客户端立即回发 pong 包,并同时发射 pingpong 两个事件,然后重置心跳超时计时器。

5.3 方法

constructor:初始化客户端。

  • 参数:uri(String)、opts(Object,可选);
  • 选项见上文第三节完整表格。

send / write:向服务器发送消息。

  • 参数:数据(String | ArrayBuffer | ArrayBufferView | Blob)、可选选项对象、可选的 drain 回调(实际绑定在 flush 事件上,见 源码 L820-L836);
  • 选项:compress(Boolean),是否压缩发送数据;此选项在浏览器端被忽略并强制为 true,Node 端默认 truefalse !== options.compress)。

close:断开客户端。源码 close() 展示了它的严谨性:若 writeBuffer 非空则等待 drain 再关闭;若正处于升级过程中,则等待 upgradeupgradeError 完成后再关闭——避免在“暂停传输中”丢失报文。

六、Transport 抽象类与传输升级机制

6.1 Transport 类

Transport 是私有(内部)抽象类,继承自 EventEmitter,定义在 transport.ts。README 列出的其对外事件:

  • poll:polling 类传输发起新请求时触发;
  • pollComplete:polling 类传输完成一次请求时触发;
  • drain:polling 类传输缓冲区排空时触发。

Transport 的保留事件接口 看,内部事件还包括 openerrorpacketclose(携带 CloseDetailsdescription + 可选 context),以及状态字段 writable(只有 open 状态才可写,见 send 方法)和 pause(onPause)(升级期间暂停传输以防丢包)。

6.2 默认传输列表

Socket 构造函数在 socket.ts L1173-L1180 中处理 transports:缺省或传字符串数组时,默认映射为 ['polling', 'websocket', 'webtransport'],再按 transports/index.ts 注册表转换为具体的传输构造器(polling → XHR、websocket → WS、webtransport → WT)。

6.3 连接、握手与心跳

源码给出了完整的生命周期:

  1. 打开_open() 选择首个传输(若 rememberUpgrade && priorWebsocketSuccess 且列表含 websocket,则直接选 websocket),创建传输并 open()socket.ts L529-L549);
  2. 握手:服务端返回 open 类型包,onHandshake 解析出 sidupgradespingIntervalpingTimeoutmaxPayloadHandshakeData 接口 L269-L275),随后触发 open 事件并启动 ping 超时计时器(延时为 pingInterval + pingTimeout);
  3. 心跳检测_resetPingTimeout 在超过 pingInterval + pingTimeout 未收到任何报文时以 "ping timeout" 关闭连接;此外源码还跟踪了 _pingTimeoutTime 时间戳,用于处理浏览器锁屏/休眠导致计时器被节流(throttle)的场景(见 _hasPingExpired)。

6.4 升级(probe)流程

SocketWithUpgrade 是带升级机制的类:握手拿到服务端的 upgrades 列表后,onOpen 会逐个调用 _probe(name)socket.ts L986-L998)。_probe 的完整流程(L1006-L1124):

  1. 创建候选传输,若目标列表中含 webtransport 且当前探测的不是 webtransport,则延迟 200ms 再打开候选传输,以“favor WebTransport”(给 HTTP/3 连接留出建立时间);
  2. 候选传输打开后发送 { type: "ping", data: "probe" } 探测包,等待服务器回 pong + probe
  3. 探测通过后进入 upgrading 状态、暂停(pause)当前传输,然后 setTransport(新传输) 并发送 { type: "upgrade" } 包,最后发射 upgrade 事件并 flush() 残留缓冲;
  4. 任何环节失败(probe 包不匹配、传输报错、socket 在探测中被关闭)都会冻结候选传输并发射 upgradeError

这与 README 中 upgradeupgrade/upgradeError 事件、以及 rememberUpgrade 选项的说明一一对应:priorWebsocketSuccess 是静态属性,在每次 open 时按“当前传输是否为 websocket”更新(L583-L584),出错时置 false,正是 rememberUpgrade 的记忆来源。

七、平台适配细节:Cookie、离线与卸载

除了 README 明示的 API,源码还体现了几处对真实运行环境的适配:

  • CookieJar(Node)withCredentials: true 时在 Node 端创建一个 CookieJar(socket.ts L479-L481),实现位于 globals.node.ts:它会解析服务器 Set-Cookie(支持 Expires/Max-Age),并在后续请求中以 cookie 头回带,模拟浏览器的 cookie 行为;
  • offline 事件:在支持 addEventListener 的环境(含 ServiceWorker)中,模块顶层注册了单一 offline 监听器,把系统级断网事件转发给所有存活的 socket 实例,触发带 "network connection lost" 描述原因的 close(socket.ts L23-L39L468-L477);
  • beforeunloadcloseOnBeforeunload: true 时监听 beforeunload,静默关闭传输;源码注释说明这是为了抹平 Firefox 与 Chrome 在页面关闭时行为不一致的问题,避免上层 Socket.IO 在页面关闭/刷新时误发 disconnect 事件。

八、特性总结与测试、开发

8.1 特性

README 的 Features 一节总结了客户端的核心特性,全部可由源码印证:

  • 轻量(依赖仅 5 个,见 package.json);
  • 浏览器与 Node.js 无缝运行(browser 字段切换 .node 模块);
  • 传输独立于 Engine——易于调试与单元测试(每个传输都是可单独导出的类,测试位于 test/);
  • 可运行在 HTML5 WebWorker 内部(offline 监听器注释明确提到 ServiceWorker 场景);
  • 可发送/接收二进制数据,具体规则见本文第四节。

8.2 测试

engine.io-clientengine.io 的测试套件互为验证:跑 engine.io 的测试即验证了客户端,反之亦然。README 给出的本地浏览器测试命令:

./node_modules/.bin/zuul --local 8080 -- test/index.js

此外 engine.io-client 自带独立测试套件(make test 会同时跑 Node 与浏览器测试,浏览器端需要配置 saucelabs 的 zuul 环境)。与 package.jsonscripts 对应:test:node 用 mocha 跑 test/index.jstest/webtransport.mjstest:browser 直接执行 zuul test/index.js。测试覆盖了连接、二进制回退(binary-fallback.js)、XMLHttpRequest、WebTransport(webtransport.mjs)等场景。

8.3 本地开发

克隆仓库后安装依赖即可开发(本仓库为 monorepo,客户端位于 packages/engine.io-client):

cd packages/engine.io-client
npm install

提交补丁前请先按上文“测试”一节运行测试;npm run compile(tsc + postcompile.sh)负责产出 CJS/ESM 双构建,npm run build(rollup)产出 dist/ 下的独立构建文件。

九、参考路径索引

主题 路径
本文核心依据(README) packages/engine.io-client/README.md
客户端主类与选项实现 packages/engine.io-client/lib/socket.ts
抽象传输层 packages/engine.io-client/lib/transport.ts
各传输实现 packages/engine.io-client/lib/transports/
浏览器/Node 全局环境差异 packages/engine.io-client/lib/globals.node.tsglobals.ts
协议规范 docs/engine.io-protocol/v4-current.md
服务端实现 packages/engine.io/README.md
上层 Socket.IO 客户端 packages/socket.io-client/README.md

engine.io-client 的价值在于把“传输差异”收敛到了 Transport 抽象之下:上层无论用长轮询还是 WebSocket,拿到的都是统一的 open/message/close 事件流与 send 写接口;而升级探测、心跳超时、cookie 回带、断网感知等工程细节则保证了它在真实网络环境下的健壮性。理解这套结构后,你可以把它当作 Socket.IO 之下的独立通信层来使用,也可以按需裁剪传输实现(SocketWithoutUpgrade + 单一传输导出)来构建更轻的客户端。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
982
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384