
简介这是一套基于 Electron 框架的 UDP 调试工具完整源码使用 JavaScript 编写面向需要快速搭建 UDP 客户端与服务器场景的开发者也适合网络相关课程演示。应用支持添加任意数量的 UDP 客户端和服务器并为每个实例指定主机与端口内置随机数据生成器可按固定时间间隔从客户端自动发送数据用于验证或测试其他 UDP 服务。压缩包内共 30 个文件包括 12 个 JavaScript 逻辑模块、5 个 CSS 样式、HTML 页面入口及 JSON 配置等体积仅 301KB轻量易部署便于直接阅读和二次开发附带的 README 文档与界面预览图能帮助使用者快速上手。已有 1300 人学习下载。借助该资源读者可快速启动并打包为独立桌面程序代码中客户端管理、服务器管理、数据生成器等模块层次清晰不仅可作为日常网络调试工具也能作为学习 Electron 桌面应用开发的可运行范例。1. 一个用 Electron 写 UDP 调试工具的真实需求做网络调试的时候最烦的就是手里没有趁手的 UDP 发包工具。TCP 有各种现成客户端但 UDP 数据报这种面向无连接的东西要么得装庞大的网络分析软件要么就得临时写脚本。这个 electron-udp 项目解决的就是这个问题它是一个基于 Electron 和 JavaScript 的桌面应用把 UDP 客户端和服务器都塞进了一个图形界面里可以创建本地 UDP 端点来发送和接收数据报适合做协议调试、设备通信测试、局域网服务探测这类场景。我拆这个项目的时候最大的感受是它麻雀虽小但把 Electron 主进程和渲染进程之间怎么协作、Node.js 原生模块怎么暴露给界面层这些基本功展示得很完整。对刚接触 Electron 的开发者来说这是一个能快速跑通、而且能直接改成自己工具的起点对熟手来说它的模块边界划分也有可参考的地方。这篇笔记就沿着「项目在做什么 → 核心代码怎么实现 → 怎么跑起来 → 怎么改」这条线把整个项目完整过一遍。2. 项目定位与技术选型为什么用 Electron 做 UDP 调试工具2.1 这个项目要解决什么问题UDP 调试最原始的做法是写 Node.js 脚本用dgram模块在终端里收发数据配合console.log看输出。这个方式有几个明显的痛点一是数据不直观十六进制和字符串混在一起看多几条消息就花了眼二是没法同时管两端的收发客户端和服务器通常要开两个终端窗口切换来切换去容易乱。electron-udp 的做法是做一个带界面的桌面应用把 UDP 服务器和客户端放进同一个窗口里启动、停止、发送、清空记录都靠按钮完成收发的每条数据报按时间顺序展示在列表里。这样一来本地协议联调、嵌入式设备通信测试这类场景就能在一个工具里闭环完成。它的适用人群很明确做物联网设备调试的开发者、写自定义 UDP 协议的嵌入式工程师、需要快速验证网络服务的后端开发。项目本身不复杂但围绕「UDP 数据报收发」这件事把完整链路都打通了。2.2 技术栈与架构分层这个项目的技术栈核心是三类Electron负责桌面应用外壳提供主进程和渲染进程的运行时环境。Node.js 原生 dgram 模块UDP socket 的创建、绑定、发送和接收全部由它完成运行在主进程侧。HTML/CSS/JavaScript 前端负责界面渲染和用户交互通过 IPC进程间通信调用主进程的能力。整体架构沿用了 Electron 标准的双进程模型。主进程持有 dgram 的 socket 实例负责真正的网络 I/O渲染进程只负责界面展示和用户操作不直接触碰网络能力。两者之间的数据交换通过ipcMain和ipcRenderer完成。这个分层有一个很实际的考量Electron 渲染进程出于安全策略默认开启了contextIsolation窗口里不能直接用 Node.js 的require。把网络操作全部收敛到主进程渲染进程只通过预先暴露的接口发指令结构更干净也符合 Electron 的安全实践。2.3 目录结构与职责划分项目源码拆解之后核心部分大致长这样electron-udp/ ├── main.js # 主进程入口创建窗口、注册 IPC、管理 UDP socket ├── preload.js # 预加载脚本通过 contextBridge 暴露安全接口 ├── renderer/ │ ├── index.html # 界面结构服务器区、客户端区、消息记录区 │ ├── style.css # 界面样式 │ └── renderer.js # 渲染进程逻辑绑定按钮事件、渲染消息列表 └── package.json # 项目配置、依赖和启动脚本main.js是核心所有 UDP 能力都在这里preload.js是渲染进程和主进程之间的桥梁renderer目录下是纯前端代码。这个结构对一个小项目来说是合理的——网络逻辑和界面逻辑完全分离想替换 UI 层只需要改renderer目录想改协议处理只需要动main.js。2.4 主进程与渲染进程的职责边界把代码读一遍之后会发现项目的职责边界非常清楚主进程main.js负责创建 BrowserWindow 窗口、创建和销毁 UDP socket、处理ipcMain收到的各种请求、把收到的数据报推送给渲染进程。渲染进程renderer.js负责读取用户输入的 IP、端口、消息内容把这些参数通过ipcRenderer发给主进程然后把主进程返回的结果展示在界面上。preload.js是两者的桥梁通过contextBridge.exposeInMainWorld把sendMessage、startServer这类方法挂到window对象上渲染进程调用window.electronUDP.sendMessage(...)时实际上是向主进程发起了 IPC 请求。这个设计的巧妙之处在于渲染进程完全不感知 UDP 协议的细节它只知道自己调了一个方法传进去了字符串然后收到了结果。协议层面的东西全部隔离在主进程里后续要换 TCP、换 WebSocket界面层几乎不用动。3. 核心模块拆解一条 UDP 数据报从进入到返回的完整链路3.1 服务器端创建 socket、绑定端口、监听消息服务器端的核心代码在主进程里流程很直接创建 dgram socket、调用bind绑定端口、监听message事件。// main.js 服务器端核心逻辑 const dgram require(dgram); let serverSocket null; function startUdpServer(port, onMessage) { // 创建 IPv4 的 UDP socket serverSocket dgram.createSocket(udp4); serverSocket.on(error, (err) { console.error(服务器错误: ${err.message}); serverSocket.close(); }); serverSocket.on(message, (msg, rinfo) { // rinfo 包含发送方的地址和端口 const text msg.toString(utf-8); onMessage({ from: ${rinfo.address}:${rinfo.port}, content: text, time: new Date().toLocaleTimeString() }); }); serverSocket.bind(port, () { const address serverSocket.address(); console.log(UDP 服务器已启动: ${address.address}:${address.port}); }); }这段代码的逻辑链路是dgram.createSocket(udp4)创建一个 UDP socketbind(port)让它监听指定端口之后所有发到这个端口的数据报都会触发message回调。回调收到的msg是 Buffer 类型需要调用toString()转成字符串rinfo里带了发送方的 IP 和端口这是回显来源地址的关键信息。有一个细节值得注意dgram.createSocket支持udp4和udp6两种协议族参数。这个项目默认用udp4如果需要支持 IPv6 场景把这里的参数换成udp6绑定时再处理一下地址格式就行。另外bind的第二个参数是回调函数它在绑定成功后才触发在这个时机读取serverSocket.address()能拿到实际分配的端口——如果不指定端口号让系统随机分配就需要靠这个回调里的返回值去确认最终端口。3.2 客户端发送数据报与回调处理客户端的核心是send方法它的完整参数链值得仔细看。// main.js 客户端发送逻辑 function sendUdpMessage(host, port, message, callback) { const clientSocket dgram.createSocket(udp4); const buffer Buffer.from(message, utf-8); clientSocket.send(buffer, 0, buffer.length, port, host, (err) { if (err) { callback({ success: false, error: err.message }); } else { callback({ success: true }); } // 发送完成后立即关闭避免 socket 泄漏 clientSocket.close(); }); }clientSocket.send的参数顺序是(buffer, offset, length, port, host, callback)。offset和length表示从 Buffer 的哪个位置开始发送、发多少字节这在一段 Buffer 里只发部分数据时很有用。callback在数据报被交给系统网络栈之后触发注意它不等于对端已经收到——UDP 本身是无连接的这个回调只代表本机发出成功。这个实现里每次发送都新建一个 socket发送完立刻关闭这是一种「短连接」式用法。优点是简单直接不需要维护 socket 生命周期缺点是高频发送时频繁创建销毁 socket 会有性能损耗。如果你的场景是每秒几十上百条消息的连续发送更好的做法是复用同一个 socket这点在后面的二次开发章节会展开。3.3 UI 层与 IPC 通信preload 的桥接设计渲染进程不能直接调用 Node.js 模块这是 Electron 的安全边界。项目通过 preload 脚本和 IPC 机制打通这条链路。// preload.js 暴露安全接口给渲染进程 const { contextBridge, ipcRenderer } require(electron); contextBridge.exposeInMainWorld(electronUDP, { startServer: (port) ipcRenderer.invoke(udp:start-server, port), stopServer: () ipcRenderer.invoke(udp:stop-server), sendMessage: (host, port, message) ipcRenderer.invoke(udp:send, host, port, message), onMessage: (callback) { // 订阅主进程推送的消息事件 ipcRenderer.on(udp:message-received, (event, data) callback(data)); } });// renderer.js 界面绑定与消息渲染 const sendBtn document.getElementById(sendBtn); const hostInput document.getElementById(hostInput); const portInput document.getElementById(portInput); const messageInput document.getElementById(messageInput); const messageList document.getElementById(messageList); sendBtn.addEventListener(click, async () { const host hostInput.value.trim(); const port parseInt(portInput.value, 10); const message messageInput.value; // 调用 preload 暴露的方法经由 IPC 到达主进程 const result await window.electronUDP.sendMessage(host, port, message); if (result.success) { appendMessage([发送] 目标 ${host}:${port} 内容: ${message}); } else { appendMessage([发送失败] ${result.error}); } }); // 订阅消息接收事件 window.electronUDP.onMessage((data) { appendMessage([接收] 来源 ${data.from} 内容: ${data.content}); });这套链路从界面按钮到 UDP socket完整路径是renderer.js里按钮的 click 事件 → 调用window.electronUDP.sendMessage→ preload 里的ipcRenderer.invoke发起 IPC 请求 → 主进程ipcMain.handle处理请求并执行实际的dgram发送 → 结果通过 Promise 返回给渲染进程。ipcRenderer.invoke和ipcMain.handle是一对 Promise 风格的 IPC API比传统的sendon方式更好用因为调用方可以直接拿到异步结果不需要额外的事件对账。这里onMessage用的是ipcRenderer.on因为它是主进程主动推送的事件属于「服务端 → 客户端」方向的单向通知用事件模型更合适。3.4 关键参数与行为说明把 main.js 里涉及的关键参数整理一遍方便改代码时对照参数位置说明常见取值udp4/udp6createSocket协议族默认udp4IPv6 环境改udp6offset/lengthsend发送 Buffer 范围整包发送时 offset0, lengthbuffer.lengthportbind/send绑定或目标端口0-655351024 以下需要特权hostsend目标地址本地测试用 127.0.0.1rinfo.addressmessage回调发送方 IP无法伪造UDP 场景下可信rinfo.portmessage回调发送方端口回显给对方时要用它两个容易搞混的端口概念bind参数里的是本机监听端口决定「数据报发到哪个端口能被这个 socket 收到」send参数里的是目标端口决定「数据报要发给对端的哪个端口」。在测试环境里服务器绑定 12345客户端发送到 127.0.0.1:12345两者必须对应上。另一个值得注意的行为差异dgram socket 在绑定之后可以用同一个 socket 既能接收又能发送。这个项目把服务器和客户端分开走了两条逻辑但底层其实都是同一个 dgram API只是关注点不同——服务器重在bindmessage监听客户端重在send。理解了这一点你就知道为什么协议栈代码可以写得这么短了。4. 运行、调试与避坑指南4.1 环境准备与启动步骤跑这个项目之前先确认本机有 Node.js 环境和 npm。Electron 本身是跨平台的Windows、macOS、Linux 都能跑但 Windows 上如果遇到权限问题或者防火墙拦截处理方式会略有差异。# 安装依赖Electron 二进制包较大耐心等 npm install # 启动开发模式 npm start如果package.json里配置了start脚本指向electron .npm start 就会拉起 Electron 窗口。首次启动时如果界面没出来先在终端看有没有报错输出——最常见的两个问题一是 Electron 镜像下载超时二是 Node 版本过低导致 dgram API 不兼容。启动后先做一个最简单的自测在服务器区填一个端口比如 12345点击启动然后切到客户端区目标地址填127.0.0.1目标端口填12345输入一条测试消息点击发送。正常的话消息记录区会先显示[发送]的记录紧接着服务器回调触发显示[接收]的记录。这两条记录同时出现就说明「发送 → 服务器监听 → 消息回调 → UI 展示」的完整链路已经通了。4.2 一个值得做的联调验证如果你身边有别的 UDP 服务比如某个设备或者另一个终端的 Node 脚本可以做跨端验证。在终端里跑一个最小化的接收端node -e const dgramrequire(dgram);const sdgram.createSocket(udp4);s.bind(9999,()console.log(listening));s.on(message,(m,r)console.log(r.address:r.port m.toString()));然后在这个工具的客户端区向127.0.0.1:9999发消息。如果终端里打印出了消息内容说明这个 Electron 应用的 UDP 发送不只是在应用内部自嗨而是真的能和外部的 Node.js 进程互通。这一步验证很重要——很多初学 Electron UDP 的人第一次测试都是在同一窗口里自收自发会误以为链路通了实际上根本没有走到真正的网络层。4.3 高频踩坑记录这个项目我拆的时候踩了不少坑下面这几条是最典型的按「现象 → 原因 → 解决」写出来大概率你也会遇到。踩坑一端口启动报 EADDRINUSE现象点击启动服务器终端或者界面上报Error: listen EADDRINUSE: address already in use :::12345。原因这个端口已经被别的程序占用了。UDP 和 TCP 一样遵循端口独占原则一个端口同时只能被一个 socket 绑定。最常见的情况是上次测试没关干净进程还挂在后台。解决先用lsof -i:12345macOS/Linux或netstat -ano | findstr 12345Windows查占用进程用 taskkill 或者 kill 掉对应进程后重试要么直接把端口换一个。从那以后我每次写网络相关的代码都会先把「端口占用检查」列进启动前 checklist。踩坑二服务器能启动但客户端发消息后收不到任何回显现象点击发送界面上[发送]记录正常但没有任何[接收]记录出现。原因这不是代码 bug大概率是数据报确实发出去了但对端没有响应或者被防火墙拦了。UDP 不保证送达也不存在 TCP 那样的连接建立过程发送端永远无法感知对端是否真正消费了数据。如果发到公网地址丢包更是家常便饭。解决先确认目标地址和端口是可达的——用 ping 测主机连通性在本机测试时优先用127.0.0.1而不是局域网 IP再看防火墙是否放行了 UDP 端口的入站规则。Windows 默认会拦截部分 UDP 入站流量需要在防火墙设置里给 Electron 应用放行。另外记得UDP 测试「发送 → 接收」必须是两个方向都有人处理你的服务器端有没有在监听代码分支是不是没走到沿着代码路径逐段排查。踩坑三Windows 上中文消息显示乱码现象发送中文内容对端收到的是测试这种乱码。原因没注意编码转换。dgram 收发的是 Buffer底层字节流本身没有编码信息。代码里发送时用Buffer.from(message, utf-8)编码接收端toString(utf-8)解码两边一致才没问题。如果对端是 Java 写的服务默认用的是 UTF-16或者对端按 GBK 解码你的 UTF-8 字节必然出现乱码。解决在自己项目里统一收发的编码方式。如果对接的是第三方系统先确认对方用什么编码。另外Buffer.from默认就是 utf-8但显式写出来更不容易产生误会。我一般会建议所有处理网络收发的代码在入口和出口都显式标注编码这个习惯能省掉大量联调时间。踩坑四preload 里暴露的接口在渲染进程里 undefined现象打开开发者工具输入window.electronUDP返回 undefined界面上的按钮全部报错。原因preload 脚本没有被正确加载或者contextIsolation配置和预加载脚本不匹配。Electron 从 12 版本开始默认开启contextIsolationpreload 脚本运行在隔离环境里不能直接往window上挂属性必须通过contextBridge。如果main.js里创建 BrowserWindow 时没有写preload路径或者路径写错了这个桥就不会建立。解决检查 BrowserWindow 的 webPreferences 配置// main.js 窗口创建配置 const win new BrowserWindow({ width: 900, height: 600, webPreferences: { preload: path.join(__dirname, preload.js), contextIsolation: true, nodeIntegration: false } });注意nodeIntegration保持 false 是安全推荐做法渲染进程不直接开 Node 集成所有能力都走 preload 桥接。如果发现接口还是 undefined在 preload 里加一句console.log(preload loaded)看开发者工具的 console 里有没有输出——没有输出就说明 preload 压根没执行。5. 二次开发从 Demo 到工具的改造路径5.1 改造方向一增加十六进制收发与协议帧解析字符串收发只是最基础的用法。做嵌入式或者自定义协议调试时最常看到的是十六进制数据报。改造思路是在渲染进程里增加一个「文本/Hex」切换开关发送时把用户输入从 hex 字符串转成 Buffer接收时把 Buffer 转回 hex 展示。// renderer.js 十六进制转换逻辑 function hexStringToBuffer(hex) { // 去掉空格每两个字符当一组转成字节 const clean hex.replace(/\s/g, ); const bytes new Uint8Array(clean.length / 2); for (let i 0; i bytes.length; i) { bytes[i] parseInt(clean.substr(i * 2, 2), 16); } return Buffer.from(bytes); } function bufferToHexString(buffer) { // Buffer 转 hex每字节两位用空格分隔 return Array.from(buffer) .map((byte) byte.toString(16).padStart(2, 0)) .join( ); }这个改动只需要动渲染进程和 preload 的传参类型主进程里的 dgram 收发逻辑不用变化。在这个基础上还可以进一步做协议帧解析——比如收到数据后按固定的头部长度和字段偏移拆解把命令字、长度、载荷、校验和分列显示。这类改造对排查私有协议问题很有价值聊到这儿能顺手把一个带seq自增和timestamp的自定义帧格式直接解析出来。5.2 改造方向二复用 socket支持多客户端会话管理原始实现里每次发送都新建 socket这在低频测试中可以接受但做性能测试或者需要维护会话状态时就不太合适。改成复用 socket 的思路启动时创建一个全局的 dgram socket发送时不再创建销毁接收时统一走一个message监听。// main.js 复用 socket 方案的简化代码 const dgram require(dgram); // 全局共享 socket应用启动后创建一次 const sharedSocket dgram.createSocket(udp4); sharedSocket.bind(() { // 绑定随机端口仅用于接收回包 console.log(shared socket bound); }); sharedSocket.on(message, (msg, rinfo) { // 同一个 socket 接收所有来源的回包创建会话表 const sessionKey ${rinfo.address}:${rinfo.port}; handleResponse(sessionKey, msg); }); function sendWithSession(host, port, payload) { return new Promise((resolve, reject) { const buffer Buffer.from(payload); sharedSocket.send(buffer, 0, buffer.length, port, host, (err) { if (err) return reject(err); // 等待对端回包3 秒超时则报错 setTimeout(() reject(new Error(response timeout)), 3000); }); }); }如果遇到的场景是「一个客户端要和多个不同地址的服务通信每个地址维护独立会话」那在共享 socket 上按rinfo.address:rinfo.port做会话表是常见做法。你可能会在改造更复杂的场景时遇到需要把回包和请求关联的问题——这个问题在 UDP 场景下的标准解法是「基于 socket 的并发超时管理配上请求序列号做关联」。这个 Demo 的 restful 不做关联也能收发但从工具走向生产这一步绕不开。5.3 改造方向三打包发布与自动重连开发调试跑通了最终要分发给别人用需要配置打包。这个项目加electron-builder或electron-packager之后一条命令就能产出安装包。# 安装打包工具 npm install --save-dev electron-builder # 打包 Windows 安装包 npx electron-builder --win # 打包 macOS 安装包在 macOS 上执行 npx electron-builder --mac打包配置可以写在package.json的build字段里指定appId、产品名、图标路径和输出目录。注意 Electron 应用打包后体积偏大这是框架决定的不要试图在这方面做什么极端优化——asar压缩机制在 resources 目录里放着不必纠结。另一个高频需求是自动重连机制。比如给设备调试时UDP 服务器启动后如果因为端口冲突退出应用应该自动尝试换个端口重绑或者弹出提示而不是直接崩溃。这类逻辑可以加在main.js的error事件处理里遇到EADDRINUSE时自动port 1重试。从那以后我每次写完 Electron 的网络功能都会强制走一遍「改端口重试 → 非法输入 → 异常断网」三个场景确认应用不会直接挂掉再收工。整体来说这个 electron-udp 项目是一个把 Electron 主进程、IPC 桥接、Node.js 原生网络模块整合在一起的完整示例。沿着上面的路径做三次递进式改造协议解析 → socket 复用 → 打包加固你的能力就能从「跑通 Demo」提升到「做成工具」。如果改造过程中遇到问题建议反复看 main.js 里 dgram 的调用方式和 preload 的桥接逻辑——项目的问题基本都集中在这两个文件里。希望帮到你。本文还有配套的精品资源点击获取