從零手寫實現 nginx-07-大文件傳輸 分塊傳輸(chunked transfer)/ 分頁傳輸(paging)

来源:https://www.cnblogs.com/houbbBlogs/p/18235855
-Advertisement-
Play Games

前言 大家好,我是老馬。很高興遇到你。 我們希望實現最簡單的 http 服務信息,可以處理靜態文件。 如果你想知道 servlet 如何處理的,可以參考我的另一個項目: 手寫從零實現簡易版 tomcat minicat 手寫 nginx 系列 如果你對 nginx 原理感興趣,可以閱讀: 從零手寫實 ...


前言

大家好,我是老馬。很高興遇到你。

我們希望實現最簡單的 http 服務信息,可以處理靜態文件。

如果你想知道 servlet 如何處理的,可以參考我的另一個項目:

手寫從零實現簡易版 tomcat minicat

手寫 nginx 系列

如果你對 nginx 原理感興趣,可以閱讀:

從零手寫實現 nginx-01-為什麼不能有 java 版本的 nginx?

從零手寫實現 nginx-02-nginx 的核心能力

從零手寫實現 nginx-03-nginx 基於 Netty 實現

從零手寫實現 nginx-04-基於 netty http 出入參優化處理

從零手寫實現 nginx-05-MIME類型(Multipurpose Internet Mail Extensions,多用途互聯網郵件擴展類型)

從零手寫實現 nginx-06-文件夾自動索引

從零手寫實現 nginx-07-大文件下載

從零手寫實現 nginx-08-範圍查詢

從零手寫實現 nginx-09-文件壓縮

從零手寫實現 nginx-10-sendfile 零拷貝

從零手寫實現 nginx-11-file+range 合併

從零手寫實現 nginx-12-keep-alive 連接復用

從零手寫實現 nginx-13-nginx.conf 配置文件介紹

從零手寫實現 nginx-14-nginx.conf 和 hocon 格式有關係嗎?

從零手寫實現 nginx-15-nginx.conf 如何通過 java 解析處理?

從零手寫實現 nginx-16-nginx 支持配置多個 server

目標

前面的內容我們實現了小文件的傳輸,但是如果文件的內容特別大,全部載入到記憶體會導致伺服器報廢。

那麼,應該怎麼解決呢?

思路

我們可以把一個非常大的文件直接拆分為多次,然後分段傳輸過去。

傳輸完成後,告訴瀏覽器已經傳輸完成了,發送一個結束標識即可。

大文件傳輸的方式

一次梭哈

這種方式通常用於發送較小的文件,因為整個文件內容會被載入到記憶體中。

代碼示例:

RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r"); // 以只讀的方式打開文件

long fileLength = randomAccessFile.length();
// 創建一個預設的HTTP響應
HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
// 設置Content Length
HttpUtil.setContentLength(response, fileLength);


// 讀取文件內容到位元組數組
byte[] fileContent = new byte[(int) fileLength];
int bytesRead = randomAccessFile.read(fileContent);
if (bytesRead != fileLength) {
    sendError(ctx, INTERNAL_SERVER_ERROR);
    return;
}

// 將文件內容轉換為FullHttpResponse
FullHttpResponse fullHttpResponse = new DefaultFullHttpResponse(HTTP_1_1, OK);
fullHttpResponse.content().writeBytes(fileContent);
fullHttpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH, fileLength);
// 寫入HTTP響應並關閉連接
ctx.writeAndFlush(fullHttpResponse).addListener(ChannelFutureListener.CLOSE);

這段代碼的主要變化如下:

  1. 讀取文件內容:使用randomAccessFile.read(fileContent)一次性讀取整個文件到位元組數組fileContent中。
  2. 創建FullHttpResponse:使用DefaultFullHttpResponse創建一個完整的HTTP響應對象,並將文件內容寫入到響應的content()中。
  3. 設置Content-Length:在FullHttpResponse的headers中設置Content-Length
  4. 發送響應並關閉連接:使用ctx.writeAndFlush(fullHttpResponse)一次性發送整個響應,並通過.addListener(ChannelFutureListener.CLOSE)確保在發送完成後關閉連接。

請註意,這種方式適用於文件大小不是很大的情況,因為整個文件內容被載入到了記憶體中。

如果文件非常大,這種方式可能會導致記憶體溢出。

對於大文件,推薦使用分塊傳輸(chunked transfer)或者分頁傳輸(paging)的方式。

分塊傳輸(chunked transfer)

分塊傳輸(Chunked Transfer)是一種HTTP協議中用於傳輸數據的方法,允許伺服器在知道整個響應內容大小之前就開始發送數據。

