предисловие
Загрузка файлов - старая тема. В случае относительно небольших файлов вы можете напрямую конвертировать файлы в потоки байтов и загружать их на сервер. Однако в случае больших файлов загружать их обычным способом. Это не случай.Хороший способ ведь мало кто это выдержит.Когда закачка файла прерывается на середине,можно только заново начать с начала при закачке файла.Такой неприятный опыт. Есть ли лучший способ загрузки? Ответ - да, но есть несколько способов загрузки, которые будут представлены ниже.
Подробное руководство
второй проход
1. Что такое второй проходС точки зрения непрофессионала, когда вы загружаете что-то для загрузки, сервер сначала выполнит проверку MD5. Если на сервере есть то же самое, он напрямую даст вам новый адрес. Фактически, вы загружаете тот же файл на сервер.Если вы хотите не загружать в секунды, на самом деле, пока вы меняете MD5, вам нужно изменить сам файл (имя нельзя изменить).Например, если вы добавите еще несколько слов в текстовый файл, MD5 изменится, и он не будет загружен в течение нескольких секунд.
2. Основная логика второй передачи, реализованная в этой статье.
- А. Используйте метод set Redis для сохранения статуса загрузки файла, где ключ — это md5 загрузки файла, а значение — бит флага, указывающий, завершена ли загрузка,
- Б. Когда бит флага равен true, загрузка завершена. Если тот же файл загружается в это время, он войдет во вторую логику загрузки. Если бит флага равен false, это означает, что загрузка не была завершена.В это время вам нужно вызвать метод set, чтобы сохранить путь записи файла номера блока, где ключом является файл загрузки md5 плюс фиксированный префикс, а значением является путь записи файла номера блока.
Частичная загрузка
1. Что такое многокомпонентная загрузкаЧастичная загрузка заключается в разделении загружаемого файла на несколько блоков данных (мы называем это частью) в соответствии с определенным размером для загрузки по отдельности.После загрузки сервер загрузит все загруженные файлы.Агрегаты интегрируются в исходный файл.
2. Сценарий многокомпонентной загрузки
- 1. Загрузка больших файлов
- 2. Сетевое окружение не очень хорошее, и есть сценарии, в которых существует риск повторной передачи.
http
1. Что возобновляется с точки останова?Возобновление точки останова происходит при загрузке или выгрузке, задача загрузки или выгрузки (файл или сжатый пакет) искусственно разделена на несколько частей, каждая часть использует поток для загрузки или выгрузки, в случае сбоя сети вы можете продолжить загрузку или загрузка незавершенных частей из частей, которые уже были загружены или загружены, без необходимости начинать загрузку или загрузку с самого начала. Возобновление точки останова в этой статье в основном предназначено для сценария загрузки точки останова.
2. Сценарии примененияВозобновление с точки останова можно рассматривать как производное от многокомпонентной загрузки, поэтому вы можете использовать возобновление с точки останова в сценариях, где можно использовать составную загрузку.
3. Реализуйте основную логику возобновления работы с точки остановаЕсли во время процесса многокомпонентной загрузки загрузка прерывается из-за аномальных факторов, таких как сбой системы или прерывание сети, клиенту необходимо записать ход загрузки. Если позднее загрузка будет снова поддерживаться, вы сможете продолжить загрузку с того места, где последняя загрузка была прервана.
Во избежание проблемы перезапуска загрузки с начала из-за удаления данных прогресса клиента после загрузки данных сегмента, чтобы загрузка продолжалась со следующего сегмента данных.
4. Этапы процесса реализации
- А. Схема 1, стандартные шаги
Разделите загружаемые файлы на блоки данных одинакового размера в соответствии с определенными правилами деления; Инициализировать задачу загрузки из нескольких частей и вернуть уникальный идентификатор этой загрузки из нескольких частей; Отправлять каждый фрагментированный блок данных по определенной стратегии (последовательной или параллельной); После того, как отправка завершена, сервер оценивает полноту загруженных данных и выполняет синтез блока данных для получения исходного файла.
- б. Схема 2, шаги, реализованные в этой статье
Внешний интерфейс (клиент) должен разбить файл в соответствии с фиксированным размером, а при запросе внутреннего интерфейса (сервера) необходимо указать номер и размер сегмента. Сервер создает файл conf для записи местоположения сегментов.Длина файла conf равна общему количеству сегментов.Каждый раз, когда сегмент загружается, в файл conf записывается 127. Затем местоположение, которое не было загружено по умолчанию 0, а тот, который был загружен, имеет значение Byte.MAX_VALUE 127 (этот шаг является основным для реализации возобновления точки останова и второй загрузки) Сервер вычисляет начальную позицию в соответствии с порядковым номером фрагмента, указанным в данных запроса, и размером каждого фрагмента (размер фрагмента фиксированный и одинаковый) и записывает данные фрагмента файла с данными считанного фрагмента файла.
5. Реализация кода загрузки фрагмента/точки останова
- А. Внешний интерфейс использует подключаемый модуль webuploader, предоставляемый Baidu, для фрагментации. Поскольку в этой статье в основном рассказывается о реализации серверного кода и о том, как webuploader выполняет фрагментацию, конкретную реализацию можно найти по следующим ссылкам:
Отправьте загрузку ex.Baidu.com/Web и…
- Б. Бэкэнд реализует запись файла двумя способами. Один из них — использовать RandomAccessFile. Если вы не знакомы с RandomAccessFile, вы можете проверить следующую ссылку:
blog.CSDN.net/Drop Peony 2015…Другой — использовать MappedByteBuffer.Для тех, кто не знаком с MappedByteBuffer, вы можете проверить следующую ссылку, чтобы узнать больше:Woohoo.Краткое описание.com/afraid/send 90866 low cost…
Основной код бэкенда для операций записи
- А. Реализация RandomAccessFile
@UploadMode(mode = UploadModeEnum.RANDOM_ACCESS)
@Slf4j
public class RandomAccessUploadStrategy extends SliceUploadTemplate {
@Autowired
private FilePathUtil filePathUtil;
@Value("${upload.chunkSize}")
private long defaultChunkSize;
@Override
public boolean upload(FileUploadRequestDTO param) {
RandomAccessFile accessTmpFile = null;
try {
String uploadDirPath = filePathUtil.getPath(param);
File tmpFile = super.createTmpFile(param);
accessTmpFile = new RandomAccessFile(tmpFile, "rw");
//这个必须与前端设定的值一致
long chunkSize = Objects.isNull(param.getChunkSize()) ? defaultChunkSize * 1024 * 1024
: param.getChunkSize();
long offset = chunkSize * param.getChunk();
//定位到该分片的偏移量
accessTmpFile.seek(offset);
//写入该分片数据
accessTmpFile.write(param.getFile().getBytes());
boolean isOk = super.checkAndSetUploadProgress(param, uploadDirPath);
return isOk;
} catch (IOException e) {
log.error(e.getMessage(), e);
} finally {
FileUtil.close(accessTmpFile);
}
return false;
}
}
- б. Реализация MappedByteBuffer
@UploadMode(mode = UploadModeEnum.MAPPED_BYTEBUFFER)
@Slf4j
public class MappedByteBufferUploadStrategy extends SliceUploadTemplate {
@Autowired
private FilePathUtil filePathUtil;
@Value("${upload.chunkSize}")
private long defaultChunkSize;
@Override
public boolean upload(FileUploadRequestDTO param) {
RandomAccessFile tempRaf = null;
FileChannel fileChannel = null;
MappedByteBuffer mappedByteBuffer = null;
try {
String uploadDirPath = filePathUtil.getPath(param);
File tmpFile = super.createTmpFile(param);
tempRaf = new RandomAccessFile(tmpFile, "rw");
fileChannel = tempRaf.getChannel();
long chunkSize = Objects.isNull(param.getChunkSize()) ? defaultChunkSize * 1024 * 1024
: param.getChunkSize();
//写入该分片数据
long offset = chunkSize * param.getChunk();
byte[] fileData = param.getFile().getBytes();
mappedByteBuffer = fileChannel
.map(FileChannel.MapMode.READ_WRITE, offset, fileData.length);
mappedByteBuffer.put(fileData);
boolean isOk = super.checkAndSetUploadProgress(param, uploadDirPath);
return isOk;
} catch (IOException e) {
log.error(e.getMessage(), e);
} finally {
FileUtil.freedMappedByteBuffer(mappedByteBuffer);
FileUtil.close(fileChannel);
FileUtil.close(tempRaf);
}
return false;
}
}
- c, код класса основного шаблона файловой операции
@Slf4j
public abstract class SliceUploadTemplate implements SliceUploadStrategy {
public abstract boolean upload(FileUploadRequestDTO param);
protected File createTmpFile(FileUploadRequestDTO param) {
FilePathUtil filePathUtil = SpringContextHolder.getBean(FilePathUtil.class);
param.setPath(FileUtil.withoutHeadAndTailDiagonal(param.getPath()));
String fileName = param.getFile().getOriginalFilename();
String uploadDirPath = filePathUtil.getPath(param);
String tempFileName = fileName + "_tmp";
File tmpDir = new File(uploadDirPath);
File tmpFile = new File(uploadDirPath, tempFileName);
if (!tmpDir.exists()) {
tmpDir.mkdirs();
}
return tmpFile;
}
@Override
public FileUploadDTO sliceUpload(FileUploadRequestDTO param) {
boolean isOk = this.upload(param);
if (isOk) {
File tmpFile = this.createTmpFile(param);
FileUploadDTO fileUploadDTO = this.saveAndFileUploadDTO(param.getFile().getOriginalFilename(), tmpFile);
return fileUploadDTO;
}
String md5 = FileMD5Util.getFileMD5(param.getFile());
Map<Integer, String> map = new HashMap<>();
map.put(param.getChunk(), md5);
return FileUploadDTO.builder().chunkMd5Info(map).build();
}
/**
* 检查并修改文件上传进度
*/
public boolean checkAndSetUploadProgress(FileUploadRequestDTO param, String uploadDirPath) {
String fileName = param.getFile().getOriginalFilename();
File confFile = new File(uploadDirPath, fileName + ".conf");
byte isComplete = 0;
RandomAccessFile accessConfFile = null;
try {
accessConfFile = new RandomAccessFile(confFile, "rw");
//把该分段标记为 true 表示完成
System.out.println("set part " + param.getChunk() + " complete");
//创建conf文件文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认0,已上传的就是Byte.MAX_VALUE 127
accessConfFile.setLength(param.getChunks());
accessConfFile.seek(param.getChunk());
accessConfFile.write(Byte.MAX_VALUE);
//completeList 检查是否全部完成,如果数组里是否全部都是127(全部分片都成功上传)
byte[] completeList = FileUtils.readFileToByteArray(confFile);
isComplete = Byte.MAX_VALUE;
for (int i = 0; i < completeList.length && isComplete == Byte.MAX_VALUE; i++) {
//与运算, 如果有部分没有完成则 isComplete 不是 Byte.MAX_VALUE
isComplete = (byte) (isComplete & completeList[i]);
System.out.println("check part " + i + " complete?:" + completeList[i]);
}
} catch (IOException e) {
log.error(e.getMessage(), e);
} finally {
FileUtil.close(accessConfFile);
}
boolean isOk = setUploadProgress2Redis(param, uploadDirPath, fileName, confFile, isComplete);
return isOk;
}
/**
* 把上传进度信息存进redis
*/
private boolean setUploadProgress2Redis(FileUploadRequestDTO param, String uploadDirPath,
String fileName, File confFile, byte isComplete) {
RedisUtil redisUtil = SpringContextHolder.getBean(RedisUtil.class);
if (isComplete == Byte.MAX_VALUE) {
redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS, param.getMd5(), "true");
redisUtil.del(FileConstant.FILE_MD5_KEY + param.getMd5());
confFile.delete();
return true;
} else {
if (!redisUtil.hHasKey(FileConstant.FILE_UPLOAD_STATUS, param.getMd5())) {
redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS, param.getMd5(), "false");
redisUtil.set(FileConstant.FILE_MD5_KEY + param.getMd5(),
uploadDirPath + FileConstant.FILE_SEPARATORCHAR + fileName + ".conf");
}
return false;
}
}
/**
* 保存文件操作
*/
public FileUploadDTO saveAndFileUploadDTO(String fileName, File tmpFile) {
FileUploadDTO fileUploadDTO = null;
try {
fileUploadDTO = renameFile(tmpFile, fileName);
if (fileUploadDTO.isUploadComplete()) {
System.out
.println("upload complete !!" + fileUploadDTO.isUploadComplete() + " name=" + fileName);
//TODO 保存文件信息到数据库
}
} catch (Exception e) {
log.error(e.getMessage(), e);
} finally {
}
return fileUploadDTO;
}
/**
* 文件重命名
*
* @param toBeRenamed 将要修改名字的文件
* @param toFileNewName 新的名字
*/
private FileUploadDTO renameFile(File toBeRenamed, String toFileNewName) {
//检查要重命名的文件是否存在,是否是文件
FileUploadDTO fileUploadDTO = new FileUploadDTO();
if (!toBeRenamed.exists() || toBeRenamed.isDirectory()) {
log.info("File does not exist: {}", toBeRenamed.getName());
fileUploadDTO.setUploadComplete(false);
return fileUploadDTO;
}
String ext = FileUtil.getExtension(toFileNewName);
String p = toBeRenamed.getParent();
String filePath = p + FileConstant.FILE_SEPARATORCHAR + toFileNewName;
File newFile = new File(filePath);
//修改文件名
boolean uploadFlag = toBeRenamed.renameTo(newFile);
fileUploadDTO.setMtime(DateUtil.getCurrentTimeStamp());
fileUploadDTO.setUploadComplete(uploadFlag);
fileUploadDTO.setPath(filePath);
fileUploadDTO.setSize(newFile.length());
fileUploadDTO.setFileExt(ext);
fileUploadDTO.setFileId(toFileNewName);
return fileUploadDTO;
}
}
Суммировать
В процессе реализации многокомпонентной загрузки клиентская и серверная часть должны сотрудничать.Например, размер файла номера блока загрузки клиентской и серверной части должен быть согласованным, в противном случае будет проблемы с загрузкой. Во-вторых, операции, связанные с файлами, являются нормальными для создания файлового сервера, например, использование fastdfs, hdfs и т. д.
Этот пример кода занимает более 30 минут, чтобы загрузить файл размером 24G, когда компьютер настроен с 4-ядерной памятью и 8G.Основное время тратится на вычисление значения md5 фронтенда, а внутренняя скорость записи по-прежнему относительно высока. Если команда проекта считает, что создание самодельного файлового сервера занимает слишком много времени, а проекту нужно только загружать и скачивать, то рекомендуется использовать oss-сервер Ali, введение в который можно найти на официальном сайте:
oss Али по сути является сервером хранения объектов, а не файловым сервером, поэтому, если есть необходимость удалить или изменить большое количество файлов, oss может быть не лучшим выбором.
В конце статьи приведена демонстрационная ссылка для загрузки формы oss.Загружая через форму oss, вы можете напрямую загрузить файл на сервер oss из внешнего интерфейса и передать давление загрузки на сервер oss:
Блог Woohoo.cn на.com/OS steam/afraid/4…
Источник: Присвоено (автор - Xiaoduye)