提升性能的利器:深入解析SectionReader

来源:https://www.cnblogs.com/chenjiazhan/archive/2023/07/02/17521340.html
-Advertisement-
Play Games

關鍵字 abstractassertbooleanbreakbyte case catch char class const continue default do double else enum extends final finally float for goto if implementi ...


一. 簡介

本文將介紹 Go 語言中的 SectionReader,包括 SectionReader的基本使用方法、實現原理、使用註意事項。從而能夠在合適的場景下,更好得使用SectionReader類型,提升程式的性能。

二. 問題引入

這裡我們需要實現一個基本的HTTP文件伺服器功能,可以處理客戶端的HTTP請求來讀取指定文件,並根據請求的Range頭部欄位返迴文件的部分數據或整個文件數據。

這裡一個簡單的思路,可以先把整個文件的數據載入到記憶體中,然後再根據請求指定的範圍,截取對應的數據返回回去即可。下麵提供一個代碼示例:

func serveFile(w http.ResponseWriter, r *http.Request, filePath string) {
    // 打開文件
    file, _ := os.Open(filePath)
    defer file.Close()

    // 讀取整個文件數據
    fileData, err := ioutil.ReadAll(file)
    if err != nil {
        // 錯誤處理
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // 根據Range頭部欄位解析請求的範圍
    rangeHeader := r.Header.Get("Range")
    ranges, err := parseRangeHeader(rangeHeader)
    if err != nil {
        // 錯誤處理
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    // 處理每個範圍並返回數據
    for _, rng := range ranges {
        start := rng.Start
        end := rng.End
        // 從文件數據中提取範圍的位元組數據
        rangeData := fileData[start : end+1]

        // 將範圍數據寫入響應
        w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, fileInfo.Size()))
        w.Header().Set("Content-Length", strconv.Itoa(len(rangeData)))
        w.WriteHeader(http.StatusPartialContent)
        w.Write(rangeData)
    }
}

type Range struct {
    Start int
    End   int
}

// 解析HTTP Range請求頭
func parseRangeHeader(rangeHeader string) ([]Range, error){}

上述的代碼實現比較簡單,首先,函數打開filePath指定的文件,使用ioutil.ReadAll函數讀取整個文件的數據到fileData中。接下來,從HTTP請求頭中Range頭部欄位中獲取範圍信息,獲取每個範圍請求的起始和終止位置。接著,函數遍歷每一個範圍信息,提取文件數據fileData 中對應範圍的位元組數據到rangeData中,然後將數據返回回去。基於此,簡單實現了一個支持範圍請求的HTTP文件伺服器。

但是當前實現其實存在一個問題,即在每次請求都會將整個文件載入到記憶體中,即使用戶只需要讀取其中一小部分數據,這種處理方式會給記憶體帶來非常大的壓力。假如被請求文件的大小是100M,一個32G記憶體的機器,此時最多只能支持320個併發請求。但是用戶每次請求可能只是讀取文件的一小部分數據,比如1M,此時將整個文件載入到記憶體中,往往是一種資源的浪費,同時從磁碟中讀取全部數據到記憶體中,此時性能也較低。

那能不能在處理請求時,HTTP文件伺服器只讀取請求的那部分數據,而不是載入整個文件的內容,go基礎庫有對應類型的支持嗎?

其實還真有,Go語言中其實存在一個SectionReader的類型,它可以從一個給定的數據源中讀取數據的特定片段,而不是讀取整個數據源,這個類型在這個場景下使用非常合適。

下麵我們先仔細介紹下SectionReader的基本使用方式,然後將其作用到上面文件伺服器的實現當中。

三. 基本使用

3.1 基本定義

SectionReader類型的定義如下:

type SectionReader struct {
   r     ReaderAt
   base  int64
   off   int64
   limit int64
}

