pytcper TCP调试助手 - 开发记录

用户文档(功能总览 / 运行环境与启动 / 界面操作说明 / 二进制分发版)见 README.md;本文档整合项目结构、源码分析、测试情况、源码 Bug 分析、构建打包与系统兼容性、常见问题、后续可扩展方向与版本历史。

一、项目结构

Tcp调试工具/
├── tcp_debug_tool.py     # 入口模块(聚合导出,`python tcp_debug_tool.py` 启动;关键符号 main())
├── tcp_utils.py          # 常量与工具函数(ENCODINGS、encode_text、decode_bytes、guess_decodable、close_socket)
├── base_tab.py           # 公共标签页基类(_BaseTab:日志区、发送区、消息泵、循环发送、历史、统计)
├── client_tab.py         # TCP 客户端标签页(ClientTab:连接/断开、自动重连、代际令牌)
├── server_tab.py         # TCP 服务端标签页(ServerTab:多客户端、定向/广播、踢线、keepalive)
├── app.py                # 主窗口与程序入口(App:Notebook 双标签页、窗口记忆、关于菜单)
├── start.pyw             # 无控制台窗口启动入口(GUI 子系统,pythonw 执行)
├── 启动调试助手.bat       # 一键启动器(本机 pythonw 后台运行,无 DOS 窗口)
├── smoke_test.py         # 无界面冒烟测试脚本(14 项)
├── build_linux.sh        # Linux 构建脚本(PyArmor 加密 + PyInstaller 打包)
└── .tcp_tool_config.json # 运行配置(窗口大小),首次关闭窗口时自动生成

依赖单向无环:tcp_utils → base_tab ← client_tab / server_tab → app → 聚合入口import tcp_debug_tool 与启动命令保持不变。

二、源码分析(技术实现要点)

  1. 线程模型:连接 / 接收 / 监听全部在后台线程执行(daemon),界面永不阻塞;socket 操作与 tkinter 界面更新通过 queue.Queue + 主线程 50ms 周期 after() 消息泵(_drain)解耦,避免跨线程操作 tkinter;
  2. 线程安全:tkinter 变量只在主线程访问;子线程通过 trace_add 同步的普通属性缓存读取编码 / 显示配置,避免跨线程访问 tk 变量(修复了切换编码后立即收发导致乱码的问题);收发统计使用独立互斥锁 _stats_lock 保护累加,多客户端并发接收不丢计数;
  3. 流式增量解码:接收端按连接维护 codecs.getincrementaldecoder 状态,多字节字符(GBK / UTF-8 等)被 TCP 拆到多个数据包时不会出现半个字节的乱码;编码或「接收显示HEX」切换时自动重建解码器;
  4. 自动编码探测:服务端无法获知客户端发送字节的编码,当按当前编码解码出替换字符(乱码)时,guess_decodable 在 UTF-8 / GBK / GB2312 / BIG5 / UTF-16LE / Latin-1 中挑选替换字符最少的方案显示(仅显示用途,不改变流式解码器状态,不误伤跨包分包);
  5. TCP keepalive 死连接检测:对每个接入的客户端 socket 开启 keepalive(空闲 30s 探测 / 间隔 10s / 失败 3 次),客户端拔网线 / 崩溃(无 FIN/RST)时 recv 最终抛错并触发 _remove_client,及时从客户端列表移除,不再无限期显示在线;
  6. 发送限时:发送统一走带超时封装的 _sendall_safe(单次 5s,空闲 30s),对端接收窗口已满或网络中断而未被 TCP 检测到时,界面不会因 sendall 阻塞而冻结;接收循环对超时单独处理为继续等待,不误判断线;
  7. 连接代际令牌:每次发起 / 断开连接递增 _conn_epoch,过期的连接成功 / 失败 / 断线消息被丢弃;连接进行中重复点击按钮会被守卫拦截,不再产生并发连接线程与界面状态错乱;
  8. 周期任务管理:消息泵、在线时长刷新、循环发送、自动重连均记录 after 句柄,关闭窗口时统一取消,无残留回调报错;
  9. 资源清理shutdown()_close_conns() 统一关闭 socket、取消周期回调;正在连接的线程在窗口关闭后不再建立连接;
  10. 健壮性细节:日志区超过 5000 行自动裁剪头部防止内存无限增长;客户端列表选中项按「ip:port」名称匹配 socket(而非索引),避免列表未刷新时发错对象,且每秒刷新重建列表时按名称恢复选中项,点选后不会因在线时长更新而丢失选择;窗口位置完全落在屏幕外时不恢复,防止窗口不可见。

