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离线节点仍然保留在列表中红色标识可手动刷新清除时间线: t0 t3 t6 t9 t12 t15 │ │ │ │ │ │ hello──► hello──► hello──► hello──► (hello 停止) ↑ ↑ ↑ ↑ last_seen3 last_seen6 last_seen9 last_seen9 now - 9 6 12 → 仍在线 t21 │ 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_selfTrue)文件接收_handle_tcp_client(conn, peer_ip) ├─ 4字节长度前缀 → JSON 解析 ├─ msg_type file_offer │ ├─ 创建接收目录: ~/Downloads/LanChat/ │ ├─ 处理文件名冲突 (添加 (1), (2)...) │ ├─ 发送 ACK │ ├─ 分块接收写入文件 │ │ └─ 每块推送进度到消息队列 │ └─ 创建 ChatMessage (is_fileTrue) └─ 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, weight1) │ ├── 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 到父容器 (anchorE/W 边距) ├─ 绘制圆角矩形背景 (polygon smooth) └─ create_window 居中嵌入 content圆角矩形算法_round_rect(c, x1, y1, x2, y2, r10) 输入: Canvas 对象, 矩形左上角(x1,y1), 右下角(x2,y2), 圆角半径r 输出: Canvas polygon ID 控制点: (x1r,y1) ──────────── (x2-r,y1) │ │ │ ┌────────────────┐ │ │ │ content │ │ │ └────────────────┘ │ │ │ (x1,y1r) (x2,y1r) │ │ │ │ (x1,y2-r) (x2,y2-r) │ │ (x1r,y2) ──────────── (x2-r,y2) → 使用 create_polygon(points, smoothTrue) smooth 自动在角点生成贝塞尔曲线气泡布局条件对齐padx_leftpadx_right背景色is_selfTruetk.E(右)400#dcf8c6is_selfFalsetk.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_selfTrue) → _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_selfTrue) → on_message() → 显示文件气泡 接收方: TCP 接收 → 文件头解析 → 创建 ~/Downloads/LanChat/ → 分块写入文件 → 每块推送进度 → UI 进度条 → 完整接收 → ChatMessage (is_fileTrue) → 显示文件气泡配置与打包PyInstaller 打包打包配置见LanChat.spec:# 关键配置datas[tkinterdnd2/tkdnd]# 需要打包 TkDnD 原生扩展库hiddenimports[tkinterdnd2]consoleFalse# 无控制台窗口依赖依赖用途安装Python ≥ 3.10运行时—tkinterGUI 框架Python 内置tkinterdnd2文件拖拽pip install tkinterdnd2PyInstaller打包 exepip install pyinstaller构建命令pyinstaller LanChat.spec端口占用端口协议用途9876UDP对等发现 (广播 监听)9877TCP消息传输、文件传输关键设计决策为什么使用单例 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(smoothTrue)绘制圆角矩形通过 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可能长时间阻塞尤其文件传输所有网络线程使用daemonTrue主窗口关闭时自动退出。UI 更新通过root.after(0, callback)线程安全地调度到主线程。错误处理策略场景处理方式UDP 端口被占用关闭 socket设置udp_sock None仅发送广播不接收TCP 端口被占用tcp_server None跳过接收不能发送消息和文件发送消息超时/失败send_message返回 FalseUI 弹出警告文件接收目录创建失败静默处理文件可能无法保存网络线程异常通用except: continue保持线程运行tkinterdnd2 未安装拖拽功能降级为文字提示文件发送走按钮版本构建当前版本通过 PyInstaller 打包为单个main.exe位于dist/目录。TkDnD 原生扩展库 (tkdnd/) 需随程序分发已在.spec中通过datas配置。
【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离线节点仍然保留在列表中红色标识可手动刷新清除时间线: t0 t3 t6 t9 t12 t15 │ │ │ │ │ │ hello──► hello──► hello──► hello──► (hello 停止) ↑ ↑ ↑ ↑ last_seen3 last_seen6 last_seen9 last_seen9 now - 9 6 12 → 仍在线 t21 │ 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_selfTrue)文件接收_handle_tcp_client(conn, peer_ip) ├─ 4字节长度前缀 → JSON 解析 ├─ msg_type file_offer │ ├─ 创建接收目录: ~/Downloads/LanChat/ │ ├─ 处理文件名冲突 (添加 (1), (2)...) │ ├─ 发送 ACK │ ├─ 分块接收写入文件 │ │ └─ 每块推送进度到消息队列 │ └─ 创建 ChatMessage (is_fileTrue) └─ 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, weight1) │ ├── 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 到父容器 (anchorE/W 边距) ├─ 绘制圆角矩形背景 (polygon smooth) └─ create_window 居中嵌入 content圆角矩形算法_round_rect(c, x1, y1, x2, y2, r10) 输入: Canvas 对象, 矩形左上角(x1,y1), 右下角(x2,y2), 圆角半径r 输出: Canvas polygon ID 控制点: (x1r,y1) ──────────── (x2-r,y1) │ │ │ ┌────────────────┐ │ │ │ content │ │ │ └────────────────┘ │ │ │ (x1,y1r) (x2,y1r) │ │ │ │ (x1,y2-r) (x2,y2-r) │ │ (x1r,y2) ──────────── (x2-r,y2) → 使用 create_polygon(points, smoothTrue) smooth 自动在角点生成贝塞尔曲线气泡布局条件对齐padx_leftpadx_right背景色is_selfTruetk.E(右)400#dcf8c6is_selfFalsetk.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_selfTrue) → _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_selfTrue) → on_message() → 显示文件气泡 接收方: TCP 接收 → 文件头解析 → 创建 ~/Downloads/LanChat/ → 分块写入文件 → 每块推送进度 → UI 进度条 → 完整接收 → ChatMessage (is_fileTrue) → 显示文件气泡配置与打包PyInstaller 打包打包配置见LanChat.spec:# 关键配置datas[tkinterdnd2/tkdnd]# 需要打包 TkDnD 原生扩展库hiddenimports[tkinterdnd2]consoleFalse# 无控制台窗口依赖依赖用途安装Python ≥ 3.10运行时—tkinterGUI 框架Python 内置tkinterdnd2文件拖拽pip install tkinterdnd2PyInstaller打包 exepip install pyinstaller构建命令pyinstaller LanChat.spec端口占用端口协议用途9876UDP对等发现 (广播 监听)9877TCP消息传输、文件传输关键设计决策为什么使用单例 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(smoothTrue)绘制圆角矩形通过 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可能长时间阻塞尤其文件传输所有网络线程使用daemonTrue主窗口关闭时自动退出。UI 更新通过root.after(0, callback)线程安全地调度到主线程。错误处理策略场景处理方式UDP 端口被占用关闭 socket设置udp_sock None仅发送广播不接收TCP 端口被占用tcp_server None跳过接收不能发送消息和文件发送消息超时/失败send_message返回 FalseUI 弹出警告文件接收目录创建失败静默处理文件可能无法保存网络线程异常通用except: continue保持线程运行tkinterdnd2 未安装拖拽功能降级为文字提示文件发送走按钮版本构建当前版本通过 PyInstaller 打包为单个main.exe位于dist/目录。TkDnD 原生扩展库 (tkdnd/) 需随程序分发已在.spec中通过datas配置。