首页
/ Electron 主进程 WebSocket 全解析:net.WebSocket 与 Chromium 网络栈的接入实战

Electron 主进程 WebSocket 全解析:net.WebSocket 与 Chromium 网络栈的接入实战

2026-09-06 18:20:34作者:仰钰奇

导读:本文围绕 Electron 的 net.WebSocket API(docs/api/web-socket.md)展开,它让主进程可以基于 Chromium 原生网络栈创建 WebSocket 连接,从而复用系统/会话级代理、平台信任库的 TLS 校验、会话 Cookie 与自定义 CA 等能力。读完本文,你将掌握 net.WebSocket 的构造与事件模型、与标准 WHATWG WebSocket 的差异与扩展、底层实现原理,并可直接在主进程中写出可用的 WebSocket 客户端代码。

一、为什么主进程需要 net.WebSocket

在 Electron 中,net 模块(docs/api/net.md)本身就是主进程网络能力的入口,其中的 net.WebSocket 是专门为满足如下诉求而设计的:

  • 复用系统或会话的代理配置,包括 PAC、WPAD 自动代理发现;
  • 平台信任库与会话的证书校验策略验证 TLS 证书(而不是只信 Node.js 自带的 CA);
  • 遵循会话级配置:自定义 CA、主机解析规则(host resolver rules)等;
  • 在开启 useSessionCookies 后,随握手发送会话 Cookie,并存储握手响应中收到的 Cookie。

简言之,当你想让主进程的 WebSocket 连接与页面渲染进程里的 fetch / XMLHttpRequest 走同一套网络基础设施(同一个 Session、同一份代理与证书配置)时,net.WebSocket 就是官方提供的答案。作为对照,Node.js 侧的全局 WebSocket 直接使用 Node 的底层实现,不感知 Electron 会话与 Chromium 网络栈的配置。

从源码可以确认,net 模块在 lib/browser/api/net.ts 中直接 export { WebSocket },而其实现体位于 lib/browser/api/net-websocket.ts(下文会做源码级剖析)。

二、使用前提:app 就绪之后

net.WebSocket 只能在应用发出 ready 事件之后使用。这与 net.requestnet.fetch 的限制一致。若在就绪前构造,会直接抛出异常。这一约束在 lib/browser/api/net-websocket.ts 中有明确实现:

// lib/browser/api/net-websocket.ts
if (!app.isReady()) {
  throw new Error('net.WebSocket can only be used after app is ready');
}

因此在主进程入口,最稳妥的写法是先 app.whenReady() 再创建连接:

const { app, net } = require('electron')

app.whenReady().then(() => {
  const ws = new net.WebSocket('wss://echo.websocket.events')
  ws.onopen = () => ws.send('hello')
  ws.onmessage = (event) => {
    console.log('received', event.data)
    ws.close()
  }
})

三、构造参数详解

3.1 new WebSocket(url[, protocols])

url string

