简介:这是一份面向Java网络编程学习者的UDP图片传输实战示例,聚焦UDP协议下图片数据的分包发送与合包还原。客户端将图片转为字节数组,加入鉴权信息后拆分为多个UDP包发送;服务端对每个包进行正确性校验,过滤非法或错误数据包,再将有效包按序合并,最终还原生成完整图片。资源共7个文件,包含6个Java源码与1份使用说明txt,压缩包约5KB,源码按客户端发送、服务端接收、UDP包头封装、文件工具等职责拆分,便于对照理解分包与合包的核心逻辑。已有431人学习下载,适合正在研究UDP可靠传输、数据校验与图片编解码的开发者参考,可帮助快速掌握UDP分包发送、鉴权校验、乱序合并及图片还原的完整实现思路,并借助说明文档完成本地调试与问题排查。
1. 为什么 UDP 发图片总丢包:从一份 Java 分包合包源码说起
用 UDP 传图片,十个工程师里有八个第一次都会翻车。TCP 帮你把顺序、重传、粘包全兜住了,换成 UDP 之后,一个 200KB 的 JPEG 拆成几十个数据报发出去,接收端要么少收几个包拼出一张花屏图,要么把别人误发的数据当成自己的包解析出一堆乱码。这份udp_分包_和包.rar就是冲着这个痛点来的:SendUdp.java在客户端把图片转成字节数组、加鉴权头、按固定长度分包发送;Server.java在服务端逐包校验、按序号合包、还原成一张完整图片。它适合正在做 Java 网络编程、需要理解 UDP 协议栈行为、或者手头有个内网传图需求又不想上 TCP 的从业者。下面我按「拆包逻辑 → 校验机制 → 合包还原 → 踩坑排查」的顺序,把这份源码拆开讲透。
2. 客户端分包:图片字节流怎么切、鉴权头怎么加
2.1 为什么不能一个 DatagramPacket 发完整张图
UDP 单包的理论上限由 IP 层决定,但实际链路里 MTU 通常是 1500 字节,扣掉 IP 头 20 字节和 UDP 头 8 字节,留给应用数据的空间只有 1472 字节。你如果直接把一张 300KB 的图片塞进一个DatagramPacket发出去,底层会触发 IP 分片,任何一个分片丢失,整个数据报在接收端都会被丢弃,而且你根本不知道丢了哪一片。常见做法是把应用层分包大小控制在 1024 到 1400 字节之间,留出余量给自定义头部。这份源码里SendUdp.java的分包逻辑就是按固定块长切字节数组,每块配一个UdpHeader。
2.2 分包的核心代码与参数含义
// SendUdp.java 核心分包逻辑(基于源码结构还原) public void sendImage(String filePath, InetAddress serverAddr, int serverPort) throws IOException { byte[] imageBytes = FIleUtils.fileToByteArray(filePath); // 整张图转字节 int totalLength = imageBytes.length; int packetSize = 1024; // 每包数据区大小,留出头部空间 int totalPackets = (totalLength + packetSize - 1) / packetSize; // 向上取整 String token = "your_auth_token_here"; // 鉴权标识,服务端用来过滤非法包 for (int i = 0; i < totalPackets; i++) { int offset = i * packetSize; int len = Math.min(packetSize, totalLength - offset); byte[] chunk = new byte[len]; System.arraycopy(imageBytes, offset, chunk, 0, len); // 构造头部:包序号 + 总包数 + 鉴权标识 + 数据长度 UdpHeader header = new UdpHeader(i, totalPackets, token, len); byte[] packetData = header.wrap(chunk); // 头部与数据拼接 DatagramPacket packet = new DatagramPacket( packetData, packetData.length, serverAddr, serverPort); socket.send(packet); } }这段代码里几个参数直接决定成败。packetSize设成 1024 是保守值,内网环境可以提到 1400,但公网或跨路由场景建议不要超过 1200。totalPackets用向上取整算出来,保证最后一块不足packetSize时也能被正确切分。token是鉴权字段,服务端拿到包之后先比对 token,不匹配的直接丢弃,这就是摘要里说的「避免其他人发送的错误包被解析」。UdpHeader.wrap()负责把序号、总包数、token、数据长度拼成一个字节数组,接收端按同样的偏移量解析。
2.3 发送节奏与缓冲区设置
连续socket.send()在本地回环测试时几乎不丢包,但一旦经过真实网卡,发送速率超过链路处理能力就会在驱动层被丢弃。我一般会在每发若干个包之后Thread.sleep(1)做微小的节流,或者用DatagramSocket.setSendBufferSize()把发送缓冲区调大。这份源码没有做流量控制,属于「尽力发送」模型,适合内网低延迟场景。如果你要跨公网传大图,建议在应用层加一个简单的停等或滑动窗口,否则丢包率会随图片体积线性上升。
3. 服务端校验:怎么判断这个 UDP 包该不该收
3.1 校验的三个层次:长度、鉴权、序号
服务端Server.java收到DatagramPacket之后不能直接往合包缓冲区里塞,必须先过三道校验。第一道是长度校验:packet.getLength()必须大于头部固定长度,否则连头部都读不全。第二道是鉴权校验:从头部解析出 token,和预设值比对,不匹配就continue。第三道是序号校验:序号必须在0到totalPackets - 1之间,防止伪造的越界序号导致数组越界。这三道过完,才把数据块写入对应位置。
3.2 校验与合包的代码实现
// Server.java 接收与校验逻辑(基于源码结构还原) byte[] imageBuffer = null; // 合包缓冲区 boolean[] receivedFlags = null; // 标记每个包是否已收到 int expectedTotal = -1; public void receiveLoop(DatagramSocket socket) throws IOException { byte[] buf = new byte[2048]; while (true) { DatagramPacket packet = new DatagramPacket(buf, buf.length); socket.receive(packet); UdpHeader header = UdpHeader.parse(packet.getData(), packet.getLength()); if (header == null) continue; // 长度不足,丢弃 if (!"your_auth_token_here".equals(header.getToken())) { continue; // 鉴权失败,丢弃 } if (expectedTotal == -1) { expectedTotal = header.getTotalPackets(); imageBuffer = new byte[expectedTotal * 1024]; // 预分配 receivedFlags = new boolean[expectedTotal]; } int seq = header.getSeq(); if (seq < 0 || seq >= expectedTotal) continue; // 序号越界 int offset = seq * 1024; System.arraycopy(header.getData(), 0, imageBuffer, offset, header.getDataLength()); receivedFlags[seq] = true; if (allReceived(receivedFlags)) { FIleUtils.byteArrayToFile(imageBuffer, "output.jpg"); break; // 或重置状态等待下一张图 } } }UdpHeader.parse()负责从原始字节里按约定偏移量读出各字段,返回null表示这个包不合法。imageBuffer按totalPackets * 1024预分配,保证每个序号都有对应的写入位置。receivedFlags数组用来判断是否所有包都到齐了,只有全部为true才触发写文件。这里有个细节:如果某个包丢了,allReceived永远返回false,程序会一直等下去。实际使用时需要加一个超时机制,比如超过 5 秒还没收齐就丢弃当前批次,让客户端重发。
3.3 为什么不用 CRC 或 MD5 做整包校验
热搜里有人问 CRC 校验和 MD5 校验工具,但在这份源码的场景里,逐包 CRC 意义不大。UDP 本身有 16 位校验和,能挡住大部分比特翻转,真正的问题是丢包和乱序,而不是单包内容出错。整图 MD5 倒是可以加:客户端发完后把整张图的 MD5 通过一个单独的控制包发过去,服务端合包后算一遍 MD5 比对,不一致就要求重传。这属于进阶用法,源码里没带,但你可以自己补上。
4. 合包还原:从字节数组到一张能打开的图片
4.1 合包的顺序保证与空洞处理
UDP 不保证顺序,所以合包时不能按到达顺序拼接,必须按头部里的序号写到正确偏移量。这就是imageBuffer和receivedFlags存在的意义。如果某个序号一直没到,缓冲区对应位置就是全零字节,直接写文件会得到一张下半部分花屏或全灰的图。所以合包完成的条件必须是「所有序号都收到」,而不是「收到了 totalPackets 个包」——因为可能收到重复包,重复包会让计数虚高但仍有空洞。
4.2 文件还原与格式验证
// FIleUtils.java 字节数组写文件 public static void byteArrayToFile(byte[] data, String outputPath) throws IOException { try (FileOutputStream fos = new FileOutputStream(outputPath)) { fos.write(data); fos.flush(); } }写完之后别急着说成功,用图片查看器打开确认能正常显示。如果打不开,先检查文件头:JPEG 以FF D8开头、FF D9结尾,PNG 以89 50 4E 47开头。用十六进制编辑器看一眼输出文件的前几个字节,就能判断是合包偏移错了还是数据本身有问题。常见翻车是offset算错,比如把seq * packetSize写成了seq * (packetSize + headerLength),导致每个块之间插入了一段头部垃圾数据,图片直接损坏。
4.3 大图传输的内存与超时边界
一张 4MB 的图片按 1024 字节分包是 4096 个包,imageBuffer要占 4MB 内存,receivedFlags占 4KB,单连接没问题。但如果你要同时处理多个客户端,每个连接都预分配这么大缓冲区,内存会迅速吃紧。常见做法是限制单张图片大小,或者用临时文件代替内存缓冲区,收到一块就RandomAccessFile.seek(offset)写入。另外超时时间要设合理:内网 3 到 5 秒,跨公网 10 到 15 秒,太短会误杀慢包,太长会让客户端干等。
5. 避坑与排查:UDP 传图最常见的五个翻车现场
5.1 现象:接收端报java.net.SocketException: Connection reset或Port unreachable
原因:客户端发得太快,服务端还没启动或已经关闭,ICMP 端口不可达消息回传导致receive()抛异常。UDP 是无连接的,但底层 ICMP 错误会反映到 socket 上。解决:在receive()外层包try-catch,捕获SocketException后继续循环,不要直接退出。同时确保服务端先启动、客户端后发送。
5.2 现象:图片能打开但下半部分全灰或花屏
原因:合包时某个序号的数据没到,缓冲区对应位置是全零。allReceived判断逻辑写错了,比如用「收到包数等于总包数」代替「所有标志位为 true」,重复包让计数达标但仍有空洞。解决:严格用boolean[]逐位检查,收到重复包时先判断receivedFlags[seq]是否已经为true,是则丢弃,避免重复写入。
5.3 现象:服务端收到大量不属于本次传输的包,解析出乱码
原因:没有做鉴权,或者鉴权 token 硬编码后泄露。同一端口上任何来源的 UDP 包都会被receive()拿到。解决:每个包头部必须带 token,服务端比对失败直接丢弃。token 可以每次传输随机生成,通过一个单独的 TCP 控制通道或预先约定好。
5.4 现象:小图正常,大图必丢包,且丢包位置随机
原因:发送速率超过链路或接收缓冲区处理能力,驱动层静默丢弃。UDP 没有拥塞控制,发多快丢多快。解决:在发送循环里加节流,每发 50 个包Thread.sleep(1);或者调大setSendBufferSize和setReceiveBufferSize到 1MB 以上。更稳妥的是在应用层实现简单的确认重传:服务端收到一批后回一个 ACK 包,客户端收到 ACK 再发下一批。
5.5 现象:ArrayIndexOutOfBoundsException在合包时抛出
原因:头部解析出的seq或totalPackets是负数或超大值,直接用来算数组下标。伪造包或解析偏移量错误都会导致。解决:解析后先做范围校验,seq必须在[0, totalPackets)内,totalPackets必须大于 0 且小于一个合理上限(比如 100000)。校验不过的包直接丢弃,不要进入合包逻辑。
6. 进阶技巧:给这份源码加上超时重传与 MD5 整图校验
源码本身是「尽力发送」模型,丢包就丢包,没有后悔药。我在实际项目里会补两个东西。第一个是超时重传:服务端每收到一个包就记录时间戳,如果 3 秒内没有收齐,就通过一个 UDP 控制包向客户端发送「缺失序号列表」,客户端只重发缺失的那些包。控制包格式很简单,头部带一个类型字段区分数据包和控制包,后面跟缺失序号数组。第二个是整图 MD5 校验:客户端发完所有数据包后,单独发一个带 MD5 字符串的控制包;服务端合包完成后计算imageBuffer的 MD5,与收到的值比对,不一致就触发全量重传。
// 服务端超时检查与缺失序号上报(伪代码示意) long lastPacketTime = System.currentTimeMillis(); while (!allReceived(receivedFlags)) { if (System.currentTimeMillis() - lastPacketTime > 3000) { List<Integer> missing = new ArrayList<>(); for (int i = 0; i < receivedFlags.length; i++) { if (!receivedFlags[i]) missing.add(i); } sendControlPacket(socket, clientAddr, clientPort, missing); // 通知客户端重发 lastPacketTime = System.currentTimeMillis(); } // 继续 receive 循环... }MD5 校验的代价是多算一次哈希,4MB 图片在普通机器上不到 50 毫秒,完全可接受。但要注意:MD5 控制包本身也可能丢,所以要么给控制包也加确认机制,要么客户端在发完数据后等待一个「校验通过」的 ACK,超时没收到就重发 MD5 包。这套组合拳打下来,内网传图的成功率能从「看运气」提到 99% 以上。
从那以后我每次用 UDP 传文件,都强制走一遍「分包大小压到 1200 以下、头部带序号和 token、服务端逐包校验、收齐后 MD5 比对」的流程,再也没出现过花屏图。希望帮到你。
本文还有配套的精品资源,点击获取