三、测试情况

  • py -m py_compile tcp_debug_tool.py:语法编译检查通过;
  • py smoke_test.py:无界面冒烟测试 14 项,覆盖:
    1. 工具函数(编码转换、HEX、时长格式化)
    2. 服务端启动 + 客户端连接
    3. 客户端列表在线时长显示
    4. 客户端 GBK 中文发送 → 服务端 GBK 接收
    5. 服务端 HEX 发送 → 客户端 HEX 显示
    6. 收发统计(条数与字节数)
    7. 广播 + 历史报文记录与切换
    8. 定时循环发送(3 次自动停止)
    9. 发送文件(原样字节)
    10. 导出收发记录
    11. 服务端断开所选客户端
    12. 自动重连(断线 → 服务恢复 → 自动连上)
    13. 断开连接 / 停止服务(资源清理无残留回调)
    14. 窗口大小记忆
  • v1.1 针对性验证(独立脚本):
    • 增量解码:GBK 多字节字符按 3 字节分包模拟 TCP 拆包,仍完整解出「你好服务器」;
    • 并发统计:8 线程 × 2000 次并发累加,收发统计无丢失(16000/16000)。
  • v1.1.1 针对性验证(独立脚本):
    • 点选客户端后等待 2 次列表刷新(2.3s),选中状态保留(curselection 不为空);
    • 选中保留后定向发送成功(客户端收到报文)、「断开所选」成功断开对应客户端。
  • v1.2.0 后续针对性验证(独立脚本):
    • 客户端 / 服务端标签页均存在「清除」按钮;
    • 输入内容后点击清除,发送框清空、历史导航索引重置。
  • v2.0.001 针对性验证(独立脚本):
    • 跨编码显示:客户端 GBK / UTF-16 发送,服务端界面保持 UTF-8,收发记录正确显示中文(自动编码探测);
    • 断开同步:客户端正常断开后服务端立即移除列表项;
    • _enable_keepalive 生效,SO_KEEPALIVE 可正常设置;
    • 13 种编码标签端到端编码 / 解码均正常,UTF-8(BOM) 前导 BOM 正确。

当前状态(2026-08-20 会话)

  • 冒烟测试 PASS 1-13 全绿(含本次修复的踢线 / 自动重连 / 断开停止);第 14 项受 app.py 最小宽度改动影响(见第四节 Bug 分析);
  • 便携 Python 3.11.16(Tk 9.0)下完整运行冒烟测试:PASS 1-13 全部通过
  • 高兼容版二进制在 X11 环境启动正常,进入 tkinter GUI 事件循环。

四、源码 Bug 分析

对全部源码逐文件审查,发现并修复 2 个真实 Bug(均在冒烟测试中验证)。