连接目标 URL。协议必须是 ws:wss:;与浏览器一致,http:https: 也会被接受并自动改写为对应的 ws:wss:。除此之外,任何 URL 带有片段标识符(fragment #...)也会被判定为非法。这些校验逻辑在 lib/browser/api/net-websocket.ts 中都有对应实现,并且在 spec/api-net-websocket-spec.ts 中有专门用例覆盖(如 “rewrites http(s) to ws(s)”“throws on URLs with a fragment”)。

protocols string | string[] | WebSocketOptions(可选)

传入一个或多个 WebSocket 子协议(subprotocol),或是 Electron 专属的 options 对象。其中:

  • 两个参数的标准形式 new net.WebSocket(url, protocols) 与 WHATWG 构造函数完全兼容(WHATWG WebSocket 规范);
  • 把第二个参数传成 options 对象属于 Electron 扩展能力,此时内部的 protocols 字段继续承载子协议声明。

从实现看,lib/browser/api/net-websocket.ts 会区分“字符串 / 字符串数组 / options 对象”三种形态,并将子协议逐一按 RFC 7230 的 token 规则校验(正则 TOKEN_RE),重复项同样会被拒绝并抛出 SyntaxError

3.2 WebSocketOptions:Electron 扩展的完整字段

options 对象的完整字段定义见 docs/api/structures/web-socket-options.md,汇总如下:

字段 类型 默认值 说明
protocols string | string[](可选) 期望的 WebSocket 子协议列表
headers Record<string, string>(可选) 随 HTTP Upgrade 握手额外发送的请求头
origin string(可选) 自动推导 握手 Origin 头的值;默认取 WebSocket URL 的 http(s) 等价源,例如连接 wss://api.example.com 时发送 Origin: https://api.example.com,使服务端与 SameSite Cookie 规则将其视为同源请求
useSessionCookies boolean(可选) false 是否在握手中发送会话 Cookie,并保存响应中携带的 Cookie
session Session(可选) 默认会话 连接所关联的 Session(见 docs/api/session.md
partition string(可选) '' 连接所关联的 partition 名称;空字符串对应默认会话。若同时提供 sessionpartition 会被忽略

这些 Electron 专属参数的目的,就是把“谁的网络栈”显式化:同一个进程里可以让不同连接归属到不同分区/会话,从而获得隔离的 Cookie、缓存与网络配置。

一个实用的带自定义请求头的连接示例(对应 spec 中 “sends extra headers with the handshake” 的行为):

const { app, net } = require('electron')

app.whenReady().then(() => {
  const ws = new net.WebSocket('wss://api.example.com/live', {
    protocols: ['chat', 'superchat'],
    headers: {
      Authorization: 'Bearer <token>',
      'User-Agent': 'my-electron-app/1.0'
    },
    origin: 'https://my-app.example.com',
    useSessionCookies: true
  })

  ws.onopen = () => console.log('connected with protocol:', ws.protocol)
  ws.onmessage = (e) => console.log('msg:', e.data)
})

四、就绪状态:静态常量与实例状态

net.WebSocket 复刻了浏览器 WebSocket 的四个就绪状态。它们既可以作为静态属性(WebSocket.CONNECTING)访问,也可通过实例属性 ws.readyState 观察当前连接处于哪个阶段:

静态常量 取值 含义
WebSocket.CONNECTING 0 正在执行打开握手
WebSocket.OPEN 1 连接已建立,可以收发数据
WebSocket.CLOSING 2 正在执行关闭握手
WebSocket.CLOSED 3 连接已关闭

源码 lib/browser/api/net-websocket.ts 中定义了 CONNECTING = 0CLOSED = 3 常量,并通过 static readonly 与实例 readonly 双重暴露,与 WHATWG 保持一致。

关键实例属性

  • ws.url(只读,string):连接解析后的最终 URL。构造时 http(s) 已被改写为 ws(s),这里看到的是改写后的真实地址。
  • ws.readyState(只读,Integer):上表四个取值之一。
  • ws.bufferedAmount(只读,Integer):已经通过 send() 排队、但尚未交给网络层的应用数据字节数。它是“发出去但还没真走”的积压水位,可用于流量控制判断。
  • ws.protocol(只读,string):服务端最终选定的子协议。连接未打开或服务端未选择任何子协议时为空字符串 ''
  • ws.extensions(只读,string):服务端协商出的扩展,例如 permessage-deflate
  • ws.binaryType(可读写,string):决定二进制消息在 message 事件中如何呈现。

binaryTypenodebuffer 是 Electron 的主进程友好扩展

ws.binaryType 支持三个取值:

取值 行为
nodebuffer(默认) 二进制消息以 Node.js Buffer 呈现,是主进程中最方便的形态,属于 Electron 扩展
arraybuffer 与渲染进程 WebSocket 行为一致,二进制以 ArrayBuffer 呈现
blob 与渲染进程 WebSocket 行为一致,二进制以 Blob 呈现

也就是说:只想要“行为与浏览器完全一致”,请把 binaryType 设为 arraybufferblob;想要主进程效率最高、最自然的二进制形态,则保留默认的 nodebuffer

从实现看(lib/browser/api/net-websocket.ts),收到二进制帧后按 binaryType 分流:arraybuffer 直接把 Buffer 底层复制出的 ArrayBuffer 交出去,blobnew Blob([data]) 包装,nodebuffer 直接把 Buffer 原样作为 event.data。相关行为在 spec/api-net-websocket-spec.ts 中分别有 “echoes binary messages as Buffer by default”“respects binaryType = arraybuffer”“respects binaryType = blob” 等用例验证。

五、发送与关闭

5.1 ws.send(data)

data 支持的类型包括:string | ArrayBufferLike | ArrayBufferView | Blob

  • 字符串按文本帧发送;
  • 其余类型按二进制帧发送;
  • readyState 仍为 CONNECTING,调用会抛出 InvalidStateError 类型的 DOMException(对应 spec 中 “throws InvalidStateError when sending while CONNECTING” 用例);
  • 既非字符串也非上述二进制类型的值,会按 WebIDL 规则被强转成 USVString 再以文本帧发出。

实现层还特别处理了 Blob 的异步读取:Blob 内容必须异步读取,为避免后续的字符串/ArrayBuffer 发送在网络上“插队”,lib/browser/api/net-websocket.ts 内部维护了一条发送队列(kSendQueue),保证 send() 调用顺序即网络发送顺序;队列中尚未交给原生层的字节会计入 bufferedAmount(spec 中 “preserves send order across Blob and non-Blob sends” 与 “counts bytes queued behind an async Blob send in bufferedAmount” 即为此设计)。

5.2 ws.close([code][, reason])

  • codeInteger,可选):关闭码,必须为 1000,或落在 30004999 区间内;
  • reasonstring,可选):人类可读的关闭原因,UTF-8 编码后不得超过 123 字节
  • 若在 CONNECTING 阶段调用 close(),会直接中止尚未完成的握手(对应 “can close while connecting” 用例);
  • 若连接已处于 CLOSING/CLOSED,再次调用 close() 会被安全忽略。

