ARTICLE DETAIL

资讯详情

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

广工计网课设实战:基于P2P的局域网即时通信系统从零跑通

广工计网课设实战:基于P2P的局域网即时通信系统从零跑通 简介这份资源是广东工业大学计算机网络课程设计的完整项目包面向正在完成计网课设的本科生及需要参考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 fnode-{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(targetself.recv_loop, daemonTrue).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)线程用daemonTrue主程序退出时自动结束。handle_incoming根据消息类型分发聊天消息打印到界面文件请求弹出确认心跳更新在线列表。发送时如果连接已断捕获异常并从连接池移除。3. 从零跑通最小可用版本环境、编译与联调步骤3.1 开发环境与依赖清单这个课设不依赖第三方库Python标准库的socket、threading、json、struct、uuid、time就够了。如果要做图形界面可以用tkinter也是标准库。推荐Python 3.8以上Windows和Linux都能跑。组件用途是否必须socketUDP广播和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之上还要做应用层ACKTCP保证字节流可靠但不保证应用层消息被处理。比如接收端解析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经常隔离客户端导致广播收不到所以最好自带一个便携路由器或者用手机热点。希望帮到你。本文还有配套的精品资源点击获取
返回列表