
简介这是一份面向高校计算机、软件工程等专业学生的计算机网络课程设计参考资料主题为基于P2P技术的局域网聊天程序适合正在完成课设、需要参考完整设计思路与实现方案的学习者。压缩包内共1个doc文档约161KB为课程设计说明书论文格式涵盖需求分析、总体设计、详细设计、系统实现编码及运行结果、总结与参考文献等章节并配有目录与摘要。文档围绕用户注册登录、聊天、文件传输、好友管理等模块展开涉及客户端、服务器端与数据库三层架构以及表示层、应用层、业务逻辑层、数据访问层的软件层次模型同时介绍了TCP、UDP协议原理与Socket编程方法。目前已有559人学习下载可作为课设选题、文档撰写与功能模块划分的参考帮助读者快速理清P2P局域网聊天程序的设计脉络与实现要点。1. 局域网聊天程序从 Socket 到多端互通的课设落地路线宿舍里几台电脑想互相发消息不想装任何第三方软件也不想连外网——这就是「局域网聊天程序」这个课设标题最朴素的出发点。它本质上是一个基于 TCP/UDP 的 C/S 或 P2P 通信小系统核心考点集中在 Socket 编程、多线程/IO 多路复用、应用层协议设计这三块。很多人做课设时卡在「能连上但消息发不全」「多客户端一上就崩」「中文乱码」这些具体问题上而不是卡在概念上。这篇笔记按「先跑通最小闭环 → 再补多客户端 → 再处理协议和边界」的顺序展开适合正在做计算机网络课设、想拿一个能演示、能答辩、代码自己能讲清楚的同学。下面所有代码以 Python 为例换 C/Java 思路一致只是 API 名字不同。2. 先跑通最小闭环TCP 服务端与客户端的一对一通信2.1 为什么课设首选 TCP 而不是 UDP局域网聊天程序在课设场景下绝大多数老师默认你选 TCP。原因很实际TCP 自带连接管理、可靠传输、字节流顺序保证你不需要自己写重传、去重、排序。UDP 虽然代码更短但一旦要保证「消息不丢、不乱序」你就得在应用层补一套序列号和 ACK 机制工作量反而更大答辩时也容易被追问「你怎么保证可靠性」。常见做法是文字聊天走 TCP如果课设要求做语音或视频片段传输再单独用 UDP 并说明理由。这个分工在答辩时是一个加分点因为它体现你理解两种协议的适用边界。TCP 服务端的生命周期是固定的四步socket()创建套接字 →bind()绑定地址端口 →listen()进入监听 →accept()取出连接。客户端是socket()→connect()。这个顺序不能乱bind之前必须先创建套接字listen之前必须先bind。2.2 最小可运行的服务端代码import socket HOST 0.0.0.0 # 监听本机所有网卡局域网内其他机器才能连进来 PORT 9000 # 端口选 1024 以上避免权限问题 server socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 重启时端口不被 TIME_WAIT 占住 server.bind((HOST, PORT)) server.listen(5) # backlog 设为 5够课设用 print(f服务端已启动监听 {PORT}) conn, addr server.accept() # 阻塞等待一个客户端 print(f客户端 {addr} 已连接) while True: data conn.recv(1024) # 每次最多收 1024 字节 if not data: # 对端关闭连接时 recv 返回空 break msg data.decode(utf-8) print(f收到{msg}) conn.sendall(f服务端已收到{msg}.encode(utf-8)) conn.close() server.close()逻辑说明SO_REUSEADDR是课设调试阶段最容易被忽略的一行。没有它你每次重启服务端都可能报Address already in use因为上一次的连接还处于 TIME_WAIT 状态。recv(1024)的 1024 是单次读取上限不是消息长度TCP 是字节流一条消息可能分多次到达这一点后面第 4 章会专门处理。sendall和send的区别是前者保证全部发完课设里统一用sendall更省心。2.3 对应的客户端代码与连接验证import socket HOST 192.168.1.100 # 换成服务端的局域网 IP不要写 127.0.0.1 PORT 9000 client socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect((HOST, PORT)) while True: msg input(请输入消息输入 quit 退出) if msg quit: break client.sendall(msg.encode(utf-8)) reply client.recv(1024) print(f服务端回复{reply.decode(utf-8)}) client.close()参数说明HOST必须填服务端的局域网 IP用ipconfigWindows或ip addrLinux查。如果填127.0.0.1只有本机能连其他机器连不上这是课设演示时最常见的翻车点。验证连通性可以先用ping 192.168.1.100确认网络层通再用telnet 192.168.1.100 9000确认端口开放。Windows 默认没开 telnet 客户端可以在「启用或关闭 Windows 功能」里勾上或者直接用 Python 客户端测。提示如果客户端报ConnectionRefusedError先确认服务端是否真的在运行再确认防火墙有没有拦。Windows 防火墙对 Python 的入站连接默认是询问或阻止答辩前一定要在「允许应用通过防火墙」里把 Python 勾上。3. 多客户端并发从单线程阻塞到多线程与 select 的选型3.1 单线程服务端为什么一上多客户端就废第 2 章的代码只能服务一个客户端因为accept()返回后程序就卡在recv()的循环里第二个客户端连上来时根本没人去accept。这是课设从「能跑」到「能用」的第一道坎。解决办法有两类多线程和 IO 多路复用。多线程的思路是主线程只负责accept每来一个客户端就开一个线程专门处理它的收发。优点是代码直观每个连接独立逻辑互不干扰。缺点是客户端一多线程就多上下文切换开销大而且共享数据比如在线用户列表要加锁容易出并发 bug。IO 多路复用的思路是用一个select或epoll同时监听所有套接字哪个有数据就处理哪个单线程就能管很多连接。优点是资源占用低没有锁的问题。缺点是代码结构绕新手容易写错事件循环。课设场景下如果在线人数不超过几十个多线程完全够用而且更好讲。如果老师明确要求「高并发」或者你想拿高分用select写一版更有说服力。3.2 多线程版服务端的完整结构import socket import threading HOST 0.0.0.0 PORT 9000 clients [] # 保存在线客户端套接字 lock threading.Lock() # 保护 clients 列表的并发修改 def broadcast(message, sender): 把消息发给除发送者外的所有在线客户端 with lock: for c in clients: if c ! sender: try: c.sendall(message) except OSError: clients.remove(c) # 发送失败说明连接已断移除 def handle_client(conn, addr): print(f{addr} 上线) with lock: clients.append(conn) try: while True: data conn.recv(1024) if not data: break text f[{addr[0]}:{addr[1]}] {data.decode(utf-8)} broadcast(text.encode(utf-8), conn) finally: with lock: if conn in clients: clients.remove(conn) conn.close() print(f{addr} 下线) server socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((HOST, PORT)) server.listen(10) print(服务端已启动) while True: conn, addr server.accept() t threading.Thread(targethandle_client, args(conn, addr), daemonTrue) t.start()逻辑说明daemonTrue让工作线程随主线程退出避免程序关不掉。broadcast里对clients的遍历和修改都放在锁里否则一个线程在遍历、另一个线程在删除会抛RuntimeError: list changed size during iteration。try/except OSError是必要的因为客户端可能已经断开但还没被recv发现此时sendall会失败。参数说明listen(10)的 10 是等待队列长度不是最大连接数课设里设 5 到 10 都行。recv(1024)依然要面对粘包问题第 4 章解决。3.3 select 版的核心差异与适用判断select版的关键是把所有需要监听的套接字放进一个列表每次循环调用select.select(readable, [], [])返回可读的套接字集合然后逐个处理。服务端套接字可读意味着有新连接客户端套接字可读意味着有数据或连接关闭。import socket import select HOST 0.0.0.0 PORT 9000 server socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((HOST, PORT)) server.listen(10) inputs [server] # 所有被监听的套接字 while True: readable, _, _ select.select(inputs, [], []) for s in readable: if s is server: conn, addr s.accept() inputs.append(conn) # 新连接加入监听 print(f{addr} 上线) else: data s.recv(1024) if data: for c in inputs: if c is not server and c is not s: c.sendall(data) else: inputs.remove(s) # 连接关闭移出监听 s.close()逻辑说明select的第一个参数是「等待可读」的套接字列表第二个是「等待可写」第三个是「等待异常」课设里通常只关心可读。inputs列表在循环中被修改但select每次调用都会重新读取它所以不会像多线程那样有迭代中修改的问题。这个版本单线程就能支撑几十个客户端代码量比多线程还少缺点是所有逻辑挤在一个循环里扩展功能时不如多线程清晰。选型建议如果课设要求做私聊、群聊、文件传输、在线列表等多个功能多线程版更容易按功能拆函数如果只要求群聊且强调并发性能select版更合适。两者都写一遍对比答辩时能讲出取舍是加分项。4. 消息边界与协议设计粘包、半包和自定义报文格式4.1 粘包和半包到底是怎么发生的TCP 是字节流协议它不保留你send的次数边界。你连续send两条消息接收端可能一次recv全收到粘包也可能一条消息分两次recv才收全半包。这不是 bug是 TCP 的设计。课设里如果只发短消息、发得慢可能一直不触发但一旦做文件传输或快速连发必然翻车。解决思路只有一条在应用层定义消息边界。常见做法有三种。一是固定长度每条消息定长不够补空格简单但浪费带宽。二是分隔符比如用\n结尾接收端按行拆适合文本聊天。三是长度前缀先发 4 字节表示消息体长度再发消息体最通用文件传输也适用。课设推荐长度前缀因为老师一问「你怎么处理粘包」你能直接答上来而且代码不复杂。4.2 长度前缀协议的收发实现import struct def send_msg(sock, text): 先发 4 字节长度再发消息体 body text.encode(utf-8) length struct.pack(!I, len(body)) # !I 表示网络字节序的无符号 4 字节整数 sock.sendall(length body) def recv_msg(sock): 先收 4 字节长度再按长度收消息体 raw_len recv_exact(sock, 4) if not raw_len: return None length struct.unpack(!I, raw_len)[0] body recv_exact(sock, length) if not body: return None return body.decode(utf-8) def recv_exact(sock, n): 确保收满 n 字节处理半包 buf b while len(buf) n: chunk sock.recv(n - len(buf)) if not chunk: return None buf chunk return buf逻辑说明struct.pack(!I, len(body))里的!表示网络字节序大端I表示 4 字节无符号整数。不同机器字节序可能不同统一用网络字节序是标准做法。recv_exact是解决半包的关键它循环接收直到凑够指定字节数recv(n - len(buf))保证不会多收。recv_msg先收 4 字节长度再按长度收消息体这样无论 TCP 怎么拆分合并应用层拿到的都是完整的一条消息。参数说明长度用 4 字节意味着单条消息最大约 4GB课设完全够用。如果做文件传输可以把文件也按这个格式分块发送每块带上类型标识。utf-8编码保证中文不乱码服务端和客户端必须统一一边用gbk一边用utf-8是乱码的常见原因。4.3 带类型字段的报文格式扩展纯文本聊天用长度前缀就够了但如果要区分「普通消息」「私聊」「文件块」「在线列表请求」就需要在消息体里再加一个类型字段。常见做法是消息体前 1 字节表示类型后面跟内容。字段长度说明总长度4 字节网络字节序表示后续所有字节数类型1 字节0x01 群聊0x02 私聊0x03 文件0x04 控制目标变长私聊时填目标用户名群聊留空内容变长实际消息或文件块这个格式在答辩时可以直接画在白板上讲比纯文字描述清楚得多。实现时把send_msg改成先拼类型和目标再算总长度即可。注意不要用\n做分隔符同时又在消息内容里允许\n否则接收端按行拆会把一条消息拆成多条。如果坚持用分隔符必须对内容里的分隔符做转义这比长度前缀麻烦。5. 避坑与排查课设演示前必须过的五道关5.1 客户端连不上服务端现象客户端报ConnectionRefusedError或一直卡在connect。原因通常是三个服务端没启动、IP 填错、防火墙拦截。解决顺序是先ping服务端 IP 确认网络通再用telnet IP 端口确认端口开放最后检查服务端是否真的在listen状态。Windows 防火墙对 Python 入站默认拦截需要在防火墙设置里放行或者临时关闭防火墙测试答辩时不要关提前配好规则。5.2 中文消息乱码现象收到的是b\xe4\xbd\xa0\xe5\xa5\xbd这种字节串或者问号。原因发送端和接收端编码不一致或者接收端直接print(data)没有decode。解决统一用utf-8发送前encode(utf-8)接收后decode(utf-8)。如果 Windows 控制台显示乱码但数据本身没错是控制台编码问题可以chcp 65001切到 UTF-8 代码页。5.3 消息发出去但对方收不到现象服务端打印了收到消息但广播后其他客户端没显示。原因广播时遍历的clients列表里没有目标客户端或者目标客户端套接字已失效但没被移除。解决在broadcast里对每个发送加try/except失败就移除同时确认新客户端accept后确实加入了clients列表。多线程版尤其要注意加锁否则列表状态可能不一致。5.4 服务端重启报端口被占用现象OSError: [Errno 98] Address already in use。原因上一次的服务端连接处于 TIME_WAIT 状态端口还没释放。解决在bind之前加setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)。如果已经加了还报说明有另一个进程占着这个端口用netstat -ano | findstr 9000Windows或lsof -i:9000Linux找到进程号杀掉。5.5 客户端直接关窗口导致服务端崩溃现象客户端点右上角关闭服务端抛异常或卡死。原因客户端异常断开时服务端recv返回空如果没处理就会继续用失效的套接字发送。解决recv返回空时break出循环在finally里关闭套接字并从clients移除。多线程版还要确保移除时加锁避免和其他线程的广播冲突。6. 进阶技巧用心跳机制和在线列表把课设做成可演示的作品课设答辩时老师最常问的两个问题是「你怎么知道对方还在线」和「你怎么保证消息一定送到」。第二个问题 TCP 已经帮你答了第一个问题需要你自己做心跳。心跳的思路很简单客户端每隔固定时间比如 30 秒给服务端发一个特殊类型的报文服务端收到后更新该客户端的最后活跃时间服务端再起一个定时线程扫描所有客户端超过一定时间比如 90 秒没收到心跳的就判定离线移除并广播下线通知。import time import threading last_active {} # {conn: 最后活跃时间戳} def heartbeat_monitor(): while True: time.sleep(30) now time.time() with lock: for conn in list(clients): if now - last_active.get(conn, 0) 90: clients.remove(conn) last_active.pop(conn, None) conn.close() broadcast(f有用户超时下线.encode(utf-8), None) # 在 handle_client 的 recv 循环里每收到一条消息就更新 # last_active[conn] time.time()参数说明心跳间隔 30 秒、超时 90 秒是常见组合允许丢两次心跳。课设演示时可以把间隔调短到 5 秒和 15 秒方便现场展示「拔网线后对方变离线」的效果。注意broadcast的第二个参数传None表示发给所有人包括触发者自己这和之前的排除发送者逻辑要区分开。在线列表的实现是在心跳基础上加一个类型为0x04的控制报文客户端连上后主动请求一次服务端把clients里的地址和用户名拼成列表返回。用户名可以在连接后第一条消息里带上服务端用一个字典维护conn到用户名的映射。我自己的习惯是课设代码写完先不急着加功能而是把「一个客户端连上、发消息、另一个客户端收到、关掉一个、另一个收到下线通知」这条链路手动走十遍每次都在不同机器上走。翻车最多的地方从来不是协议设计而是防火墙、IP 填错、编码不统一这三个看起来最不起眼的问题。把这三样在答辩前固定成检查清单比多写两百行功能代码有用。希望帮到你。本文还有配套的精品资源点击获取