当前位置: 首页 > news >正文

【python】我用AI辅助开发了LanChat 局域网即时通讯的小软件

LanChat 局域网即时通讯软件 — 技术文档

起初做这个项目是我工作中有两台电脑工作,另一台又是外网,不方便下载聊天工具,那么我就突发奇想用AI辅助开发一个局域网聊天工具,方便复制和发送文件,而且关闭聊天框后会自动清除聊天记录,方便上班发悄悄话!!!

github 源代码链接在此

项目概述

LanChat 是一款基于 Python 的局域网即时通讯软件,支持文本聊天、文件传输、在线用户自动扫描,采用 TCP + UDP 混合协议实现 P2P 通信。


点击发送就可以发送任何文件


右击气泡即可复制内容,和保存文件

目录结构

lan_message/ ├── main.py # 程序入口 ├── network.py # 网络层(UDP 发现 + TCP 通信) ├── ui.py # Tkinter 图形界面 ├── LanChat.spec # PyInstaller 打包配置 ├── dist/ # 构建产物 │ ├── main.exe │ └── main.rar └── docs/ └── 技术文档.md # 本文

架构设计

整体架构

通信架构

网络层 —network.py

核心数据类

字段说明
Peername,ip,last_seen,online对等节点信息
ChatMessagesender,content,timestamp,is_file,file_name,file_size,file_path,is_self聊天消息

常量

常量说明
DISCOVERY_PORT9876UDP 广播发现端口
TCP_PORT9877TCP 消息传输端口
BROADCAST_ADDR255.255.255.255子网广播地址
DISCOVERY_INTERVAL3s心跳广播间隔
PEER_TIMEOUT12s对等节点超时阈值
BUFFER_SIZE65536网络缓冲区大小
CHUNK_SIZE32768文件传输分块大小

NetworkManager 类

线程模型
start() ├── _udp_listener() [daemon] ← 共享 UDP socket,持续监听 "hello" 广播 ├── _udp_broadcaster() [daemon] ← 共享 UDP socket,每 3s 发送 "hello" 心跳 ├── _tcp_server_thread() [daemon] ← 监听 TCP 端口,处理消息/文件接收 └── _queue_processor() [daemon] ← 消费消息队列,回调 UI 层
启动流程
NetworkManager.__init__() └─ 注册回调 (on_message, on_file_progress, on_peers_changed) NetworkManager.start() ├─ 创建单例 UDP socket (SO_BROADCAST | SO_REUSEADDR) ├─ bind 0.0.0.0:9876 │ ├─ 成功 → 启动 _udp_listener │ └─ 失败 → close socket, self.udp_sock = None (仅发送广播) ├─ 启动 _udp_broadcaster ├─ 启动 _tcp_server_thread └─ 启动 _queue_processor
对等发现机制

心跳与超时检测
  • 每个节点每 3s 广播一次{"type": "hello", "name": hostname}
  • 接收方记录peer.last_seen = time.time(), 设置peer.online = True
  • 广播线程每轮循环检查now - peer.last_seen > 12s,超时则标记online = False
  • 离线节点仍然保留在列表中(红色标识),可手动刷新清除
时间线: t=0 t=3 t=6 t=9 t=12 t=15 │ │ │ │ │ │ hello──► hello──► hello──► hello──► (hello 停止) ↑ ↑ ↑ ↑ last_seen=3 last_seen=6 last_seen=9 last_seen=9 now - 9 = 6 < 12 → 仍在线 t=21 │ now - 9 = 12 ≥ 12 → offline
消息发送 (TCP)
send_message(peer_ip, content) ├─ 创建 TCP 连接 → peer_ip:9877 ├─ 4字节小端长度前缀 + JSON 消息体 │ {"type":"msg", "sender", "content", "time"} ├─ sendall └─ close
文件发送 (TCP)
send_file(peer_ip, file_path) ├─ TCP 连接 → peer_ip:9877 ├─ 发送文件头: {"type":"file_offer", "name","size","sender","time"} ├─ 等待接收方 ACK (1字节 0x01) ├─ 分块发送 (CHUNK_SIZE = 32KB) │ └─ 每块发送后推送进度到队列 ├─ close └─ 创建本地 ChatMessage (is_self=True)
文件接收
_handle_tcp_client(conn, peer_ip) ├─ 4字节长度前缀 → JSON 解析 ├─ msg_type == "file_offer" │ ├─ 创建接收目录: ~/Downloads/LanChat/ │ ├─ 处理文件名冲突 (添加 (1), (2)...) │ ├─ 发送 ACK │ ├─ 分块接收写入文件 │ │ └─ 每块推送进度到消息队列 │ └─ 创建 ChatMessage (is_file=True) └─ msg_type == "msg" └─ 创建 ChatMessage → 入队
线程安全设计
  • peers字典由peers_lock(threading.Lock) 保护
  • 所有 UI 更新通过root.after(0, callback)委托到主线程
  • 网络线程通过queue.Queue异步传递消息到 UI 层
  • _queue_processor单线程消费消息队列,避免竞态