SectionReader包含了四個欄位:

  • r:一個實現了ReaderAt介面的對象,它是數據源。
  • base: 數據源的起始位置,通過設置base欄位,可以調整數據源的起始位置。
  • off:讀取的起始位置,表示從數據源的哪個偏移量開始讀取數據,初始化時一般與base保持一致。
  • limit:數據讀取的結束位置,表示讀取到哪裡結束。

同時還提供了一個構造器方法,用於創建一個SectionReader實例,定義如下:

func NewSectionReader(r ReaderAt, off int64, n int64) *SectionReader {
   // ... 忽略一些驗證邏輯
   // remaining 代表數據讀取的結束位置,為 base(偏移量) + n(讀取位元組數)
   remaining = n + off
   return &SectionReader{r, off, off, remaining}
}

NewSectionReader接收三個參數,r 代表實現了ReadAt介面的數據源,off表示起始位置的偏移量,也就是要從哪裡開始讀取數據,n代表要讀取的位元組數。通過NewSectionReader函數,可以很方便得創建出SectionReader對象,然後讀取特定範圍的數據。

3.2 使用方式

SectionReader 能夠像io.Reader一樣讀取數據,唯一區別是會被限定在指定範圍內,只會返回特定範圍的數據。

下麵通過一個例子來說明SectionReader的使用,代碼示例如下:

package main

import (
        "fmt"
        "io"
        "strings"
)

func main() {
        // 一個實現了 ReadAt 介面的數據源
        data := strings.NewReader("Hello,World!")

        // 創建 SectionReader,讀取範圍為索引 2 到 9 的位元組
        // off = 2, 代表從第二個位元組開始讀取; n = 7, 代表讀取7個位元組
        section := io.NewSectionReader(data, 2, 7)
        // 數據讀取緩衝區長度為5
        buffer := make([]byte, 5)
        for {
                // 不斷讀取數據,直到返回io.EOF
                n, err := section.Read(buffer)
                if err != nil {
                        if err == io.EOF {
                                // 已經讀取到末尾,退出迴圈
                                break
                        }
                        fmt.Println("Error:", err)
                        return
                }

                fmt.Printf("Read %d bytes: %s\n", n, buffer[:n])
        }
}

上述函數使用 io.NewSectionReader 創建了一個 SectionReader,指定了開始讀取偏移量為 2,讀取位元組數為 7。這意味著我們將從第三個位元組(索引 2)開始讀取,讀取 7 個位元組。

然後我們通過一個無限迴圈,不斷調用Read方法讀取數據,直到讀取完所有的數據。函數運行結果如下,確實只讀取了範圍為索引 2 到 9 的位元組的內容:

Read 5 bytes: llo,W
Read 2 bytes: or

因此,如果我們只需要讀取數據源的某一部分數據,此時可以創建一個SectionReader實例,定義好數據讀取的偏移量和數據量之後,之後可以像普通的io.Reader那樣讀取數據,SectionReader確保只會讀取到指定範圍的數據。

3.3 使用例子

這裡回到上面HTTP文件伺服器實現的例子,之前的實現存在一個問題,即每次請求都會讀取整個文件的內容,這會代碼記憶體資源的浪費,性能低,響應時間比較長等問題。下麵我們使用SectionReader 對其進行優化,實現如下:

func serveFile(w http.ResponseWriter, r *http.Request, filePath string) {
        // 打開文件
        file, err := os.Open(filePath)
        if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
                return
        }
        defer file.Close()

        // 獲取文件信息
        fileInfo, err := file.Stat()
        if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
                return
        }

        // 根據Range頭部欄位解析請求的範圍
        rangeHeader := r.Header.Get("Range")
        ranges, err := parseRangeHeader(rangeHeader)
        if err != nil {
                http.Error(w, err.Error(), http.StatusBadRequest)
                return
        }

        // 處理每個範圍並返回數據
        for _, rng := range ranges {
                start := rng.Start
                end := rng.End

                // 根據範圍創建SectionReader
                section := io.NewSectionReader(file, int64(start), int64(end-start+1))

                // 將範圍數據寫入響應
                w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, fileInfo.Size()))
                w.WriteHeader(http.StatusPartialContent)
                io.CopyN(w, section, section.Size())
        }
}

