spring mvc異步請求 sse 大文件下載 斷點續傳下載Range

學習連接

異步Servlet3.0

Spring Boot 處理異步請求(DeferredResult 基礎案例、DeferredResult 超時案例、DeferredResult 擴展案例、DeferredResult 方法匯總)

spring.io mvc Asynchronous Requests 官網文檔

spring.io webflux&webclient官網文檔

SpringBoot+vue 大文件分片下載
SpringBoot+vue文件上傳&下載&預覽&大文件分片上傳&文件上傳進度
spring mvc異步請求 & sse & 大文件下載 & 斷點續傳下載

文章目錄

    • 學習連接
    • springmvc異步請求
      • DeferredResult
        • 示例
      • Callable
        • 示例
      • 異步處理
        • springmvc異步處理流程
        • Exception Handling異常處理
        • Interception攔截
        • 與WebFlux相比
      • HTTP流(ResponseBodyEmitter )
        • 示例
      • sse(SseEmitter)
      • 原數據直傳
        • 示例(文件下載)
          • 后端代碼
          • 前端代碼
        • 示例(大文件下載)
          • 后端代碼
          • 前端代碼1
          • 前端代碼2
          • 前端代碼3
        • 斷點續傳概念
          • 概述
          • Range
          • Content-Range
      • 響應式類型
      • 斷開連接
      • 配置
        • Servlet容器
        • Spring MVC

springmvc異步請求

Spring MVC廣泛接入Servlet 3.0異步請求處理:

  • DeferredResult和Callable返回值,并為單個異步返回值提供基本支持。
  • 控制器可以流式傳輸多個值,包括SSE和原始數據。
  • 控制器可以使用反應式客戶端并返回響應處理的反應式類型。

DeferredResult

一旦在Servlet容器中啟用了異步請求處理特征,控制器方法就可以用DeferredResult包裝任何支持的控制器方法返回值,如下例所示:

@GetMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {DeferredResult<String> deferredResult = new DeferredResult<String>();// Save the deferredResult somewhere..return deferredResult;
}// From some other thread...
deferredResult.setResult(result);

控制器可以從不同的線程異步地產生返回值——例如,響應外部事件(JMS消息)、定時任務或其他事件。

