從零手寫實現 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
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...