<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          一個(gè)Demo搞定前后端大文件分片上傳、斷點(diǎn)續(xù)傳、秒傳

          共 26064字,需瀏覽 53分鐘

           ·

          2023-11-09 17:52

          1前言

          文件上傳在項(xiàng)目開(kāi)發(fā)中再常見(jiàn)不過(guò)了,大多項(xiàng)目都會(huì)涉及到圖片、音頻、視頻、文件的上傳,通常簡(jiǎn)單的一個(gè)Form表單就可以上傳小文件了,但是遇到大文件時(shí)比如1GB以上,或者用戶(hù)網(wǎng)絡(luò)比較慢時(shí),簡(jiǎn)單的文件上傳就不能適用了,用戶(hù)辛苦傳了好幾十分鐘,到最后發(fā)現(xiàn)上傳失敗,這樣的系統(tǒng)用戶(hù)體驗(yàn)是非常差的。

          或者用戶(hù)上傳到一半時(shí),把應(yīng)用退出了,下次進(jìn)來(lái)再次上傳,如果讓他從頭開(kāi)始傳也是不合理的。本文主要通過(guò)一個(gè)Demo從前端、后端用實(shí)戰(zhàn)代碼演示小文件上傳、大文件分片上傳、斷點(diǎn)續(xù)傳、秒傳的開(kāi)發(fā)原理。

          2小文件上傳

          小文件小傳非常的簡(jiǎn)單,本項(xiàng)目后端我們使用SrpingBoot 3.1.2 + JDK17,前端我們使用原生的JavaScript+spark-md5.min.js實(shí)現(xiàn)。

          后端代碼

          POM.xml使用springboot3.1.2JAVA版本使用JDK17

          <parent>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-parent</artifactId>
              <version>3.1.2</version>
              <relativePath/> <!-- lookup parent from repository -->
          </parent>
          <groupId>com.example</groupId>
          <artifactId>uploadDemo</artifactId>
          <version>0.0.1-SNAPSHOT</version>
          <name>uploadDemo</name>
          <description>uploadDemo</description>
          <properties>
              <java.version>17</java.version>
          </properties>
          <dependencies>
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-web</artifactId>
              </dependency>
          </dependencies>
          <build>
              <plugins>
                  <plugin>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-maven-plugin</artifactId>
                  </plugin>
              </plugins>
          </build>

          JAVA接文件接口:

          @RestController
          public class UploadController {

              public static final String UPLOAD_PATH = "D:\\upload\\";

              @RequestMapping("/upload")
              public ResponseEntity<Map<String, String>> upload(@RequestParam MultipartFile file) throws IOException {
                  File dstFile = new File(UPLOAD_PATH, String.format("%s.%s", UUID.randomUUID(), StringUtils.getFilename(file.getOriginalFilename())));
                  file.transferTo(dstFile);
                  return ResponseEntity.ok(Map.of("path", dstFile.getAbsolutePath()));
              }

          }

          前端代碼

          <!DOCTYPE html>
          <html lang="en">
          <head>
              <meta charset="UTF-8">
              <title>upload</title>
          </head>
          <body>
          upload

          <form enctype="multipart/form-data">
              <input type="file" name="fileInput" id="fileInput">
              <input type="button" value="上傳" onclick="uploadFile()">
          </form>

          上傳結(jié)果
          <span id="uploadResult"></span>

          <script>
              var  uploadResult=document.getElementById("uploadResult")
              function uploadFile({
                  var fileInput = document.getElementById('fileInput');
                  var file = fileInput.files[0];
                  if (!file) return// 沒(méi)有選擇文件

                  var xhr = new XMLHttpRequest();
                  // 處理上傳進(jìn)度
                  xhr.upload.onprogress = function(event{
                      var percent = 100 * event.loaded / event.total;
                      uploadResult.innerHTML='上傳進(jìn)度:' + percent + '%';
                  };
                  // 當(dāng)上傳完成時(shí)調(diào)用
                  xhr.onload = function({
                      if (xhr.status === 200) {
                          uploadResult.innerHTML='上傳成功'+ xhr.responseText;
                      }
                  }
                  xhr.onerror = function({
                      uploadResult.innerHTML='上傳失敗';
                  }
                  // 發(fā)送請(qǐng)求
                  xhr.open('POST''/upload'true);
                  var formData = new FormData();
                  formData.append('file', file);
                  xhr.send(formData);
              }
          </script>

          </body>
          </html>

          注意事項(xiàng)

          在上傳過(guò)程會(huì)報(bào)文件大小限制錯(cuò)誤,主要有三個(gè)參數(shù)需要設(shè)置:

          org.apache.tomcat.util.http.fileupload.impl.SizeLimitExceededException: the request was rejected because its size (46302921) exceeds the configured maximum (10485760)

          這里需在springboot的application.properties 或者application.yml中添加max-file-sizemax-request-size配置項(xiàng),默認(rèn)大小分別是1M和10M,肯定不能滿(mǎn)足我們上傳需求的。

          spring.servlet.multipart.max-file-size=1024MB  
          spring.servlet.multipart.max-request-size=1024MB

          如果使用nginx報(bào) 413狀態(tài)碼413 Request Entity Too Large,Nginx默認(rèn)最大上傳1MB文件,需要在nginx.conf配置文件中的 http{ }添加配置項(xiàng):client_max_body_size 1024m

          3大文件分片上傳

          前端

          前端上傳流程

          大文件分片上傳前端主要有三步:

          前端上傳代碼計(jì)算文件MD5值用了spark-md5這個(gè)庫(kù),使用也是比較簡(jiǎn)單的。這里為什么要計(jì)算MD5簡(jiǎn)單說(shuō)一下,因?yàn)槲募趥鬏攲?xiě)入過(guò)程中可能會(huì)出現(xiàn)錯(cuò)誤,導(dǎo)致最終合成的文件可能和原文件不一樣,所以要對(duì)比一下前端計(jì)算的MD5和后端計(jì)算的MD5是不是一樣,保證上傳數(shù)據(jù)的一致性。

          <!DOCTYPE html>
          <html lang="en">
          <head>
              <meta charset="UTF-8">
              <title>分片上傳</title>
              <script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js"></script>
          </head>
          <body>
          分片上傳

          <form enctype="multipart/form-data">
              <input type="file" name="fileInput" id="fileInput">
              <input type="button" value="計(jì)算文件MD5" onclick="calculateFileMD5()">
              <input type="button" value="上傳" onclick="uploadFile()">
              <input type="button" value="檢測(cè)文件完整性" onclick="checkFile()">
          </form>

          <p>
              文件MD5:
              <span id="fileMd5"></span>
          </p>
          <p>
              上傳結(jié)果:
              <span id="uploadResult"></span>
          </p>
          <p>
              檢測(cè)文件完整性:
              <span id="checkFileRes"></span>
          </p>


          <script>
              //每片的大小
              var chunkSize = 1 * 1024 * 1024;
              var uploadResult = document.getElementById("uploadResult")
              var fileMd5Span = document.getElementById("fileMd5")
              var checkFileRes = document.getElementById("checkFileRes")
              var  fileMd5;


              function  calculateFileMD5(){
                  var fileInput = document.getElementById('fileInput');
                  var file = fileInput.files[0];
                  getFileMd5(file).then((md5) => {
                      console.info(md5)
                      fileMd5=md5;
                      fileMd5Span.innerHTML=md5;
                  })
              }

              function uploadFile({
                  var fileInput = document.getElementById('fileInput');
                  var file = fileInput.files[0];
                  if (!file) return;
                  if (!fileMd5) return;


                  //獲取到文件
                  let fileArr = this.sliceFile(file);
                  //保存文件名稱(chēng)
                  let fileName = file.name;

                  fileArr.forEach((e, i) => {
                      //創(chuàng)建formdata對(duì)象
                      let data = new FormData();
                      data.append("totalNumber", fileArr.length)
                      data.append("chunkSize", chunkSize)
                      data.append("chunkNumber", i)
                      data.append("md5", fileMd5)
                      data.append("file"new File([e],fileName));
                      upload(data);
                  })


              }

              /**
               * 計(jì)算文件md5值
               */

              function getFileMd5(file{
                  return new Promise((resolve, reject) => {
                      let fileReader = new FileReader()
                      fileReader.onload = function (event{
                          let fileMd5 = SparkMD5.ArrayBuffer.hash(event.target.result)
                          resolve(fileMd5)
                      }
                      fileReader.readAsArrayBuffer(file)
                  })
              }


             function upload(data{
                 var xhr = new XMLHttpRequest();
                 // 當(dāng)上傳完成時(shí)調(diào)用
                 xhr.onload = function ({
                     if (xhr.status === 200) {
                         uploadResult.append( '上傳成功分片:' +data.get("chunkNumber")+'\t' ) ;
                     }
                 }
                 xhr.onerror = function ({
                     uploadResult.innerHTML = '上傳失敗';
                 }
                 // 發(fā)送請(qǐng)求
                 xhr.open('POST''/uploadBig'true);
                 xhr.send(data);
              }

              function checkFile({
                  var xhr = new XMLHttpRequest();
                  // 當(dāng)上傳完成時(shí)調(diào)用
                  xhr.onload = function ({
                      if (xhr.status === 200) {
                          checkFileRes.innerHTML = '檢測(cè)文件完整性成功:' + xhr.responseText;
                      }
                  }
                  xhr.onerror = function ({
                      checkFileRes.innerHTML = '檢測(cè)文件完整性失敗';
                  }
                  // 發(fā)送請(qǐng)求
                  xhr.open('POST''/checkFile'true);
                  let data = new FormData();
                  data.append("md5", fileMd5)
                  xhr.send(data);
              }

              function sliceFile(file{
                  const chunks = [];
                  let start = 0;
                  let end;
                  while (start < file.size) {
                      end = Math.min(start + chunkSize, file.size);
                      chunks.push(file.slice(start, end));
                      start = end;
                  }
                  return chunks;
              }

          </script>

          </body>
          </html>
          前端注意事項(xiàng)

          前端調(diào)用uploadBig接口有四個(gè)參數(shù):

          計(jì)算大文件的MD5可能會(huì)比較慢,這個(gè)可以從流程上進(jìn)行優(yōu)化,比如上傳使用異步去計(jì)算文件MD5、不計(jì)算整個(gè)文件MD5而是計(jì)算每一片的MD5保證每一片數(shù)據(jù)的一致性。

          后端

          后端就兩個(gè)接口/uploadBig用于每一片文件的上傳和/checkFile檢測(cè)文件的MD5。

          /uploadBig接口設(shè)計(jì)思路

          接口總體流程:

          這里需要注意的:

          • MD5.conf每一次檢測(cè)文件不存在里創(chuàng)建個(gè)空文件,使用byte[] bytes = new byte[totalNumber];將每一位狀態(tài)設(shè)置為0,從0位天始,第N位表示第N個(gè)分片的上傳狀態(tài),0-未上傳 1-已上傳,當(dāng)每將上傳成功后使用randomAccessConfFile.seek(chunkNumber)將對(duì)就設(shè)置為1。

          • randomAccessFile.seek(chunkNumber * chunkSize);可以將光標(biāo)移到文件指定位置開(kāi)始寫(xiě)數(shù)據(jù),每一個(gè)文件每將上傳分片編號(hào)chunkNumber都是不一樣的,所以各自寫(xiě)自己文件塊,多線(xiàn)程寫(xiě)同一個(gè)文件不會(huì)出現(xiàn)線(xiàn)程安全問(wèn)題。

          • 大文件寫(xiě)入時(shí)用RandomAccessFile可能比較慢,可以使用MappedByteBuffer內(nèi)存映射來(lái)加速大文件寫(xiě)入,不過(guò)使用MappedByteBuffer如果要?jiǎng)h除文件可能會(huì)存在刪除不掉,因?yàn)閯h除了磁盤(pán)上的文件,內(nèi)存的文件還是存在的。

          MappedByteBuffer寫(xiě)文件的用法:

          FileChannel fileChannel = randomAccessFile.getChannel();  
          MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, chunkNumber * chunkSize, fileData.length);  
          mappedByteBuffer.put(fileData);
          /checkFile接口設(shè)計(jì)思路

          /checkFile接口流程:

          大文件上傳完整JAVA代碼:

          @RestController
          public class UploadController {

              public static final String UPLOAD_PATH = "D:\\upload\\";

              /**
               * @param chunkSize   每個(gè)分片大小
               * @param chunkNumber 當(dāng)前分片
               * @param md5         文件總MD5
               * @param file        當(dāng)前分片文件數(shù)據(jù)
               * @return
               * @throws IOException
               */

              @RequestMapping("/uploadBig")
              public ResponseEntity<Map<String, String>> uploadBig(@RequestParam Long chunkSize, @RequestParam Integer totalNumber, @RequestParam Long chunkNumber, @RequestParam String md5, @RequestParam MultipartFile file) throws IOException {
                  //文件存放位置
                  String dstFile = String.format("%s\\%s\\%s.%s", UPLOAD_PATH, md5, md5, StringUtils.getFilenameExtension(file.getOriginalFilename()));
                  //上傳分片信息存放位置
                  String confFile = String.format("%s\\%s\\%s.conf", UPLOAD_PATH, md5, md5);
                  //第一次創(chuàng)建分片記錄文件
                  //創(chuàng)建目錄
                  File dir = new File(dstFile).getParentFile();
                  if (!dir.exists()) {
                      dir.mkdir();
                      //所有分片狀態(tài)設(shè)置為0
                      byte[] bytes = new byte[totalNumber];
                      Files.write(Path.of(confFile), bytes);
                  }
                  //隨機(jī)分片寫(xiě)入文件
                  try (RandomAccessFile randomAccessFile = new RandomAccessFile(dstFile, "rw");
                       RandomAccessFile randomAccessConfFile = new RandomAccessFile(confFile, "rw");
                       InputStream inputStream = file.getInputStream()) {
                      //定位到該分片的偏移量
                      randomAccessFile.seek(chunkNumber * chunkSize);
                      //寫(xiě)入該分片數(shù)據(jù)
                      randomAccessFile.write(inputStream.readAllBytes());
                      //定位到當(dāng)前分片狀態(tài)位置
                      randomAccessConfFile.seek(chunkNumber);
                      //設(shè)置當(dāng)前分片上傳狀態(tài)為1
                      randomAccessConfFile.write(1);
                  }
                  return ResponseEntity.ok(Map.of("path", dstFile));
              }


              /**
               * 獲取文件分片狀態(tài),檢測(cè)文件MD5合法性
               *
               * @param md5
               * @return
               * @throws Exception
               */

              @RequestMapping("/checkFile")
              public ResponseEntity<Map<String, String>> uploadBig(@RequestParam String md5) throws Exception {
                  String uploadPath = String.format("%s\\%s\\%s.conf", UPLOAD_PATH, md5, md5);
                  Path path = Path.of(uploadPath);
                  //MD5目錄不存在文件從未上傳過(guò)
                  if (!Files.exists(path.getParent())) {
                      return ResponseEntity.ok(Map.of("msg""文件未上傳"));
                  }
                  //判斷文件是否上傳成功
                  StringBuilder stringBuilder = new StringBuilder();
                  byte[] bytes = Files.readAllBytes(path);
                  for (byte b : bytes) {
                      stringBuilder.append(String.valueOf(b));
                  }
                  //所有分片上傳完成計(jì)算文件MD5
                  if (!stringBuilder.toString().contains("0")) {
                      File file = new File(String.format("%s\\%s\\", UPLOAD_PATH, md5));
                      File[] files = file.listFiles();
                      String filePath = "";
                      for (File f : files) {
                          //計(jì)算文件MD5是否相等
                          if (!f.getName().contains("conf")) {
                              filePath = f.getAbsolutePath();
                              try (InputStream inputStream = new FileInputStream(f)) {
                                  String md5pwd = DigestUtils.md5DigestAsHex(inputStream);
                                  if (!md5pwd.equalsIgnoreCase(md5)) {
                                      return ResponseEntity.ok(Map.of("msg""文件上傳失敗"));
                                  }
                              }
                          }
                      }
                      return ResponseEntity.ok(Map.of("path", filePath));
                  } else {
                      //文件未上傳完成,反回每個(gè)分片狀態(tài),前端將未上傳的分片繼續(xù)上傳
                      return ResponseEntity.ok(Map.of("chucks", stringBuilder.toString()));
                  }

              }
              
          }

          配合前端上傳演示分片上傳,依次按如下流程點(diǎn)擊按鈕:

          斷點(diǎn)續(xù)傳

          有了上面的設(shè)計(jì)做斷點(diǎn)續(xù)傳就比較簡(jiǎn)單的,后端代碼不需要改變,只要修改前端上傳流程就好了:

          用/checkFile接口,文件里如果有未完成上傳的分片,接口返回chunks字段對(duì)就的位置值為0,前端將未上傳的分片繼續(xù)上傳,完成后再調(diào)用/checkFile就完成了斷點(diǎn)續(xù)傳

          {
              "chucks""111111111100000000001111111111111111111111111"
          }

          秒傳

          秒傳也是比較簡(jiǎn)單的,只要修改前端代碼流程就好了,比如張三上傳了一個(gè)文件,然后李四又上傳了同樣內(nèi)容的文件,同一文件的MD5值可以認(rèn)為是一樣的(雖然會(huì)存在不同文件的MD5一樣,不過(guò)概率很小,可以認(rèn)為MD5一樣文件就是一樣),10萬(wàn)不同文件MD5相同概率為110000000000000000000000000000\frac{1}{10000000000000000000000000000}100000000000000000000000000001,福利彩票的中頭獎(jiǎng)的概率一般為11000000\frac{1}{1000000}10000001,具體計(jì)算方法可以參考走近消息摘要--Md5產(chǎn)生重復(fù)的概率,所以MD5沖突的概率可以忽略不計(jì)。

          當(dāng)李四調(diào)用/checkFile接口后,后端直接返回了李四上傳的文件路徑,李四就完成了秒傳。大部分云盤(pán)秒傳的思路應(yīng)該也是這樣,只不過(guò)計(jì)算文件HASH算法更為復(fù)雜,返回給用戶(hù)文件路徑也更為安全,要防止被別人算出文件路徑了。

          秒傳前端代碼流程:

          4總結(jié)

          本文從前端和后端兩個(gè)方面介紹了大文件的分片上傳、斷點(diǎn)繼續(xù)、秒傳設(shè)計(jì)思路和實(shí)現(xiàn)代碼,所有代碼都是親測(cè)可以直接使用。

          來(lái)源:juejin.cn/post/7266265543412351030
               
               

          往期推薦:

          Netty+SpringBoot 打造一個(gè) TCP 長(zhǎng)連接通訊方案

          7k Star,一款開(kāi)源的 Kafka 管理平臺(tái),功能齊全、頁(yè)面美觀!

          SpringBoot+RabbitMQ+Redis 開(kāi)發(fā)一個(gè)秒殺系統(tǒng),細(xì)節(jié)打滿(mǎn)(附源碼)

          一套干凈的企業(yè)數(shù)據(jù)管理系統(tǒng),拿來(lái)直接用

          基于 Spring Boot 的車(chē)牌識(shí)別系統(tǒng)(附項(xiàng)目地址)

          騰訊低代碼神器開(kāi)源!拖拽開(kāi)發(fā),爽的飛起~

          瀏覽 606
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  中文字幕亚洲乱伦 | 婷婷五月天国产 | 特级西西高清4Www电影 | 先锋人妻啪啪av资源网站 | 亚洲成人黄色电影 |