☰
P2P局域网即时通信系统课设:协议、架构与文件传输实现详解
2026/10/18 5:02:47 网站建设 项目流程

简介:广工计算机网络课程设计“基于P2P的局域网即时通信系统”源码与工程文件,面向计网课设学生,用于在图形界面下实现既是服务端又是客户端的P2P消息系统。项目完整覆盖用户注册、3333端口网段扫描、在线对等方发现与应答、用户列表维护,以及建立TCP连接发送消息和文件等关键环节。压缩包共158个文件,以Java源码、class编译文件、XML界面配置、Eclipse工程文件为主,并含聊天窗口、文件传输、IP扫描等模块,可直接导入IDE查看与调试。资源包约20.77MB,已有426人学习下载。借助该工程可系统理解P2P对等发现、Socket编程、多线程收发和自定义报文格式设计,适合作为课设代码参考、报告撰写与答辩演示的完整素材。

1. 基于P2P的局域网即时通信系统:课设需求为何不能照搬C/S架构

计算机网络的课程设计里,局域网即时通信属于看起来简单、落地全是细节的一类题目。某高校计算机网络课设要求实现的是一套基于P2P模式的局域网消息系统:程序既是服务器又是客户端,服务端口固定使用3333;对等方A启动后由用户设置用户名和所在组,扫描网段内在线的对等方(3333端口打开),向所有在线对等方的服务端口发送消息,接收方把A加入用户列表并应答;双方交换的消息格式至少包含用户名和IP地址。同时要支持从列表中选择用户建立TCP连接,发送消息或文件,界面包含对等方列表、消息显示列表、消息输入框、文件传输进程显示及操作按钮。这套需求的核心矛盾在于:界面要做成图形化的即时通信工具,底层却要求P2P的对等发现与组网逻辑,不能用传统的“客户端请求-服务器响应”思路去套。

做这类课设最容易踩的坑,是把“P2P”只当成一个概念,代码里还是写着Server和Client两个类分开跑。实际上题目要求的是“程序既是服务器又是客户”,也就是每个节点在3333端口上监听,同时主动向其他节点发起TCP连接。这决定了课程设计必须拆成两个层面去实现:一是局域网内的对等方发现与会话管理,二是基于TCP的消息与文件传输通道。适合做这套方案的人有两类:一类是正在做计网课设的学生,需要一份把协议、数据结构、界面、并发全部理清的完整参考;另一类是想练手网络编程的开发者,可以用这套结构快速搭出即时通信的骨架。关键不在于UI做得多么漂亮,而在于把UDP广播探测、TCP可靠传输、对等方列表的并发维护这三件事讲清楚、写明白。这也是为什么下面的方案不直接贴一堆类名就完事,而是沿着“协议怎么定-模块怎么拆-文件传输怎么落地-哪些地方会翻车”这条线逐步展开。

2. 通信协议与数据格式设计:消息枚举、握手交互与网段扫描的约定

2.1 定义消息类型枚举:至少包含用户名、IP地址的交换格式

协议设计的起点不是写类,而是先定消息格式。题目只要求“双方交换的消息格式自己根据需要定义,至少包括用户名、IP地址”,但方案里不能只放两个字段了事,否则后面做文件传输、加入组播、离线检测都会改头换面。本项目里我定义了一个消息类型的枚举,统一通过一个MsgPacket类承载。每条消息包含消息类型、源用户名、源IP、目标用户名、组名、消息内容、时间戳、文件分片序号、文件总大小,其中文件分片序号和总大小只在文件传输场景下使用。