参数合法性检查同样发生在 lib/browser/api/net-websocket.ts 中:关闭码先做 WebIDL 的 [Clamp] 收敛(0..65535),随后校验 10003000..4999,否则抛 InvalidAccessErrorreason 超过 123 字节则抛 SyntaxError。spec 中的 “validates close() arguments” 用例覆盖了这些边界。

ws.close(1000, 'client shutdown')          // 正常关闭
ws.close(4000)                             // 自定义应用关闭码(3000–4999)
ws.close()                                 // 无参数默认关闭

六、事件模型:EventTarget 而非 EventEmitter

net.WebSocket 继承自 Node 的 EventTarget,而不是 EventEmitter。因此:

  • 监听事件应使用 addEventListener('open', ...) 或等价的 on* 事件处理属性(onopenonmessageonerroronclose);
  • ws.onmessage = handler 写法与 addEventListener 等价,二者可以混用,且重新赋值 on* 会先移除旧的处理器(spec 中 “replaces a previously-assigned handler” 用例印证)。

类定义中的 CloseEvent 是 Node 环境下的一个小型规范兼容 polyfill,用以保证 event.codeevent.reasonevent.wasClean 与渲染进程中的行为完全一致。

四类事件的语义

事件 触发时机 事件对象要点
open 连接建立、打开握手完成 此后 ws.protocolws.extensions 才反映服务端协商结果
message 收到消息 event.data 对文本帧是 string;二进制帧按 binaryTypeBuffer / ArrayBuffer / Blob
error 连接失败 紧随其后必然还会触发一次 close;触发 errorreadyState 已为 CLOSED
close 连接以任何方式关闭 CloseEvent 携带 codereasonwasClean

值得特别说明的失败可诊断性设计:当连接失败时(例如握手被服务端拒绝、网络不可达),close 事件的 code1006,且 Electron 会把 reason 填充为底层网络错误的简短描述——这样即使没有挂着调试器,开发者也能在日志里直接看到失败原因。spec 中 “fires error then close when the connection fails” 与 “readyState is CLOSED inside the error handler” 用例验证了“先 errorclose”以及错误处理回调内部状态机的一致性。

七、完整实战示例:带会话与认证头的连接

综合以上 API,给出一个可直接运行的完整主进程示例:

const { app, net } = require('electron')

