可靠UDP文件传输收发端异常:末尾数据包缺失修复方案
可靠UDP文件传输最后一包数据缺失问题解决
问题原因
- 固定缓冲区未处理实际读取长度:原代码通常按固定1KB缓冲区读取文件,最后一次读取的有效字节数往往小于1024,但发送时仍传递整个缓冲区或按固定长度发送,导致接收端无法识别有效数据的实际边界,最终丢失部分数据。
- 缺少结束标记与可靠确认:发送端发送最后一包后未传递明确的结束信号,接收端可能持续等待后续数据包;或发送端未等待接收端对最后一包的确认就关闭连接,导致最后一包未被正确接收写入。
修改后的Sender2代码
import java.io.FileInputStream; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; public class Sender2 { private static final int BUFFER_SIZE = 1024; private static final int PORT = 8888; public static void main(String[] args) { try (DatagramSocket socket = new DatagramSocket(); FileInputStream fis = new FileInputStream("source.txt")) { InetAddress receiverAddr = InetAddress.getByName("localhost"); byte[] buffer = new byte[BUFFER_SIZE]; int readLen; int seq = 0; // 循环发送文件数据包 while ((readLen = fis.read(buffer)) != -1) { // 数据包格式:[4字节序号][4字节数据长度][实际有效数据] byte[] sendData = new byte[8 + readLen]; // 写入序号 sendData[0] = (byte) (seq >> 24); sendData[1] = (byte) (seq >> 16); sendData[2] = (byte) (seq >> 8); sendData[3] = (byte) seq; // 写入实际数据长度 sendData[4] = (byte) (readLen >> 24); sendData[5] = (byte) (readLen >> 16); sendData[6] = (byte) (readLen >> 8); sendData[7] = (byte) readLen; // 复制有效数据 System.arraycopy(buffer, 0, sendData, 8, readLen); DatagramPacket packet = new DatagramPacket(sendData, sendData.length, receiverAddr, PORT); socket.send(packet); // 等待接收端确认,不匹配则重发 byte[] ackBuffer = new byte[4]; DatagramPacket ackPacket = new DatagramPacket(ackBuffer, ackBuffer.length); socket.receive(ackPacket); int ackSeq = ((ackBuffer[0] & 0xFF) << 24) | ((ackBuffer[1] & 0xFF) << 16) | ((ackBuffer[2] & 0xFF) << 8) | (ackBuffer[3] & 0xFF); if (ackSeq != seq) { fis.skip(-readLen); // 回退文件指针,重新读取当前包 continue; } seq++; } // 发送结束标记包:序号为0xFFFFFFFF,数据长度为0 byte[] endData = new byte[8]; endData[0] = (byte) 0xFF; endData[1] = (byte) 0xFF; endData[2] = (byte) 0xFF; endData[3] = (byte) 0xFF; DatagramPacket endPacket = new DatagramPacket(endData, endData.length, receiverAddr, PORT); socket.send(endPacket); // 确认结束包被接收 byte[] endAckBuffer = new byte[4]; DatagramPacket endAckPacket = new DatagramPacket(endAckBuffer, endAckBuffer.length); socket.receive(endAckPacket); int endAckSeq = ((endAckBuffer[0] & 0xFF) << 24) | ((endAckBuffer[1] & 0xFF) << 16) | ((endAckBuffer[2] & 0xFF) << 8) | (endAckBuffer[3] & 0xFF); while (endAckSeq != 0xFFFFFFFF) { socket.send(endPacket); socket.receive(endAckPacket); endAckSeq = ((endAckBuffer[0] & 0xFF) << 24) | ((endAckBuffer[1] & 0xFF) << 16) | ((endAckBuffer[2] & 0xFF) << 8) | (endAckBuffer[3] & 0xFF); } System.out.println("文件发送完成"); } catch (Exception e) { e.printStackTrace(); } } }
修改后的Receiver2代码
import java.io.FileOutputStream; import java.net.DatagramPacket; import java.net.DatagramSocket; public class Receiver2 { private static final int BUFFER_SIZE = 1024 + 8; // 预留8字节的序号和长度头部 private static final int PORT = 8888; public static void main(String[] args) { try (DatagramSocket socket = new DatagramSocket(PORT); FileOutputStream fos = new FileOutputStream("dest.txt")) { byte[] buffer = new byte[BUFFER_SIZE]; int expectedSeq = 0; while (true) { DatagramPacket packet = new DatagramPacket(buffer, buffer.length); socket.receive(packet); // 解析数据包头部 int seq = ((buffer[0] & 0xFF) << 24) | ((buffer[1] & 0xFF) << 16) | ((buffer[2] & 0xFF) << 8) | (buffer[3] & 0xFF); int dataLen = ((buffer[4] & 0xFF) << 24) | ((buffer[5] & 0xFF) << 16) | ((buffer[6] & 0xFF) << 8) | (buffer[7] & 0xFF); // 识别结束标记包 if (seq == 0xFFFFFFFF && dataLen == 0) { byte[] ackData = new byte[4]; ackData[0] = (byte) 0xFF; ackData[1] = (byte) 0xFF; ackData[2] = (byte) 0xFF; ackData[3] = (byte) 0xFF; DatagramPacket ackPacket = new DatagramPacket(ackData, ackData.length, packet.getAddress(), packet.getPort()); socket.send(ackPacket); break; } // 处理有序数据包,写入有效长度数据 if (seq == expectedSeq) { fos.write(buffer, 8, dataLen); fos.flush(); // 发送序号确认 byte[] ackData = new byte[4]; ackData[0] = (byte) (seq >> 24); ackData[1] = (byte) (seq >> 16); ackData[2] = (byte) (seq >> 8); ackData[3] = (byte) seq; DatagramPacket ackPacket = new DatagramPacket(ackData, ackData.length, packet.getAddress(), packet.getPort()); socket.send(ackPacket); expectedSeq++; } else { // 发送上一个正确序号的确认,要求重发 byte[] ackData = new byte[4]; ackData[0] = (byte) ((expectedSeq - 1) >> 24); ackData[1] = (byte) ((expectedSeq - 1) >> 16); ackData[2] = (byte) ((expectedSeq - 1) >> 8); ackData[3] = (byte) (expectedSeq - 1); DatagramPacket ackPacket = new DatagramPacket(ackData, ackData.length, packet.getAddress(), packet.getPort()); socket.send(ackPacket); } } System.out.println("文件接收完成"); } catch (Exception e) { e.printStackTrace(); } } }
代码修改说明
- 明确数据包格式:新增4字节序号+4字节数据长度的固定头部,接收端可精准提取每包的有效数据长度,彻底解决最后一包的边界识别问题。
- 结束标记机制:发送端在文件传输完成后发送特殊结束包,接收端识别后立即停止接收,避免无限等待或遗漏最后一包。
- 可靠确认重发:每包数据发送后等待接收端的序号确认,若确认不匹配则回退文件指针重发,保证所有数据包(包括最后一包)都能被正确接收。
内容的提问来源于stack exchange,提问作者yyfcoder
相关产品推荐
相关产品推荐

