Electron WebSocketOptions 深度解析:net.WebSocket 的六项连接选项及其底层实现
WebSocketOptions 是 Electron net.WebSocket 构造函数的专属扩展参数对象,用于在主进程中使用 Chromium 原生网络栈建立 WebSocket 连接时控制子协议协商、握手请求头、Origin 身份、Cookie 行为与 Session 归属。本文基于当前仓库中该结构的定义文档与实现源码,逐项讲解这六个字段的语义、默认值与相互优先级,并溯源到 JS 层参数校验(lib/browser/api/net-websocket.ts)与原生层建连逻辑(shell/browser/api/electron_api_web_socket.cc),帮助你在生产环境中正确配置带鉴权的长连接。
一、WebSocketOptions 定位:WHATWG 兼容之上的 Electron 扩展
Electron 提供的 net.WebSocket 是 WHATWG WebSocket 接口的"平替"(drop-in replacement):它运行在主进程,通过 Chromium 网络服务(Network Service)发起连接,因此可以复用系统/Session 代理配置、平台信任库的 TLS 校验以及 Session 级网络策略,而这是 Node.js 侧 ws 库无法获得的。
构造函数签名为 new WebSocket(url[, protocols]),其中第二个参数可以是:
- 字符串或字符串数组:标准 WHATWG 形式,仅表示子协议;
WebSocketOptions对象:Electron 扩展形式,除携带protocols外,还能控制握手行为。
从源码结构看,这一"双形态"参数在 JS 层构造函数中被显式拆解(lib/browser/api/net-websocket.ts#L105-L140):字符串归一为数组、数组逐项 String() 化,对象则整体作为 options 传递,且对象中的 protocols 字段会再次按"字符串或字符串数组"规则解析。因此传对象是标准的严格超集,完全向后兼容。
此外有两个硬前提,源码中均有强制检查:
- 仅在
app就绪后可用,否则抛出net.WebSocket can only be used after app is ready(lib/browser/api/net-websocket.ts#L81-L83); - 仅在主进程可用,原生
WebSocketWrapper::Create中!electron::IsBrowserProcess()会直接抛出类型错误(shell/browser/api/electron_api_web_socket.cc#L98-L102)。
二、六个字段全解
WebSocketOptions 的完整定义(与 docs/api/structures/web-socket-options.md 一致,JS 侧接口见 lib/browser/api/net-websocket.ts#L43-L50):
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
protocols |
string | string[] |
否 | 空 | 请求的 WebSocket 子协议列表 |
headers |
Record<string, string> |
否 | 无 | 附加到打开握手(opening handshake)的额外 HTTP 头 |
origin |
string |
否 | WebSocket URL 的 http(s) 等价源 | 握手时发送的 Origin 头取值 |
useSessionCookies |
boolean |
否 | false |
是否在握手中发送 Session Cookie,并存储握手响应中的 Cookie |
session |
Session |
否 | 默认 Session | 连接关联的 Session 实例 |
partition |
string |
否 | 空字符串(即默认 Session) | 连接关联的分区名;提供 session 时该字段被忽略 |
2.1 protocols:子协议,带严格的 Token 校验
子协议名称必须满足 WHATWG 规范中 HTTP token 语法。JS 层用正则 ^[!#$%&'*+\-.^_|~0-9a-zA-Z]+$对每一项做校验,且不允许重复项,违例直接抛出SyntaxError类型的DOMException`(lib/browser/api/net-websocket.ts#L23-L30、#L124-L130):
const { net } = require('electron')
// 合法:标准两参形式
const a = new net.WebSocket('wss://chat.example.com', ['chat', 'superchat'])
// 等价:options 形式
const b = new net.WebSocket('wss://chat.example.com', { protocols: ['chat'] })
// 抛 DOMException:'bad protocol' 含空格
// 抛 DOMException:['a', 'a'] 含重复项
测试用例 spec/api-net-websocket-spec.ts#L62-L68 验证了含空格与重复项均会抛错;#L97-L107 验证了服务端 handleProtocols 选中 chat 后,客户端 ws.protocol 会反映协商结果。
2.2 headers:附加握手请求头,逐项做合法性校验
headers 中的键值对会被追加到 WebSocket 打开握手的 HTTP 请求上(典型用途是携带 Authorization 之类的鉴权头)。原生侧不是无脑透传:每一项都会经过 net::HttpUtil::IsValidHeaderName / IsValidHeaderValue 校验,任一非法即抛出 Invalid header name or value(shell/browser/api/electron_api_web_socket.cc#L120-L131),随后打包为 network::mojom::HttpHeader 传入 CreateWebSocket。
测试证据见 spec/api-net-websocket-spec.ts#L314-L324:
const ws = new net.WebSocket(url, { headers: { 'X-Custom': 'electron' } })
// 服务端握手请求中可观察到 headers['x-custom'] === 'electron'
2.3 origin:默认取 URL 的 http(s) 等价源
origin 决定握手 Origin 头的值,同时作为请求的发起方(request initiator)参与 SameSite Cookie 匹配。不指定时的默认行为是把 ws:/wss: 协议改写为 http:/https: 后取源:连接 wss://api.example.com 会发送 Origin: https://api.example.com。这样服务端和 SameSite Cookie 规则都会把该连接当作"同源/first-party"请求对待(shell/browser/api/electron_api_web_socket.cc#L133-L146)。
// 默认:ws://127.0.0.1:3000 → Origin: http://127.0.0.1:3000
const ws = new net.WebSocket('ws://127.0.0.1:3000')
// 显式覆盖
const ws2 = new net.WebSocket(url, { origin: 'https://my-app.example' })
对应测试:spec/api-net-websocket-spec.ts#L326-L348 分别断言了"默认 Origin 等于 ws 地址替换为 http 后的结果"和"显式 origin 原样送达服务端"。
2.4 useSessionCookies:默认 false,显式开启才走 Session Cookie
这是 Electron 特有的安全默认值。从源码看,JS 层将 useSessionCookies 原样透传给原生层(lib/browser/api/net-websocket.ts#L132-L140),原生 Start() 中据此选择 mojom 选项(shell/browser/api/electron_api_web_socket.cc#L193-L210):
useSessionCookies: true→network::mojom::kWebSocketOptionNone(不拦截 Cookie,配合 2.3 的 first-party Origin,SameSite Cookie 也能随握手发出);- 缺省(
false)→network::mojom::kWebSocketOptionBlockAllCookies(完全阻断 Cookie 发送与存储)。
测试 spec/api-net-websocket-spec.ts#L350-L376 给出了完整闭环:
const ses = session.fromPartition(`net-websocket-cookies-${Date.now()}`)
await ses.cookies.set({ url: url.replace('ws://', 'http://'), name: 'ws', value: 'cookie' })
// 开启:服务端可收到 Cookie
new net.WebSocket(url, { session: ses, useSessionCookies: true }) // headers.cookie 包含 'ws=cookie'
// 不开启:即使同一 Session 已写入 Cookie,握手中也不出现 Cookie 头
new net.WebSocket(url, { session: ses }) // headers.cookie === undefined
需要鉴权状态与 HTTP 请求共享时,才应显式开启;否则默认隔离可避免主进程长连接意外携带浏览器级会话凭据。
2.5 session 与 partition:Session 归属与优先级
这两个字段决定连接挂在哪个 Session(即哪个 BrowserContext 的网络栈、代理、自定义 CA、Host 解析规则下):
session:直接传入Session实例;partition:分区名,空字符串对应默认 Session;若同时提供session,则partition被忽略。
原生层的解析顺序是"session 优先,其次 partition,再退化为默认 Session",解析失败会抛出 Failed to resolve session(shell/browser/api/electron_api_web_socket.cc#L151-L162):
if (!opts.Get("session", &session)) {
if (opts.Get("partition", &partition))
session = Session::FromPartition(args->isolate(), partition);
else
session = Session::FromPartition(args->isolate(), "");
}
建连时使用的是该 Session 对应 BrowserContext 默认 Storage Partition 的 NetworkContext(shell/browser/api/electron_api_web_socket.cc#L187-L188)。因此"指定独立 partition 的 WebSocket 不受默认 Session 的 setProxy/setCertificateVerifyProc 影响"是可直接依赖的行为。partition 名称的语义与 docs/api/session.md 中 session.fromPartition(partition) 一致(区分大小写、支持 persist: 前缀的持久化分区等规则同样适用,具体以该文档为准)。
三、完整数据流:从构造参数到 Network Service
把源码串起来,一次 new net.WebSocket(url, options) 的调用链如下:
- JS 构造期(lib/browser/api/net-websocket.ts):
app.isReady()检查 → URL 解析与 scheme 归一化(http(s):改写为ws(s):,含 fragment 直接抛错)→ 拆解protocols/options→ 调用process._linkedBinding('electron_common_net').createWebSocket({...}); - 原生构造期(shell/browser/api/electron_api_web_socket.cc#L98-L171):
WebSocketWrapper::Create校验 URL 与headers,计算默认 Origin,按 2.5 的优先级解析 Session,通过cppgc::MakeGarbageCollected创建包装对象并立即Start(); - 建连期(
Start()):绑定 handshake mojom pipe 后,向 Session 的NetworkContext发起CreateWebSocket,参数中包含请求方 Origin(net::IsolationInfo::CreateForInternalRequest(origin_))、附加头、Cookie 拦截策略与流量注解;握手失败时CloseEvent的code为1006,且 Electron 会把net::ERR_*形式的底层网络错误写入reason,便于免调试器定位问题(docs/api/web-socket.md 的 Events 一节与测试 spec/api-net-websocket-spec.ts#L109-L120 均验证了这一点)。
底层绑定接口的形状见 typings/internal-ambient.d.ts#L124-L147:createWebSocket(CreateWebSocketOptions) 返回 WebSocketWrapper,其 open/message/closing/close/error 五个事件正是 JS 层 EventTarget 事件的来源。
四、实战示例:带鉴权头与独立 Session 的连接
综合以上字段,一个典型的主进程鉴权 WebSocket 客户端如下:
const { app, net, session } = require('electron')
app.whenReady().then(() => {
// 为长连接使用独立分区,避免与默认 Session 共享状态
const ses = session.fromPartition('ws-secure')
const ws = new net.WebSocket('wss://api.example.com/stream', {
protocols: ['v1'], // 子协议协商
headers: { Authorization: `Bearer ${token}` }, // 握手鉴权头
origin: 'https://api.example.com', // 与握手目标同源,服务端按同源处理
useSessionCookies: true, // 允许发送/存储该 Session 的 Cookie
session: ses // 关联独立 Session(partition 将被忽略)
})
ws.binaryType = 'nodebuffer' // 主进程下默认值,二进制以 Buffer 送达
ws.onopen = () => ws.send(JSON.stringify({ type: 'subscribe' }))
ws.onmessage = (e) => { /* e.data 为 string 或 Buffer */ }
ws.onerror = () => { /* 之后必然跟随 close 事件 */ }
ws.onclose = (e) => {
// 连接失败时 e.code === 1006,e.reason 携带 net::ERR_* 描述
console.log('closed', e.code, e.reason, e.wasClean)
}
})
几点使用注意(均有仓库依据):
send()在CONNECTING状态调用会抛InvalidStateError(lib/browser/api/net-websocket.ts#L224-L227);close(code, reason)中code必须是1000或3000–4999,reason的 UTF-8 长度不得超过 123 字节,超限抛DOMException(lib/browser/api/net-websocket.ts#L275-L291,测试见 spec/api-net-websocket-spec.ts#L141-L152);bufferedAmount同时统计"已进入原生层"与"仍排队在 JS 发送队列"的字节,Blob 异步读取期间后续send()也会被计入并保证顺序(lib/browser/api/net-websocket.ts#L253-L273,测试 spec/api-net-websocket-spec.ts#L271-L310)。
五、小结
WebSocketOptions 六个字段覆盖了主进程 WebSocket 的关键决策点:protocols 决定协商内容,headers/origin 塑造握手的 HTTP 身份,useSessionCookies 控制凭据是否随连接流动,session/partition 决定连接落在哪个网络栈上。其设计取向清晰——与 WHATWG 完全兼容的裸两参形式保持零差异,而 Electron 扩展项全部默认保守(不发 Cookie、Origin 默认 first-party),把风险开关交给调用者显式打开。理解 JS 层校验与原生 CreateWebSocket 的参数映射后,即可在主进程中构建行为可预期、鉴权可控的长连接。
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 StartedRust0623
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