type Range struct {
        Start int
        End   int
}
// 解析HTTP Range請求頭
func parseRangeHeader(rangeHeader string) ([]Range, error) {}

在上述優化後的實現中,我們使用 io.NewSectionReader 創建了 SectionReader,它的範圍是根據請求頭中的範圍信息計算得出的。然後,我們通過 io.CopyNSectionReader 中的數據直接拷貝到響應的 http.ResponseWriter 中。

上述兩個HTTP文件伺服器實現的區別,只在於讀取特定範圍數據方式,前一種方式是將整個文件載入到記憶體中,再截取特定範圍的數據;而後者則是通過使用 SectionReader,我們避免了一次性讀取整個文件數據,並且只讀取請求範圍內的數據。這種優化能夠更高效地處理大文件或處理大量併發請求的場景,節省了記憶體和處理時間。

四. 實現原理

4.1 設計初衷

SectionReader的設計初衷,在於提供一種簡潔,靈活的方式來讀取數據源的特定部分。

4.2 基本原理

SectionReader 結構體中offbaselimit欄位是實現只讀取數據源特定部分數據功能的重要變數。

type SectionReader struct {
   r     ReaderAt
   base  int64
   off   int64
   limit int64
}

由於SectionReader需要保證只讀取特定範圍的數據,故需要保存開始位置和結束位置的值。這裡是通過baselimit這兩個欄位來實現的,base記錄了數據讀取的開始位置,limit記錄了數據讀取的結束位置。

通過設定baselimit兩個欄位的值,限制了能夠被讀取數據的範圍。之後需要開始讀取數據,有可能這部分待讀取的數據不會被一次性讀完,此時便需要一個欄位來說明接下來要從哪一個位元組繼續讀取下去,因此SectionReader也設置了off欄位的值,這個代表著下一個帶讀取數據的位置。

在使用SectionReader讀取數據的過程中,通過baselimit限制了讀取數據的範圍,off則不斷修改,指向下一個帶讀取的位元組。

4.3 代碼實現

4.3.1 Read方法說明

func (s *SectionReader) Read(p []byte) (n int, err error) {
    // s.off: 將被讀取數據的下標
    // s.limit: 指定讀取範圍的最後一個位元組,這裡應該保證s.base <= s.off
   if s.off >= s.limit {
      return 0, EOF
   }
   // s.limit - s.off: 還剩下多少數據未被讀取
   if max := s.limit - s.off; int64(len(p)) > max {
      p = p[0:max]
   }
   // 調用 ReadAt 方法讀取數據
   n, err = s.r.ReadAt(p, s.off)
   // 指向下一個待被讀取的位元組
   s.off += int64(n)
   return
}

SectionReader實現了Read 方法,通過該方法能夠實現指定範圍數據的讀取,在內部實現中,通過兩個限制來保證只會讀取到指定範圍的數據,具體限制如下:

  • 通過保證 off 不大於 limit 欄位的值,保證不會讀取超過指定範圍的數據
  • 在調用ReadAt方法時,保證傳入切片長度不大於剩餘可讀數據長度

通過這兩個限制,保證了用戶只要設定好了數據開始讀取偏移量 base 和 數據讀取結束偏移量 limit欄位值,Read方法便只會讀取這個範圍的數據。

4.3.2 ReadAt 方法說明

func (s *SectionReader) ReadAt(p []byte, off int64) (n int, err error) {
    // off: 參數指定了偏移位元組數,為一個相對數值
    // s.limit - s.base >= off: 保證不會越界
   if off < 0 || off >= s.limit-s.base {
      return 0, EOF
   }
   // off + base: 獲取絕對的偏移量
   off += s.base
   // 確保傳入位元組數組長度 不超過 剩餘讀取數據範圍
   if max := s.limit - off; int64(len(p)) > max {
      p = p[0:max]
      // 調用ReadAt 方法讀取數據
      n, err = s.r.ReadAt(p, off)
      if err == nil {
         err = EOF
      }
      return n, err
   }
   return s.r.ReadAt(p, off)
}

