首页
/ Electron net 模块深度解析:基于 Chromium 原生网络栈的 HTTP、fetch、DNS 解析与 WebSocket 请求实战

Electron net 模块深度解析:基于 Chromium 原生网络栈的 HTTP、fetch、DNS 解析与 WebSocket 请求实战

2026-09-06 15:34:47作者:侯霆垣

net 是 Electron 提供的客户端网络请求 API,它直接使用 Chromium 的原生网络库(而非 Node.js 的 http/https 实现)来发起 HTTP/HTTPS 请求,并额外提供网络在线状态检测、DNS 解析和 WHATWG 兼容的 WebSocket 连接能力。读完本文,你将掌握 net.requestnet.fetchnet.isOnlinenet.resolveHostnet.WebSocket 五个核心成员的完整用法、参数取值与限制,并能结合 Electron 仓库源码理解这些 API 从 JS 层到 Chromium network 组件的完整调用链,以及如何在主进程中正确处理代理、自定义协议与 Cookie 场景。

一、net 模块定位:为什么用 Chromium 网络栈而不是 Node.js 的 http

net 模块可用在主进程与 Utility 进程中(docs/api/net.md 头部声明 Process: Main、Utility)。它的 API 组件——类、方法、属性、事件名——刻意与 Node.js 保持一致,因此从 http.request 迁移过来几乎是无缝的,但底层走的是 Chromium 的网络栈。

官方文档给出的选用 net 而非 Node.js 原生模块的理由(非穷举):

  • 自动管理系统代理配置,支持 wpad 协议与 proxy PAC 配置文件;
  • HTTPS 请求自动隧道(CONNECT tunneling)
  • 支持认证代理,覆盖 basic、digest、NTLM、Kerberos 或 negotiate 认证方案;
  • 支持流量监控型代理,即 Fiddler 一类用于访问控制与监控的代理。

最基础的使用方式(官方示例原样保留):

const { app } = require('electron')

app.whenReady().then(() => {
  const { net } = require('electron')
  const request = net.request('https://github.com')
  request.on('response', (response) => {
    console.log(`STATUS: ${response.statusCode}`)
    console.log(`HEADERS: ${JSON.stringify(response.headers)}`)
    response.on('data', (chunk) => {
      console.log(`BODY: ${chunk}`)
    })
    response.on('end', () => {
      console.log('No more data in response.')
    })
  })
  request.end()
})

生命周期约束:必须在 app ready 之后使用

文档明确规定:net API 只能在应用发出 ready 事件之后使用,在此之前调用会抛出错误。这一点在源码中得到印证——lib/browser/api/net.tsrequest() 的第一行检查:

if (!app.isReady()) {
  throw new Error('net module can only be used after app is ready')
}

net.WebSocket 的构造函数做了同样的检查(lib/browser/api/net-websocket.ts),所以 net.WebSocket 也必须在 app.whenReady() 之后构造。

二、net.request(options):创建 ClientRequest

签名与行为

net.request(options)
  options: ClientRequestConstructorOptions | string
  返回: ClientRequest

该方法用提供的 options 创建 ClientRequest 实例,选项直接透传给 ClientRequest 构造函数。根据 options 中指定的协议 scheme,net.request 既可以发起安全(https)请求,也可以发起非安全(http)请求。

从源码看,request() 只是构造了 JS 层的 ClientRequestlib/browser/api/net.ts),而 ClientRequest 的 JS 实现位于 lib/common/api/net-client-request.ts,其底层真正发起请求的是原生绑定中的 URL Loader——shell/common/api/electron_api_net.ccelectron_common_net 绑定注册了 createURLLoader(对应 SimpleURLLoaderWrapper::Create),这就是 Chromium URLLoader 的封装。也就是说,net.request 的请求体数据流最终交给的是 Chromium 的 network::SimpleURLLoader,而不是 Node 的 libuv socket。

