简介:这份资源是广东工业大学计算机网络课程设计的完整项目包,面向正在完成计网课设的本科生及需要参考P2P通信实现的开发者。项目构建了一个局域网内的即时通信系统,程序同时充当服务器与客户端,服务端口固定为3333,涵盖用户注册、对等方列表获取、在线扫描、消息与文件传输等核心功能,并配有图形用户界面展示对等方列表、消息记录与文件传输进度。压缩包共158个文件,约20.77MB,以java源码、class字节码、xml配置、properties属性文件及index索引为主,另含少量js、log与dat数据文件,完整保留了工程源码与运行痕迹。目前已有424人学习下载。通过该资源可掌握P2P架构下节点发现、TCP连接建立、消息格式定义与文件传输的完整实现思路,适合作为课设参考或网络编程练手项目。
1. 广工计网课设选P2P:为什么局域网即时通信值得动手做一遍
广工计算机网络课设里,「基于P2P的局域网即时通信系统」这个题目每年都有人选,但真正跑通并讲清楚的人不多。它要解决的核心问题是:在同一个局域网内,不依赖中心服务器,让多台机器互相发现、建立连接、收发消息。听起来简单,但动手后你会发现,局域网发现、P2P打洞、消息可靠传输这三件事,每一件都有坑。这个题目适合已经学过计算机网络基础、想用代码把TCP/UDP、Socket、多线程串起来的同学。它不需要公网服务器,两台笔记本连同一个路由器就能跑,调试成本低,但涉及的知识点覆盖了传输层、应用层和网络编程的大部分核心内容。如果你正在选课设题目,或者已经选了但不知道从哪下手,下面这套方案可以让你从零跑通一个可演示的系统。
2. 局域网P2P即时通信的系统拆解:从节点发现到消息投递
2.1 为什么选UDP做发现、TCP做消息通道
局域网内做节点发现,常见做法是UDP广播或组播。UDP广播不需要预先知道对方IP,一个节点往广播地址发心跳包,其他节点监听同一个端口就能收到。TCP适合做消息通道,因为它自带可靠传输、有序到达和流量控制,省去自己实现ACK和重传的麻烦。
选型理由很直接:发现阶段要求轻量、快速、容忍丢包,UDP广播天然匹配;消息阶段要求不丢、不乱序,TCP是现成的。如果全用UDP,消息可靠性要自己写,课设周期内很难做稳;如果全用TCP,发现阶段就得维护一张IP列表逐个尝试连接,节点动态加入时体验很差。
我一般会这样划分职责:UDP广播只负责「我在这里」和「我要走了」两类心跳;TCP长连接负责文本消息、文件传输和在线状态同步。两者用同一个节点ID关联,收到UDP心跳后,如果TCP还没连上,就主动发起连接。
2.2 节点发现协议的心跳包设计
心跳包不能太大,否则广播风暴时路由器压力大;也不能太小,否则携带的信息不够。常见做法是JSON格式,字段包括节点ID、昵称、TCP监听端口、时间戳。节点ID用UUID生成,保证局域网内唯一。
import socket import json import uuid import time import threading BROADCAST_PORT = 37020 BROADCAST_ADDR = '255.255.255.255' class DiscoveryService: def __init__(self, tcp_port): self.node_id = str(uuid.uuid4())[:8] self.tcp_port = tcp_port self.nickname = f"node-{self.node_id}" self.running = True self.peers = {} # node_id -> {ip, tcp_port, last_seen} def build_heartbeat(self): return json.dumps({ "type": "HEARTBEAT", "node_id": self.node_id, "nickname": self.nickname, "tcp_port": self.tcp_port, "timestamp": time.time() }).encode('utf-8') def broadcast_loop(self): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) while self.running: try: sock.sendto(self.build_heartbeat(), (BROADCAST_ADDR, BROADCAST_PORT)) except Exception as e: print(f"[发现] 广播失败: {e}") time.sleep(3) # 每3秒广播一次 sock.close() def listen_loop(self): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('', BROADCAST_PORT)) while self.running: try: data, addr = sock.recvfrom(1024) msg = json.loads(data.decode('utf-8')) if msg.get("node_id") == self.node_id: continue # 忽略自己的广播 self.peers[msg["node_id"]] = { "ip": addr[0], "tcp_port": msg["tcp_port"], "nickname": msg.get("nickname", "unknown"), "last_seen": time.time() } except Exception: pass sock.close()这段代码里,SO_BROADCAST允许发送广播包,SO_REUSEADDR让多个进程可以绑定同一个端口(调试时有用)。心跳间隔设为3秒,超时判定一般设10秒,也就是连续3个心跳没收到就认为节点离线。peers字典用节点ID做key,避免同一IP多节点冲突。
参数调整建议:如果局域网设备多,广播间隔可以放宽到5秒;如果演示时要求快速发现,可以缩到1秒,但要注意广播风暴。超时时间不要小于心跳间隔的2倍,否则网络抖动会误判离线。
2.3 TCP消息通道的建立与消息帧格式
发现节点后,TCP连接由节点ID较小的一方主动发起,避免双方同时连接造成重复。消息帧用「长度前缀 + JSON体」的方式,解决TCP粘包问题。长度前缀用4字节大端整数,表示后续JSON体的字节数。
import struct def send_message(sock, msg_dict): body = json.dumps(msg_dict).encode('utf-8') header = struct.pack('>I', len(body)) sock.sendall(header + body) def recv_message(sock): header = recv_exact(sock, 4) if not header: return None length = struct.unpack('>I', header)[0] body = recv_exact(sock, length) if not body: return None return json.loads(body.decode('utf-8')) def recv_exact(sock, n): data = b'' while len(data) < n: chunk = sock.recv(n - len(data)) if not chunk: return None data += chunk return datastruct.pack('>I', len(body))里的>表示大端字节序,I表示4字节无符号整数。接收端先读4字节拿到长度,再读对应字节数,保证每次读到一个完整消息。消息体里至少包含type、from、to、content、timestamp五个字段。type区分聊天消息、文件传输请求、在线状态更新。
注意:TCP是字节流,不保证每次
recv返回一个完整消息。长度前缀法是解决粘包最稳妥的方式,不要用\n分隔,因为消息内容里可能包含换行符。
2.4 多线程模型:一个收、一个发、一个发现
课设级别的系统不需要epoll或asyncio,三个线程足够:发现线程负责UDP广播和监听,接收线程负责从TCP连接读消息,主线程处理用户输入和发送。每个TCP连接对应一个接收线程,连接数不多时完全够用。
class PeerConnection: def __init__(self, sock, node_id): self.sock = sock self.node_id = node_id self.alive = True threading.Thread(target=self.recv_loop, daemon=True).start() def recv_loop(self): while self.alive: msg = recv_message(self.sock) if msg is None: self.alive = False break handle_incoming(msg) def send(self, msg_dict): if self.alive: send_message(self.sock, msg_dict)线程用daemon=True,主程序退出时自动结束。handle_incoming根据消息类型分发:聊天消息打印到界面,文件请求弹出确认,心跳更新在线列表。发送时如果连接已断,捕获异常并从连接池移除。
3. 从零跑通最小可用版本:环境、编译与联调步骤
3.1 开发环境与依赖清单
这个课设不依赖第三方库,Python标准库的socket、threading、json、struct、uuid、time就够了。如果要做图形界面,可以用tkinter,也是标准库。推荐Python 3.8以上,Windows和Linux都能跑。
| 组件 | 用途 | 是否必须 |
|---|---|---|
| socket | UDP广播和TCP连接 | 必须 |
| threading | 多线程收发 | 必须 |
| json | 消息序列化 | 必须 |
| struct | 长度前缀打包 | 必须 |
| tkinter | 图形界面 | 可选 |
| uuid | 节点ID生成 | 必须 |
两台机器连同一个路由器,关闭防火墙或放行对应端口。Windows下如果UDP广播收不到,检查「公用网络」的防火墙设置,临时关闭测试。
3.2 启动流程与联调顺序
先在一台机器上启动,确认UDP广播能发出、TCP端口能监听。再在第二台机器启动,观察第一台的peers字典是否出现第二个节点。如果发现成功,再测试TCP连接和消息收发。
# 机器A python main.py --nickname Alice --tcp-port 9001 # 机器B python main.py --nickname Bob --tcp-port 9002启动后,机器A的日志应该出现类似[发现] 新节点: Bob (192.168.1.102:9002)的输出。如果没有,先检查两台机器的IP是否在同一网段,再检查广播地址是否正确。有些路由器默认关闭广播转发,需要在路由器设置里开启。
3.3 消息收发的最小验证
发现节点后,手动触发一次TCP连接,发送一条测试消息。验证顺序:先确认TCP连接建立成功,再确认消息帧能正确解析,最后确认界面能显示。
# 测试:向指定节点发送消息 def test_send(peer_ip, peer_port, content): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(5) sock.connect((peer_ip, peer_port)) send_message(sock, { "type": "CHAT", "from": "tester", "to": "peer", "content": content, "timestamp": time.time() }) sock.close()如果连接超时,检查目标端口是否监听、防火墙是否放行。如果消息发出但对方没显示,检查接收端的recv_message是否被阻塞在recv_exact里,通常是长度前缀解析出错。打印原始字节流对比发送端和接收端的长度值,能快速定位。
3.4 在线列表与离线检测的联动
在线列表依赖UDP心跳的last_seen字段。一个独立的清理线程每5秒扫描一次peers,把超过10秒没更新的节点标记为离线,并从TCP连接池里移除对应连接。
def cleanup_loop(discovery, connections): while discovery.running: now = time.time() offline = [nid for nid, info in discovery.peers.items() if now - info["last_seen"] > 10] for nid in offline: discovery.peers.pop(nid, None) conn = connections.pop(nid, None) if conn: conn.alive = False conn.sock.close() time.sleep(5)清理线程和发现线程共享peers字典,Python的GIL保证单条字典操作的原子性,但遍历时修改会报错。所以先收集离线ID列表,再逐个删除。TCP连接关闭时,接收线程会因为recv返回空而退出,不需要额外通知。
4. 避坑与排查:P2P局域网通信最容易翻车的5个点
4.1 广播包发得出去收不到
现象:日志显示广播发送成功,但peers始终为空。原因通常是操作系统防火墙拦截了入站UDP包,或者绑定的端口被其他程序占用。解决:Windows下在「高级安全防火墙」里添加入站规则放行UDP端口;Linux下用ss -ulnp | grep 37020检查端口占用。如果端口被占,换一个不常用的端口,比如37021。
4.2 TCP连接建立后立刻断开
现象:connect成功,但发送第一条消息时sendall抛异常。原因多半是接收端在accept后没有启动接收线程,或者接收线程里recv_message解析出错导致异常退出。解决:在accept后立即打印日志确认连接建立,接收线程里用try/except包裹,出错时打印原始字节流。常见错误是长度前缀用了本机字节序,发送端和接收端不一致。
4.3 消息粘包导致JSON解析失败
现象:接收端偶尔报json.decoder.JSONDecodeError。原因是发送端连续调用两次send,接收端一次recv读到了两条消息的拼接。解决:严格使用长度前缀法,每次recv只读4字节头,再读指定长度。不要依赖recv的返回边界。如果已经用了长度前缀还出错,检查struct.pack的格式是否和unpack一致。
4.4 节点ID冲突导致连接混乱
现象:两个节点互相认为对方是自己,或者连接池里同一个ID对应多个IP。原因是节点ID生成用了时间戳或随机数,碰撞概率虽小但存在。解决:用uuid.uuid4()生成完整UUID,取前8位作为显示ID,内部用完整UUID做key。如果演示时发现冲突,重启节点重新生成即可。
4.5 局域网内多网卡导致广播发错网段
现象:笔记本同时连了WiFi和有线网,广播包从WiFi发出,但目标节点在有线网段,收不到。原因是255.255.255.255只会从默认路由对应的网卡发出。解决:绑定广播到指定网卡的IP,或者用子网广播地址(比如192.168.1.255)。代码里可以通过socket.gethostbyname(socket.gethostname())获取本机IP,再计算子网广播地址。
提示:调试时先用
tcpdump或 Wireshark 抓包,确认广播包是否真的发到了网络上。软件层面的日志只能证明「发送调用成功」,不能证明「包离开了网卡」。
5. 进阶技巧:用消息序号和ACK做轻量可靠层
5.1 为什么TCP之上还要做应用层ACK
TCP保证字节流可靠,但不保证应用层消息被处理。比如接收端解析JSON后处理逻辑抛异常,发送端并不知道。课设演示时如果要求「消息必达」,可以在应用层加一个简单的ACK机制:每条聊天消息带一个自增序号,接收端处理成功后回一个ACK帧,发送端收到ACK才标记为已送达。
class ReliableSender: def __init__(self, conn): self.conn = conn self.seq = 0 self.pending = {} # seq -> (msg, timestamp) self.lock = threading.Lock() def send_reliable(self, msg_dict): with self.lock: self.seq += 1 seq = self.seq msg_dict["seq"] = seq self.pending[seq] = (msg_dict, time.time()) self.conn.send(msg_dict) threading.Timer(2.0, self.check_ack, args=(seq,)).start() def check_ack(self, seq): with self.lock: if seq in self.pending: msg, _ = self.pending.pop(seq) self.conn.send(msg) # 重传一次 threading.Timer(4.0, self.check_ack, args=(seq,)).start() def on_ack(self, seq): with self.lock: self.pending.pop(seq, None)send_reliable发送后启动一个2秒定时器,如果没收到ACK就重传,再等4秒还没收到就放弃。on_ack在接收端回ACK时调用,清除待确认队列。这个机制不追求工业级可靠,但能让演示时「消息不丢」的承诺站得住。
5.2 文件传输的分块与进度显示
文本消息跑通后,文件传输是自然的扩展。把文件按64KB分块,每块带文件ID、块序号、总块数,接收端按序写入临时文件,全部收齐后重命名。进度显示用已收块数除以总块数。
| 参数 | 建议值 | 说明 |
|---|---|---|
| 块大小 | 64KB | 太大占内存,太小帧头开销高 |
| 并发块数 | 1 | 课设级别串行发送即可 |
| 超时重传 | 3秒 | 和消息ACK一致 |
| 临时文件后缀 | .part | 收齐后去掉 |
发送端每发一块等一个ACK,接收端写盘后回ACK。这样速度慢但逻辑简单,适合演示。如果要快,可以滑动窗口,但课设没必要。
5.3 验证清单:演示前必做的5项检查
演示前按这个清单过一遍,能避免大部分翻车:两台机器互相ping通;防火墙放行UDP和TCP端口;启动顺序先A后B,观察发现日志;发一条文本消息确认显示;发一个小于1MB的文件确认进度和完整性。如果时间允许,模拟一个节点中途离线,确认在线列表能在10秒内更新。
我自己的习惯是,演示前一天晚上用两台笔记本在同一个路由器下完整跑三遍,每遍换不同的端口和昵称。血泪经验是,教室的WiFi经常隔离客户端,导致广播收不到,所以最好自带一个便携路由器,或者用手机热点。希望帮到你。
本文还有配套的精品资源,点击获取