Bug 1(已修复):Linux 下直接 close 正被阻塞 recv 的 socket 不生效

  • 位置base_tab.py / client_tab.py / server_tab.py 中所有对连接 socket 的直接 close()
  • 现象:服务端「断开所选客户端」后,客户端始终感知不到断开;冒烟测试第 11 项稳定失败(踢线后客户端未断开
  • 根因:Linux 上若在另一线程正阻塞于 recv() 时直接 close() 该 socket,关闭不会真正生效——对端收不到 FIN,阻塞的 recv 线程永久阻塞(Windows 上 close 会中断 recv,因此开发机上未暴露)。已用纯 socket 脚本复现确认
  • 修复tcp_utils.py 新增 close_socket()shutdown(SHUT_RDWR) 唤醒阻塞线程,再 close;替换全部相关关闭点(含客户端断开、服务端踢线/移除/停止)
  • 验证:冒烟测试第 11-13 项(踢线、自动重连、断开/停止)全部通过

Bug 2(已修复):_accept_loop 双重读取 self.server_sock 的竞态

  • 位置server_tab.py _accept_loop
  • 现象:循环条件与 accept() 调用分别读取 self.server_sock,停止服务瞬间可能读到 None 后调用 None.accept()AttributeError 使 accept 线程崩溃
  • 修复:循环顶部一次性捕获 srv = self.server_sock,为 None 即退出,消除竞态窗口
  • 验证:冒烟测试第 2/13 项(服务端启动/停止)通过

其他审查结论(非缺陷,未改动)

  • _refresh_stats_label 无锁读取统计字典:GIL 下仅可能短暂滞后,无害
  • CONFIG_FILE 基于 __file__ 定位:单文件打包后配置写入临时解包目录,窗口大小记忆无法跨运行持久化(见第五节)
  • 冒烟测试第 14 项(窗口大小记忆):根因是工作区 app.py 在会话中途被外部修改,minsize 由 720 提升至 1200(标题同步增加空格),最小宽度超过需恢复的 900,窗口被钳制到 1200 宽导致断言失败——非显示环境问题(此前"环境伪影"的结论已更正)。该改动与代码原注释的设计意图(先恢复尺寸、再设最小值避免钳制)相冲突,会使窗口大小记忆对较窄窗口失效;若不需要可还原。在原始 minsize(720) 下,第 14 项在会话早期已完整通过(PASS 1-14 全绿)

五、构建、打包与系统兼容性

5.1 源码加密(PyArmor,可选加固)

pyarmor gen -O obf tcp_debug_tool.py tcp_utils.py base_tab.py client_tab.py server_tab.py app.py
  • 6 个模块加密为字节码 blob + 引导头,源码不可直接阅读;
  • 加密产物整体移入 obf/,可在打包时一并以 --add-data "obf:obf"--paths obf 打入二进制;
  • 加密提高静态破解门槛,运行时仍需还原字节码,无法绝对不可破解(客户端加密方案通病);
  • PyArmor trial 版产物标注 (trial),正式分发请注册许可。

5.2 构建命令

本机版(系统 Python,x86_64,要求 glibc ≥ 2.38):

python3 -m PyInstaller --onefile --name pytcper --windowed --clean \
  --distpath dist-linux --workpath build/pyinstaller-onefile tcp_debug_tool.py

高兼容版(便携 Python 构建,可运行于 openEuler 22.03 等 glibc ≥ 2.17 系统):

# 下载 python-build-standalone 便携 Python(glibc 2.17 兼容、内置 tkinter,任意目录解压,以 /tmp/python 为例):
curl -L -o pybs.tar.gz "https://github.com/astral-sh/python-build-standalone/releases/download/<tag>/cpython-3.11.16%2B<tag>-x86_64-unknown-linux-gnu-install_only.tar.gz"
tar -xzf pybs.tar.gz
/tmp/python/bin/python3 -m pip install pyinstaller

# 构建(注意显式补齐 Tcl/Tk 9.0 库与数据目录,默认 hook 不收集 9.0):
/tmp/python/bin/python3 -m PyInstaller --onefile --name pytcper --windowed --clean \
  --distpath dist-linux-oe2203 --workpath build/pyinstaller-portable \
  --add-binary "/tmp/python/lib/libtcl9.0.so:." \
  --add-binary "/tmp/python/lib/libtcl9tk9.0.so:." \
  --add-binary "/tmp/python/lib/thread3.0.6/libtcl9thread3.0.6.so:." \
  --add-data "/tmp/python/lib/tcl9.0:_tcl_data" \
  --add-data "/tmp/python/lib/tk9.0:_tk_data" \
  tcp_debug_tool.py

目录方式(启动快,便于调试排查):

python3 -m PyInstaller --name pytcper --windowed --clean \
  --distpath build --workpath build/pyinstaller-dir tcp_debug_tool.py

5.3 产物清单

产物 路径 大小 说明
单文件二进制(高兼容版) dist-linux-oe2203/pytcper 22 MB 便携 Python 构建,要求 glibc ≥ 2.17,可运行于 openEuler 22.03(推荐分发
单文件二进制(本机版) dist-linux/pytcper 12 MB 系统 Python 构建,要求 glibc ≥ 2.38,仅限新系统
目录方式二进制 build/pytcper/pytcper 1.6 MB 含运行时依赖目录,启动更快
aarch64 发布包 release/pytcper-v2.0.001-linux-aarch64-onefile.tar.gz 12 MB Linux ARM64,PyArmor 加密 + PyInstaller 打包
构建规格 pytcper.spec 可复用:pyinstaller pytcper.spec

5.4 运行验证

  • 本机版 / 目录方式 / aarch64 产物均已在 X11 显示环境后台启动 3-4 秒,确认正常进入 tkinter GUI 事件循环(与 build_linux.sh 验证方式一致);
  • 高兼容版二进制在 X11 环境启动正常(捆绑 Tk 9.0);便携 Python 3.11.16 下完整运行冒烟测试 PASS 1-13 全部通过

5.5 系统兼容性(glibc 版本要求)与 openEuler 22.03 适配

  • 二进制运行时依赖的不是 gcc 版本,而是 glibc(动态链接器)版本;gcc 仅在构建期使用(本环境 gcc 12.3.1)。
  • 本机版(系统 Python 3.11.13,构建于 openEuler 25.09 / glibc 2.38):捆绑的 libpython3.11.solibtk/libtcllibX11 等引用 GLIBC_2.38 符号(如 __isoc23_strtolfmod),要求 glibc ≥ 2.38,无法在 openEuler 22.03(glibc 2.34)上运行(报 version GLIBC_2.38 not found)。
  • 高兼容版(便携 Python 3.11.16):经实测,外层 bootloader 最高要求 GLIBC_2.14,全部捆绑库最高要求 GLIBC_2.17(libpython 2.17、libtcl9tk9 2.15、libtcl9 2.14、_tkinter 2.2),可运行于 openEuler 22.03(glibc 2.34),也覆盖 CentOS 7(2.17)、Ubuntu 18.04+、Debian 10+ 等 glibc ≥ 2.17 的系统。

降低 glibc 要求的方法(三选一):

  1. 便携 Python 构建(本仓库采用):python-build-standalone 的 CPython 针对 glibc 2.17 构建,产物自动兼容 glibc ≥ 2.17(见 5.2)。构建机无需容器 / 特权;
  2. openEuler 22.03 容器内构建docker run --rm -v $PWD:/src openeuler/openeuler:22.03-lts-sp4 bash -c "dnf install -y python3 python3-tkinter && pip install pyinstaller && cd /src && python3 -m PyInstaller ..."
  3. 在目标系统本机构建:直接在 openEuler 22.03 机器上装 python3 + python3-tkinter + pyinstaller 后执行 5.2 原命令。

5.6 说明与限制

  • 架构绑定 / 不支持交叉编译:Windows 上生成的 exe 只含 Windows 解释器与 DLL;Linux 产物必须在 Linux x86_64 环境(已装 Python 3.8+ 且 python3-tk / python3-tkinter)构建;aarch64 同理需在 ARM64 机器构建;
  • 无需 Python:exe 与 Linux 单文件均为自包含(内嵌 Python 运行时与 tkinter),目标机无需安装 Python
  • 单文件模式的配置持久化tcp_utils.CONFIG_FILE 基于 __file__ 定位,单文件打包后落在临时解包目录(每次运行重建),窗口大小记忆无法跨运行持久化;如需持久化,可将配置路径改为用户目录(如 ~/.config/pytcper/);
  • 构建产物不入库dist/build/dist-linux/dist-linux-oe2203/obf/*.spec 均为构建输出,已在 .gitignore 忽略。

六、无控制台窗口启动(优化 DOS 黑窗)

6.1 原因

py / python 直接运行 tcp_debug_tool.py 时,解释器为控制台子系统程序,每次打开都会弹出黑色 DOS 窗口。GUI 应用不应有此控制台。

6.2 方案

改用 pythonw(GUI 子系统 Python,随官方安装包提供,位于安装目录)运行,启动时不创建控制台窗口。

入口 说明
start.pyw .pyw 扩展名脚本,双击 / 运行默认由 pythonw 执行,无控制台窗口
启动调试助手.bat 一键启动器:定位 pythonw 后台运行 tcp_debug_tool.pystart "" pythonw ...),批处理窗口立即自行退出

.bat 内已做回退:找不到 pythonw 时退回 python(此时会保留控制台,但属兜底)。

6.3 优先级建议

  • 日常使用:双击 启动调试助手.batstart.pyw,均无黑窗;
  • 开发 / 调试:仍用 py tcp_debug_tool.py(控制台可查看打印与报错)。

6.4 覆盖验证

  • py -m py_compile start.pyw 通过;
  • pythonw 实际启动 tcp_debug_tool.py,进程正常进入 GUI 事件循环(tasklist 可见 pythonw.exe 存活),无控制台窗口。

七、常见问题

问题 处理方式
启动服务报「地址被占用」 端口被其他程序占用,更换监听端口
连接超时 / 拒绝 确认目标服务地址、端口、防火墙放行
接收中文乱码 收发双方使用一致的编码(如设备用 GBK,工具就选 GBK)
HEX 发送报错 输入需为偶数个十六进制字符,如 01 0A FF
循环发送没反应 确认已连接服务器 / 已选中目标客户端,发送失败会自动停止并提示

八、后续可扩展方向

  • 报文保存与回放(录制 → 重放)
  • 收发数据按关键字过滤 / 高亮
  • 多路连接同时管理
  • 数据加解密(AES / 自定义算法)

九、版本更新记录

v2.0.001(2026-08-19)— 版本号更新

变更 说明
更新 软件版本号由 v1.3.0 更新为 v2.0.001

v1.3.0(2026-08-18)— 更名与版本号更新

变更 说明
更新 软件名由「TCP 调试助手」更名为「pytcper TCP调试助手」(窗口标题、关于对话框、文档同步)
更新 软件版本号由 v1.2.0 提升至 v1.3.0

v1.2.0 后续变更(2026-08-18)— 功能增强与重构

变更 说明
新增 发送区「清除」按钮(客户端 / 服务端通用),一键清空发送框并重置历史导航起点
重构 单文件(1036 行)拆分为模块化结构:tcp_utils / base_tab / client_tab / server_tab / app + 聚合入口,依赖单向无环,import tcp_debug_tool 与启动命令保持不变
更新 「关于开发者」对话框标签由「开发版本所有者」改为「版权所有者」
更新 README 拆分为用户文档 + 开发记录(dev-doc.md)

验证py -m py_compile 全部模块通过;smoke_test.py 14 项回归通过;清除按钮功能实测通过。

v1.2.0(2026-08-18)— 新增「关于开发者」

变更 说明
新增 菜单栏「帮助 → 关于开发者」对话框,显示版权所有者(micoder)与项目地址,点击地址可跳转浏览器打开
更新 软件版本号由 v1.1.1 提升至 v1.2.0

v1.1.1(2026-08-18)— 服务端列表选中修复

级别 问题 修复方案
中等 服务端客户端列表每秒刷新(在线时长更新)时重建列表,清空用户点选的客户端:无法断开所选、无法定向发送,仅广播可用 刷新前记录选中项的客户端名称,重建后按名称恢复选中并滚动到可见位置;客户端若已断开则自然不恢复

验证:点选后等待 2.3s(跨 2 次刷新)选中保留;定向发送 / 断开所选均正常;py -m py_compile 通过;smoke_test.py 14 项全部通过。

v1.1(2026-08-18)— Bug 修复版本

级别 问题 修复方案
严重 多字节字符(GBK/UTF-8)跨 TCP 分包时被截断成乱码 接收端改用按连接维护的增量解码器(codecs.getincrementaldecoder),编码/HEX 视图切换自动重建
严重 对端异常时 sendall 无超时阻塞,主线程冻结、界面卡死 发送统一走带超时的 _sendall_safe(单次 5s / 空闲 30s);接收循环对 socket.timeout 单独处理为继续等待
中等 双击「连接」启动多个连接线程;过期的连接成功/失败/断线消息导致界面状态错乱 引入连接代际令牌 _conn_epoch 过滤过期消息;_connecting 守卫拦截重复点击
多客户端并发接收时统计累加丢计数 _bump_stats 加独立互斥锁保护
关闭窗口瞬间正在连接的 socket 泄漏 _connect_worker 在窗口已关闭时关闭 socket 并退出
HEX 发送 + 自动追加换行会在字节流尾部多出 0D 0A HEX 发送模式下不再追加换行
日志区无限增长 超过 5000 行自动裁剪头部
客户端断开到列表刷新的间隙,选中索引错位可能发错对象 选中项按「ip:port」名称匹配 socket(取最新连接)
手动编辑发送框后按 ↑/↓ 历史跳转起点错乱 导航前比对当前文本与历史项,不一致时重新定位
窗口位置恢复到已移除的屏幕外区域,窗口不可见 恢复前校验几何位置是否落在当前屏幕内

验证py -m py_compile 通过;smoke_test.py 14 项全部通过;增量解码与并发统计两项针对性脚本验证通过。