示例
@Slf4j
@RestController
@RequestMapping("/async")
public class TestController {@RequestMapping("testDeferred")public DeferredResult<String> testDeferred(Long timeoutValue) {log.info("testDeferred");// 1、timeoutValue為null,表示不超時.// 2、如果超時了,將返回這里默認值timeoutResult(不會影響給deferredResult設置值的線程)DeferredResult<String> deferredResult = new DeferredResult<>(timeoutValue,()->{return "timeoutValue";});new Thread(()->{try {log.info("異步處理 start");TimeUnit.SECONDS.sleep(5);// 此方法可以檢測是否已超時,// 也就是即使超時,當前new的線程也會繼續執行,下面setResult方法也會執行,只是不會把設置的值給前端,因為超時的默認值已經給了。log.info("是否超時: {}", deferredResult.isSetOrExpired());} catch (Exception e) {log.info("異步處理異常: {}", e);deferredResult.setErrorResult("error~");return;}deferredResult.setResult("testDeferred~");log.info("異步處理 end");}).start();log.info("testDeferred end");return deferredResult;}}

Callable

控制器可以使用java.util.concurrent.Callable包裝任何支持的返回值,如下例所示:

@ResponseBody
@RequestMapping("test11")
public Callable<String> processUpload() {return new Callable<String>() {public String call() throws Exception {log.info("call");return "data";}};
}

該callable會交給配置的taskExecutor執行。

示例
@RequestMapping("testCallable")
public Callable<String> testCallable(Integer timeout) {log.info("testCallable start");Callable<String> callable = new Callable<String>() {@Overridepublic String call() throws Exception {TimeUnit.SECONDS.sleep(timeout);// task-1線程執行的log.info("testCallable 異步執行");return "call result";}};log.info("testCallable end");return callable;}

異步處理

springmvc異步處理流程

以下是Servlet異步請求處理的非常簡潔的概述:

  • 可以通過調用request.startAsync()將ServletRequest置于異步模式。這樣做的主要效果是Servlet(以及任何過濾器)可以退出,但響應保持打開狀態,以便稍后完成處理。
  • 對request.startAsync()的調用返回AsyncContext,您可以使用它來進一步控制異步處理。例如,它提供了dispatch方法,類似于Servlet API的轉發,只是它允許應用程序在Servlet容器線程上繼續請求處理。
  • ServletRequest可以訪問到對當前的DispatcherType,您可以使用它來區分處理初始請求、異步調度、轉發和其他類型。

DeferredResult處理流程如下:

  • 控制器返回一個DeferredResult并將其保存在某個可以訪問的內存的隊列或列表中。
  • Spring MVC調用request.startAsync()。
  • 同時,DispatcherServlet和所有配置的過濾器退出請求處理線程,但響應保持打開狀態。
  • 應用程序從某個線程設置DeferredResult,Spring MVC將請求分派(dispatcher)回Servlet容器。
  • 再次調用DispatcherServlet,并使用異步生成的返回值恢復處理

Callable處理流程如下:

  • 控制器返回一個Callable。
  • Spring MVC調用request.startAsync()并將Callable提交給TaskExecutor以在單獨的線程中進行處理。
  • 同時,DispatcherServlet和所有過濾器退出Servlet容器線程,但響應保持打開狀態。
  • 最終Callable產生一個結果,Spring MVC將請求分派回Servlet容器以完成處理。
  • 再次調用DispatcherServlet,并使用來自Callable的異步生成的返回值恢復處理。

有關進一步的背景和上下文,您還可以閱讀在Spring MVC 3.2中介紹異步請求處理支持的博客文章。

(經過查看DeferredResultMethodReturnValueHandler,發現還可以返回ListenableFuture、CompletionStage類型的返回值。
源碼的重點是在:WebAsyncManager的創建和使用、StandardServletAsyncWebRequest對異步請求的封裝、RequestMappingHandlerAdapter#invokeHandlerMethod對分發之后的處理)

Exception Handling異常處理

當您使用DeferredResult時,您可以選擇是調用setResult還是setErrorResult并帶有異常。在這兩種情況下,Spring MVC都會將請求分派回Servlet容器以完成處理。然后將其視為控制器方法返回給定值或產生給定異常。然后異常通過常規異常處理機制(例如,調用@ExceptionHandler方法)。

當您使用Callable時,會出現類似的處理邏輯,主要區別在于結果是從Callable返回的,或者由Callable引發異常。

Interception攔截

HandlerInterceptor實例可以是AsyncHandlerInterceptor類型,以在初始請求啟動異步處時接收afterConcurrentHandlingStarted回調(而不是postHandle和afterCompletion)。

HandlerInterceptor實現還可以注冊CallableProcessingInterceptor或DeferredResultProcessingInterceptor,以便更深入地與異步請求的生命周期集成(例如,處理超時事件)。AsyncHandlerInterceptor了解更多詳細信息。

DeferredResult提供了onTimeout(Runnable)和onCompletion(Runnable)回調。有關詳細信息,請參閱DeferredResultjavadoc。Callable可以替換為公開超時和完成回調的其他方法的WebAsyncTask。

與WebFlux相比

Servlet API最初是為Filter-Servlet鏈而構建的。Servlet 3.0中添加的異步請求處理允許應用程序退出Filter-Servlet鏈,但保留響應以供進一步處理。Spring MVC異步支持是圍繞這種機制構建的。當控制器返回DeferredResult時,Filter-Servlet鏈就會退出,Servlet容器線程就會釋放。稍后,當設置DeferredResult時,會進行ASYNC分派(到同一個URL),在此期間,控制器會再次映射,但不會調用它,而是使用DeferredResult值(就像控制器返回它一樣)來恢復處理。

相比之下,Spring WebFlux既不基于Servlet API構建,也不需要這樣的異步請求處理特征,因為它在設計上是異步的。異步處理內置于所有框架契約中,并在請求處理的所有階段得到支持。

從編程模型的角度來看,Spring MVC和Spring WebFlux都支持異步和響應式類型作為控制器方法中的返回值。Spring MVC甚至支持流,包括響應式背壓機制。但是,對響應的單個寫入仍然是阻塞的(并且在單獨的線程上執行),這與WebFlux不同,WebFlux依賴于非阻塞io,并且每次寫入不需要額外的線程。

另一個根本區別是Spring MVC不支持controller方法參數中的異步或反應式類型(例如@RequestBody、@RequestPart等),也不支持將異步和反應式類型作為Model屬性。而webflux支持所有。

HTTP流(ResponseBodyEmitter )

您可以對單個異步返回值使用DeferredResult和Callable。如果您想生成多個異步值并將它們寫入響應怎么辦?本節介紹如何執行此操作。

您可以使用ResponseBodyEmitter返回值生成對象流,其中每個對象都使用HttpMessageConverter序列化并寫入響應,如下例所示:

@GetMapping("/events")
public ResponseBodyEmitter handle() {ResponseBodyEmitter emitter = new ResponseBodyEmitter();// Save the emitter somewhere..return emitter;
}// In some other thread
emitter.send("Hello once");// and again later on
emitter.send("Hello again");// and done at some point
emitter.complete();

您還可以使用ResponseBodyEmitter作為ResponseEntity中的主體,讓您自定義響應的狀態和標頭。

當emitter拋出IOException(例如,如果遠程客戶端斷開連接)時,應用程序不負責清理連接,也不應調起emitter.complete或emitter.completeWithErrorError。相反,servlet容器會自動啟動AsyncListener錯誤通知,其中Spring MVC進行completeWithError調用。該調用反過來執行對應用程序的最后一次ASYNC分發,然后Spring MVC調用配置的異常解析器并完成請求。

(查看ResponseBodyEmitterReturnValueHandler,得知ResponseBodyEmitter也是基于DeferredResult來實現的)

示例
@RequestMapping("emitter")
public ResponseEntity<ResponseBodyEmitter> responseBodyEmitter() {log.info("testEmitter start");ResponseBodyEmitter emitter = new ResponseBodyEmitter(10000L);emitter.onCompletion(() -> {log.info("testEmitter onCompletion");});emitter.onTimeout(() -> {log.info("testEmitter onTimeout");});emitter.onError((e) -> {log.info("testEmitter onError: {}", e);});new Thread(()->{for (int i = 0; i < 10; i++) {try {emitter.send("testEmitter~" + i);log.info("testEmitter~" + i);} catch (IOException e) {throw new RuntimeException(e);}try {TimeUnit.MILLISECONDS.sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}if (i > 5) {emitter.completeWithError(new ArithmeticException("計算錯誤"));}}log.info("發送完畢~");emitter.complete();}).start();log.info("testEmitter end");HttpHeaders httpHeaders = new HttpHeaders();httpHeaders.add("Access-Control-Allow-Origin", "*");httpHeaders.add("Content-Type", "text/html");httpHeaders.add("Cache-Control", "no-cache");httpHeaders.add("Transfer-Encoding", "chunked");return new ResponseEntity<ResponseBodyEmitter>(emitter, httpHeaders, HttpStatus.OK);
}
<!DOCTYPE html>
<html>
<head><title>分塊數據實時展示</title>
</head>
<body><h1>實時數據接收:</h1><div id="output" style="border: 1px solid #ccc; padding: 10px; height: 300px; overflow-y: auto;"></div><script>// 要實現瀏覽器逐步接收并顯示分塊傳輸的數據,可以通過以下步驟使用 Fetch API(原生支持流式響應)配合前端實時渲染。// 啟動請求并處理流式響應async function fetchStreamData() {const outputDiv = document.getElementById('output');try {const response = await fetch('http://localhost:8080/async/emitter');// 獲取可讀流const reader = response.body.getReader();const decoder = new TextDecoder();// 持續讀取數據塊while (true) {const { done, value } = await reader.read();if (done) break; // 流結束// 解碼數據并追加到頁面const chunk = decoder.decode(value, { stream: true });outputDiv.innerHTML += chunk;outputDiv.scrollTop = outputDiv.scrollHeight; // 自動滾動到底部}console.log('數據接收完成');} catch (error) {console.error('請求失敗:', error);outputDiv.innerHTML += '請求失敗: ' + error.message;}}// 頁面加載后自動啟動fetchStreamData();</script>
</body>
</html>

sse(SseEmitter)

SseEmitter(ResponseBodyEmitter的子類)支持Server-Sent Events,其中從服務器發送的事件根據W3C SSE規范進行格式化。要從控制器生成SSE流,返回SseEmitter,如下例所示:

@GetMapping(path="/events", produces=MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter handle() {SseEmitter emitter = new SseEmitter();// Save the emitter somewhere..return emitter;
}// In some other thread
emitter.send("Hello once");// and again later on
emitter.send("Hello again");// and done at some point
emitter.complete();

雖然SSE是流式傳輸到瀏覽器的主要選項,但請注意,IE不支持 Server-Sent Events。考慮將Spring的WebSocket與SockJS作為兜底(包括SSE)一起使用以覆蓋大部分瀏覽器。

有關異常處理的說明,請參見HTTP流處理章節。

(使用與ResponseBodyEmitter完全一致,因為就是基于ResponseBodyEmitter。)

留意下:StreamingHttpOutputMessage 這個

原數據直傳

有時,繞過消息轉換機制并直接通過響應輸出流(OutputStream)進行流式傳輸非常有用(例如實現文件下載功能)。為此,您可以將返回值的類型設為StreamingResponseBody,如下方示例所示:

@GetMapping("/download")
public StreamingResponseBody handle() {return new StreamingResponseBody() {@Overridepublic void writeTo(OutputStream outputStream) throws IOException {// write...}};
}

您可以使用StreamingResponseBody作為ResponseEntity中的主體來自定義響應的狀態和標頭。

(它內部是通過WebAsyncTask包裝Callbale來實現的。它相比于直接使用repsonse的outputStream寫入,是異步的,不會阻塞處理請求的線程。它支持分塊傳輸嗎?這點存疑。)

示例(文件下載)
后端代碼
@GetMapping("/download")
public ResponseEntity<StreamingResponseBody> handle() {File file = new File("D:\\Projects\\practice\\demo-boot\\src\\main\\resources\\test.png");StreamingResponseBody streamingResponseBody = new StreamingResponseBody() {@Overridepublic void writeTo(OutputStream outputStream) throws IOException {byte[] buffer = new byte[1024];FileInputStream fis = new FileInputStream(file);int len = -1;while ((len = fis.read(buffer)) != -1) {outputStream.write(buffer, 0, len);}outputStream.flush();fis.close();}};return ResponseEntity.ok().header(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*").header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getName() + "\"").contentType(MediaType.APPLICATION_OCTET_STREAM).contentLength(file.length()).body(streamingResponseBody);
前端代碼
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>File Download Example</title>
</head>
<body><h1>File Download Example</h1><button onclick="downloadWithFetch()">Download with Fetch (Streaming)</button><script>async function downloadWithFetch(filename) {try {const response = await fetch(`http://127.0.0.1:8080/async/download`);console.log(response.ok);if (!response.ok) {throw new Error('File not found');}// 獲取文件名,可以從Content-Disposition頭部解析let downloadFilename = 'demo.png';const contentDisposition = response.headers.get('Content-Disposition');if (contentDisposition && contentDisposition.indexOf('filename=') !== -1) {downloadFilename = contentDisposition.split('filename=')[1].replace(/"/g, '');}// 創建Blob對象并下載const blob = await response.blob();const url = window.URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = downloadFilename;document.body.appendChild(a);a.click();// 清理window.URL.revokeObjectURL(url);document.body.removeChild(a);} catch (error) {console.error('Download failed:', error);alert('Download failed: ' + error.message);}}</script>
</body>
</html>
示例(大文件下載)
后端代碼
@Slf4j
@RestController
@RequestMapping("/download")
public class LargeFileDownloadController {// 配置線程池(用于異步處理)// 或 Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());// 安全下載目錄private static final Path SAFE_BASE_DIR = Paths.get("D:\\Projects\\practice\\demo-boot\\file");@CrossOrigin(origins = "*",methods = {RequestMethod.GET, RequestMethod.HEAD, RequestMethod.POST},allowedHeaders = "*",exposedHeaders = {"Accept-Ranges", "Content-Range", "Content-Type", "Content-Length"})@GetMapping("/{fileName}")public ResponseEntity<StreamingResponseBody> downloadLargeFile(HttpServletRequest request,HttpServletResponse response,@PathVariable String fileName,@RequestHeader(value = "Range", required = false) String rangeHeader) {// 1. 安全校驗if (!isValidFileName(fileName)) {return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();}Path filePath = SAFE_BASE_DIR.resolve(fileName).normalize();// 2. 文件存在性檢查if (!Files.exists(filePath) || Files.isDirectory(filePath)) {return ResponseEntity.notFound().build();}try {// 3. 獲取文件信息long fileSize = Files.size(filePath);if (request.getMethod().equals("HEAD")) {return ResponseEntity.status(HttpStatus.OK).header("Accept-Ranges", "bytes").contentLength(fileSize).body(null);}HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);headers.setContentDisposition(ContentDisposition.builder("attachment").filename(fileName).build());log.info("Range請求頭: {}", rangeHeader);// 4. 處理斷點續傳(Range請求)if (rangeHeader != null && rangeHeader.startsWith("bytes=")) {log.info("斷點續傳請求");return handleRangeRequest(filePath, fileSize, rangeHeader);}log.info("完整文件下載請求");// 5. 完整文件下載headers.setContentLength(fileSize);StreamingResponseBody responseBody = output -> {try (InputStream is = Files.newInputStream(filePath)) {byte[] buffer = new byte[64 * 1024]; // 64KB緩沖區int bytesRead;while ((bytesRead = is.read(buffer)) != -1) {output.write(buffer, 0, bytesRead);log.info("寫入字節數: {}", bytesRead);output.flush();}} catch (IOException e) {log.error("文件下載中斷", e);throw new RuntimeException(e);}};return ResponseEntity.ok().headers(headers).body(responseBody);} catch (IOException e) {return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();}}// 處理斷點續傳(HTTP 206 Partial Content)private ResponseEntity<StreamingResponseBody> handleRangeRequest(Path filePath, long fullSize, String rangeHeader) throws IOException {// 解析Range頭(示例簡化實現)String[] ranges = rangeHeader.substring(6).split("-");long start = Long.parseLong(ranges[0]);long end = ranges.length > 1 ? Long.parseLong(ranges[1]) : fullSize - 1;long contentLength = end - start + 1;HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);headers.setContentLength(contentLength);headers.set("Content-Range", String.format("bytes %d-%d/%d", start, end, fullSize));StreamingResponseBody responseBody = output -> {try (RandomAccessFile raf = new RandomAccessFile(filePath.toFile(), "r")) {byte[] buffer = new byte[64 * 1024];raf.seek(start);log.info("RandomAccessFile跳轉到: {}", start);// 還需要讀的剩余字節數long remaining = contentLength;while (remaining > 0) {int readSize = (int) Math.min(buffer.length, remaining);int bytesRead = raf.read(buffer, 0, readSize);log.info("讀取字節數: {}", bytesRead);if (bytesRead == -1) {log.info("無數據可讀了");break;}output.write(buffer, 0, bytesRead);output.flush();remaining -= bytesRead;}log.info("讀完了: {}", remaining);}};return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT).headers(headers).body(responseBody);}// 校驗文件名合法性(防止路徑遍歷)private boolean isValidFileName(String fileName) {return fileName.matches("[a-zA-Z0-9_\\-]+\\.?[a-zA-Z0-9_\\-]+");}
}
前端代碼1

1、不占用tomcat的線程;
2、支持斷點續傳需要客戶端支持;
3、前端能夠看到進度
在這里插入圖片描述

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><title>ajax 文件導出</title><script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head><body><button type="button" onclick="downloadFile()">下載</button><script type="text/javascript">const downloadFile = async () => {try {const response = await axios({method: 'get',url: `http://127.0.0.1:8080/download/test.mp4`,responseType: 'blob',onDownloadProgress: (progressEvent) => {const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);console.log(`下載進度: ${percent}%`);},});// 創建下載鏈接const url = window.URL.createObjectURL(new Blob([response.data]));const link = document.createElement('a');link.href = url;link.setAttribute('download', 'test.mp4');document.body.appendChild(link);link.click();link.remove();} catch (error) {console.error('下載失敗:', error);if (error.response?.status === 404) {alert('文件不存在');}}};</script>
</body></html>
前端代碼2

在這里插入圖片描述

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>斷點續傳下載示例</title><style>.container {max-width: 600px;margin: 20px auto;padding: 20px;box-shadow: 0 0 10px rgba(0,0,0,0.1);}.progress-container {width: 100%;height: 20px;background-color: #f0f0f0;border-radius: 10px;margin: 20px 0;}.progress-bar {height: 100%;background-color: #4CAF50;border-radius: 10px;transition: width 0.3s ease;}button {padding: 10px 20px;background-color: #4CAF50;color: white;border: none;border-radius: 5px;cursor: pointer;}button:hover {background-color: #45a049;}#status {margin-top: 10px;color: #666;}#downloadLink {display: none;margin-top: 20px;color: #2196F3;text-decoration: none;}</style>
</head>
<body><div class="container"><button id="controlBtn">開始下載</button><div class="progress-container"><div id="progressBar" class="progress-bar" style="width: 0%"></div></div><div id="status">準備就緒</div><a id="downloadLink" download>下載文件</a></div><script>const fileUrl = 'http://127.0.0.1:8080/download/test.mp4'; // 替換為實際文件URLlet controller = null;let isDownloading = false;let receivedBytes = 0;let totalBytes = 0;let chunks = [];// 初始化IndexedDBconst initDB = () => {return new Promise((resolve, reject) => {const request = indexedDB.open('ResumableDownloadDB', 1);request.onupgradeneeded = (event) => {const db = event.target.result;if (!db.objectStoreNames.contains('downloads')) {db.createObjectStore('downloads', { keyPath: 'url' });}};request.onsuccess = () => resolve(request.result);request.onerror = () => reject(request.error);});};// 保存下載進度const saveProgress = async () => {const db = await initDB();const transaction = db.transaction('downloads', 'readwrite');const store = transaction.objectStore('downloads');store.put({ url: fileUrl, receivedBytes, chunks });};// 加載下載進度const loadProgress = async () => {const db = await initDB();return new Promise((resolve) => {const transaction = db.transaction('downloads');const store = transaction.objectStore('downloads');const request = store.get(fileUrl);request.onsuccess = () => {if (request.result) {receivedBytes = request.result.receivedBytes;chunks = request.result.chunks || [];}resolve();};});};// 更新進度顯示const updateProgress = () => {const progress = (receivedBytes / totalBytes * 100).toFixed(1);document.getElementById('progressBar').style.width = `${progress}%`;document.getElementById('status').textContent = `已下載 ${progress}% (${formatBytes(receivedBytes)} / ${formatBytes(totalBytes)})`;};// 字節單位轉換const formatBytes = (bytes) => {if (bytes === 0) return '0 B';const units = ['B', 'KB', 'MB', 'GB'];const i = Math.floor(Math.log(bytes) / Math.log(1024));return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`;};// 開始/暫停下載const toggleDownload = async () => {if (isDownloading) {// 暫停下載controller.abort();isDownloading = false;await saveProgress();document.getElementById('controlBtn').textContent = '繼續下載';} else {// 開始/繼續下載isDownloading = true;document.getElementById('controlBtn').textContent = '暫停下載';try {await loadProgress();// 獲取文件大小if (totalBytes === 0) {const headRes = await fetch(fileUrl, { method: 'HEAD' });totalBytes = parseInt(headRes.headers.get('Content-Length'), 10);if (!headRes.headers.get('Accept-Ranges')) {throw new Error('服務器不支持斷點續傳');}}controller = new AbortController();const response = await fetch(fileUrl, {headers: { 'Range': `bytes=${receivedBytes}-` },signal: controller.signal});if (response.status !== 206) {throw new Error('服務器不支持范圍請求');}const reader = response.body.getReader();while (true) {const { done, value } = await reader.read();if (done) break;chunks.push(value.buffer);receivedBytes += value.byteLength;updateProgress();}// 下載完成const blob = new Blob(chunks);const url = URL.createObjectURL(blob);// 清理數據庫記錄const db = await initDB();const transaction = db.transaction('downloads', 'readwrite');transaction.objectStore('downloads').delete(fileUrl);// 顯示下載鏈接document.getElementById('downloadLink').href = url;document.getElementById('downloadLink').style.display = 'inline';document.getElementById('status').textContent = '下載完成';} catch (err) {if (err.name === 'AbortError') {console.log('下載已暫停');} else {console.error('下載錯誤:', err);document.getElementById('status').textContent = `錯誤: ${err.message}`;}}isDownloading = false;document.getElementById('controlBtn').textContent = '開始下載';}};document.getElementById('controlBtn').addEventListener('click', toggleDownload);</script>
</body>
</html>
前端代碼3

在這里插入圖片描述

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><script src="https://unpkg.com/axios/dist/axios.min.js"></script></head><body><button onClick="downloadChunks()">下載</button>
</body>
<script>function downloadChunks() {const chunkdownloadUrl = 'http://localhost:8080/download/test.mp4'// 分片下載大小 5MBconst chunkSize = 1024 * 1024 * 20;// 文件總大小(需要請求后端獲得)let fileSize = 0;axios.head(chunkdownloadUrl).then(res => {// 定義 存儲所有的分片的數組let chunks = [];// 獲取文件總大小fileSize = res.headers['content-length']// 計算分片數量const chunksNum = Math.ceil(fileSize / chunkSize)// 定義下載文件分片的方法function downloadChunkFile(chunkIdx) {if (chunkIdx >= chunksNum) {alert('分片索引不可超過分片數量')return}let start = chunkIdx * chunkSizelet end = Math.min(start + chunkSize - 1, fileSize - 1)const range = `bytes=${start}-${end}`;axios({url: chunkdownloadUrl,method: 'post',headers: {Range: range},responseType: 'arraybuffer'}).then(response => {chunks.push(response.data)if (chunkIdx == chunksNum - 1) {// 下載好了console.log(chunks, 'chunks');// 組合chunks到單個文件const blob = new Blob(chunks);console.log(blob, 'blob');const link = document.createElement('a');link.href = window.URL.createObjectURL(blob);link.download = 'demo.mp4';link.click();return} else {++chunkIdxdownloadChunkFile(chunkIdx)}})}downloadChunkFile(0)})}
</script></html>
斷點續傳概念
概述

所謂斷點續傳,其實只是指下載,也就是要從文件已經下載的地方開始繼續下載。在以前版本的HTTP協議是不支持斷點的,HTTP/1.1開始就支持了。一般斷點下載時才用到Range和Content-Range實體頭。HTTP協議本身不支持斷點上傳,需要自己實現。

Range

Range:用于客戶端到服務端的請求,在請求頭中,指定第一個字節的位置和最后一個字節的位置,可以通過改字段指定下載文件的某一段大小及其單位,字節偏移從0開始。典型格式:

Ranges: (unit=first byte pos)-[last byte pos]

  • Ranges: bytes=4000- 下載從第4000字節開始到文件結束部分

  • Ranges: bytes=0~N 下載第0-N字節范圍的內容

  • Ranges: bytes=M-N 下載第M-N字節范圍的內容

  • Ranges: bytes=-N 下載最后N字節內容

以下幾點需要注意:

  • 這個數據區間是個閉合區間,起始值是0,所以“Range: bytes=0-1”這樣一個請求實際上是在請求開頭的2個字節。

  • “Range: bytes=-200”,它不是表示請求文件開始位置的201個字節,而是表示要請求文件結尾處的200個字節。

  • 如果last byte pos小于first byte pos,那么這個Range請求就是無效請求,server需要忽略這個Range請求,然后回應一個200,把整個文件發給client。

  • 如果last byte pos大于等于文件長度,那么這個Range請求被認為是不能滿足的,server需要回應一個416,Requested range not satisfiable。

示例解釋:

  • 表示頭500個字節:bytes=0-499

  • 表示第二個500字節:bytes=500-999

  • 表示最后500個字節:bytes=-500

  • 表示500字節以后的范圍:bytes=500-

  • 第一個和最后一個字節:bytes=0-0,-1

  • 同時指定幾個范圍:bytes=500-600,601-999

Content-Range

用于響應頭,指定整個實體中的一部分的插入位置,他也指示了整個實體的長度。在服務器向客戶返回一個部分響應,它必須描述響應覆蓋的范圍和整個實體長度。一般格式:
Content-Range: bytes (unit first byte pos) - [last byte pos]/[entity legth]

Header示例

GET /test.rar HTTP/1.1 
Connection: close 
Host: 116.1.219.219 
Range: bytes=0-801 //一般請求下載整個文件是bytes=0- 或不用這個頭

一般正常回應

HTTP/1.1 200 OK 
Content-Length: 801      
Content-Type: application/octet-stream 
Content-Range: bytes 0-800/801 //801:文件總大小

一個最簡單的斷點續傳實現大概如下:
1.客戶端下載一個1024K的文件,已經下載了其中512K
2.網絡中斷,客戶端請求續傳,因此需要在HTTP頭中申明本次需要續傳的片段:Range:bytes=512000-
這個頭通知服務端從文件的512K位置開始傳輸文件
3. 服務端收到斷點續傳請求,從文件的512K位置開始傳輸,并且在HTTP頭中增加:
Content-Range:bytes 512000-/1024000
并且此時服務端返回的HTTP狀態碼應該是206,而不是200。

但是在實際場景中,會出現一種情況,即在終端發起續傳請求時,URL對應的文件內容在服務端已經發生變化,此時續傳的數據肯定是錯誤的。如何解決這個問題了?顯然此時我們需要有一個標識文件唯一性的方法。在RFC2616中也有相應的定義,比如實現Last-Modified來標識文件的最后修改時間,這樣即可判斷出續傳文件時是否已經發生過改動。同時RFC2616中還定義有一個ETag的頭,可以使用ETag頭來放置文件的唯一標識,比如文件的MD5值。

終端在發起續傳請求時應該在HTTP頭中申明If-Match 或者If-Modified-Since 字段,幫助服務端判別文件變化。

另外RFC2616中同時定義有一個If-Range頭,終端如果在續傳是使用If-Range。If-Range中的內容可以為最初收到的ETag頭或者是Last-Modfied中的最后修改時候。服務端在收到續傳請求時,通過If-Range中的內容進行校驗,校驗一致時返回206的續傳回應,不一致時服務端則返回200回應,回應的內容為新的文件的全部數據。

響應式類型

Spring MVC支持在控制器中使用響應式客戶端庫(也可以閱讀WebFlux部分中的響應式庫)。這包括來自spring-webflux的WebClient和其他,例如Spring Data響應式數據存儲庫。在這種情況下,能夠從控制器方法返回響應式類型很方便。

反應式返回值處理如下:

  • 單值promise(promise)會被自動適配,其處理方式與使用DeferredResult類似。例如:Reactor框架的Mono或RxJava的Single均支持這種適配。

  • 對于采用流式媒體類型(如 application/stream+json 或 text/event-stream)的多值流,框架會自動適配,其處理方式類似于使用 ResponseBodyEmitter 或 SseEmitter。例如:Reactor 的 Flux 或 RxJava 的 Observable 均支持此類適配。應用程序也可以直接返回 Flux<ServerSentEvent> Observable<ServerSentEvent>

  • 對于使用其他任何媒體類型(例如 application/json)的多值流,框架會將其適配為類似 DeferredResult<List<?>> 的處理方式。

Spring MVC通過spring-core的ReactiveAdapterRegistry支持React和RxJava,這使得它可以適應多個反應式庫。

對于流式傳輸到響應,支持反應式反壓機制,但對響應的寫入仍然是阻塞的,并且通過配置 TaskExecutor在單獨的線程上運行,以避免阻塞上游源(例如從WebClient返回的Flux)。默認情況下,SimpleAsyncTaskExecutor用于阻塞寫入,但在負載下不適合。如果您計劃使用反應式類型流式傳輸,您應該使用MVC配置來配置任務執行器。

斷開連接

當遠程客戶端消失時,Servlet API不提供任何通知。因此,在流式傳輸響應時,無論是通過SseEmitter還是反應式類型,定期發送數據都很重要,因為如果客戶端斷開連接,寫入就會失敗。發送可以采取空(僅注釋)SSE事件或任何其他數據的形式,對方必須將其解釋為心跳并忽略。

或者,考慮使用具有內置心跳機制的Web消息傳遞解決方案(例如基于WebSocket的STOMP或帶有SockJS的WebSocket)。

配置

必須在Servlet容器級別啟用異步請求處理特征。MVC配置還公開了幾個異步請求選項。

Servlet容器

過濾器和Servlet聲明有一個true標志,需要設置為asyncSupported以啟用異步請求處理。此外,應聲明過濾器映射以處理ASYNC javax.servlet.DispatchType。

在Java配置中,當您使用AbstractAnnotationConfigDispatcherServletInitializer初始化Servlet容器時,這是自動完成的。

在web.xml配置中,您可以添加<async-supported>true</async-supported>到DispatcherServlet和Filter聲明,并添加<dispatcher>ASYNC</dispatcher>到濾波器映射。

Spring MVC

MVC配置公開了以下與異步請求處理相關的選項:

  • Java配置:使用configureAsyncSupport回調/回傳WebMvcConfigurer。
  • XML命名空間:使用<async-support>下的<mvc:annotation-driven>元素。

您可以配置以下內容:

  • 異步請求的默認超時值,如果未設置,則取決于底層Servlet容器。
  • AsyncTaskExecutor用于在使用反應式類型流式傳輸時阻止寫入,以及執行從控制器方法返回的Callable實例。如果您使用反應式類型流式傳輸或具有返回Callable的控制器方法,我們強烈建議配置此屬性,因為默認情況下,它是一個SimpleAsyncTaskExecutor。
  • DeferredResultProcessingInterceptor實現和CallableProcessingInterceptor實現。

請注意,您還可以在DeferredResult、ResponseBodyEmitter和SseEmitter上設置默認超時值SseEmitter對于Callable,您可以使用WebAsyncTask提供超時值。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/77431.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/77431.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/77431.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

一問看懂——支持向量機SVM(Support Vector Machine)

目錄 蕪湖~~~支持向量機&#xff08;SVM&#xff09; 1. 引言 2. 基本思想 3. 數學模型 3.1 超平面定義 3.2 分類間隔與目標函數 3.3 軟間隔與松弛變量 4. 核函數方法&#xff08;Kernel Trick&#xff09; 4.1 核函數定義 4.2 常用核函數 5. SVM 的幾種類型 6. SV…

藍橋杯 拼數(字符串大小比較)

題目描述 設有 n 個正整數 a1?…an?&#xff0c;將它們聯接成一排&#xff0c;相鄰數字首尾相接&#xff0c;組成一個最大的整數。 輸入格式 第一行有一個整數&#xff0c;表示數字個數 n。 第二行有 n 個整數&#xff0c;表示給出的 n 個整數 ai?。 輸出格式 一個正整…

Elasticsearch 系列專題 - 第三篇:搜索與查詢

搜索是 Elasticsearch 的核心功能之一。本篇將介紹如何構建高效的查詢、優化搜索結果,以及調整相關性評分,幫助你充分發揮 Elasticsearch 的搜索能力。 1. 基礎查詢 1.1 Match Query 與 Term Query 的區別 Match Query:用于全文搜索,會對查詢詞進行分詞。 GET /my_index/_…

本地電腦使用sshuttle命令將網絡流量代理到ssh連接的電腦去實現訪問受限網絡

本地電腦使用sshuttle命令將網絡流量代理到ssh連接的電腦去實現訪問受限網絡 安裝使用 工作過程中, 經常會遇到, 需要訪問客戶內網環境的問題, 一般都需要安轉各式各樣的VPN客戶端到本地電腦上, 軟件多了也會造成困擾, 所有, 找了一款還不錯的命令工具去解決這個痛點 安裝 官方…

雙相機結合halcon的條碼檢測

以下是針對提供的C#代碼的詳細注釋和解釋&#xff0c;結合Halcon庫的功能和代碼結構進行說明&#xff1a; --- ### **代碼整體結構** 該代碼是一個基于Halcon庫的條碼掃描類GeneralBarcodeScan&#xff0c;支持單臺或雙臺相機的條碼檢測&#xff0c;并通過回調接口返回結果。…

python基礎語法12-迭代器與生成器

Python 生成器與迭代器詳解 在 Python 中&#xff0c;生成器和迭代器是處理大量數據時的強大工具。它們能夠幫助我們節省內存&#xff0c;避免一次性加載過多數據。生成器通過 yield 關鍵字實現&#xff0c;允許我們逐步產生數據&#xff0c;而迭代器通過實現特定的接口&#…

公司內部建立pypi源

有一篇建立apt源的文章在這里&#xff0c;需要的可以查看&#xff1a;公司內部建立apt源-CSDN博客 server: pip install pypiserver mkdir -d pypi/packages cp test.whl pypi/packages pypi-server run --port 8080 /home/xu/pypi/packages & 網頁訪問&#xff1a;http:…

VMware Workstation/Player 的詳細安裝使用指南

以下是 VMware Workstation/Player 的完整下載、安裝指南&#xff0c;包含詳細步驟、常見問題及解決方法&#xff0c;以及進階使用技巧&#xff0c;適用于 Windows 和 macOS 用戶。 VMware Workstation/Player 的詳細安裝使用指南—目錄 一、下載與安裝詳細指南1. 系統要求2. 下…

藍橋杯python組考前準備

1.保留k位小數 round(10/3, 2) # 第二個參數表示保留幾位小數 2.輸入代替方案&#xff08;加速讀取&#xff09; import sys n int(sys.stdin.readline()) # 讀取整數&#xff08;不加int就是字符串&#xff09; a, b map(int, sys.stdin.readline().split()) # 一行讀取多個…

【JSON2WEB】16 login.html 登錄密碼加密傳輸

【JSON2WEB】系列目錄 【JSON2WEB】01 WEB管理信息系統架構設計 【JSON2WEB】02 JSON2WEB初步UI設計 【JSON2WEB】03 go的模板包html/template的使用 【JSON2WEB】04 amis低代碼前端框架介紹 【JSON2WEB】05 前端開發三件套 HTML CSS JavaScript 速成 【JSON2WEB】06 JSO…

計算機網絡起源

互聯網的起源和發展是一個充滿創新、突破和變革的歷程&#xff0c;從20世紀60年代到1989年&#xff0c;這段時期為互聯網的誕生和普及奠定了堅實的基礎。讓我們詳細回顧這一段激動人心的歷史。 計算機的發展與ARPANET的建立&#xff08;20世紀60年代&#xff09; 互聯網的誕生…

洛谷P1824進擊的奶牛簡單二分

題目如下 代碼如下 謝謝觀看

如何建立高效的會議機制

建立高效的會議機制需做到&#xff1a;明確會議目標、制定并提前分發議程、控制會議時長、確保有效溝通與反饋、及時跟進執行情況。其中&#xff0c;明確會議目標是核心關鍵&#xff0c;它直接決定了會議的方向與效率。只有明確目標&#xff0c;會議才不會偏離初衷&#xff0c;…

開源AI大模型AI智能名片S2B2C商城小程序:科技浪潮下的商業新引擎

摘要&#xff1a; 本文聚焦于科技迅猛發展背景下&#xff0c;開源AI大模型、AI智能名片與S2B2C商城小程序的融合應用。通過分析元宇宙、人工智能、區塊鏈、5G等前沿科技帶來的商業變革&#xff0c;闡述開源AI大模型AI智能名片S2B2C商城小程序在整合資源、優化服務、提升用戶體驗…

基于大模型構建金融客服的技術調研

OpenAI-SB api接口 https://openai-sb.com/ ChatGPT與Knowledge Graph (知識圖譜)分享交流 https://www.bilibili.com/video/BV1bo4y1w72m/?spm_id_from333.337.search-card.all.click&vd_source569ef4f891360f2119ace98abae09f3f 《要研究的方向和準備》 https://ww…

WSA(Windows Subsystem for Android)安裝LSPosed和應用教程

windows安卓子系統WSA的Lsposed和shamiko的安裝教程 WSA(Windows Subsystem for Android)安裝LSPosed和應用教程 一、環境準備 在開始之前,請確保: 已經安裝好WSA(Windows Subsystem for Android)已經安裝好ADB工具下載好LSPosed和Shamiko框架安裝包 二、連接WSA 首先需要…

辛格迪客戶案例 | 河南宏途食品實施電子合約系統(eSign)

01 河南宏途食品有限公司&#xff1a;食品行業的數字化踐行者 河南宏途食品有限公司&#xff08;以下簡稱“宏途食品”&#xff09;作為國內食品行業的創新企業&#xff0c;專注于各類食品的研發、生產和銷售。公司秉承“質量為先、創新驅動、服務至上”的核心價值觀&#xff…

手機靜態ip地址怎么獲取?方法與解析?

而在某些特定情境下&#xff0c;我們可能需要為手機設置一個靜態IP地址。本文將詳細介紹手機靜態IP地址詳解及獲取方法 一、什么是靜態IP地址&#xff1f; 靜態IP&#xff1a;由用戶手動設置的固定IP地址&#xff0c;不會因網絡重啟或設備重連而改變。 動態IP&#xff1a;由路…

天下飛飛【老飛飛服務端】+客戶端+數據庫測試帶視頻教程

天下飛飛服務器搭建測試視頻 天下飛飛【老飛飛服務端】客戶端數據庫測試帶視頻教程 完整安裝教程。 測試環境 系統server2019 sql2022數據庫 sql的安裝 odbc搭建 sql加載數據庫 此測試端能用于服務器搭建測試。 下載地址為&#xff1a;https://download.csdn.net/d…

Gitea的安裝和配置以及應用

Gitea的安裝和配置以及應用 一、安裝 1、創建數據庫和數據庫賬戶&#xff08;pg&#xff09; su – postgres -c "psql" CREATE ROLE gitea WITH LOGIN PASSWORD gitea; CREATE DATABASE giteadb WITH OWNER gitea TEMPLATE template0 ENCODING UTF8 LC_COLLATE …