从 IP Helper API、Restart Manager 到 Electron + C++ DLL 架构,说明 OccupyFinder 如何在本机查询端口占用与文件锁,以及结束进程的能力边界。
本文面向对技术原理感兴趣的同学和读者,尽量用真实、可验证的方式解释:Windows 如何记录「谁占了哪个端口、谁锁了哪个文件」;OccupyFinder 调用了哪些系统 API;界面上的「结束进程」究竟做了什么;又有哪些场景查不全或杀不掉。若您只想了解如何使用,请先阅读 《功能介绍》。
| 章节 | 内容 |
|---|---|
| 一、从两个报错说起 | 端口占用与文件被锁背后是什么 |
| 二、Windows 网络与进程基础 | 连接表、PID、权限 |
| 三、和 netstat / Handle 的区别 | 本工具在做什么 |
| 四、整体架构 | Electron + C++ DLL |
| 五、端口占用查询 | IP Helper API 全流程 |
| 六、文件占用查询 | Restart Manager 全流程 |
| 七、结束进程与资源释放 | TerminateProcess 与边界 |
| 八、JSON 契约与界面层 | C ABI → React 表格 |
| 九、权限与完整性级别 | 为何非管理员会 <access denied> |
| 十、真实场景走查 | 开发端口、日志文件、系统服务 |
| 十一、能力边界 FAQ | 能做什么、不能做什么 |
| 附录 A | API 与错误码对照 |
开发时常见:
Error: listen EADDRINUSE: address already in use :::8080
含义是:本机已有一个(或多个)套接字绑定了 8080。操作系统维护一张「当前 TCP/UDP 连接与监听表」,每条记录带有本地地址、端口、协议、连接状态,以及所属进程 PID。
OccupyFinder 的端口页,就是把这张表按您输入的端口号(或「留空列出全部监听」)筛出来,再补上进程名、路径、用户名,方便定位是谁在听。
删除或覆盖文件时,若其他进程仍打开着该文件的句柄(handle),资源管理器会提示「文件正在使用」。
Windows 从 Vista 起提供 Restart Manager(重启管理器) API:安装程序在更新被占用的 DLL/EXE 前,可先问系统「哪些进程正在使用这些文件」,必要时提示用户关闭。OccupyFinder 借用同一套机制做只读枚举——列出持有该路径相关资源的进程。
需要先说清楚:OccupyFinder 不会单独「释放端口」或「解锁文件句柄」。
| 用户操作 | 实际发生的事 |
|---|---|
| 端口页点「结束进程」 | 对占用该端口的进程调用 TerminateProcess,进程退出后内核回收其全部套接字 |
| 文件页点「结束进程」 | 对持有文件句柄的进程同样调用 TerminateProcess,句柄随进程销毁而关闭 |
这是粗暴但常见的排障手段:适合您确认可以关掉的 node.exe、临时日志查看器等;不适合不经确认就结束 sqlservr.exe、System 等关键服务。
| 概念 | 说明 |
|---|---|
| bind / listen | 服务进程在端口上「占位」,等待连接;TCP 状态常为 LISTEN |
| ESTABLISHED | 已建立的 TCP 连接;也占用本地端口,但语义与「端口被服务监听」不同 |
| UDP | 无连接状态;只要 socket 绑定了端口,表中就会出现对应条目 |
OccupyFinder 在 port = 0(列出全部) 时:
LISTEN 状态(dwState == 2),相当于「当前谁在监听」;—)。指定具体端口号时,TCP/UDP 不过滤 LISTEN,该端口上的全部条目都会返回(含已建立连接)。
内核为每个进程分配 PID(Process ID)。IP Helper API 扩展 TCP/UDP 表的关键字段是 dwOwningPid——「这条连接/监听归谁管」。
知道 PID 之后,还可以:
OpenProcess + QueryFullProcessImageNameW → 可执行文件路径;OpenProcessToken + LookupAccountSidW → 运行账户(如 NT AUTHORITY\SYSTEM)。若当前用户权限不足,OpenProcess 失败,界面会显示 <access denied>,但 PID 与端口信息通常仍可见(来自内核表,不依赖打开目标进程)。
文件占用查询不遍历全盘句柄表(那是 Sysinternals Handle 的做法),而是:
RmGetList 返回正在使用该资源的进程列表及应用类型(服务、资源管理器、普通窗口等)。空闲文件返回 空数组 [],与「路径不存在」区分开(后者返回 E_NOT_FOUND)。
| 工具 | 数据来源 | OccupyFinder 的差异 |
|---|---|---|
netstat -ano |
同源的 IP Helper 表 | 已解析 PID→进程名/路径/用户;图形筛选协议与常用端口 |
| 任务管理器 | 按进程看资源 | 难以「从端口 5173 反查进程」 |
| Handle.exe | 对象管理器句柄 | 命令行、偏文件;OccupyFinder 用 Restart Manager + 图形界面,并合并端口查询 |
| TCPView | 类似扩展表 | 功能接近;OccupyFinder 是轻量自研壳 + 文件占用 |
结论:端口侧与 netstat/TCPView 同源;文件侧与安装程序更新前用的 Restart Manager 同源。OccupyFinder 的价值在于合并场景、友好展示、本机一体化,而非发明新的内核钩子。
工具分三层:React 界面、Electron 主进程、C++ 查询引擎(backend.dll)。
flowchart TB
User[用户在 PortPage / FilePage 点查询或结束进程]
UI[Electron Renderer React]
Main[Electron 主进程]
DLL[C++ sdtech_occupyfinder.dll]
Kernel[Windows IP Helper / Restart Manager / Process API]
User --> UI
UI -->|IPC: of:queryPort / of:queryFile / of:terminateProcess| Main
Main -->|koffi 调用 C ABI| DLL
DLL -->|GetExtendedTcpTable RmGetList TerminateProcess| Kernel
DLL -->|JSON 字符串| Main
Main --> UI
Renderer 进程沙箱化,不宜直接加载原生 DLL。主进程:
backend.dll,绑定 sdtech_occupyfinder_query_port 等导出函数;ipcMain.handle('of:queryPort', …) 暴露给 Preload;sdtech_json、日志、错误码)一致;npm run test:backend,26 项)。对外稳定接口定义在 sdtech_occupyfinder_c.h:
| 函数 | 作用 |
|---|---|
sdtech_occupyfinder_init / cleanup |
库生命周期 |
sdtech_occupyfinder_query_port |
端口查询,port=0 列出全部监听/绑定 |
sdtech_occupyfinder_query_file |
文件占用查询 |
sdtech_occupyfinder_terminate_process |
结束进程 |
sdtech_occupyfinder_free_string |
释放 JSON 输出缓冲区 |
protocol_filter 取值:"all" | "TCP" | "UDP"。