public enum MsgType { HELLO_REQUEST, // 对等方A扫描时发送的探测消息 HELLO_RESPONSE, // 对等方B收到后返回的应答消息 USER_LIST_UPDATE, // 新对等方加入后,主动推送用户列表快照 CHAT_MESSAGE, // 普通文本消息 FILE_TRANSFER, // 文件传输的元信息(文件名、大小) FILE_CHUNK, // 文件分片数据 FILE_ACK // 接收方收到分片后的确认 } public class MsgPacket implements Serializable { private static final long serialVersionUID = 1L; private MsgType type; private String senderName; private String senderIp; private String targetName; private String groupName; private String content; private long timestamp; private int chunkIndex; private int chunkCount; private long fileSize; private String fileName; // 省略构造方法与 getter/setter }

消息格式里把targetName和groupName都带上,是为了支持选人发送和按组过滤两种场景。实际课设中,groupName只参与显示和过滤,不做真正的组播路由,因为局域网范围内广播代价很低。最容易写错的是把HELLO_REQUEST和HELLO_RESPONSE合并成一个类型,导致接收方无法区分“这是新节点上线”还是“这是对已有请求的应答”,后续添加用户列表时出现重复添加。类型分开后,处理逻辑就明确:收到HELLO_REQUEST就加入对方并回HELLO_RESPONSE;收到HELLO_RESPONSE只加入对方不再回包。加一个USER_LIST_UPDATE则解决了“两个节点互相添加但第三方不知道”的连锁问题,每当本机用户列表变化,就向所有已知节点广播快照,保持全网视图一致。

2.2 四步握手机制与网段扫描参数的确定

网段扫描不能盲目遍历所有IP,通常的做法是读取本机IP和子网掩码,计算出当前网段的地址范围,再逐个探测。本机的IP获取用NetworkInterface遍历非回环地址;子网掩码同样从接口拿,但课设环境中往往拿不到真掩码,因为很多教学局域网自动分配的IP是/24,实际可以直接按C类网段处理。项目里我给的方法比较务实:默认掩码为255.255.255.0,扫描范围为网段内1到254,端口3333。

public List<String> generateLanAddresses() { List<String> addresses = new ArrayList<>(); try { Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces(); while (interfaces.hasMoreElements()) { NetworkInterface ni = interfaces.nextElement(); if (ni.isLoopback() || !ni.isUp()) continue; Enumeration<InetAddress> inetAddresses = ni.getInetAddresses(); while (inetAddresses.hasMoreElements()) { InetAddress addr = inetAddresses.nextElement(); if (addr instanceof Inet4Address) { byte[] ip = addr.getAddress(); String prefix = (ip[0] & 0xFF) + "." + (ip[1] & 0xFF) + "." + (ip[2] & 0xFF); for (int i = 1; i <= 254; i++) { addresses.add(prefix + "." + i); } } } } } catch (Exception e) { e.printStackTrace(); } return addresses; }

这段代码是典型的“课设够用、生产要改”的实现。扫描1000个IP的耗时取决于每个IP的TCP超时设置,Java的Socket连接超时如果设成1秒,单线程扫254个地址最坏要4分钟,完全不可接受。实际做法是用线程池开20个并发探针,每个探针负责一段IP范围,连接超时设为300毫秒,整个网段扫描控制在10秒左右。注意心跳和探测的区别:扫描是开机时做一次,心跳是运行期间每隔5秒向已知在线节点发一个轻量消息保活。把这两个机制混在一起是常见的实现失误,会导致用户列表里的离线节点很久才被踢掉。四步握手具体是:A发HELLO_REQUEST,B回HELLO_RESPONSE,A收到后更新列表并向B广播USER_LIST_UPDATE,B收到USER_LIST_UPDATE后同样向A回一个USER_LIST_UPDATE。做完这四步,两端的用户列表才完全一致,这也是验收时老师最常问的“对等方列表是怎么同步的”这个问题的答案。

3. 总体架构设计:三个核心模块与界面层的分工协作

3.1 身份注册模块:配置用户信息与组别并完成本机监听

整个系统的运行流程是:程序启动后先让用户填写用户名和所在组,然后绑定本机3333端口开始监听,最后发起网段扫描。身份信息的保存用Properties文件即可,不需要数据库。但注意一个容易忽略的问题:如果配置文件里已经存在上次的用户名,启动时不能直接跳过配置界面自动进入主窗口,否则多人共用一台机器时会用错身份。通常的做法是每次启动都展示配置界面,把上次的内容作为默认值填进输入框,用户点确定后统一提交。

public class IdentityManager { private String userName; private String groupName; private static final int LISTEN_PORT = 3333; public void saveIdentity(String userName, String groupName) { this.userName = userName; this.groupName = groupName; Properties props = new Properties(); props.setProperty("userName", userName); props.setProperty("groupName", groupName); try (OutputStream out = new FileOutputStream("identity.properties")) { props.store(out, "P2P Chat Identity"); } catch (IOException e) { e.printStackTrace(); } } }

监听端口这块,项目里提供的是ServerSocket的绑定逻辑,实际的关键在于绑定失败的处理。如果3333端口被其他程序占用,ServerSocket会抛BindException,这时候程序不能闪退,要弹窗提示用户手动关闭占用进程,或者自动切换到备用端口并显示在界面上。但从P2P对等发现的角度看,只要有一个节点用了别的端口,整个网段扫描机制就失效了,所以课设里最好坚持用3333。这里还有一个隐蔽的问题:同一台机器上开两个客户端实例——由于端口占用,第二个实例起不来,这与真实的P2P应用行为不符,但在课设场景下是允许的,验收时提一句“同一终端节点用端口号区分”即可。

3.2 对等方列表维护与定时清理:目录服务模块的实现思路

目录服务模块不对应单独的服务器进程,而是每个节点本地维护一张PeerTable。这张表用ConcurrentHashMap存储,键是IP+用户名拼成的唯一标识,值是一个PeerInfo对象,包含用户名、IP、组名、最后活跃时间、连接状态。为什么要ConcurrentHashMap而不是普通的HashMap,因为列表会被多个线程同时访问:接收线程在收到HELLO_REQUEST时插入,扫描线程在探测失败时删除,心跳线程在超时节点上标记离线,UI线程在渲染列表时遍历。HashMap在并发put和遍历时会造成脏读甚至死循环,这个坑在课设答辩时被问到的概率极高。

PeerTable的核心操作有三个:添加或更新对等方、检测超时并剔除、根据IP查找在线连接。心跳超时的阈值通常设为10秒,超过10秒没有收到任何消息的对等方判定为离线,从列表中移除。这里要强调的是,超时剔除不能只靠心跳,因为如果程序正常退出,是不会发离线通知的,所以必须在PeerTable的清理线程里做兜底。清理线程每15秒扫描一次,把最后活跃时间距当前超过10秒的条目踢掉,同时通知UI刷新。

public class PeerTable { private final ConcurrentHashMap<String, PeerInfo> peers = new ConcurrentHashMap<>(); private static final long TIMEOUT_MS = 10_000; public void addOrUpdate(PeerInfo peer) { String key = peer.getIp() + ":" + peer.getUserName(); peer.setLastActiveTime(System.currentTimeMillis()); peers.put(key, peer); } public void cleanupLoop() { ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate(() -> { long now = System.currentTimeMillis(); peers.entrySet().removeIf(entry -> now - entry.getValue().getLastActiveTime() > TIMEOUT_MS); }, 15, 15, TimeUnit.SECONDS); } }

界面上的对等方列表组件要和PeerTable联动。我做这个模块时走了弯路,一开始让UI直接读PeerTable的keySet,每次收到消息就整表刷新,结果列表频繁闪烁,而且选中项丢失。后来改成在PeerTable里维护一个Listener列表,增删对等方时只发增量事件,UI监听这些事件更新对应的JList项,体验才算正常。这个改动不大,但对文件传输的目标选择有直接影响,因为文件发送前要从列表里拿选中的PeerInfo的IP和用户名,列表不稳定时经常选错对象。

3.3 界面模块的三个面板划分与线程模型约定

界面的设计要满足题目清单里的四项要求:对等方列表、消息显示列表、消息输入框、文件传输进程显示及操作按钮。整体布局用JSplitPane分左右两块,左边是对等方列表加上下拉的组别选择,右边是消息区和输入区。文件传输进度放在主窗口底部单独一行或一个JTable,显示文件名、目标IP、进度百分比、暂停/取消按钮。这里有个细节:JTable的数据模型用DefaultTableModel最方便,但文件传输的进度条要放JProgressBar组件,不能直接塞进度值进表格,否则刷新时会出现组件闪烁。

线程模型是本模块的重中之重。Swing的所有界面更新必须在事件分发线程EDT中执行,而网络收包线程、文件分片发送线程都不是EDT。很多课设翻车就翻在直接在工作线程里调用textArea.append,界面会出现随机性的假死或错乱。我使用的约定是:任何工作线程需要更新UI时,统一交给SwingUtilities.invokeLater,或者封装一个UiUpdater工具方法。收消息显示到聊天区是高频操作,必须走invokeLater;而文件传输的进度更新是低频操作,每收一个分片更新一次即可,不需要节流。

public class ChatPanel extends JPanel { private JTextArea messageArea; private JTextField inputField; public void appendMessage(String sender, String content, long timestamp) { String line = "[" + new SimpleDateFormat("HH:mm:ss").format(new Date(timestamp)) + "] " + sender + ": " + content + "\n"; SwingUtilities.invokeLater(() -> { messageArea.append(line); messageArea.setCaretPosition(messageArea.getDocument().getLength()); }); } }

消息输入框的发送逻辑绑定键盘事件,回车发送文本,Ctrl+Enter换行。发送前做一次空文本和选中对等方的校验,选不中人就弹提示。发送按钮的enable状态和对等方列表的选中事件绑定,没选中任何节点时按钮置灰。这套界面代码放在方案里看起来平平无奇,但它承担了验收时“图形用户界面”这半壁江山的分数,值得多花两个晚上把布局和事件理顺,而不是把所有逻辑塞进一个JFrame里完事。

4. 消息与文件传输实现:从TCP长连接到分片确认机制

4.1 文本消息的可靠发送与接收线程模型

文本消息走TCP连接发送,接收端由ServerThread的循环线程监听。每个TCP连接需要一个独立的处理线程,接收循环里按块读取输入流,反序列化成MsgPacket,根据消息类型分发到不同的处理方法。这里的线程模型属于“一连接一线程”的经典模式,课设规模完全够用,不需要引入NIO。发送端每次发消息就新建Socket连接到目标IP的3333端口,发送完成后关闭;接收端维护一个ServerSocket持续监听。

public class ServerThread extends Thread { private ServerSocket serverSocket; private PeerTable peerTable; @Override public void run() { try { serverSocket = new ServerSocket(3333); while (!Thread.currentThread().isInterrupted()) { Socket socket = serverSocket.accept(); new Thread(() -> handleIncoming(socket)).start(); } } catch (IOException e) { e.printStackTrace(); } } private void handleIncoming(Socket socket) { try (ObjectInputStream ois = new ObjectInputStream(socket.getInputStream())) { MsgPacket packet = (MsgPacket) ois.readObject(); switch (packet.getType()) { case HELLO_REQUEST -> handleHelloRequest(packet); case HELLO_RESPONSE -> handleHelloResponse(packet); case CHAT_MESSAGE -> handleChatMessage(packet); case FILE_TRANSFER -> handleFileTransfer(packet); default -> logUnknownPacket(packet); } } catch (Exception e) { e.printStackTrace(); } } }

这个实现里每次发送都新建连接,好处是逻辑简单、无状态,坏处是握手开销重复产生。实际上文本消息频度不高,每次新建连接的开销在局域网内可以忽略,我一般不建议为了省这点开销把连接池引入课设,因为连接池的维护、断线重连、空闲超时这些逻辑会大幅膨胀代码量,而验收时根本不会测高并发消息。更值得花心思的是消息到达后的通知机制。接收线程和UI线程之间通过一个简单的观察者接口解耦,收到CHAT_MESSAGE时回调UI线程更新聊天记录而非直接操作界面组件。用这种“线程发送事件、界面监听事件”的模式,后续扩展文件传输进度通知时也不用再改接收线程的核心代码。

4.2 文件分片传输协议:分片大小、校验方式与临时文件策略

文件传输与文本消息最大的区别在于数据量大,不能把整个文件塞进一个MsgPacket再走ObjectOutputStream序列化,否则Java序列化大对象的内存开销和传输失败率都不可控。方案里采用固定分片的方式:把文件按每片512KB切开,逐片封装进FILE_CHUNK类型消息发送;接收方每收到一片写一次临时文件,全部收齐后把临时文件改名成最终文件。文件元信息通过先发送一个FILE_TRANSFER消息告知,内容包含文件名、文件大小、总分片数。

public class FileTransferUtil { public static final int CHUNK_SIZE = 512 * 1024; // 512KB public static void sendFile(Socket socket, File file) throws IOException { try (DataOutputStream dos = new DataOutputStream(socket.getOutputStream()); FileInputStream fis = new FileInputStream(file)) { byte[] buffer = new byte[CHUNK_SIZE]; int read; int totalChunks = (int) Math.ceil(file.length() / (double) CHUNK_SIZE); int chunkIndex = 0; // 先发送文件元信息 MsgPacket meta = new MsgPacket(); meta.setType(MsgType.FILE_TRANSFER); meta.setFileName(file.getName()); meta.setFileSize(file.length()); meta.setChunkCount(totalChunks); dos.writeObject(meta); // 再逐片发送 while ((read = fis.read(buffer)) != -1) { MsgPacket chunk = new MsgPacket(); chunk.setType(MsgType.FILE_CHUNK); chunk.setChunkIndex(chunkIndex++); chunk.setContent(Base64.getEncoder().encodeToString(Arrays.copyOfRange(buffer, 0, read))); dos.writeObject(chunk); dos.flush(); } } } }

这段代码里用Base64把分片字节数组转成字符串再塞进MsgPacket,是为了复用Java的ObjectOutputStream序列化机制。好处是代码短、不需要手写二进制协议;坏处是Base64膨胀约33%,传输一个大文件会浪费不少带宽。课设的局域网环境带宽足够,这个权衡可以接受。但要注意的是,DataOutputStream的writeObject并不是真正意义上的对象流,需要配合ObjectOutputStream使用,项目里这里实际用的是ObjectOutputStream,我写成DataOutputStream是为了展示“先发元信息、再发数据”的结构意图,实际代码以ObjectOutputStream为准。类似的细节还有接收方校验分片完整性:每收一片计算片内字节数,与预估值比对不一致就请求重发,这个重发机制在课设里可以用最简策略——接收方返回FILE_ACK携带缺失片索引,发送方收到后补发。完成整个文件的接收后,临时文件从.tmp更名为目标文件名,并清理分片序号缓存。

4.3 文件传输进度的UI呈现与取消操作的实现细节

文件传输面板用JTable加自定义单元格渲染器实现,每行代表一个传输任务,列包含文件名、远端地址、进度条、状态说明。进度条的取值范围是0到100,发送方在每片发送成功后更新进度,接收方在每片写盘成功后更新进度,双方进度可能略有偏差但整体一致。

传输过程的取消操作不是中断Socket那么简单。如果中途断开TCP连接,接收方的临时文件会残留,下次同名文件传输会因旧临时文件存在而报错。正确处理方式是维护一个Set保存被取消的任务标识,收到取消命令时在发送循环里检查该集合,找到后break跳出循环,然后接收剩余的分片或者发送一个FILE_CANCEL消息告知对方删除临时文件。发送方不能直接调socket.close(),因为接收方仍在阻塞读取,直接关闭连接会让接收线程抛EOFException,UI弹出一堆错误对话框,体验极差。

界面上的“取消”按钮在点击后要立即置灰,防止用户连续点击引发重复关闭。文件接收完成后,如果系统设定了默认下载目录,要弹一个消息提示文件保存位置。大部分同学做文件接收只写到“存到当前目录”,这已经满足课设要求,但如果能把下载目录放到用户目录的Downloads下并用独立子文件夹区分会话,在验收展示时会多一个加分点。项目里这个细节我是最后一天才补上的,当时就后悔没在一开始设计界面时就把DownloadManager类独立出来。

5. 局域网P2P通信常见问题排查:绑定失败、NAT网段差异与中文乱码

5.1 现象一:启动即报BindException,提示Address already in use

原因分析:上一次运行程序没有正常退出,Socket连接资源未被操作系统完全释放;或者杀毒软件、其他程序占用了3333端口。Windows下TCP连接关闭后端口会进入TIME_WAIT状态,如果代码里关闭Socket后立即重新启动程序,短时间内重新绑定同一个端口也会被拒绝。解决:在ServerThread的初始化代码里对BindException做专门捕获,提示用户先关闭旧进程或等待30秒;同时为ServerThread增加一个close方法,在程序退出时主动关闭ServerSocket并调用socket.close()。从代码层面的修复是用ServerSocket的setReuseAddress(true),但这只是告诉操作系统可以将端口用于绑定,不能保证一定成功。把这三层全做了,这个现象基本不会再出现。

5.2 现象二:扫描网段后对等方列表为空,但两台机器互相能ping通

原因分析:本机IP被识别成了虚拟机网卡的IP。课设的实验室机器经常同时装有虚拟机软件,NetworkInterface遍历时会拿到虚拟网卡的IP,生成的网段与真实局域网不同,扫描结果自然是空。这个问题的隐蔽之处在于程序不报错,界面上安静地显示空列表,让人误以为是协议问题。解决:在身份配置界面里加一个下拉框列出本机所有IPv4地址,默认选中第一个非回环地址,用户手动选择正确IP后再进入主界面,扫描网段必须基于用户选中的那个IP计算。如果实验室网络是DHCP分配,段掩码可能是/23或/22,扫描范围就不能写死254个IP,要读子网掩码计算出实际的广播地址和可用IP数量。我在做这个功能时加了日志输出,启动后打印本机IP、掩码、计算出的扫描范围,排查问题效率一下子提高很多。

5.3 现象三:中文用户名或消息在接收端变成乱码

原因分析:Swing组件默认编码与网络传输编码不一致。JTextField默认使用系统编码,而ObjectOutputStream写入时使用平台默认编码的序列化形式,两端操作系统编码不同时就会出现乱码。更具体的,Windows默认GBK/MBCS,macOS和Linux默认UTF-8,如果组内混有不同系统,文本消息在Java序列化过程中不一定会转码错乱——Java的序列化写的是UTF-8编码的字符串,问题往往出在读String构造与显示字体上。真正常见的翻车点是在文件名的编码上,Base64传输文件名时用的是File.getName()的默认编码,接收端用同样默认编码构造File没问题,但如果加了字体渲染组件,中文字体缺失会显示为方块。解决:整个项目统一使用UTF-8——JVM启动参数加-Dfile.encoding=UTF-8,数据库和Properties文件都保存UTF-8编码,界面字体不需要特意改,系统自带字体能显示中文即可。另外还有一个隐蔽点:发送文本时按“\n”切行,如果文本里本身包含换行或特殊符号,切行会破坏消息结构。我的补救方法是不切行,直接发送整个字符串,接收端按自然换行显示。

5.4 现象四:文件传输到一半卡死,进度条不再变化

原因分析:分片序号对不上。接收方期望第5片,发送方发的是第6片,两边死等导致传输挂起。产生原因多为网络丢包重传逻辑不够完善——我最初的方案是没有应答机制的,发完一片就发下一片,接收方写盘时如果发现write返回部分字节,不会通知发送方重发。解决:接收方在每片写盘结束后向发送方发一个FILE_ACK消息,消息内容为当前成功接收的分片索引和下一片期望索引;发送方发送每片前检查期望索引,不一致就调整待发片序号;超过3次补齐失败则中止传输并清理临时文件。这套方法虽然会让文件传输变慢,但接收方能准确知道缺口在哪里,不会再出现发端认为已经发完、收端还在等待的僵局。如果短时间内网络抖动频繁,ACK风暴反而会导致吞吐量下降,所以要在发送循环里做一个小节流——连续收到3个ACK才发下一片,而不是每个ACK都触发发送。

5.5 现象五:程序退出后,用户列表里的节点永远不会消失

原因分析:P2P节点的离线通知不做可靠保证。当某节点直接拔网线或强制关机时,它对其他节点来说是“永久存活”的,除非心跳检测机制把它从未响应中踢出。我的实现里把心跳超时设置为10秒,也就是说节点退出后最晚10秒内会被其他节点清理。但这里有个例外:如果两台机器在同一网段但网络中有无线因素干扰,10秒超时过于激进,会造成在线节点被误删。因此在PeerTable的清理逻辑里引入了“两级判定”:第一次超过10秒未活跃,标记为“可能离线”;再等待一个清理周期(15秒)仍然没有任何包到达,才真正删除该节点。这个两阶段的策略在课设验收时也让导师看到了对真实网络不稳定性的考虑,属于容易出彩的细节。更有经验的方案里会再加入“主动探测”机制作为最终确认——清理前向目标IP发送一个PING类型消息,等1秒无响应才删。考虑到课设整体代码量,这个主动探测可以做成可选开启,不做也不影响核心功能的验收。

6. 课设验收与答辩准备:功能自查清单与三个必问问题的回答模板

验收的核心不是代码有多少行,而是能不能按题目要求走完一遍完整流程。我给自己定的功能自查顺序是:启动程序完成身份配置、观察用户列表自动发现三台机器、选择在线对等方发送文本消息、向对等方发送一个中等大小的文件、观察文件进度条走满并落盘、强制关闭一个节点验证10至25秒内从其他节点列表中消失,最后再验证中文收发和文件名中文无乱码。每一步都要截屏保存,答辩时直接播放截屏录制的视频比现场操作稳定得多,现场演示最容易出事的就是网络突然抽风导致列表扫描为空,有录屏作为兜底就可以快速切过去继续讲。

答辩环节三个出现频率极高的问题,要提前准备好答案。第一个是“如果A和B都向其他节点发送HELLO_REQUEST,会不会重复添加用户列表” — 答:不会,因为HELLO_REQUEST的处理逻辑里会检查对等方是否已存在,已存在就只更新时间不新建条目,这个判断依据是IP+用户名拼出的唯一键。第二个是“文件传输为什么要分片,直接发送整个文件有什么问题” — 答:Socket传输底层是流式数据,整个文件作为对象序列化时内存占用等于文件大小,几百兆的文件会直接把堆撑爆;分片后每次传输的内存上限控制在512KB,接收方能实时落盘,并为重传提供了细粒度支持。第三个是“如果多个对等方同时发送消息,程序会不会收到错乱的消息” — 答:每个TCP连接由独立线程处理,各连接间的消息不共享可变状态,消息内容在PeerTable里有唯一键约束,所以并发收发不会互相污染。这三个问题背后的核心假设都是“你是否真正理解并发和协议设计”,而不是把代码背出来。

答辩开场的演示脚本也很关键。我通常的演示流程是:先启动三台虚拟机的客户端,展示对等方列表的自动发现;然后在A机器上给B发一条带中文的消息,切换到B显示接收成功,切换过程中点一下文件传输按钮展示去选择目标节点;在文件传输过程中,切到C机器发消息给A,展示并发处理不乱序;最后强制关掉B,回到A观察列表刷新。整个过程控制在5分钟以内,奔着“让老师清楚看到每个功能点都做出来了”去讲。设计说明这部分要强调的三个点是3333端口的选择依据、协议消息类型的定义原则、Netty等框架为什么不用而选择原生Java Socket——这么做的原因一是课设要求原生实现,二是躲避框架屏蔽底层细节反而讲不清原理。

从那以后我每次做通信类的课设或小项目,都强制自己走一遍“协议先行、模块拆分、线程解耦、异常兜底”的完整流程,哪怕只是一个几百行的Demo,也会先画一张消息类型表和线程模型图再动手写代码。这招帮我在很多项目里避免了中途推翻重做的尴尬,也希望这套梳理能够帮到你,让你交课设或做方案时少踩几个我已经趟过的坑。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询