一. 簡介
本文將介紹 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.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
的介紹,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/556496.html
標籤:其他
上一篇:初識識別符號
下一篇:返回列表