相关约束与配套文档

  • ClientRequest 的完整选项(urlmethodheaderssessioncredentialscacheredirectpartition 等)与事件说明见 ClientRequest 文档
  • 请求发出前会经过 Chromium 的网络拦截层,主进程注册在 shell/browser/net/electron_url_loader_factory.cc 中的自定义 URL loader factory 会对请求做协议路由与拦截;
  • 需要监控、改写或取消请求时,可以配合 webRequest API(net 发起的请求同样会触发 webRequest 处理器,见下文 net.fetch 一节);
  • 想抓取完整网络日志进行排查,可使用 netLogsession.startNetLog()

三、net.fetch(input[, init]):主进程里的 fetch()

签名与语义

net.fetch(input, init?)
  input: string | GlobalRequest
  init: RequestInit & { bypassCustomProtocolHandlers?: boolean }  (可选)
  返回: Promise<GlobalResponse>

net.fetch 以与渲染进程中 fetch() 相同的方式发送请求,但走 Chromium 网络栈;这与 Node 内置 fetch()(走 Node.js HTTP 栈)不同。官方示例:

async function example () {
  const response = await net.fetch('https://my.app')
  if (response.ok) {
    const body = await response.json()
    // ... use the result.
  }
}

注意 net.fetch() 默认从 default session 发起请求;要从其他 session 发起,应使用 ses.fetch()

已声明的限制(Limitations)

文档明确列出三点,使用 net.fetch 时应当知晓:

  • 不支持 data:blob: scheme;
  • integrity 选项的值会被忽略;
  • 返回的 Response 对象的 .type.url 值不正确。

自定义协议与 bypassCustomProtocolHandlers

这是 net.fetch 最容易被忽视、也最实用的非标准扩展。默认情况下,net.fetch 发出的请求可以打到 自定义协议 以及 file: URL,并会触发已注册的 webRequest 处理器。而在 RequestInit 中设置非标准选项 bypassCustomProtocolHandlers: true 后,该请求不会再调用自定义协议处理器——这使得"在协议处理器里转发请求到内置处理器"成为可能(例如按 URL 区分自研应用内容页与外网请求)。即便绕过自定义协议,webRequest 处理器仍会被触发。

文档给出的官方示例:

protocol.handle('https', (req) => {
  if (req.url === 'https://my-app.com') {
    return new Response('<body>my app</body>')
  } else {
    return net.fetch(req, { bypassCustomProtocolHandlers: true })
  }
})

注意:在 Utility 进程 中,自定义协议不受支持。

源码视角:net.fetch 是如何实现的

lib/browser/api/net.tsnet.fetch 只是一行委托:

export function fetch(input: RequestInfo, init?: RequestInit): Promise<Response> {
  return session.defaultSession.fetch(input, init)
}

net.fetch 恒等于 session.defaultSession.fetch,这也是"如何换 session"的答案——直接调 ses.fetch(input, init)

