1. 目标
内网需要将文件上传到外网显示。
2. 背景
内网(edge)将拍摄的媒体文件上传到外网(core)上,并在前端页面显示。
3. 方案
使用消息中间件来传递消息,将文件信息放到消息中传输。由于消息有大小限制,文件可以通过分片,多次消息上报,接收方获取到消息之后,将其中的切片信息组装,还原可以得到原文件。
3.1 时序图
3.2 示例伪代码
消息处理接口
interface MessageBroker {
void sendMessage(String message);
boolean hasPendingMessages();
void waitForAllMessagesSent();
}
/**
* Author:yang
* Date:2024-09-11 10:02
* 实现消息处理接口MessageBroker
*/
public class HandleMessage implements MessageBroker {
/**
* 消息发送处理逻辑
*/
@Override
public void sendMessage(String message) {
System.out.println("消息发送逻辑");
}
/**
* 检查是否有待发送的消息
*/
@Override
public boolean hasPendingMessages() {
// 消息队列为空判断
return false;
}
/**
* 等待所有消息发送完成
*/
@Override
public void waitForAllMessagesSent() {
}
}
边缘设备端(媒体文件产生端)
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Base64;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Author:yang
* Date:2024-09-11 9:49
*/
public class FileSliceUploader {
private static final int CHUNK_SIZE = 1024 * 1024; // 定义每个切片的大小,例如1MB
private static final Queue<String> messageQueue = new ConcurrentLinkedQueue<>();
/**
* 上传文件
* @param filePath 文件路径
* @param broker 消息中间件
*/
public static void uploadFile(String filePath, MessageBroker broker) {
File file = new File(filePath);
FileInputStream fis = null;
byte[] buffer = new byte[CHUNK_SIZE];
int bytesRead;
try {
fis = new FileInputStream(file);
int totalChunks = (int) Math.ceil((double) file.length() / CHUNK_SIZE);
String md5 = calculateMD5(file); // 计算文件的MD5值
// 发送文件信息消息
messageQueue.offer(createFileInfoMessage(file.getName(), totalChunks, md5));
broker.sendMessage(messageQueue.poll());
// 读取文件并分片发送
int chunkIndex = 0;
while ((bytesRead = fis.read(buffer, 0, CHUNK_SIZE)) != -1) {
byte[] chunkData = Arrays.copyOf(buffer, bytesRead);
String base64Data = Base64.getEncoder().encodeToString(chunkData);
messageQueue.offer(createFileChunkMessage(file.getName(), chunkIndex, base64Data));
broker.sendMessage(messageQueue.poll());
chunkIndex++;
}
// 冗余设计:启动一个线程池,不断检查队列并发送消息
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
while (!messageQueue.isEmpty() || broker.hasPendingMessages()) {
String message = messageQueue.poll();
if (message != null) {
broker.sendMessage(message);
}
}
broker.waitForAllMessagesSent();
});
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private static String calculateMD5(File file) {
// 实现MD5计算逻辑
return "md5value"; // 假设的MD5值
}
private static String createFileInfoMessage(String fileName, int totalChunks, String md5) {
// 创建包含文件信息的消息
return "File: " + fileName + ", TotalChunks: " + totalChunks + ", MD5: " + md5;
}
private static String createFileChunkMessage(String fileName, int chunkIndex, String base64Data) {
// 创建包含文件切片的消息
return "File: " + fileName + ", ChunkIndex: " + chunkIndex + ", Data: " + base64Data;
}
public static void main(String[] args) {
MessageBroker broker = new HandleMessage(); // 假设的消息中间件
// 在实际项目中可以使用存在媒体文件是触发回调
uploadFile("path/to/your/file.jpg", broker);
}
}
核心业务端
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Base64;
/**
* Author:yang
* Date:2024-09-11 9:52
*/
public class CoreReceiver{
public void receiveMessage(String message,MessageBroker broker) {
// 处理接收到的消息
if (message.startsWith("File:")) {
// 解析文件信息
String[] parts = message.split(", ");
String fileName = parts[1].split(": ")[1];
int totalChunks = Integer.parseInt(parts[2].split(": ")[1]);
String md5 = parts[3].split(": ")[1];
// 初始化文件组装
File file = new File("output/" + fileName);
FileOutputStream fos = null;
try {
fos = new FileOutputStream(file);
for (int i = 0; i < totalChunks; i++) {
String chunkMessage = receiveNextChunkMessage();
if (chunkMessage != null) {
String base64Data = chunkMessage.split(", ")[2].split(": ")[1];
byte[] chunkData = Base64.getDecoder().decode(base64Data);
fos.write(chunkData);
}
}
} catch (IOException e) {
e.printStackTrace();
}
// 校验MD5
if (calculateMD5(file).equals(md5)) {
// 发送确认消息给edge
broker.sendMessage("File " + fileName + " received and verified.");
} else {
// 发送错误消息
broker.sendMessage("File " + fileName + " verification failed.");
}
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private String receiveNextChunkMessage() {
// 从消息队列中接收下一个切片消息
return null; // 实现具体逻辑
}
private String calculateMD5(File file) {
// 实现MD5计算逻辑
return "md5value"; // 假设的MD5值
}
}
4. 示例代码优化
4.1 消息中间件
消息中间件可以是EMQX,RocketMQ以及其他的一些消息中间件。代码中使用对应的客户端去处理消息发送、消息接收以及其他的一些消息逻辑。通过publish和subscribe。
4.2 文件属性处理
可以定义一个文件属性类,其中包含切片数量、索引、大小以及其他的一些相关信息。
4.3 消息处理
设备端发送的切片数据消息可以通过列表存储或者其他什么方式,使用定时任务不断的轮训重发消息(防止部分消息丢失导致数据不完整)。当核心端收到全部的切片数据组装好之后,给设备端发送对应的文件已经收到,设备端将消息从列表中删除,相当于取消切片数据重发
4.4 切片数据处理
文件的切片数据可以将其通过Base64.getEncoder().encodeToString(chunkData)转换为String类型,有特殊要求,如JSON,可以再转换一次。核心端收到消息后,采用逆过程去解析数据,可以得到Base64的String字符串,通过Base64.getDecoder().decode(base64Data)得到对应的字节数组,将切片数据按照顺序写入文件,即为原媒体文件。