UI 层 —ui.py

窗口层级

MainWindow (DnDTk) ├── PanedWindow │ ├── 左侧面板 (Frame, 220px) │ │ ├── Label "在线用户" │ │ ├── Button "🔄 刷新" │ │ └── Listbox (peer_listbox) │ │ │ └── 右侧容器 (Frame, weight=1) │ ├── welcome_frame (初始欢迎页) │ └── ChatView (聊天视图) │ ├── 顶部导航 (← 返回 + 标题) │ ├── Canvas + Scrollbar (消息区域) │ │ └── msg_frame │ │ └── ChatBubbleFrame ×N │ ├── 拖拽提示条 │ └── 底部输入区 │ ├── Text (多行输入) │ ├── Button "发送" │ ├── Button "📎 发送文件" │ └── Label (进度提示)

ChatBubbleFrame — 聊天气泡

渲染流程
__init__(parent, msg) ├─ 创建 Canvas ├─ 创建 content Frame (Canvas 的子控件) ├─ 组装内容 (时间 + 文本/文件控件) ├─ 临时 pack + create_window 测量尺寸 │ ├─ content.winfo_width() │ └─ content.winfo_height() ├─ 删除临时窗口 ├─ 配置 Canvas 最终尺寸 (宽度 + RADIUS, 高度 + 4) ├─ pack 到父容器 (anchor=E/W + 边距) ├─ 绘制圆角矩形背景 (polygon + smooth) └─ create_window 居中嵌入 content
圆角矩形算法
_round_rect(c, x1, y1, x2, y2, r=10) 输入: Canvas 对象, 矩形左上角(x1,y1), 右下角(x2,y2), 圆角半径r 输出: Canvas polygon ID 控制点: (x1+r,y1) ──────────── (x2-r,y1) │ │ │ ┌────────────────┐ │ │ │ content │ │ │ └────────────────┘ │ │ │ (x1,y1+r) (x2,y1+r) │ │ │ │ (x1,y2-r) (x2,y2-r) │ │ (x1+r,y2) ──────────── (x2-r,y2) → 使用 create_polygon(points, smooth=True) smooth 自动在角点生成贝塞尔曲线
气泡布局
条件对齐padx_leftpadx_right背景色
is_self=Truetk.E(右)400#dcf8c6
is_self=Falsetk.W(左)040#ffffff

定时刷新机制

机制触发方式间隔
UDP 事件驱动on_peers_changed回调实时 (收到 hello 时)
定时器兜底_start_peer_refresh_timer2s
手动刷新用户点击 “🔄 刷新”

拖拽文件发送

  • 依赖tkinterdnd2库(基于 TkDnD 原生扩展)
  • MainWindow使用DnDTk()替代tk.Tk()作为根窗口,使所有子控件继承拖放能力
  • ChatView._setup_drag_drop()注册DND_FILES类型
  • 拖放时解析event.data,提取文件路径,调用network.send_file()

右键菜单

消息类型菜单项
文本消息复制文本、复制消息内容
文件消息打开文件、另存文件…、复制消息内容

数据流

文本消息发送

用户输入 → Enter / 点击发送 → ChatView._send_message() → 创建 ChatMessage (is_self=True) → _add_message() 本地显示 → 线程: network.send_message(peer_ip, text) → TCP 连接 → JSON 编码 → sendall → 接收方 _handle_tcp_client() → ChatMessage 入队 → _queue_processor() → on_message() callback → MainWindow._on_message() → root.after(0, ...) → ChatView.receive_message() → _add_message() 显示

文件发送

用户选择文件 / 拖拽 → network.send_file(peer_ip, path) → TCP 连接 → 发送文件头 → 接收方 ACK → 分块发送 (每块32KB) → 每块发完后推送进度到队列 → on_file_progress() → ChatView.update_progress() → 发送完成 → 创建 ChatMessage (is_self=True) → on_message() → 显示文件气泡 接收方: TCP 接收 → 文件头解析 → 创建 ~/Downloads/LanChat/ → 分块写入文件 → 每块推送进度 → UI 进度条 → 完整接收 → ChatMessage (is_file=True) → 显示文件气泡

配置与打包

PyInstaller 打包

打包配置见LanChat.spec:

# 关键配置datas=['tkinterdnd2/tkdnd']# 需要打包 TkDnD 原生扩展库hiddenimports=['tkinterdnd2']console=False# 无控制台窗口

依赖

依赖用途安装
Python ≥ 3.10运行时
tkinterGUI 框架Python 内置
tkinterdnd2文件拖拽pip install tkinterdnd2
PyInstaller打包 exepip install pyinstaller

构建命令

pyinstaller LanChat.spec

端口占用

端口协议用途
9876UDP对等发现 (广播 + 监听)
9877TCP消息传输、文件传输

关键设计决策

为什么使用单例 UDP socket?