這在發送大文件或動態生成的內容時非常有用。

以下是使用Netty實現分塊傳輸的一個示例:

RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r"); // 以只讀的方式打開文件
long fileLength = randomAccessFile.length();

// 創建一個預設的HTTP響應
HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);

// 由於是分塊傳輸,移除Content-Length頭
response.headers().remove(HttpHeaderNames.CONTENT_LENGTH);

// 如果request中有KEEP ALIVE信息
if (HttpUtil.isKeepAlive(request)) {
    response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}

// 將HTTP響應寫入Channel
ctx.write(response);

// 分塊傳輸文件內容
final int chunkSize = 8192; // 設置分塊大小
ByteBuffer buffer = ByteBuffer.allocate(chunkSize);
while (true) {
    int bytesRead = randomAccessFile.read(buffer.array());
    if (bytesRead == -1) { // 文件讀取完畢
        break;
    }
    buffer.limit(bytesRead);
    // 寫入分塊數據
    ctx.write(new DefaultHttpContent(Unpooled.wrappedBuffer(buffer)));
    buffer.clear(); // 清空緩衝區以供下次使用
}

// 寫入最後一個分塊,即空的HttpContent,表示傳輸結束
ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT).addListener(ChannelFutureListener.CLOSE);

這段代碼的主要變化如下:

  1. 移除Content-Length:由於是分塊傳輸,我們不需要在響應頭中設置Content-Length

  2. 分塊讀取文件:使用一個固定大小的緩衝區ByteBuffer來分塊讀取文件內容。

  3. 發送分塊數據:在迴圈中,每次讀取文件內容到緩衝區後,創建一個DefaultHttpContent對象,並將緩衝區的數據包裝在Unpooled.wrappedBuffer()中,然後寫入Channel。

  4. 發送結束標記:在文件讀取完畢後,發送一個空的LastHttpContent對象,以標記HTTP消息體的結束。

  5. 關閉連接:在發送完最後一個分塊後,使用addListener(ChannelFutureListener.CLOSE)確保關閉連接。

分頁傳輸

分頁傳輸通常是指將大文件分成多個小的部分(頁),然後逐個發送這些部分。

這種方式適用於在網路編程中傳輸大文件,因為它可以減少記憶體的使用,並且允許接收方逐步處理數據。

在Netty中,實現分頁傳輸通常涉及到手動控制數據的發送,而不是使用HTTP分塊編碼(chunked encoding)。

以下是一個簡化的分頁傳輸實現示例,我們將使用Netty的FileRegion來實現高效的文件傳輸:

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.FileRegion;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.stream.ChunkedFile;

import java.io.RandomAccessFile;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.Paths;

public class FilePageTransfer {

    public static void sendFile(ChannelHandlerContext ctx, Path filePath) {
        try {
            RandomAccessFile randomAccessFile = new RandomAccessFile(filePath.toFile(), "r");
            FileChannel fileChannel = randomAccessFile.getChannel();

            long fileSize = fileChannel.size();
            long position = 0;
            final long pageSize = 8192; // 定義每頁的大小,可以根據實際情況調整

            while (position < fileSize) {
                long remaining = fileSize - position;
                long size = remaining > pageSize ? pageSize : remaining;

                // 使用FileRegion進行傳輸
                FileRegion region = new DefaultFileRegion(fileChannel, position, size);
                ((SocketChannel) ctx.channel()).write(region);

                // 更新位置
                position += size;

                // 檢查傳輸是否成功
                if (!region.isWritten()) {
                    // 傳輸失敗,可以進行重試或者發送錯誤響應
                    break;
                }
            }

            // 發送結束標記
            ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT).addListener(ChannelFutureListener.CLOSE);
        } catch (IOException e) {
            e.printStackTrace();
            // 發送錯誤響應
            ctx.writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND));
        }
    }
}

在這個示例中,我們定義了一個sendFile方法,它接受一個ChannelHandlerContext和一個文件路徑Path作為參數。以下是該方法的主要步驟:

  1. 打開文件:使用RandomAccessFile打開要傳輸的文件,並獲取FileChannel

  2. 計算文件大小:通過fileChannel.size()獲取文件的總大小。

  3. 分頁傳輸:使用一個迴圈來逐頁讀取文件內容。在每次迭代中,我們計算要傳輸的數據塊的大小,並使用FileRegion來表示這部分數據。

  4. 寫入Channel:將FileRegion寫入Netty的Channel

  5. 更新位置:更新position變數以指向下一頁的開始位置。

  6. 檢查傳輸狀態:通過region.isWritten()檢查數據是否成功寫入。

  7. 發送結束標記:傳輸完成後,發送LastHttpContent.EMPTY_LAST_CONTENT來標記消息結束,並關閉連接。

  8. 錯誤處理:如果在傳輸過程中發生異常,發送一個錯誤響應。

