提升性 能的利器:深 入解 析SectionReader
一. 簡介
本文將介紹 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 mainimport ( "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,WRead 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.CopyN
將
SectionReader
中的資料直接複製到響應的
http.ResponseWriter
中。
上述兩個HTTP檔案伺服器實現的區別,只在於讀取特定範圍資料方式,前一種方式是將整個檔案載入到記憶體中,再擷取特定範圍的資料;而後者則是透過使用
SectionReader
,我們避免了一次性讀取整個檔案資料,並且只讀取請求範圍內的資料。這種最佳化能夠更高效地處理大檔案或處理大量併發請求的場景,節省了記憶體和處理時間。
四. 實現原理
4.1 設計初衷
SectionReader
的設計初衷,在於提供一種簡潔,靈活的方式來讀取資料來源的特定部分。
4.2 基本原理
SectionReader
結構體中
off
,
base
,
limit
欄位是實現只讀取資料來源特定部分資料功能的重要變數。
type SectionReader struct { r ReaderAt base int64 off int64 limit int64}
由於
SectionReader
需要保證只讀取特定範圍的資料,故需要儲存開始位置和結束位置的值。這裡是透過
base
和
limit
這兩個欄位來實現的,
base
記錄了資料讀取的開始位置,
limit
記錄了資料讀取的結束位置。
透過設定
base
和
limit
兩個欄位的值,限制了能夠被讀取資料的範圍。之後需要開始讀取資料,有可能這部分待讀取的資料不會被一次性讀完,此時便需要一個欄位來說明接下來要從哪一個位元組繼續讀取下去,因此
SectionReader
也設定了
off
欄位的值,這個代表著下一個帶讀取資料的位置。
在使用
SectionReader
讀取資料的過程中,透過
base
和
limit
限制了讀取資料的範圍,
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
方法進行讀取操作,在這個過程中,也保證了讀取資料範圍不會超過
base
和
limit
欄位指定的資料範圍。
這個方法提供了一種靈活的方式,能夠在限定的資料範圍內,隨意指定偏移量來讀取資料,不過需要注意的是,該方法並不會影響例項中
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
值在
base
和
limit
之間是至關重要的。保證
off
值在
base
和
limit
之間的好處是確保讀取操作在有效的資料範圍內進行,避免讀取錯誤或超出範圍的訪問。如果
off
值小於
base
或大於等於
limit
,讀取操作可能會導致錯誤或返回 EOF。
一個良好的實踐方式是使用
NewSectionReader
函式來建立
SectionReader
例項。
NewSectionReader
函式會檢查 off 值是否在有效範圍內,並自動調整
off
值,以確保它在
base
和
limit
之間。
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
的介紹。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70034822/viewspace-2992119/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 提升效率的利器——Rocket Typist Pro 文字快速輸入工具
- 證件識別介面-提升業務效率與準確性的利器
- Sensei for Mac:提升Mac效能的終極利器Mac
- Java 註解 (Annotation)淺入深出Java
- 提升專案執行效率的關鍵利器
- 好用的API彙總:提升開發效率的利器API
- 前端開發 Mock 利器,效率提升 100%!前端Mock
- Kotlin 效能優化利器 —— Sqeuence 原理淺析Kotlin優化
- 提升工作效率的視窗管理利器——Rectangle for MacMac
- JProfiler for Mac/win- 提升Java應用程式的效能利器!MacJava
- 機器學習深版04:提升機器學習
- 深析Synchronized關鍵字(小白慎入,深入jvm原始碼,兩萬字長文)synchronizedJVM原始碼
- 社群文章|MOSN 社群效能分析利器——Holmes 原理淺析
- 從淺入深瞭解Koa2原始碼原始碼
- 《何以入深論》
- 淺析賦值、淺拷貝、深拷貝的區別賦值
- CRM系統能提升公司的哪些效率?
- iMovie 入門教程:影片編輯的利器
- 淺入深出的微前端MicroApp前端APP
- promise由淺入深Promise
- java多型性淺析Java多型
- 深層屬性,輕鬆提取
- 線性模型是否真的能給出一個很好的解釋?模型
- 死磕生菜 -- lettuce 間歇性發生 RedisCommandTimeoutException 的深層原理及解決方案RedisException
- iMovie 入門教程:視訊編輯的利器
- 【由淺入深學MySQL】- MySQL連線查詢詳解MySql
- Flutter 入門 — Container 屬性詳解FlutterAI
- DevSecOps 提升安全性的五種方式dev
- 淺入深出Vue:路由Vue路由
- 淺入深出Vue:元件Vue元件
- MySQL索引由淺入深MySql索引
- 遊戲,墜入深淵遊戲
- JavaScript Promise由淺入深JavaScriptPromise
- 由淺入深講解責任鏈模式,理解Tomcat的Filter過濾器模式TomcatFilter過濾器
- Vue入門淺析Vue
- 一入前端深似海,從此紅塵是路人系列第三彈之淺析JavaScript閉包前端JavaScript
- 入坑 docsify,一款神奇的文件生成利器!
- [轉帖]由淺入深瞭解GC入門篇(一):什麼是垃圾回收?GC