最初_udp_listener_udp_broadcaster各自创建独立的 UDP socket,但 Windows 上多 socket 绑定同一端口行为不可靠(SO_REUSEADDR无法保证所有平台都能接收广播)。改为在start()中创建单个 socket(同时设置SO_BROADCASTSO_REUSEADDR),两个线程共享此 socket 分别执行sendtorecvfrom,避免了端口冲突问题。

为什么用 Canvas 绘制气泡背景?

Tkinter 原生控件不支持圆角。采用Canvas.create_polygon(smooth=True)绘制圆角矩形,通过 12 个控制点生成平滑贝塞尔曲线,替代了原始的 Frame 背景方案,实现聊天气泡的圆角效果。

为什么使用create_window要求子控件关系?

Tkinter 的canvas create window要求嵌入的控件必须是 Canvas 的直接子控件(或同一窗口树中的后代)。将contentFrame 的父控件设置为self.canvas(而非self)才能正确通过create_window嵌入并显示。

线程模型设计考量

线程职责为什么独立
_udp_listener持续接收广播recvfrom是阻塞调用
_udp_broadcaster定时发送心跳 + 超时检测需要精确计时循环
_tcp_server_thread接受 TCP 连接accept是阻塞调用
_queue_processor消费消息队列串行化网络 → UI 消息
TCP handler ×N处理单个连接recv可能长时间阻塞(尤其文件传输)

所有网络线程使用daemon=True,主窗口关闭时自动退出。UI 更新通过root.after(0, callback)线程安全地调度到主线程。


错误处理策略

场景处理方式
UDP 端口被占用关闭 socket,设置udp_sock = None,仅发送广播不接收
TCP 端口被占用tcp_server = None,跳过接收(不能发送消息和文件)
发送消息超时/失败send_message返回 False,UI 弹出警告
文件接收目录创建失败静默处理,文件可能无法保存
网络线程异常通用except: continue保持线程运行
tkinterdnd2 未安装拖拽功能降级为文字提示,文件发送走按钮

版本构建

当前版本通过 PyInstaller 打包为单个main.exe,位于dist/目录。TkDnD 原生扩展库 (tkdnd/) 需随程序分发,已在.spec中通过datas配置。

http://www.jsqmd.com/news/1098971/

相关文章:

  • 基于AWS构建Agentic AI智能体:从原理到实战,实现工作流自动化与复利增长
  • 从API报错到本地拦截:电子面单快递公司前置校验改造
  • 3步轻松解密QQ音乐加密音频:qmcdump让你的音乐重获自由
  • SwiftKey整合GPT-4 Turbo:移动端AI输入范式重构
  • FreeRTOS 内核 IPC 通信全家桶——队列、信号量、互斥量、任务通知选型指南
  • VLA-Adapter论文解读(二):三大关键发现
  • 灵衢协议学习——物理层(三)
  • YOLO vs Halcon缺陷检测实战:别被AI焦虑绑架,选对技术才是真本事
  • Advanced XRay技术深度解析:如何通过方块渲染优化实现高效矿石定位
  • 管道泄漏识别 图像数据集 油气泄漏监测 水管泄漏检测图像数据
  • Android 7系统输入(五):应用侧 — InputChannel、ViewRootImpl与事件消费
  • 英雄联盟国服免费换肤终极指南:R3nzSkin完全教程
  • 抖音内容保存终极指南:douyin-downloader让你的收藏变得轻松高效
  • 英伟达“技术没有秘密“合理吗:研发总监拆解护城河的真相
  • 多 Agent 路由设计:当不同渠道、不同用户需要匹配不同“大脑”
  • 智能零售结账系统 文具用品识别数据集 YOLO与OpenCV实现+文具店橡皮+铅笔+尺子识别
  • 链表相关的算法
  • 北京昆仑数智-sql学习笔记
  • 爬虫去重别只会用Set!Python实现亿级数据清洗的4种工业级方案
  • 【VMware OVF导出终极指南】:20年资深架构师亲授5大避坑要点与3种加速导出实战技巧
  • 【数字孪生国标落地第一个月,我给新能源行业测了测段位】
  • 主流开源LLM(Qwen、ChatGLM等)的本地化部署
  • 验厂时,食品工作服需要注意什么?
  • GoalFlow:四、轨迹评分筛选模块(Trajectory Scorer, M3)
  • ps怎么调整图片大小?ps调整图片大小快捷键
  • 虚拟摇杆vJoy:Windows游戏控制器模拟的技术深度解析
  • 查新报告分为哪几种?科技查新、查收查引与专利查新区别
  • 基于 VC++ 与机器人 SDK 的工业多轴示教器软件设计与实现
  • 驾驶行为识别 打电话识别数据集 驾驶注意力监控 驾驶分心识别数据集 危险驾驶行为检测 抽烟打电话 睡觉 吃东西识别图像数据集第10149期
  • Metasploit渗透测试实战:从漏洞利用到后渗透操作详解