請註意,這個示例是一個簡化的版本,它沒有處理HTTP協議的細節,也沒有設置HTTP頭信息。

在實際的HTTP伺服器實現中,你需要在發送文件內容之前發送一個包含適當頭信息的HTTP響應。

此外,LastHttpContent.EMPTY_LAST_CONTENT用於HTTP/1.1,如果你使用的是HTTP/1.0,可能需要不同的處理方式。

改進後的核心代碼

統一的分發

為了避免實現膨脹,難以管理,我們將實現全部抽象。

protected NginxRequestDispatch getDispatch(NginxRequestDispatchContext context) {
    final FullHttpRequest requestInfoBo = context.getRequest();
    final NginxConfig nginxConfig = context.getNginxConfig();
    // 消息解析不正確
    /*如果無法解碼400*/
    if (!requestInfoBo.decoderResult().isSuccess()) {
        return NginxRequestDispatches.http400();
    }
    // 文件
    File targetFile = getTargetFile(requestInfoBo, nginxConfig);
    // 是否存在
    if(targetFile.exists()) {
        // 設置文件
        context.setFile(targetFile);
        // 如果是文件夾
        if(targetFile.isDirectory()) {
            return NginxRequestDispatches.fileDir();
        }
        long fileSize = targetFile.length();
        if(fileSize <= NginxConst.BIG_FILE_SIZE) {
            return NginxRequestDispatches.fileSmall();
        }
        return NginxRequestDispatches.fileBig();
    }  else {
        return NginxRequestDispatches.http404();
    }
}

大文件的核心邏輯

大文件我們使用 chunk 的方式

    public void doDispatch(NginxRequestDispatchContext context) {
        final FullHttpRequest request = context.getRequest();
        final File targetFile = context.getFile();
        final String bigFilePath = targetFile.getAbsolutePath();
        final long fileLength = targetFile.length();


        logger.info("[Nginx] match big file, path={}", bigFilePath);

        HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
        response.headers().set(HttpHeaderNames.CONTENT_DISPOSITION, "attachment; filename=\"" + targetFile.getName() + "\"");
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, InnerMimeUtil.getContentType(targetFile));
        response.headers().set(HttpHeaderNames.CONTENT_LENGTH, fileLength);

        final ChannelHandlerContext ctx = context.getCtx();
        ctx.write(response);

        // 分塊傳輸文件內容
        long totalLength = targetFile.length();
        long totalRead = 0;

        try(RandomAccessFile randomAccessFile = new RandomAccessFile(targetFile, "r")) {
            ByteBuffer buffer = ByteBuffer.allocate(NginxConst.CHUNK_SIZE);
            while (true) {
                int bytesRead = randomAccessFile.read(buffer.array());
                if (bytesRead == -1) { // 文件讀取完畢
                    break;
                }
                buffer.limit(bytesRead);
                // 寫入分塊數據
                ctx.write(new DefaultHttpContent(Unpooled.wrappedBuffer(buffer)));
                buffer.clear(); // 清空緩衝區以供下次使用

                // process 可以考慮加一個 listener
                totalRead += bytesRead;
                logger.info("[Nginx] bigFile process >>>>>>>>>>> {}/{}", totalRead, totalLength);
            }

            // 發送結束標記
            ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT)
                    .addListener(ChannelFutureListener.CLOSE);
        } catch (Exception e) {
            logger.error("[Nginx] bigFile meet ex", e);
        }
    }

這裡採用的是直接下載的方式。

當然,也可以實現線上播放,但是試了下效果不好,後續有時間可以嘗試下。

測試日誌