app.whenReady().then(async () => {
  const ws = new net.WebSocket('wss://push.example.com/socket', {
    protocols: 'json-v2',
    headers: { Authorization: `Bearer ${await getToken()}` },
    useSessionCookies: true
  })

  // on* 事件处理属性写法
  ws.onopen = () => {
    console.log('[open] url =', ws.url, '| protocol =', ws.protocol, '| ext =', ws.extensions)
    ws.send(JSON.stringify({ type: 'subscribe', channel: 'news' }))
  }

  // 也支持 addEventListener
  ws.addEventListener('message', (event) => {
    console.log('[message]', typeof event.data, event.data)

    // 发送缓冲水位监控示例
    if (ws.bufferedAmount > 1 * 1024 * 1024) {
      console.warn('backpressure: buffered bytes =', ws.bufferedAmount)
    }
  })

  ws.onerror = () => console.error('[error]') // error 之后必有关闭事件
  ws.onclose = (event) => {
    // 失败场景下 code === 1006,reason 是底层网络错误描述
    console.log(`[close] code=${event.code} wasClean=${event.wasClean} reason=${event.reason}`)
    if (event.code !== 1000) scheduleReconnect()
  }

  // 30 秒后正常关闭
  setTimeout(() => ws.close(1000, 'bye'), 30_000)
})

八、源码级原理:从 JS 到 Chromium 网络栈的调用链

要理解 net.WebSocket 与普通 WebSocket 的本质差异,可以沿代码路径走一遍它的落地实现:

  1. JS 层 API 封装:类实现在 lib/browser/api/net-websocket.ts。它负责 WHATWG 兼容性细节:URL 协议改写与片段校验、子协议 token/去重校验、readyState 状态机、MessageEvent/CloseEvent 派发、binaryType 分流、发送队列与 bufferedAmount 记账。
  2. 模块导出lib/browser/api/net.tsexport { WebSocket },使其作为 net.WebSocket 暴露。
  3. 原生桥接:JS 层通过 process._linkedBinding('electron_common_net') 调用 createWebSocket(见 shell/common/api/electron_api_net.ccdict.SetMethod("createWebSocket", &WebSocketWrapper::Create)),将 urlprotocolsheadersoriginuseSessionCookiessessionpartition 一并传入 C++ 层。
  4. C++ 包装器:连接本体由 electron::api::WebSocketWrapper 承载,声明于 shell/browser/api/electron_api_web_socket.h。它持有 ElectronBrowserContext,据此把连接归入指定会话,并驱动 Chromium 的网络栈完成代理协商、TLS 校验与握手。

这解释了第一节中的能力来源:因为连接最终创建于 Chromium 网络栈、绑定到 Electron 的 BrowserContext/会话上下文,所以 PAC/WPAD 代理、平台信任库证书校验、partition 分区隔离、useSessionCookies 等能力天然可用。

九、常见问题与注意事项小结

  • “app 未 ready 就报错”:所有 net.WebSocket 构造必须放在 app.whenReady() 之后;
  • “发了 http:/https: 链接”:不必担心,构造器会自动改写为 ws:/wss:;但带 #fragment 的 URL 是非法的;
  • “协议没生效”ws.protocolopen 事件之前为空,需在连接打开后再读取服务端最终协商结果;
  • “二进制数据形态不对”:默认 nodebuffer(主进程最友好);需要与渲染进程完全一致的行为请改设 arraybufferblob
  • send() 在握手期抛异常”:这是规范行为——CONNECTING 阶段 send() 会抛 InvalidStateError,应等 open 后再发;
  • “连接失败难排查”:关注 close 事件的 code(失败为 1006)与 reason(Electron 已填充底层网络错误描述);
  • “要不要带 Cookie”:默认不发会话 Cookie,需要时才显式开启 useSessionCookies: true
  • “事件 API 差异”:它是 EventTarget,请用 addEventListeneron* 属性,不要依赖 EventEmitteron()/once() 方法。

以上各条目对应的行为与边界,均在仓库 spec/api-net-websocket-spec.ts 中拥有成体系的 spec 用例保障,可作为继续深挖各细节的第一手资料。

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