面试题答案
一键面试校验和机制
- 发送端:
- 在发送文件前,对文件数据进行校验和计算。可以使用常见的校验和算法,如CRC(循环冗余校验)、MD5、SHA - 1等。例如,使用Java的
java.security.MessageDigest
类来计算MD5校验和:
import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.io.File; import java.io.FileInputStream; import java.io.IOException; public class ChecksumCalculator { public static String calculateMD5(File file) throws NoSuchAlgorithmException, IOException { MessageDigest md = MessageDigest.getInstance("MD5"); FileInputStream fis = new FileInputStream(file); byte[] dataBytes = new byte[1024]; int nread; while ((nread = fis.read(dataBytes)) != -1) { md.update(dataBytes, 0, nread); } byte[] mdbytes = md.digest(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < mdbytes.length; i++) { sb.append(Integer.toString((mdbytes[i] & 0xff) + 0x100, 16).substring(1)); } fis.close(); return sb.toString(); } }
- 在发送文件数据时,将计算好的校验和一并发送给接收端。可以在文件数据的头部添加校验和字段,例如通过自定义协议格式:
// 假设校验和为32位字符串 String checksum = ChecksumCalculator.calculateMD5(file); OutputStream os = socket.getOutputStream(); os.write(checksum.getBytes()); // 发送文件数据 FileInputStream fis = new FileInputStream(file); byte[] buffer = new byte[1024]; int length; while ((length = fis.read(buffer)) != -1) { os.write(buffer, 0, length); } fis.close();
- 在发送文件前,对文件数据进行校验和计算。可以使用常见的校验和算法,如CRC(循环冗余校验)、MD5、SHA - 1等。例如,使用Java的
- 接收端:
- 接收数据时,先读取校验和字段。
InputStream is = socket.getInputStream(); byte[] checksumBytes = new byte[32]; is.read(checksumBytes); String receivedChecksum = new String(checksumBytes);
- 接收完文件数据后,对接收到的文件数据进行同样的校验和计算。
// 接收文件数据到临时文件 File tempFile = File.createTempFile("receivedFile", null); FileOutputStream fos = new FileOutputStream(tempFile); byte[] buffer = new byte[1024]; int length; while ((length = is.read(buffer)) != -1) { fos.write(buffer, 0, length); } fos.close(); String calculatedChecksum = ChecksumCalculator.calculateMD5(tempFile);
- 比较接收到的校验和与计算得到的校验和。如果两者一致,说明文件数据完整无误;否则,请求发送端重新发送文件。
if (calculatedChecksum.equals(receivedChecksum)) { // 文件数据完整,处理文件 } else { // 请求重传 OutputStream os = socket.getOutputStream(); os.write("RETRANSMIT".getBytes()); }
重传机制
- 发送端:
- 序号管理:为每个发送的数据包分配一个唯一的序号。可以在自定义协议头中添加序号字段。例如:
class Packet { int sequenceNumber; byte[] data; // 构造函数等其他方法 }
- 超时重传:维护一个发送数据包的列表和对应的定时器。当发送一个数据包后,启动一个定时器。如果在定时器超时前没有收到接收端的确认(ACK),则重新发送该数据包。可以使用
java.util.Timer
或ScheduledExecutorService
来实现定时器功能。例如,使用ScheduledExecutorService
:
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1); Map<Integer, ScheduledFuture<?>> sequenceNumberToFuture = new HashMap<>(); Packet packet = new Packet(sequenceNumber, data); OutputStream os = socket.getOutputStream(); os.write(packet.toBytes());// 将Packet转换为字节数组发送 ScheduledFuture<?> future = executorService.schedule(() -> { if (!ackReceived(sequenceNumber)) { // 重传 os.write(packet.toBytes()); } }, 5, TimeUnit.SECONDS); sequenceNumberToFuture.put(sequenceNumber, future);
- 窗口机制:为了提高传输效率,可以采用滑动窗口机制。发送端维护一个发送窗口,窗口内的数据包可以连续发送而无需等待每个数据包的ACK。窗口大小可以根据网络状况动态调整。例如,初始窗口大小设为10个数据包:
int windowSize = 10; int nextSequenceNumber = 0; while (true) { for (int i = 0; i < windowSize && nextSequenceNumber < totalPackets; i++) { Packet packet = new Packet(nextSequenceNumber, data[nextSequenceNumber]); OutputStream os = socket.getOutputStream(); os.write(packet.toBytes()); ScheduledFuture<?> future = executorService.schedule(() -> { if (!ackReceived(nextSequenceNumber)) { // 重传 os.write(packet.toBytes()); } }, 5, TimeUnit.SECONDS); sequenceNumberToFuture.put(nextSequenceNumber, future); nextSequenceNumber++; } // 等待部分ACK,更新窗口 // 逻辑实现略 }
- 接收端:
- 确认发送:当接收到一个数据包时,检查其序号是否正确。如果正确,回复一个确认消息(ACK)给发送端,ACK消息中包含已正确接收的数据包序号。
InputStream is = socket.getInputStream(); byte[] packetBytes = new byte[1024]; int length = is.read(packetBytes); Packet packet = Packet.fromBytes(packetBytes, length); if (packet.sequenceNumber == expectedSequenceNumber) { // 处理数据包 OutputStream os = socket.getOutputStream(); os.write(("ACK " + packet.sequenceNumber).getBytes()); expectedSequenceNumber++; }
- 乱序处理:如果接收到的数据包序号不连续,说明数据包可能乱序到达。接收端可以缓存这些乱序的数据包,等待缺失的数据包到达后再按序处理。可以使用一个
Map<Integer, Packet>
来缓存乱序数据包,键为数据包序号。
Map<Integer, Packet> outOfOrderPackets = new HashMap<>(); if (packet.sequenceNumber > expectedSequenceNumber) { outOfOrderPackets.put(packet.sequenceNumber, packet); } else if (packet.sequenceNumber < expectedSequenceNumber) { // 可能是重复包,丢弃 } else { // 正常按序包,处理 OutputStream os = socket.getOutputStream(); os.write(("ACK " + packet.sequenceNumber).getBytes()); expectedSequenceNumber++; // 检查缓存中是否有可按序处理的包 while (outOfOrderPackets.containsKey(expectedSequenceNumber)) { Packet cachedPacket = outOfOrderPackets.get(expectedSequenceNumber); // 处理缓存包 os.write(("ACK " + expectedSequenceNumber).getBytes()); expectedSequenceNumber++; outOfOrderPackets.remove(expectedSequenceNumber); } }