真正的实现体是 lib/browser/api/net-fetch.ts 中的 fetchWithSession。从源码结构看,它的工作方式可以归纳为:

  1. 构造 Request:用全局 Request 包装 input/init,构造失败则 Promise 直接 reject;若 req.signal 已被 abort,按 fetch 规范的 Abort 流程拒绝并取消 body 流(net-fetch.ts#L37-L55);
  2. 转换为 ClientRequest:把 req 的 method、url、origin、credentials、cache、referrerPolicy、redirect 组装成 ClientRequestConstructorOptions(经由 allowAnyProtocol),底层依旧走 net.request 的 Chromium 链路;bypassCustomProtocolHandlers 被写入内部字段 _urlLoaderOptionsnet-fetch.ts#L87-L100);
  3. Header 与 mode 处理credentials: 'same-origin' 且无 origin 时会放宽为 'include';非 cors 的 mode 通过 Sec-Fetch-Mode 头传递(net-fetch.ts#L102-L109);
  4. 响应装配:收到 response 事件后把 Node 风格的 headers 装入 Headers;对 101/204/205/304 与 HEAD 请求返回空 body,否则用 Readable.toWeb 把响应流包成 ReadableStream 构造 Responsenet-fetch.ts#L111-L132)。代码注释还特别说明:protocol.handle 会把返回的 Response 原样转交、不经过 JS 泵流,因此响应上挂了 __fetch 内部字段保留原始 loader 与流;
  5. 请求体管道:用 Writable.toWebClientRequest(本身是 Writable)转成 WritableStream,用 req.body.pipeTo(...) 流式写入请求体,完成后 r.end()net-fetch.ts#L138-L143)。

由此可以推断:net.fetch 本质是"以 fetch 语义封装的 ClientRequest",请求与响应体均为流式传输,支持 AbortSignal 取消,但受限于底层实现才有前文列出的 data:/blob:integrity.type/.url 等限制。

四、net.isOnline()net.online:网络状态检测

net.isOnline() → boolean   当前是否有网络连接
net.online  → boolean       只读属性,含义相同

文档对返回值的解读很关键:返回 false 是比较强的"用户无法连接远端站点"的信号;但返回 true 并不能下结论——即便某条链路是通的,也不能保证针对某个特定远端站点的连接尝试会成功。因此在线状态适合做"离线模式入口/提示"级别的判断,不应替代对具体请求成败的监听。

源码上,isOnline 是原生绑定方法,JS 侧只做透传(lib/browser/api/net.ts):

const { isOnline } = process._linkedBinding('electron_common_net')

其实现位于 shell/common/api/electron_api_net.cc

bool IsOnline() {
  return !net::NetworkChangeNotifier::IsOffline();
}

即直接查询 Chromium 的 net::NetworkChangeNotifier。这说明 net.isOnline() 反映的是 Chromium 网络栈对系统网络可达性的判断,与 Node 的 os.networkInterfaces() 或浏览器 navigator.onLine 的实现来源一致。JS 层的 net.online 属性则是每次访问都调用 isOnline() 的 getter(lib/browser/api/net.ts#L31-L33),所以它是"实时"的,缓存其值没有意义。

五、net.resolveHost(host, [options]):DNS 解析

签名与参数

net.resolveHost(host, options?)
  host: string                    要解析的主机名
  options (可选):
    queryType: 'A' | 'AAAA'       DNS 查询类型;缺省时解析器根据 IPv4/IPv6 配置选择 A、AAAA 或两者
    source:                       解析结果来源,缺省为 'any'
      - 'any' (默认)     解析器自行选择,结果可能来自 DNS、mDNS、HOSTS 文件等
      - 'system'          仅来自系统/OS(如 getaddrinfo() 系统调用)
      - 'dns'             仅来自 DNS 查询
      - 'mdns'            仅来自 Multicast DNS 查询
      - 'localOnly'       不使用外部来源,仅来自本地快速来源(缓存、hosts 文件、IP 字面量等)
    cacheUsage:                  允许使用哪些 DNS 缓存条目
      - 'allowed' (默认)    非过期缓存可用
      - 'staleAllowed'      过期(过期时间或网络变化)缓存也允许
      - 'disallowed'        完全不使用 host 缓存
    secureDnsPolicy:             本次请求的 Secure DNS 行为
      - 'allow' (默认)
      - 'disable'
返回: Promise<ResolvedHost>

返回的 ResolvedHost 结构包含 endpoints: ResolvedEndpoint[],即该主机名解析出的 DNS 条目列表。

net.fetch 同理,net.resolveHostdefault session 发起解析;要针对其他 session 解析,应使用 ses.resolveHost()

源码佐证

JS 层同样一行委托到 session.defaultSession.resolveHostlib/browser/api/net.ts#L25-L27)。C++ 侧,resolveHost 作为 electron_common_net 绑定方法注册(shell/common/api/electron_api_net.cc#L92),实际的解析函数实现在 shell/browser/net/resolve_host_function.ccresolve_host_function.h。可以推断它最终调用的是 Chromium net 组件的 HostResolver(sourcecacheUsagesecureDnsPolicy 这些参数与 Chromium HostResolver::Options 的语义一一对应),这也是这些参数取值如此"网络组件味"的原因。

典型用途:在网络诊断工具中确认某域名在当前环境下解析到哪些 IP、排查 DNS 污染或劫持、或在应用内实现自己的本地域名映射前先用 localOnly 查看 hosts 文件生效情况。

六、net.WebSocket:主进程的 WHATWG WebSocket

属性说明

net.WebSocketWebSocket 类的构造器引用,仅在主进程可用(Utility 进程中不可用)。它用于通过 Chromium 网络栈创建 WHATWG 兼容 的 WebSocket 连接。官方示例:

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

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

完整接口(事件、属性、options 如 sessionpartitionuseSessionCookies、自定义 headers 等)见 WebSocket 文档

实现细节:JS 封装 + 原生 wrapper

net.request/net.fetch 不同,net.WebSocket 的 JS 实现是 Electron 自己写的一个较完整的 WHATWG WebSocket 模拟类,位于 lib/browser/api/net-websocket.ts,底层依赖原生绑定 electron_common_netcreateWebSocket(C++ 实现为 shell/browser/api/electron_api_web_socket.cc 中的 WebSocketWrapper)。几个从源码可确认的设计点:

  • URL 规范化:按 WHATWG WebSocket 规范解析 URL,http: 自动改写为 ws:https: 改写为 wss:;拒绝携带 fragment 的 URL(net-websocket.ts#L85-L103);
  • subprotocol 校验:按 RFC 7230 token 规则校验 subprotocol 名称并去重,非法则抛 SyntaxErrornet-websocket.ts#L23-L25L124-L130);
  • binaryType 支持三种取值'blob' | 'arraybuffer' | 'nodebuffer',其中 nodebuffer(默认)直接暴露 Node Buffer,方便主进程做二进制处理(net-websocket.ts#L150-L170);
  • 有序发送队列Blob 需要异步读取,源码用一条 Promise 发送队列序列化出站写入,保证后发字符串/ArrayBuffer 不会"超车"先发 Blob;队列中的字节计入 bufferedAmountnet-websocket.ts#L224-L273);
  • close 语义与规范一致:close code 必须为 1000 或 3000–4999,reason 不超过 123 个 UTF-8 字节(net-websocket.ts#L275-L296)。

验证

针对 net.WebSocket 的测试位于 spec/api-web-socket-spec.ts,覆盖消息收发、subprotocol、close code 校验等场景,可作为行为符合性的重要参考。

七、实用建议与常见坑

结合文档与源码,实际使用 net 模块时建议:

  1. 一切 net 调用放在 app.whenReady() 之后net.requestnet.WebSocket 都会在 ready 之前抛错,这是 JS 层显式检查(见 lib/browser/api/net.ts#L15-L17);
  2. 代理与多 session 场景优先用 session 级 APInet.fetch/net.resolveHost 固定走 default session,需要按分区隔离 Cookie/缓存/代理时改用 ses.fetch() / ses.resolveHost()
  3. net.isOnline() 只作弱信号true 不代表具体请求必成功,false 才可较放心地判定离线;
  4. 主进程需要 WebSocket 时用 net.WebSocket,而不是 Node 的 ws 包或 Node 18+ 内置 WebSocket,前者走 Chromium 网络栈,能享受系统代理配置,并支持自定义 session/partition
  5. 协议处理器内部转发自请求时务必带上 bypassCustomProtocolHandlers: true,否则会造成对 protocol.handle 的无限递归(文档第三节示例即此用途);
  6. 排查网络问题:配合 webRequest 观察/改写请求、netLog 抓取请求日志、session 设置 resolveHost 级别的诊断。

参考文件

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