SectionReader還提供了ReadAt方法,能夠指定偏移量處實現數據讀取。它根據傳入的偏移量off欄位的值,計算出實際的偏移量,並調用底層源的ReadAt方法進行讀取操作,在這個過程中,也保證了讀取數據範圍不會超過baselimit欄位指定的數據範圍。

這個方法提供了一種靈活的方式,能夠在限定的數據範圍內,隨意指定偏移量來讀取數據,不過需要註意的是,該方法並不會影響實例中off欄位的值。

4.3.3 Seek 方法說明

func (s *SectionReader) Seek(offset int64, whence int) (int64, error) {
   switch whence {
   default:
      return 0, errWhence
   case SeekStart:
      // s.off = s.base + offset
      offset += s.base
   case SeekCurrent:
      // s.off = s.off + offset
      offset += s.off
   case SeekEnd:
      // s.off = s.limit + offset
      offset += s.limit
   }
   // 檢查
   if offset < s.base {
      return 0, errOffset
   }
   s.off = offset
   return offset - s.base, nil
}

SectionReader也提供了Seek方法,給其提供了隨機訪問和靈活讀取數據的能力。舉個例子,假如已經調用Read方法讀取了一部分數據,但是想要重新讀取該數據,此時便可以使Seek方法將off欄位設置回之前的位置,然後再次調用Read方法進行讀取。

五. 使用註意事項

5.1 註意off值在base和limit之間

當使用 SectionReader 創建實例時,確保 off 值在 baselimit 之間是至關重要的。保證 off 值在 baselimit 之間的好處是確保讀取操作在有效的數據範圍內進行,避免讀取錯誤或超出範圍的訪問。如果 off 值小於 base 或大於等於 limit,讀取操作可能會導致錯誤或返回 EOF。

一個良好的實踐方式是使用 NewSectionReader 函數來創建 SectionReader 實例。NewSectionReader 函數會檢查 off 值是否在有效範圍內,並自動調整 off 值,以確保它在 baselimit 之間。

5.2 及時關閉底層數據源

當使用SectionReader時,如果沒有及時關閉底層數據源可能會導致資源泄露,這些資源在程式執行期間將一直保持打開狀態,直到程式終止。在處理大量請求或長時間運行的情況下,可能會耗盡系統的資源。

下麵是一個示例,展示了沒有關閉SectionReader底層數據源可能引發的問題:

func main() {
    file, err := os.Open("data.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    section := io.NewSectionReader(file, 10, 20)

    buffer := make([]byte, 10)
    _, err = section.Read(buffer)
    if err != nil {
        log.Fatal(err)
    }

    // 沒有關閉底層數據源,可能導致資源泄露或其他問題
}

在上述示例中,底層數據源是一個文件。在程式結束時,沒有顯式調用file.Close()來關閉文件句柄,這將導致文件資源一直保持打開狀態,直到程式終止。這可能導致其他進程無法訪問該文件或其他與文件相關的問題。

因此,在使用SectionReader時,要註意及時關閉底層數據源,以確保資源的正確管理和避免潛在的問題。

六. 總結

本文主要對SectionReader進行了介紹。文章首先從一個基本HTTP文件伺服器的功能實現出發,解釋了該實現存在記憶體資源浪費,併發性能低等問題,從而引出了SectionReader

接下來介紹了SectionReader的基本定義,以及其基本使用方法,最後使用SectionReader對上述HTTP文件伺服器進行優化。接著還詳細講述了SectionReader的實現原理,從而能夠更好得理解和使用SectionReader

最後,講解了SectionReader的使用註意事項,如需要及時關閉底層數據源等。基於此完成了SectionReader的介紹。


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

-Advertisement-
Play Games
更多相關文章
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...