资讯动态

P2P局域网即时通信系统实战:UDP发现与TCP传输

发布时间:2026/10/19 11:10:50 来源:尧图企业网站定制
简介广东工业大学计算机网络课程设计实现方案基于点对点模式的局域网即时通信系统适合正在完成计网课设或学习网络编程的学生。系统采用“既是服务器又是客户端”架构利用固定端口扫描网段在线对等方通过自定义协议消息完成注册、对等方发现与应答支持选择用户建立连接实现消息和文件传输界面含对等方列表、消息区、输入框及文件传输控件。压缩包共158个文件以源码文件、工程配置和界面类为主另有扩展配置文件等总大小约20.77MB目录完整可导入IDE直接查看调试。目前已有426人学习下载可学习多线程Socket编程、连接管理、界面布局及协议设计也可作为课设报告撰写和答辩参考。无论是从零复现还是扩展功能都很有帮助。1. 只开一个端口干四件事P2P局域网即时通信系统的真实形态把服务端和客户端塞进同一个程序、同一个端口3333听起来像个黑匣子——但这正是某高校计网课设里最常见的一道题。这套基于P2P的局域网即时通信系统启动后自己监听3333端口等别人连同时向整个网段广播“我上线了”把回应消息的人加进对等方列表双击列表里的名字就能开聊天窗发文本、传文件都走新建的TCP连接。界面是Swing画的对等方列表、消息显示区、输入框、文件传输进度条一个不少整体是“程序既是服务器又是客户”的典型写法。适合正在做计网课设的人、想搞懂UDP与TCP在真实项目里怎么分工的人以及打算拿它二次开发做局域网协作工具的人。2. 技术选型与类结构从class文件名反推一个Swing Socket项目2.1 为什么是Swing Socket而不是WebSocket拿到资源包先别急着解压看一眼里面的.class文件名项目骨架基本就出来了ChatWindow、ProgressBar、SendIPtoLAN、ReceiveIP、testSever、friendlist_doubleclick、sendFilebt_adapter外加一个看起来不相关的assumedExternalFilesCache——那是IDE缓存目录混进来了直接忽略。真正核心的可编译实体就是这七个。这套课设的约束条件很明确图形用户界面、局域网内运行、程序既是服务器又是客户。在这个前提下Java Swing加java.net原生Socket就是最稳的组合。Swing是JDK自带的不引入任何第三方依赖Socket和DatagramSocket也是标准库javac编完就能跑。为什么不选WebSocketWebSocket要建立在HTTP握手之上通信双方得有明确的“服务端”和P2P“人人平等”的模型天然冲突课设评审也不看重这个。为什么不选Netty在一个局域网课设里引入异步事件循环框架复杂度直接翻倍跑起来还需要额外jar包得不偿失。一句话总结技术栈Java 8 Swing Socket DatagramSocket全部靠JDK内置类搞定。这种选型哲学也是学生课设应该有的样子——把精力放在协议设计和并发处理上而不是花在环境搭建上。2.2 目录结构与核心类职责从class文件反推这套程序的类职责大概如下表类名职责备注SendIPtoLAN启动时向局域网广播自己的注册信息用UDP广播实现“扫描网段在线对等方”ReceiveIP监听UDP端口接收广播和应答内部类GetPacket负责循环收包testSever主入口启动3333端口TCP服务类名Sever是拼写遗留不影响运行testSever$ServerThread处理每个TCP连接每accept一个连接就new一个线程testSever$ServerThread$FileSave接收文件流并写入磁盘文件传输的落盘逻辑ChatWindow聊天主窗口消息列表、输入框、文件按钮都在这里friendlist_doubleclick对等方列表双击监听器双击触发私聊窗口打开sendFilebt_adapter发送文件按钮事件适配器按钮叫sendFilebt应该是sendFileButton缩写说实话testSever和sendFilebt_adapter这两个命名一看就是课设现场写出来的拼写随意、大小写不统一但逻辑结构反而清晰网络层、UI层、事件层分得开读起来不费劲。如果我要给这份源码写注释会保留这些类名不动只在类头上标注职责因为改名意味着动所有引用课设阶段不值当。2.3 双通道模型UDP找人与TCP传话整个系统最核心的架构决策是网络层分了两个通道UDP负责“发现”TCP负责“传输”。这个分工不是拍脑袋而是由两种协议的特性决定的。UDP的优势是支持广播——一个DatagramPacket发到255.255.255.255整个子网都能收到。P2P场景里你不知道网段里有哪些机器在线没办法逐个建立TCP连接去探测广播是唯一高效的方式。TCP做不到广播它必须知道对方的IP才能发起三次握手。所以“扫描在线对等方”这一步必须走UDP。TCP的优势是可靠、有序。聊天消息不能丢文件更不能缺字节。消息和文件走TCP连接借助ACK机制和重传机制保证数据完整。这也是计网课设里老师最想看到的考点两种协议的使用场景判断。还有一个细节值得注意3333端口被两个角色共用。TCP监听用ServerSocket绑定3333UDP的DatagramSocket也绑定3333。这么设计的好处是协议简单——不管TCP还是UDP对端都往3333发就行不用记两个端口。坏处是防火墙拦截时两个通道一起挂后面避坑章节会重点讲。3. 核心链路拆解从UDP广播找人到TCP传文件的完整流程3.1 对等方发现SendIPtoLAN广播与ReceiveIP应答先看注册协议的消息格式。题目要求“双方交换的消息格式自己根据需要定义至少包括用户名、IP地址”我用的是管道符分隔的文本协议REGISTER|用户名|所在组|本机IP ACK|用户名|所在组|本机IP MSG_文本内容 FILE_|文件名|文件大小用|做分隔符在Java里解析成本极低String.split(\\|)一行搞定比JSON省去序列化依赖。文本协议还有一个好处抓包时肉眼可读调bug方便。下面是SendIPtoLAN发送广播的代码逻辑我没有贴完整类只贴核心方法public void broadcastRegister(String userName, String groupName) { DatagramSocket socket null; try { socket new DatagramSocket(); // 允许设置广播选项某些系统默认关闭 socket.setBroadcast(true); // 构造注册消息REGISTER|用户名|所在组|本机IP String msg REGISTER| userName | groupName | InetAddress.getLocalHost().getHostAddress(); byte[] buf msg.getBytes(UTF-8); // 广播地址255.255.255.255端口3333 InetAddress broadcastAddr InetAddress.getByName(255.255.255.255); DatagramPacket packet new DatagramPacket(buf, buf.length, broadcastAddr, 3333); socket.send(packet); socket.setSoTimeout(3000); // 循环接收应答直到超时 while (true) { byte[] recvBuf new byte[1024]; DatagramPacket recvPacket new DatagramPacket(recvBuf, recvBuf.length); socket.receive(recvPacket); String response new String(recvPacket.getData(), 0, recvPacket.getLength(), UTF-8); if (response.startsWith(ACK|)) { Peer peer parsePeer(response, recvPacket.getAddress()); peerListModel.addElement(peer); } } } catch (SocketTimeoutException e) { // 3秒后没有新应答结束扫描 System.out.println(广播扫描完成); } catch (Exception e) { e.printStackTrace(); } finally { if (socket ! null) socket.close(); } }逻辑说明程序启动后先发一条UDP广播然后阻塞等待应答setSoTimeout(3000)设置了3秒超时——3秒内收到的所有ACK包都被解析成对等方加入列表超时后自动结束扫描。这个超时值很关键设太短会漏掉响应慢的机器设太长会让用户等太久。参数说明255.255.255.255是受限广播地址能覆盖当前子网所有主机DatagramSocket()无参构造会让系统分配一个随机临时端口不影响接收应答因为应答包会带上发送方的地址和端口parsePeer从ACK消息里拆出用户名、所在组合IP组装成Peer对象塞进JList的数据模型。再来看看ReceiveIP一侧是怎么处理收到的广播。ReceiveIP$GetPacket这个内部类名字已经说明了它的工作方式——一个专门收包的循环线程public class GetPacket implements Runnable { Override public void run() { byte[] buf new byte[1024]; DatagramPacket packet new DatagramPacket(buf, buf.length); while (running) { socket.receive(packet); // 阻塞等待广播 String msg new String(packet.getData(), 0, packet.getLength(), UTF-8); if (msg.startsWith(REGISTER|)) { // 别人广播上线先把对方加入自己的列表 Peer peer parseRegister(msg, packet.getAddress()); if (!isSelf(peer)) { peerListModel.addElement(peer); } // 再回一条ACK把自己的信息告诉对方 String ack ACK| myName | myGroup | myIp; byte[] ackBuf ack.getBytes(UTF-8); DatagramPacket ackPacket new DatagramPacket( ackBuf, ackBuf.length, packet.getAddress(), packet.getPort()); socket.send(ackPacket); } else if (msg.startsWith(ACK|)) { // 自己广播后的应答SendIPtoLAN那边也会收到 Peer peer parsePeer(msg, packet.getAddress()); if (!contains(peer)) { peerListModel.addElement(peer); } } } } }逻辑说明这个线程常驻后台专门等别人发REGISTER。收到别人的注册广播后做了两件事——把自己列表里加上对方同时回一条ACK。对方收到ACK后再把本机加进它的列表。这样一来A广播一次A和所有在线对等方就互相知道了符合题目里“对等方A把回应消息的其它对等方加入用户列表”的要求。参数说明packet.getAddress()和packet.getPort()非常重要——不能自己构造回包地址因为广播包的源端口是发送方的临时端口直接取packet里带的地址和端口回才能保证ACK抵达正确的进程。另外isSelf判断要保留否则自己广播的REGISTER会被自己的ReceiveIP收到把自己加进列表出现“自己和自己聊天”的诡异情况。3.2 3333端口的TCP服务端testSever与ServerThreadUDP负责把人凑齐TCP负责干活。testSever这个类的职责是启动TCP服务端监听3333端口。先看它最外层的accept循环public class testSever { public static void main(String[] args) throws Exception { // 监听3333端口和UDP发现共用一个端口号 ServerSocket serverSocket new ServerSocket(3333); System.out.println(服务端已启动监听端口3333); while (true) { // 阻塞等待客户端连接 Socket socket serverSocket.accept(); System.out.println(收到连接 socket.getInetAddress()); // 每个连接一个线程互不阻塞 new Thread(new ServerThread(socket)).start(); } } }accept()是阻塞方法有连接进来才返回一个已建立的Socket。这里必须用多线程——如果串行处理一个客户端正在传大文件其他客户端的消息就得排队等着体验极差。每accept一个连接就new Thread是最简单也最有效的并发模型。ServerThread的run方法处理具体数据我按协议分成文本和文件两路public class ServerThread implements Runnable { private Socket socket; public ServerThread(Socket socket) { this.socket socket; } Override public void run() { try { DataInputStream in new DataInputStream( new BufferedInputStream(socket.getInputStream())); // 先读4字节int拿到数据长度解决粘包问题 int len in.readInt(); byte[] data new byte[len]; in.readFully(data); // 必须读满len个字节 String payload new String(data, UTF-8); if (payload.startsWith(MSG_)) { // 文本消息直接追加到聊天窗口 String content payload.substring(4); SwingUtilities.invokeLater(() - { chatArea.append(content \n); }); } else if (payload.startsWith(FILE_)) { // 文件头FILE_|文件名|文件大小 String[] parts payload.split(\\|); String fileName parts[1]; long fileSize Long.parseLong(parts[2]); // 交给FileSave处理落盘避免阻塞当前线程 new Thread(new FileSave(socket, fileName, fileSize)).start(); return; // FileSave接管socket当前线程不再处理 } in.close(); socket.close(); } catch (Exception e) { e.printStackTrace(); } } }逻辑说明DataInputStream是InputStream的包装提供readInt()和readFully()这类按需读取的便捷方法。先读int长度再读内容是TCP编程的标准套路——TCP是流式协议没有消息边界如果不加长度前缀接收端根本不知道一条消息在哪结束、下一条从哪开始。readFully保证读够长度不会因为网络抖动只读到半个包。参数说明MSG_和FILE_是两个协议前缀对应2.1里定义的文本协议。MSG_后面直接跟聊天内容FILE_后面是管道符分隔的文件名和大小这里把文件接收单独交给FileSave线程因为文件可能很大几秒甚至几十秒的IO不能让连接处理线程一直占着。ServerThread$FileSave的落盘逻辑核心就是循环读写public class FileSave implements Runnable { private Socket socket; private String fileName; private long fileSize; Override public void run() { try { // 从socket读取剩余的文件字节流 InputStream in socket.getInputStream(); FileOutputStream fos new FileOutputStream(received_ fileName); byte[] buffer new byte[8192]; long totalRead 0; int readSize; while (totalRead fileSize (readSize in.read(buffer)) ! -1) { fos.write(buffer, 0, readSize); totalRead readSize; // 更新进度条注意切回EDT线程 long finalTotal totalRead; SwingUtilities.invokeLater(() - { progressBar.setValue((int) (finalTotal * 100 / fileSize)); }); } fos.close(); socket.close(); } catch (Exception e) { e.printStackTrace(); } } }逻辑说明文件头里已经告诉接收方文件总大小所以循环条件用totalRead fileSize控制读满为止。buffer用8KB的数组兼顾内存占用和IO效率。进度条计算公式finalTotal * 100 / fileSize得到的是0到100的整数作为JProgressBar的值。3.3 消息与文件传输ChatWindow、friendlist_doubleclick与sendFilebt_adapterUI入口是ChatWindow它的布局很简单上方一个只读的JTextArea显示历史消息中间是对等方列表JList下方一个输入框加“发送”按钮和“发送文件”按钮。friendlist_doubleclick是JList的双击监听器双击一个对等方就打开一个新的私聊窗口。发送文本的核心代码Socket socket new Socket(peer.getIp(), 3333); DataOutputStream out new DataOutputStream( new BufferedOutputStream(socket.getOutputStream())); String msg MSG_ inputField.getText(); byte[] data msg.getBytes(UTF-8); out.writeInt(data.length); // 先写长度 out.write(data); // 再写内容 out.flush(); socket.close();逻辑说明这里和ServerThread的读取是严格对称的——先writeInt写长度再写内容接收方才能正确还原消息边界。这组对称的读写代码是整个TCP通信的核心契约发送方和接收方必须一致否则解析错乱。sendFilebt_adapter是发送文件按钮的事件适配器我直接继承了MouseAdapter或实现了ActionListener。处理文件逻辑时有一个重要教训——不能在事件线程里做IO否则界面卡死new Thread(() - { try { File file new File(filePath); Socket socket new Socket(peer.getIp(), 3333); DataOutputStream out new DataOutputStream( new BufferedOutputStream(socket.getOutputStream())); // 先发文件头FILE_|文件名|文件大小 String header FILE_| file.getName() | file.length(); byte[] headerBytes header.getBytes(UTF-8); out.writeInt(headerBytes.length); out.write(headerBytes); // 再发文件内容 FileInputStream fis new FileInputStream(file); byte[] buffer new byte[8192]; int readSize; long totalSent 0; while ((readSize fis.read(buffer)) ! -1) { out.write(buffer, 0, readSize); totalSent readSize; // 进度条更新必须回到Swing事件线程 long sent totalSent; long total file.length(); SwingUtilities.invokeLater(() - { progressBar.setValue((int) (sent * 100 / total)); }); } out.flush(); fis.close(); socket.close(); } catch (Exception e) { e.printStackTrace(); } }, FileSendThread).start();逻辑说明文件发送的协议顺序是“文件头先走文件体后走”。接收方ServerThread先读到文件头解析出文件名和大小然后FileSave接着从同一个socket流里把文件字节读出来。这里有个隐蔽的坑——FILE_的文件头发出去后发送方紧接着写文件内容两段数据在TCP流里是无缝连接的接收方必须readFully先读完文件头再进入文件体循环顺序一错整个文件就废了。参数说明buffer用8192字节即8KB。这个值经过长期实践在局域网环境里吞吐量足够内存占用也可控。每次write之后没有立刻flush而是全部写完再flush减少系统调用次数文件较大时这个优化能明显提升发送速度。4. 跑通与复现从源码到两台机器互发消息的实测路径4.1 环境准备与项目导入这套代码跑起来的门槛很低JDK 8及以上就够不需要Maven不需要Gradle不需要任何第三方jar。我一般直接在IntelliJ IDEA里新建一个空项目把源码文件全部拷贝到src目录IDEA会自动识别并编译。如果你习惯命令行下面这个流程更直接。准备工作清单安装JDK 8java -version能正常输出版本号两台上网设备或者一台机器双开两个Java进程做回环测试确保局域网互通——两台机器能互相ping通关闭防火墙或在防火墙里放行3333端口的TCP和UDP入站防火墙是最容易被忽略的一步。UDP广播发出去了对方机器的防火墙直接丢弃表现为“我广播了但列表永远是空的”。Windows上最简单的验证方式是运行窗口输入wf.msc打开高级安全防火墙在入站规则里新建一条规则放行3333端口TCPUDP。4.2 编译与启动顺序命令行编译的命令javac -encoding UTF-8 *.java必须带上-encoding UTF-8否则含中文的源码在Windows默认GBK编码下编译出来是乱码。编译成功后目录里会生成一堆.class文件主入口是testSeverjava testSever启动顺序有讲究。正确姿势是先启动A机器上的实例再启动B机器上的实例。A先进入等待接收状态B启动时发广播A收到后回ACK两边列表互相出现对方。如果顺序反了也没关系A启动时B已经在线A的广播B能收到——P2P模型里无论谁先启动后启动的那一方都会主动广播先启动的一方靠监听端口发现对方。单机双开做测试时同一个JVM里的两个进程都需要绑定3333端口。Windows和Linux同一台机器上允许不同进程绑定同一个端口吗答案是区分场景TCP的ServerSocket绑定3333两个进程会冲突第二个会报Address already in use。所以双开测试时要么改第二个实例的端口要么借助虚拟网卡要么直接在两台物理机或两台虚拟机上测。我一般用VMware开两台Linux虚拟机各配一个NAT网卡的独立IP跑起来最真实。4.3 功能验证清单我把课设验收标准拆成六个步骤照着走一遍就知道系统功能完不完整在A机器启动一个实例设置用户名“A同学”、所在组“一组”在B机器启动一个实例设置用户名“B同学”、所在组“二组”观察A的“对等方列表”——B应该出现在列表里反之亦然双击列表里的B打开聊天窗口输入“你好B”点发送B的聊天窗口里能看到A发的消息B回一条A能看到点击“发送文件”按钮选一个大小约1MB的文件发给B观察进度条走完B的本地目录出现received_文件名第3步很容易出问题如果列表里看不到对方优先检查防火墙和网段。执行ping B的IP看通不通再看两台机器子网掩码是否一致——一个是255.255.255.0、一个是255.255.0.0广播域就不同UDP广播永远无法跨子网让对方收到。4.4 网络连通性快检命令验证3333端口TCP监听是否正常netstat -an | grep 3333Windows用netstat -an | findstr 3333。输出里应看到0.0.0.0:3333 LISTENING说明ServerSocket起来了。再执行telnet 目标机器IP 3333能连通说明TCP通道正常连不上十有八九是防火墙。UDP通道没法用telnet测验证方式更直接在A机器后台开着程序在B机器用nping或简单Java程序往3333发任意UDP包看A的ReceiveIP那边有没有日志输出。没有日志输出就说明UDP包被拦了先放行防火墙再跑。5. 避坑记录局域网P2P聊天调试中的五个高频翻车点5.1 防火墙拦截广播有去无回现象程序正常启动用户名和组都设置好了广播也在发但对等方列表永远是空的等多久都刷不出来。原因系统防火墙默认拦截未授权程序的入站连接。UDP广播发出去了对方的防火墙把包丢掉对方收不到你的REGISTER自然不回ACK即使对方广播过来你的防火墙同样会拦截。解决在Windows防火墙入站规则里放行3333端口的TCP和UDP或者直接给java.exe添加“允许”规则。更快的办法是把网络配置文件改成“专用网络”Windows默认对专用网络的防火墙限制要宽松一些。Linux环境用sudo ufw allow 3333/tcp和sudo ufw allow 3333/udp放行。改完重启程序列表立刻能刷出来。5.2 单机双开调试端口冲突与看不到自己现象想在本地开两个实例做测试第二个实例启动直接报java.net.BindException: Address already in use或者两个实例分别改了端口结果互相在列表里看不到对方。原因3333端口被第一个实例占用第二个实例的ServerSocket(3333)无法再绑定。改端口虽然避开了冲突但两边监听的端口不一致广播的REGISTER发到3333对方在另一个端口监听根本收不到。解决能用两台机器测就别单机双开这是最省心的方法。实在只有一台机器就起两台VMware虚拟机网络模式选NAT虚拟机之间天然在同一网段。另一个思路是给本机网卡加一个回环适配器Microsoft Loopback Adapter手动配一个额外IP一个实例绑原IP一个实例绑回环IP也能模拟两台主机。5.3 消息粘包与中文乱码TCP流的边界问题现象发消息偶尔正常偶尔两条消息黏在一起显示成一行中文内容偶尔变成“?????”或者后半段是乱码。原因TCP是流式协议多次write写入的数据会被合并成一段连续字节接收方无法直接判断哪一截是第一条消息、哪一截是第二条。中文乱码则是因为发送方用了默认编码Windows下是GBK接收方用UTF-8解码两边编码不一致必然乱。解决协议里统一加长度前缀。发送方先把消息字节长度用writeInt写进去再写内容接收方先readInt拿到长度再readFully读对应长度的字节这样粘包和半包同时解决。编码统一硬性规定UTF-8编译器加-encoding UTF-8代码里getBytes(UTF-8)和new String(data, UTF-8)成对出现。5.4 大文件发送界面假死Swing EDT线程阻塞现象给对等方发一个几百MB的视频点完“发送文件”按钮整个窗口立刻拖不动、点不了进度条也不动要等文件传完界面才恢复。原因文件发送的IO操作直接跑在Swing事件分发线程EDT上。EDT负责所有界面重绘和事件响应一旦被长时间阻塞窗口就进入“未响应”状态。进度条不动是因为文件还没读完进度更新代码根本没机会执行。解决文件收发逻辑全部丢到独立线程里。发送按钮触发后new Thread(...).start()跑IOProgressBar的更新用SwingUtilities.invokeLater切回EDT。接收侧也同理FileSave本身就应该是一个独立线程。改完后界面始终保持流畅就算文件很大也能一边传一边拖动窗口。5.5 广播地址与子网掩码不匹配扫描不到任何对等方现象几台机器明明连着同一个路由器IP都是192.168.1.x但程序互扫不到对方。执行ipconfig发现有人子网掩码是255.255.255.0有人是255.255.0.0。原因UDP广播255.255.255.255虽然叫“受限广播”但实际能不能跨子网取决于路由器的隔离策略和主机的路由配置。子网掩码不一致时一个主机认为对方不在同一广播域广播包可能只发到本机网段就结束了。解决先用ipconfig或ifconfig确认所有参与通信的机器子网掩码一致以及网段相同比如都是192.168.1.0/24。跨网段场景就不要依赖广播了改成配置一个已知的“列表服务器”地址或者让用户手动输入对等方的IP——这是课设阶段最务实的兜底方案。6. 二次开发指南把课设改造成带心跳和持久化的可用工具这版课设的雏形能跑通验收但离“通用工具”还差三步每一步我都在实际改造中踩过一遍直接给改造思路。第一步加心跳与离线检测。现在项目里对等方退出后列表里还留着它的名字点它通信直接报连接拒绝。你需要一个后台定时任务每5秒广播一次HELLO|用户名同时记录每个对等方的最后心跳时间。超过15秒没收到心跳就从列表里剔除。这个机制对P2P场景尤其重要因为不存在中心服务器来维护在线状态全靠节点互报“我还在”。// 心跳线程每5秒广播一次自己的存在 ScheduledExecutorService scheduler Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate(() - { String hello HELLO| myName | myIp; // 复用SendIPtoLAN的发送代码构造DatagramPacket发到3333端口 broadcast(hello); }, 0, 5, TimeUnit.SECONDS); // 收到HELLO时更新心跳时间 MapString, Long lastSeen new ConcurrentHashMap(); if (msg.startsWith(HELLO|)) { lastSeen.put(peerKey(msg), System.currentTimeMillis()); }第二步协议升级加消息序号与ACK。现在文本消息发出去没有回执对方收到与否完全不知道。改造时每条MSG_消息带上自增序号接收方处理完回一条ACK_MSG|序号。发送方如果在超时时间内没收到回执就重发。这个机制本质是在TCP之上做应用层确认——TCP只管字节到达不管业务消息是否完整呈现给用户。第三步持久化与架构演进。聊天记录目前关窗就丢我一般会引入SQLite或直接追加写文件。每条消息追加一行日志启动时按对话对象分组加载回聊天窗口。这不算难但值得做。从架构上讲这套纯P2P模型在跨网段场景会失效——广播跨不了路由器。更成熟的演进方向是中心化信令服务器服务器只管注册、维护在线列表、转发小消息文件传输仍然走P2P直连。这就是从“纯P2P”演进到“混合P2P”的过程也是很多即时通信软件的真实形态。从那以后我每次跑局域网通信类项目都会强制走一遍流程先确认防火墙放行、再确认子网掩码一致、最后才打开代码查bug。这套课设的代码虽然命名随意但协议设计和线程模型都是合格的范本值得照着写一遍。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