【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
核心数据类
| 类 | 字段 | 说明 |
|---|---|---|
Peer | name,ip,last_seen,online | 对等节点信息 |
ChatMessage | sender,content,timestamp,is_file,file_name,file_size,file_path,is_self | 聊天消息 |
常量
| 常量 | 值 | 说明 |
|---|---|---|
DISCOVERY_PORT | 9876 | UDP 广播发现端口 |
TCP_PORT | 9877 | TCP 消息传输端口 |
BROADCAST_ADDR | 255.255.255.255 | 子网广播地址 |
DISCOVERY_INTERVAL | 3s | 心跳广播间隔 |
PEER_TIMEOUT | 12s | 对等节点超时阈值 |
BUFFER_SIZE | 65536 | 网络缓冲区大小 |
CHUNK_SIZE | 32768 | 文件传输分块大小 |
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_left | padx_right | 背景色 |
|---|---|---|---|---|
is_self=True | tk.E(右) | 40 | 0 | #dcf8c6 |
is_self=False | tk.W(左) | 0 | 40 | #ffffff |
定时刷新机制
| 机制 | 触发方式 | 间隔 |
|---|---|---|
| UDP 事件驱动 | on_peers_changed回调 | 实时 (收到 hello 时) |
| 定时器兜底 | _start_peer_refresh_timer | 2s |
| 手动刷新 | 用户点击 “🔄 刷新” | — |
拖拽文件发送
- 依赖
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 | 运行时 | — |
| tkinter | GUI 框架 | Python 内置 |
| tkinterdnd2 | 文件拖拽 | pip install tkinterdnd2 |
| PyInstaller | 打包 exe | pip install pyinstaller |
构建命令
pyinstaller LanChat.spec端口占用
| 端口 | 协议 | 用途 |
|---|---|---|
| 9876 | UDP | 对等发现 (广播 + 监听) |
| 9877 | TCP | 消息传输、文件传输 |
关键设计决策
为什么使用单例 UDP socket?
最初_udp_listener和_udp_broadcaster各自创建独立的 UDP socket,但 Windows 上多 socket 绑定同一端口行为不可靠(SO_REUSEADDR无法保证所有平台都能接收广播)。改为在start()中创建单个 socket(同时设置SO_BROADCAST和SO_REUSEADDR),两个线程共享此 socket 分别执行sendto和recvfrom,避免了端口冲突问题。
为什么用 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配置。