[INFO] [2024-05-26 15:53:58.498] [nioEventLoopGroup-3-3] [c.g.h.n.s.h.NginxNettyServerHandler.channelRead0] - [Nginx] channelRead writeAndFlush start request=HttpObjectAggregator$AggregatedFullHttpRequest(decodeResult: success, version: HTTP/1.1, content: CompositeByteBuf(ridx: 0, widx: 0, cap: 0, components=0))
GET /mime/2.mp4 HTTP/1.1
Host: 192.168.1.12:8080
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
content-length: 0, id=40a5effffe257be0-00001c6c-00000003-0824dff434805bd3-b09fd676
[INFO] [2024-05-26 15:53:58.498] [nioEventLoopGroup-3-3] [c.g.h.n.s.r.d.h.AbstractNginxRequestDispatchFullResp.doDispatch] - [Nginx] match big file, path=D:\data\nginx4j\mime\2.mp4
[INFO] [2024-05-26 15:53:58.514] [nioEventLoopGroup-3-3] [c.g.h.n.s.r.d.h.AbstractNginxRequestDispatchFullResp.doDispatch] - [Nginx] bigFile process >>>>>>>>>>> 8388608/668918096
...
[INFO] [2024-05-26 15:53:59.616] [nioEventLoopGroup-3-3] [c.g.h.n.s.r.d.h.AbstractNginxRequestDispatchFullResp.doDispatch] - [Nginx] bigFile process >>>>>>>>>>> 668918096/668918096
[INFO] [2024-05-26 15:53:59.627] [nioEventLoopGroup-3-3] [c.g.h.n.s.h.NginxNettyServerHandler.channelRead0] - [Nginx] channelRead writeAndFlush DONE id=40a5effffe257be0-00001c6c-00000003-0824dff434805bd3-b09fd676

小結

本節我們實現了一個大文件的下載處理,主要思想就是分段。

可以考慮類似於視頻軟體,採用分段載入實時播放的方式。

下一節,我們考慮實現以下文件的範圍查詢。

我是老馬,期待與你的下次重逢。

開源地址

為了便於大家學習,已經將 nginx 開源

https://github.com/houbb/nginx4j


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Web 性能是 Web 開發的一個重要方面,側重於網頁載入速度以及對用戶輸入的響應速度 通過優化網站來改善性能,可以在為用戶提供更好的體驗 網頁性能既廣泛又非常深入 1. 為什麼性能這麼重要? 1. 性能關乎留住用戶 性能對於任何線上業務都至關重要 與載入速度緩慢、讓人感覺運行緩慢的網站相比,載入速... ...
  • Don't Talk, code is here: 重點是startRecord 方法 <template> <div> <el-tooltip class="item" effect="dark" content="再次點擊 【開始錄音】 即為重新錄製,之前錄製的將被作廢" placement=" ...
  • title: Vue.js 動畫與過渡效果實戰 date: 2024/6/4 updated: 2024/6/4 description: 這篇文章介紹瞭如何在網頁設計中使用過渡動畫和組件效果,以及如何利用模式和列表展示信息。還提到了使用鉤子實現組件間通信的方法。 categories: 前端開發 ...
  • 以用戶為中心的性能指標是理解和改進站點體驗的關鍵點 一、以用戶為中心的性能指標 1. 指標是用來幹啥的? 指標是用來衡量性能和用戶體驗的 2. 指標類型 感知載入速度:網頁可以多快地載入網頁中的所有視覺元素並將其渲染到屏幕上 載入響應速度:頁面載入和執行組件快速響應用戶互動所需的 JavaScrip... ...
  • 隨著Web應用變得越來越複雜,而jQuery的功能卻顯得過於簡單,難以應對這些複雜的需求。比如,對於一些需要大量動態交互的應用程式,jQuery的功能並不足夠強大。此外,由於jQuery所寫應用的代碼結構較為混亂,其中包含了大量的全局變數和函數,例如,全局變數"$"和"jQuery"都指向了jQue... ...
  • 基於React的SSG靜態站點渲染方案 靜態站點生成SSG - Static Site Generation是一種在構建時生成靜態HTML等文件資源的方法,其可以完全不需要服務端的運行,通過預先生成靜態文件,實現快速的內容載入和高度的安全性。由於其生成的是純靜態資源,便可以利用CDN等方案以更低的成 ...
  • 結構化開發方法 基本思想:自頂向下,逐步求精,過程抽象,模塊化技術 概念: 結構化程式設計:按照一定的原則與原理,組織編寫正確且易讀的程式的軟體技術。 結構化分析設計:數據流圖、數據字典、模塊結構圖。 優勢:合理性(管理複雜性的有效手段:分解,抽象,層次)、正確性(依據規約,完成任務) 程式 & 抽 ...
  • 01-什麼是Spring IOC 和DI ? IOC : 控制翻轉 , 它把傳統上由程式代碼直接操控的對象的調用權交給容 器,通過容器來實現對 象組件的裝配和管理。所謂的“控制反轉”概念就是對組件對象控制權的轉 移,從程式代碼本身 轉移到了外部容器。 DI : 依賴註入,在我們創建對象的過程中,把對 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...