ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python简易IM系统:Socket+CSV实现即时通信教学骨架

Python简易IM系统:Socket+CSV实现即时通信教学骨架 简介本资源是一套基于Python开发的简易微信系统课程设计源码面向计算机专业本科生及Python初学者用于理解客户端-服务器架构、内存数据库管理与基础网络通信原理。压缩包共20个文件含7个核心Python源码如server.py、client.py、database.py等、4个CSV数据表user.csv、friend.csv等、3个编译后pyc文件、1个Word课程报告、1个README说明文档及1个可执行客户端exe整体仅598KB轻量易部署。已有306人学习下载适合课程设计实践、毕设参考或分布式系统入门教学。读者可完整获得从服务端启动/关闭机制通过system.py调用、三层数据库操作封装内存层→功能层→指令层到GUI界面、消息存储与好友关系管理的全链路实现逻辑并附带图文并茂的课程报告与清晰目录结构便于快速理解模块职责与调试路径。1. 这不是微信客户端而是一套可调试、可拆解的即时通信教学骨架你双击wechat.exe看到登录界面输入账号密码能发消息、加好友、查历史记录——但它既不连腾讯服务器也不走微信协议。这是一个用纯 Python 实现的本地局域网级简易微信系统核心逻辑全部摊开在server.py、client.py、database.py和GUI.py里。它不追求高并发或加密传输而是把「用户注册→好友关系建立→消息收发→离线存储」这整条链路用最直白的内存操作CSV 文件持久化TCP socket 通信串起来。适合课程设计学生快速理解 IM 系统分层结构也适合 Python 初学者拿来做 socket 编程、GUI 事件驱动、文件 I/O 和简单状态管理的综合练手项目。所有模块耦合度低database.py只管增删查server.py只管指令路由GUI.py只响应按钮点击——改一行代码就能看到效果而不是面对几百行黑盒框架无从下手。2. 三层数据库架构从 CSV 文件读取到内存缓存再到业务封装2.1 底层数据表操作CSV 文件即数据库database.py是唯一入口整个系统没有使用 SQLite 或 MySQL而是将user.csv、friend.csv、message.csv三张表作为持久化载体。database.py封装了所有底层操作关键函数包括# database.py def load_csv(filename): 从CSV文件加载数据到内存列表返回二维列表 if not os.path.exists(filename): return [] with open(filename, r, encodingutf-8) as f: reader csv.reader(f) return list(reader) def save_csv(filename, data): 将内存中的二维列表写入CSV文件 with open(filename, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerows(data)提示load_csv()在服务器启动时被server.py调用一次将全部 CSV 内容载入内存后续所有查询、插入、删除均在内存中进行save_csv()仅在关键操作如注册成功、发送消息后触发落盘。这种“内存优先异步落盘”策略避免了每次操作都读写磁盘但需注意程序异常退出会导致最新变更丢失——课程设计中可接受生产环境必须加事务或改用 SQLite。2.1.1 表结构与字段含义必须对齐三张 CSV 文件采用固定列顺序无表头靠位置索引访问字段文件名字段顺序索引从 0 开始示例值user.csvusername,password,nickname,statuszhangsan,123456,张三,onlinefriend.csvuser_a,user_b,status1已添加zhangsan,lisi,1message.csvsender,receiver,content,timestamp,is_readzhangsan,lisi,你好,2024-06-15 10:22:31,0注意message.csv中is_read0表示未读客户端收到消息后需主动更新该字段为1friend.csv中(a,b)和(b,a)视为同一关系代码中需双向判断否则会出现“A 加了 B但 B 好友列表里没 A”的逻辑漏洞。2.2 中间层封装规避空指针与越界访问的健壮性包装database.py提供的get_user_by_name(username)、add_friend(user_a, user_b)等函数并非直接调用load_csv()而是先做存在性校验再执行操作。例如# database.py def get_user_by_name(username): users load_csv(user.csv) for row in users: if len(row) 2 and row[0] username: # 防止空行或字段不足导致 index error return row return None # 显式返回 None而非抛异常上层可安全判断 def add_friend(user_a, user_b): friends load_csv(friend.csv) # 检查是否已存在该关系双向 exists any((row[0] user_a and row[1] user_b) or (row[0] user_b and row[1] user_a) for row in friends if len(row) 2) if not exists: friends.append([user_a, user_b, 1]) save_csv(friend.csv, friends) return True return False2.2.1 为什么必须做len(row) 2判断原始 CSV 文件可能因手动编辑产生空行或字段缺失如zhangsan,,张三若直接row[1]访问会触发IndexError。课程设计中常见错误是忽略 CSV 解析的鲁棒性导致服务器一收到异常格式数据就崩溃。此处用len(row) N显式兜底比try-except更利于定位问题源头。2.3 顶层指令映射server.py如何将 socket 指令转为数据库操作服务器监听 TCP 端口默认8080接收客户端发来的纯文本指令格式为CMD|arg1|arg2|...。server.py的核心循环如下# server.py while True: conn, addr server_socket.accept() data conn.recv(1024).decode(utf-8).strip() if not data: continue parts data.split(|) cmd parts[0] if cmd REGISTER: username, password, nickname parts[1], parts[2], parts[3] result db.register_user(username, password, nickname) # 调用中间层封装 conn.send(fRESULT|{result}.encode(utf-8)) elif cmd LOGIN: username, password parts[1], parts[2] result db.login_user(username, password) # 封装了密码比对和状态更新 conn.send(fRESULT|{result}.encode(utf-8)) elif cmd SEND_MSG: sender, receiver, content parts[1], parts[2], parts[3] db.save_message(sender, receiver, content) # 自动添加时间戳并落盘 conn.send(bRESULT|OK)参数说明parts[0]是命令名parts[1:]是参数。所有指令均以|分隔避免空格歧义SEND_MSG不返回消息内容只确认接收成功防止敏感信息泄露RESULT|OK是约定响应格式客户端client.py严格按此解析。3. 客户端 GUI 与网络通信PyQt5 实现的事件驱动交互流3.1 GUI 层结构GUI.py与client.py的职责分离GUI.py仅负责界面渲染与事件绑定所有业务逻辑登录、发消息、刷新好友列表由client.py处理。这种分离让界面可替换比如换成 Tkinter而不影响通信逻辑。主窗口类WeChatWindow继承自QMainWindow关键控件包括QLineEdit用户名、密码、消息输入框QPushButton登录、发送、添加好友QListWidget在线好友列表实时更新QTextEdit消息显示区域只读事件绑定示例# GUI.py self.login_btn.clicked.connect(self.on_login_click) # 点击登录按钮 → 触发 on_login_click 方法 self.send_btn.clicked.connect(self.on_send_click) def on_login_click(self): username self.username_input.text().strip() password self.password_input.text().strip() if not username or not password: self.show_error(用户名和密码不能为空) return # 调用 client.py 的 login 方法传入回调函数处理响应 client.login(username, password, self.on_login_success, self.on_login_fail)3.1.1 回调函数机制避免 GUI 线程阻塞client.py的login()函数内部启动独立线程执行 socket 连接与通信防止conn.recv()阻塞主线程导致界面冻结# client.py def login(username, password, success_cb, fail_cb): def _do_login(): try: sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((127.0.0.1, 8080)) # 默认连接本地服务器 sock.send(fLOGIN|{username}|{password}.encode(utf-8)) response sock.recv(1024).decode(utf-8) if response.startswith(RESULT|OK): success_cb(username) # 在主线程执行回调 else: fail_cb(登录失败 response.split(|, 1)[1]) except Exception as e: fail_cb(f连接失败{str(e)}) finally: sock.close() threading.Thread(target_do_login, daemonTrue).start()注意daemonTrue确保线程随主程序退出而终止success_cb和fail_cb是 GUI 层传入的函数对象用于在登录成功后跳转主界面或弹出错误提示这是 PyQt5 中处理异步操作的标准做法。3.2 消息收发流程从输入框到 CSV 文件的完整链路用户在输入框键入文字点击“发送”后经历以下步骤GUI.py获取receiver当前选中好友和content输入框文本client.py构造SEND_MSG|zhangsan|lisi|你好啊并发送至服务器server.py解析指令调用db.save_message(zhangsan, lisi, 你好啊)database.py在内存中追加新消息行并调用save_csv(message.csv, ...)持久化服务器返回RESULT|OK客户端收到后清空输入框关键点消息仅存于message.csv不推送给接收方接收方需主动轮询每 2 秒发一次GET_MSGS|lisi指令获取新消息# client.py - 轮询逻辑 def start_polling(receiver): def _poll(): while True: try: sock socket.socket() sock.connect((127.0.0.1, 8080)) sock.send(fGET_MSGS|{receiver}.encode(utf-8)) resp sock.recv(4096).decode(utf-8) if resp.startswith(MESSAGES|): msgs resp[9:].split(||) # 消息用 || 分隔 for msg in msgs: if msg.strip(): # 在 GUI 主线程中更新消息显示 QApplication.postEvent( gui_window, UpdateMessageEvent(msg) ) time.sleep(2) # 每2秒轮询一次 except: break threading.Thread(target_poll, daemonTrue).start()参数说明GET_MSGS|lisi返回格式为MESSAGES|zhangsan|lisi|你好啊|2024-06-15 10:22:31|0||zhangsan|lisi|收到|2024-06-15 10:23:05|0客户端按||拆分后再按|解析每条消息的发送者、接收者、内容、时间、已读状态。4. 服务端启停与调试绕过 Windows 任务管理器的精准控制方案4.1 启动服务器system.py的隐藏开关与进程隔离system.py是一个轻量级控制脚本其核心是调用 Windowsstart /b python server.py后台启动服务端进程。但默认状态下关键行被注释# system.py # print(system(start /b python server.py)) # ← 此行需取消注释才能启动 # print(system(taskkill /im python.exe /f)) # ← 此行需取消注释才能强制关闭提示start /b启动的进程不创建新终端窗口避免干扰/b参数确保后台运行否则server.py会独占命令行窗口。若需查看日志可临时改为start python server.py此时会弹出独立 CMD 窗口显示print()输出。4.1.1 如何验证服务器是否真正运行不要依赖窗口是否存在而应检查端口占用netstat -ano | findstr :8080若返回类似TCP 0.0.0.0:8080 0.0.0.0:0 LISTENING 12345则PID 12345对应的进程即为server.py。可通过tasklist | findstr 12345确认进程名。4.2 关闭服务器taskkill的精确匹配与风险规避直接taskkill /im python.exe /f会杀死所有 Python 进程包括你正在调试的client.py或database.py。更安全的做法是限定进程名# system.py - 改进版关闭逻辑 import os os.system(taskkill /f /fi imagename eq python.exe /fi services eq )但本项目更推荐在server.py中加入优雅退出机制监听特定指令如SHUTDOWN收到后执行save_csv()并break循环。修改server.py主循环# server.py while True: conn, addr server_socket.accept() data conn.recv(1024).decode(utf-8).strip() if data SHUTDOWN: # 新增指令 print(收到关机指令正在保存数据...) db.save_all() # 假设 database.py 提供此方法 break # ...原有指令处理逻辑 server_socket.close() print(服务器已停止)然后在system.py中发送关机指令# system.py - 安全关闭方式 def shutdown_server(): try: sock socket.socket() sock.connect((127.0.0.1, 8080)) sock.send(bSHUTDOWN) sock.close() print(服务器已收到关机指令) except: print(无法连接服务器尝试强制关闭...) os.system(taskkill /f /im python.exe /fi pid gt 0) # 调用 shutdown_server() 替代原 taskkill 命令4.3 调试技巧用print()定位数据流向断点当消息发不出或好友列表为空时不要盲目重启按以下顺序加print()在client.py的send_message()开头打印f准备发送{cmd}在server.py的recv()后打印f收到指令{data}在database.py的save_message()中打印f写入消息到CSV{row}在GUI.py的on_login_success()中打印f登录成功当前用户{username}关键观察点若第 1 步有输出但第 2 步无输出说明客户端未连上服务器检查 IP/端口若第 2 步有输出但第 3 步无输出说明指令解析失败检查|分隔符是否被误删若第 3 步有输出但message.csv无新增说明save_csv()路径错误确认工作目录是项目根目录。5. 课程设计落地技巧三处可立即加分的改造点5.1 添加消息时间戳格式化从2024-06-15 10:22:31到10:22message.csv存储的是datetime.now().strftime(%Y-%m-%d %H:%M:%S)但 GUI 显示时应精简。在GUI.py解析消息后对时间字段做二次处理# GUI.py - 消息显示前处理 def format_time(timestamp_str): try: dt datetime.strptime(timestamp_str, %Y-%m-%d %H:%M:%S) return dt.strftime(%H:%M) # 只显示时:分 except: return timestamp_str # 解析失败则原样返回 # 在更新消息文本时调用 msg_text f[{format_time(time_field)}] {sender}: {content}效果避免消息区域被长日期挤占符合主流 IM 界面习惯try-except兜底防止 CSV 中时间格式异常导致整个消息渲染失败。5.2 实现“已读回执”客户端主动标记is_read1当前message.csv中is_read字段始终为0无法体现消息是否被对方查看。可在client.py的消息接收逻辑中对自己作为接收者的消息批量更新状态# client.py - 接收消息后立即标记已读 def mark_messages_as_read(receiver): messages db.load_csv(message.csv) updated False for row in messages: if len(row) 5 and row[1] receiver and row[4] 0: # receiver 匹配且未读 row[4] 1 # 标记为已读 updated True if updated: db.save_csv(message.csv, messages)调用时机在UpdateMessageEvent处理完新消息后立即执行mark_messages_as_read(current_user)。这样下次启动客户端时历史消息将正确显示“已读”。5.3 修复好友列表重复friend.csv双向去重查询get_friends(username)函数当前只查user_a username漏掉user_b username的情况。修正如下# database.py def get_friends(username): friends load_csv(friend.csv) result set() # 用 set 去重 for row in friends: if len(row) 2: if row[0] username and row[2] 1: # user_a 是自己 result.add(row[1]) elif row[1] username and row[2] 1: # user_b 是自己 result.add(row[0]) return list(result)为什么用set避免同一好友因多次添加操作如 A 加 B 后 B 又加 A在列表中出现两次list(result)转回列表供QListWidget.addItems()使用。此修复能让课程报告中“好友关系管理”章节的截图更严谨。实际部署时只需在server.py启动前手动清空user.csv、friend.csv、message.csv三文件即可获得干净测试环境。所有功能均不依赖外部服务一台装有 Python 3.6 和 PyQt5 的 Windows 机器即可完整运行。本文还有配套的精品资源点击获取
返回列表