Electron 主进程 WebSocket 全解析:net.WebSocket 与 Chromium 网络栈的接入实战
导读:本文围绕 Electron 的
net.WebSocketAPI(docs/api/web-socket.md)展开,它让主进程可以基于 Chromium 原生网络栈创建 WebSocket 连接,从而复用系统/会话级代理、平台信任库的 TLS 校验、会话 Cookie 与自定义 CA 等能力。读完本文,你将掌握net.WebSocket的构造与事件模型、与标准 WHATWGWebSocket的差异与扩展、底层实现原理,并可直接在主进程中写出可用的 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.request、net.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 名称;空字符串对应默认会话。若同时提供 session,partition 会被忽略 |
这些 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 = 0…CLOSED = 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事件中如何呈现。
binaryType:nodebuffer 是 Electron 的主进程友好扩展
ws.binaryType 支持三个取值:
| 取值 | 行为 |
|---|---|
nodebuffer(默认) |
二进制消息以 Node.js Buffer 呈现,是主进程中最方便的形态,属于 Electron 扩展 |
arraybuffer |
与渲染进程 WebSocket 行为一致,二进制以 ArrayBuffer 呈现 |
blob |
与渲染进程 WebSocket 行为一致,二进制以 Blob 呈现 |
也就是说:只想要“行为与浏览器完全一致”,请把 binaryType 设为 arraybuffer 或 blob;想要主进程效率最高、最自然的二进制形态,则保留默认的 nodebuffer。
从实现看(lib/browser/api/net-websocket.ts),收到二进制帧后按 binaryType 分流:arraybuffer 直接把 Buffer 底层复制出的 ArrayBuffer 交出去,blob 则 new 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])
code(Integer,可选):关闭码,必须为1000,或落在3000–4999区间内;reason(string,可选):人类可读的关闭原因,UTF-8 编码后不得超过 123 字节;- 若在
CONNECTING阶段调用close(),会直接中止尚未完成的握手(对应 “can close while connecting” 用例); - 若连接已处于
CLOSING/CLOSED,再次调用close()会被安全忽略。
参数合法性检查同样发生在 lib/browser/api/net-websocket.ts 中:关闭码先做 WebIDL 的 [Clamp] 收敛(0..65535),随后校验 1000 或 3000..4999,否则抛 InvalidAccessError;reason 超过 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*事件处理属性(onopen、onmessage、onerror、onclose); ws.onmessage = handler写法与addEventListener等价,二者可以混用,且重新赋值on*会先移除旧的处理器(spec 中 “replaces a previously-assigned handler” 用例印证)。
类定义中的 CloseEvent 是 Node 环境下的一个小型规范兼容 polyfill,用以保证 event.code、event.reason、event.wasClean 与渲染进程中的行为完全一致。
四类事件的语义
| 事件 | 触发时机 | 事件对象要点 |
|---|---|---|
open |
连接建立、打开握手完成 | 此后 ws.protocol、ws.extensions 才反映服务端协商结果 |
message |
收到消息 | event.data 对文本帧是 string;二进制帧按 binaryType 为 Buffer / ArrayBuffer / Blob |
error |
连接失败 | 紧随其后必然还会触发一次 close;触发 error 时 readyState 已为 CLOSED |
close |
连接以任何方式关闭 | CloseEvent 携带 code、reason、wasClean |
值得特别说明的失败可诊断性设计:当连接失败时(例如握手被服务端拒绝、网络不可达),close 事件的 code 为 1006,且 Electron 会把 reason 填充为底层网络错误的简短描述——这样即使没有挂着调试器,开发者也能在日志里直接看到失败原因。spec 中 “fires error then close when the connection fails” 与 “readyState is CLOSED inside the error handler” 用例验证了“先 error 后 close”以及错误处理回调内部状态机的一致性。
七、完整实战示例:带会话与认证头的连接
综合以上 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 的本质差异,可以沿代码路径走一遍它的落地实现:
- JS 层 API 封装:类实现在 lib/browser/api/net-websocket.ts。它负责 WHATWG 兼容性细节:URL 协议改写与片段校验、子协议 token/去重校验、
readyState状态机、MessageEvent/CloseEvent派发、binaryType分流、发送队列与bufferedAmount记账。 - 模块导出:lib/browser/api/net.ts 中
export { WebSocket },使其作为net.WebSocket暴露。 - 原生桥接:JS 层通过
process._linkedBinding('electron_common_net')调用createWebSocket(见 shell/common/api/electron_api_net.cc 中dict.SetMethod("createWebSocket", &WebSocketWrapper::Create)),将url、protocols、headers、origin、useSessionCookies、session、partition一并传入 C++ 层。 - 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.protocol在open事件之前为空,需在连接打开后再读取服务端最终协商结果; - “二进制数据形态不对”:默认
nodebuffer(主进程最友好);需要与渲染进程完全一致的行为请改设arraybuffer或blob; - “
send()在握手期抛异常”:这是规范行为——CONNECTING阶段send()会抛InvalidStateError,应等open后再发; - “连接失败难排查”:关注
close事件的code(失败为1006)与reason(Electron 已填充底层网络错误描述); - “要不要带 Cookie”:默认不发会话 Cookie,需要时才显式开启
useSessionCookies: true; - “事件 API 差异”:它是
EventTarget,请用addEventListener或on*属性,不要依赖EventEmitter的on()/once()方法。
以上各条目对应的行为与边界,均在仓库 spec/api-net-websocket-spec.ts 中拥有成体系的 spec 用例保障,可作为继续深挖各细节的第一手资料。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00