端口页:port=0 时枚举 TCP LISTEN 与 UDP 绑定,并按端口号排序
实现位于 PortQueryManager::queryPortJson(PortQueryManager.cpp)。
sequenceDiagram
participant UI as PortPage
participant Main as Electron 主进程
participant DLL as PortQueryManager
participant IPH as iphlpapi.dll
participant PI as ProcessInfo
UI->>Main: queryPort(port, protocol)
Main->>DLL: sdtech_occupyfinder_query_port
alt 包含 TCP
DLL->>IPH: GetExtendedTcpTable IPv4/IPv6
IPH-->>DLL: MIB_TCPTABLE_OWNER_PID 行
end
alt 包含 UDP
DLL->>IPH: GetExtendedUdpTable IPv4/IPv6
IPH-->>DLL: MIB_UDPTABLE_OWNER_PID 行
end
loop 每一行
DLL->>PI: resolveProcessDetails(pid)
PI-->>DLL: processName path user
end
DLL-->>Main: JSON 数组
Main-->>UI: PortOccupant[]
与许多 Win32「先问大小、再分配」API 相同:
nullptr 缓冲区 → 返回 ERROR_INSUFFICIENT_BUFFER,得到所需 size;std::vector<BYTE>,再次调用 → 得到 MIB_TCPTABLE_OWNER_PID 或 IPv6 的 MIB_TCP6TABLE_OWNER_PID。使用的表类型:TCP_TABLE_OWNER_PID_ALL——包含连接状态与 Owning PID。
核心过滤逻辑(简化):
// listAllListening = (port == 0)
if (listenOnly && row.dwState != 2) continue; // 2 = MIB_TCP_STATE_LISTEN
const uint16_t localPort = ntohs(row.dwLocalPort);
if (targetPort != 0 && localPort != targetPort) continue;
UDP 表类型为 UDP_TABLE_OWNER_PID。无 LISTEN 状态,故:
port == 0:返回所有 UDP 绑定;InetNtopA + "%s:%u" → 如 0.0.0.0:8080;[%s]:%u → 如 [::]:53。TCP 状态映射为可读字符串(LISTEN、ESTABLISHED 等)。
port == 0 时,结果按端口号升序,同端口再按协议、地址字符串排序。
同一 PID 可能对应多行(多地址族、多连接),ProcessInfo 用 unordered_map 缓存 resolveProcessDetails(pid),避免重复 OpenProcess。
| 字段 | 含义 |
|---|---|
protocol |
"TCP" / "UDP" |
localAddress |
本地地址:端口 |
state |
TCP 状态;UDP 为 "—" |
pid |
所属进程 ID |
processName |
可执行文件名;无权限时为 <access denied> |
processPath |
完整路径 |
user |
可选,运行账户 |
实现位于 FileQueryManager::queryFileJson(FileQueryManager.cpp)。
flowchart TD
Start[queryFileJson path] --> Exists{GetFileAttributesW 存在?}
Exists -->|否| NotFound[E_NOT_FOUND]
Exists -->|是| Session[RmStartSession]
Session --> Register[RmRegisterResources 注册路径]
Register --> Size[RmGetList 第一次: 取 procInfoNeeded]
Size --> List[RmGetList 第二次: 填充 RM_PROCESS_INFO]
List --> End[RmEndSession]
End --> JSON[组装 JSON + resolveProcessDetails]
步骤说明:
| 步骤 | API | 说明 |
|---|---|---|
| 1 | GetFileAttributesW |
路径必须存在;不存在直接 E_NOT_FOUND |
| 2 | RmStartSession |
创建 RM 会话,获得 sessionKey |
| 3 | RmRegisterResources |
注册 1 个文件/目录路径(UTF-16) |
| 4 | RmGetList(两次) |
第一次 nullptr 缓冲区得所需数量;第二次填入 RM_PROCESS_INFO 数组 |
| 5 | RmEndSession |
关闭会话 |
根据 RM_PROCESS_INFO.ApplicationType 映射为中文标签:
| RM 类型 | 界面 lockKind |
|---|---|
RmService |
服务 |
RmExplorer |
资源管理器 |
RmMainWindow / RmOtherWindow |
应用程序 / 窗口 |
RmCritical |
关键进程 |
| 其他 | 占用 |
?、* 的路径字符(E_INVALID_ARG);RmGetList sizing 可能失败(工程内 GTest 对目录场景改为「目录内文件」验证);[],前端显示绿色空态:「未发现占用进程,可尝试删除该文件」。
文件页:对临时测试文件查询,无进程占用时的空态
| 维度 | Restart Manager | Handle.exe |
|---|---|---|
| 入口 | 给定路径,问「谁妨碍我更新/使用它」 | 枚举进程或全局句柄,再过滤 |
| 权限 | 受 UAC 影响,系统进程可能不全 | 需管理员才较完整 |
| 适用 | 安装程序、OccupyFinder 文件页 | 深度调试 |
实现位于 terminateProcessByPid(ProcessTerminator.cpp)。这是 OccupyFinder 唯一的「释放资源」手段。
sequenceDiagram
participant UI as PortPage / FilePage
participant Main as 主进程
participant DLL as ProcessTerminator
participant OS as Windows 内核
UI->>UI: ConfirmModal 用户确认
UI->>Main: terminateProcess(pid)
Main->>DLL: sdtech_occupyfinder_terminate_process
DLL->>DLL: 保护 PID 4 / 自身 / 非法值
DLL->>OS: OpenProcess PROCESS_TERMINATE
DLL->>OS: TerminateProcess(exitCode=1)
OS-->>DLL: 成功或 ACCESS_DENIED
UI->>UI: 刷新查询
| 条件 | 返回 |
|---|---|
pid <= 0 |
E_PROTECTED |
pid == 4(System) |
E_PROTECTED |
| 等于当前进程 PID | E_PROTECTED |
OpenProcess 失败 |
E_ACCESS_DENIED |
TerminateProcess 失败 |
E_ACCESS_DENIED |
没有「只关闭某个 socket」或「只释放某个句柄」的 API 封装——那是驱动级或调试器级能力,超出本工具范围。
| 场景 | 预期 |
|---|---|
| 开发服务器占 5173 | 进程退出 → 端口释放 → 可重新 npm run dev |
| 记事本打开日志 | 进程退出 → 句柄关闭 → 可删除文件 |
| 系统服务占 443 | 往往 E_ACCESS_DENIED 或杀后服务自动重启;不应随意结束 |
前端在成功后调用 runQuery() 自动刷新列表,确认占用是否消失。
Preload 暴露 window.occupyFinderAPI.queryPort / queryFile / terminateProcess。Renderer 的 queryService.ts 只做错误码映射与类型包装。
| 码 | 常量 | 界面典型文案 |
|---|---|---|
0 |
OK |
成功 |
-1 |
E_INVALID_ARG |
参数无效 |
-2 |
E_NOT_FOUND |
路径不存在 |
-10 |
E_ACCESS_DENIED |
权限不足 / 终止失败 |
-11 |
E_PROTECTED |
无法终止该系统进程 |
-99 |
E_INTERNAL |
内部错误 |
DLL 通过 copy_string 分配 JSON 字符串;主进程 koffi 读取后调用 sdtech_occupyfinder_free_string 释放,避免泄漏。
主进程 of:isElevated 通过 net session 是否成功粗测是否提升权限;非管理员时 PortPage/FilePage 显示黄条,与 <access denied> 占位符呼应。
IP Helper 与 Restart Manager 返回的 PID 来自内核/系统服务,一般不需要打开目标进程。
而 QueryFullProcessImageNameW 需要 OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)。目标若为高完整性(如 SYSTEM 服务),普通用户进程会被 UIPI 拒绝 → processName / processPath 显示 <access denied>。
OpenProcess(PROCESS_TERMINATE) 同样受完整性级别与 DACL 约束。保护进程、关键系统服务、其他用户的进程,常返回 E_ACCESS_DENIED。
query_port(5173, "all") → 返回 TCP LISTEN 行,pid 指向 node.exe;terminate_process(pid);EADDRINUSE 消失。query_port(0, "all") → 仅 TCP LISTEN + 全部 UDP 绑定;<access denied>。query_file("D:\\logs\\app.log") → Restart Manager 返回占用进程列表;[] → 文件未被占用,可能是路径错误、权限或网络盘延迟;| 测试 | 验证点 |
|---|---|
PortQueryApiTest |
非法端口、port=0 仅 LISTEN、排序、自发进程监听 135 |
FileQueryApiTest |
空闲文件 []、共享/独占锁持有进程 |
TerminateProcessApiTest |
PID 4、自身、不存在进程 |
运行:cd Frontend && npm run test:backend。
Q:能不能只释放端口、不杀进程?
A:当前实现不能。需自行在任务管理器结束线程/服务,或使用目标软件自带的「停止」功能。OccupyFinder 仅提供 TerminateProcess。
Q:UDP 为什么状态是 —?
A:UDP 无 TCP 式连接状态机;表项只表示「该进程绑定了此端口」。
Q:port=0 为什么 TCP 只有 LISTEN?
A:有意设计为「监听一览」;若查某端口上的所有连接(含 ESTABLISHED),请输入具体端口号。
Q:文件夹路径为什么有时失败?
A:Restart Manager 对「目录」与「文件」的支持因系统/路径而异;建议指向具体文件,或保证目录内存在可注册的文件路径。
Q:和 Linux lsof / fuser 一样吗?
A:思路类似(查端口/查文件占用),但本 DLL 仅实现 Windows;非 Win32 平台 API 返回 E_INTERNAL。
Q:数据会上传吗?
A:不会。查询与终止均在本地 DLL 完成,JSON 只经 Electron IPC 到界面。
Q:结束进程安全吗?
A:取决于您结束的进程。工具在 UI 层有确认框,DLL 层拒绝 PID 4 与自身;其余风险由操作者承担。
| 用户动作 | DLL 入口 | 核心 Win32 API |
|---|---|---|
| 查端口 | query_port |
GetExtendedTcpTable / GetExtendedUdpTable |
| 查文件 | query_file |
RmStartSession → RmRegisterResources → RmGetList |
| 结束进程 | terminate_process |
OpenProcess + TerminateProcess |
| 进程详情 | (内部) | QueryFullProcessImageNameW、OpenProcessToken |
| 条件 | 端口查询 | 文件查询 | 终止进程 |
|---|---|---|---|
| 参数非法 | E_INVALID_ARG |
E_INVALID_ARG |
E_PROTECTED |
| 路径不存在 | — | E_NOT_FOUND |
— |
| 无占用 | 空数组 | 空数组 | — |
| 权限不足 | 行内 <access denied> |
可能列表不全 | E_ACCESS_DENIED |
| 系统/自身 PID | — | — | E_PROTECTED |
OccupyFinder 的原理可以浓缩为:
端口占用问 IP Helper 表,文件占用问 Restart Manager,进程信息问 Process API;「清理」则是经用户确认后终止进程,由内核回收端口与句柄。
读懂这三条系统接口,就读懂了本工具的能力边界:能查的是内核愿意告诉你的 PID 与连接,能放的是你愿意结束的进程。
若您想亲手试用,请参阅 《功能介绍》。若希望继续深入,可在 Microsoft Learn 查阅 GetExtendedTcpTable、Restart Manager、TerminateProcess、UIPI 等主题。