鍵.png)
使用這些基本 REST API 最佳實踐構(gòu)建出色的 API
cd golang-react-music-streaming
make setup
完成項目設(shè)置需要克隆所需的分支并安裝所有包和依賴項。
一旦項目設(shè)置完畢,我們就可以著手討論架構(gòu)決策及其增強了。
下圖表示項目當(dāng)前狀態(tài)下的體系結(jié)構(gòu)。
此體系結(jié)構(gòu)是一個簡單的整體,它將所有后端功能(例如緩存、歌曲管理和數(shù)據(jù)庫連接)整合到單個服務(wù)器或域中。為了確保服務(wù)器的帶寬不被異常使用,我們將文件服務(wù)委托給了外部存儲,這在架構(gòu)中體現(xiàn)為將存儲組件置于服務(wù)器域之外。
面對這種新構(gòu)建的可擴展架構(gòu),我們遇到了一個挑戰(zhàn):用戶能夠獲取直接下載鏈接,可能會繞過流媒體服務(wù),從而違背了通過我們的應(yīng)用程序進行內(nèi)容流式傳輸?shù)某踔浴?/p>
為了優(yōu)化系統(tǒng),我們將通過整合流服務(wù)組件來重新規(guī)劃架構(gòu)。
在更新后的架構(gòu)中,我們在服務(wù)器上新增了一個名為Streaming Service的組件。此組件負責(zé)與數(shù)據(jù)庫交互,以檢索歌曲的相關(guān)信息。值得注意的是,存儲組件不再直接與客戶端進行通信,而是改為與Streaming Service進行通信。
那么,Streaming Service是如何運作的呢?當(dāng)用戶發(fā)起歌曲請求時,API Gateway會將該請求重定向至Streaming Service。隨后,Streaming Service會查詢數(shù)據(jù)庫以獲取歌曲信息,并利用存儲文件的URL進行下載。下載完成后,文件會被分割成多個字節(jié)塊,并通過HTTP范圍請求向客戶端進行緩沖傳輸。
盡管惡意用戶可能會嘗試尋找下載歌曲的途徑,但我們可以通過加密緩沖的塊來增強安全性,加密密鑰由客戶端和服務(wù)器共同持有。不過,這一加密選項我們將在后文中詳細討論。
我們計劃使用Golang來編寫Streaming Service。之所以選擇Golang,是因為我們打算構(gòu)建一個流式處理引擎,并充分利用其強大的并發(fā)處理能力,以應(yīng)對每分鐘可能需要處理的數(shù)千個請求。盡管Python也可用于構(gòu)建流式處理服務(wù),特別是在開發(fā)便捷性更為關(guān)鍵且性能要求相對寬松的場景下,但對于需要高效處理大量并發(fā)請求且延遲要求低的服務(wù)來說,Go通常是更優(yōu)的選擇。
現(xiàn)在,我們對架構(gòu)的變更有了更深入的理解,接下來,讓我們著手添加流式處理引擎。
Streaming Engine 將用 Golang 編寫,并將作為單獨的服務(wù)運行。以下是這項服務(wù)的要求:
/songs/listen/{id}
,其中?<id>
?代表用戶希望收聽的歌曲的 ID。該終結(jié)點將用于從存儲組件中檢索歌曲。在明確這些要求后,我們接下來討論 HTTP 范圍請求的相關(guān)知識。
HTTP 范圍請求是一種技術(shù),它使客戶端能夠請求資源的特定部分,這一功能在處理大文件(如視頻或音頻流)時尤為重要。流式處理服務(wù)普遍采用這一方法,以確保內(nèi)容能夠幾乎立即開始播放,而無需用戶等待整個文件的預(yù)先下載。
為了深入理解HTTP范圍請求的工作機制,讓我們逐步剖析這一過程。
當(dāng)客戶端請求資源時,服務(wù)器通常會使用整個文件進行響應(yīng)。但是,在處理大文件時,服務(wù)器可能會表明它支持范圍請求,從而允許客戶端分批下載文件。
* The client starts by sending a standard HTTP GET
request to retrieve the resource.
* If the file is large, the server might respond with the full resource or include an Accept-Ranges: bytes
header to indicate that range requests are supported.
接下來,客戶端可以使用范圍請求功能僅下載文件的必要部分。
* To request a specific portion of the resource, the client includes a Range
header in its request:
```json
Range: bytes=0-1023
```
* This header specifies the desired byte range, in this case, the first 1024 bytes.
收到此請求后,服務(wù)器將僅使用文件的請求部分進行響應(yīng)。
* The server responds with an HTTP 206 Partial Content
status, indicating that it is sending only a portion of the resource:
```json
Content-Range: bytes 0-1023/5000
```
客戶端收到此部分后,可以根據(jù)需要繼續(xù)請求文件的其他部分。
* If more data is needed, the client requests the next segment:
```json
Range: bytes=1024-2047
```
* The server then responds with the next chunk, continuing this process until the entire file is downloaded or the client has obtained all the necessary parts.
HTTP 范圍請求具有多種優(yōu)勢,使其在涉及大型文件的情況下特別有用。
通過允許客戶端僅下載他們需要的文件部分,HTTP 范圍請求提供了幾個關(guān)鍵優(yōu)勢。
為了說明此過程在實際場景中的工作原理,請考慮流式傳輸歌曲的示例。
當(dāng)用戶流式傳輸歌曲時,客戶端和服務(wù)器會通過一系列請求和響應(yīng)進行通信。
GET /audio.mp3 HTTP/1.1 Range: bytes=0-2047
HTTP/1.1 206 Partial Content Content-Range: bytes 0-2047/100000
GET /audio.mp3 HTTP/1.1 Range: bytes=2048-4095
HTTP/1.1 206 Partial Content Content-Range: bytes 2048-4095/100000
GET /audio.mp3 HTTP/1.1 Range: bytes=8192-10239
HTTP/1.1 206 Partial Content Content-Range: bytes 8192-10239/100000
此示例凸顯了 HTTP 范圍請求如何助力實現(xiàn)高效且用戶友好的流式傳輸,通過允許立即播放和更順暢的文件傳輸,從而提供更流暢的用戶體驗。
解釋了 HTTP Range 請求后,讓我們用 Golang 編寫實現(xiàn)。我們將構(gòu)建一個 API 來為終端節(jié)點提供服務(wù),然后編寫一個函數(shù)來處理通過 HTTP Range Requests 進行的流式處理。
在本節(jié)中,我們將使用 Golang 構(gòu)建流式處理服務(wù)。在項目的根目錄中,創(chuàng)建一個名為 的新文件夾。此目錄將包含用 Golang 編寫的流式處理后端。
mkdir streaming-engine
cd streaming-engine
然后,在此目錄中,運行以下行以創(chuàng)建 Golang 項目。
go mod init streaming-engine
然后安裝所需的依賴項,例如 Mux、Sqlite3 驅(qū)動程序和 gorm 以與數(shù)據(jù)庫交互。
go get github.com/gorilla/mux
go get gorm.io/driver/sqlite
go get gorm.io/gorm
安裝完成后,創(chuàng)建一個名為?main.go
?的文件(我們將在這個文件中放置后端邏輯內(nèi)容)。
現(xiàn)在項目已經(jīng)設(shè)置完成,我們可以著手為流式處理引擎編寫代碼了。首先,我們從必要的導(dǎo)入和基本結(jié)構(gòu)體定義開始。
package main
import (
"fmt"
"io"
"log"
"net/http"
"strconv"
"strings"
"github.com/gorilla/mux"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
var db *gorm.DB
var err error
// Song represents the song model in the existing music_song table
type Song struct {
ID uint gorm:"column:id"
Name string gorm:"column:name"
File string gorm:"column:file"
Author string gorm:"column:author"
Thumbnail string gorm:"column:thumbnail"
}
// TableName overrides the table name used by Gorm
func (Song) TableName() string {
return "music_song"
}
在上面的代碼中,我們導(dǎo)入了所需的包,以幫助編寫流處理程序函數(shù)和設(shè)置 API。我們還定義了變量,例如用于數(shù)據(jù)庫初始化和跟蹤整個應(yīng)用程序中的錯誤。該結(jié)構(gòu)體被定義為表示表中的歌曲模型。我們覆蓋了 Gorm 使用的默認(rèn)表名,以確保它正確映射到現(xiàn)有的數(shù)據(jù)庫表。
接下來,我們繼續(xù)編寫用于數(shù)據(jù)庫初始化的函數(shù):
func initDB() {
// Initialize SQLite connection
db, err = gorm.Open(sqlite.Open("../backend/db.sqlite3"), &gorm.Config{})
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
}
在之前的步驟中,我們定義了使用特定包的函數(shù)來初始化數(shù)據(jù)庫連接。請確保 SQLite 數(shù)據(jù)庫文件的路徑與您的項目結(jié)構(gòu)相匹配,如有需要,請進行相應(yīng)調(diào)整。接下來,我們將編寫一個名為?initDBgorm
?的函數(shù)來打開數(shù)據(jù)庫。
繼續(xù)進行,我們還將編寫一個將在流處理程序函數(shù)中使用的函數(shù)。
func getSongID(r *http.Request) (int, error) {
params := mux.Vars(r)
id, err := strconv.Atoi(params["id"])
return id, err
}
在上面的代碼中,我們使用 從 URL 參數(shù)中提取歌曲 ID。該函數(shù)將 ID 從字符串轉(zhuǎn)換為整數(shù),并返回 ID 以及遇到的任何錯誤。
func getSongFromDB(id int) (Song, error) {
var song Song
err := db.First(&song, id).Error
return song, err
}
該函數(shù)查詢數(shù)據(jù)庫以檢索給定 ID 的歌曲詳細信息。它返回歌曲數(shù)據(jù)以及查詢期間出現(xiàn)的任何錯誤。
func fetchFile(fileURL string) (*http.Response, error) {
fullURL := "http://localhost:8000/media/" + fileURL
resp, err := http.Get(fullURL)
if err != nil || resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("file not found on the server")
}
return resp, nil
}
在上面的代碼中,通過將 附加到基 URL 來構(gòu)建媒體文件的完整 URL,并執(zhí)行 HTTP GET 請求以檢索文件。如果未找到文件或出現(xiàn)問題,它將返回響應(yīng)或錯誤。
func parseRangeHeader(rangeHeader string, fileSize int64) (int64, int64, error) {
bytesRange := strings.Split(strings.TrimPrefix(rangeHeader, "bytes="), "-")
start, err := strconv.ParseInt(bytesRange[0], 10, 64)
if err != nil {
return 0, 0, err
}
var end int64
if len(bytesRange) > 1 && bytesRange[1] != "" {
end, err = strconv.ParseInt(bytesRange[1], 10, 64)
if err != nil {
return 0, 0, err
}
} else {
end = fileSize - 1
}
if start > end || end >= fileSize {
return 0, 0, fmt.Errorf("invalid range")
}
return start, end, nil
}
在上面的代碼中,我們解析HTTP請求的頭,以確定客戶端想要接收的文件的開始和結(jié)束字節(jié)。我們處理范圍規(guī)范中的任何錯誤,并返回開始和結(jié)束字節(jié)位置。
func writePartialContent(w http.ResponseWriter, start, end, fileSize int64, resp *http.Response) error {
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, fileSize))
w.Header().Set("Accept-Ranges", "bytes")
w.Header().Set("Content-Length", strconv.FormatInt(end-start+1, 10))
w.Header().Set("Content-Type", "audio/mpeg")
w.WriteHeader(http.StatusPartialContent)
// Create a channel for the buffered data and a wait group for synchronization
dataChan := make(chan []byte)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
buffer := make([]byte, 1024) // 1KB buffer size
bytesToRead := end - start + 1
for bytesToRead > 0 {
n, err := resp.Body.Read(buffer)
if err != nil && err != io.EOF {
http.Error(w, "Error reading file", http.StatusInternalServerError)
return
}
if n == 0 {
break
}
if int64(n) > bytesToRead {
n = int(bytesToRead)
}
dataChan <- buffer[:n]
bytesToRead -= int64(n)
}
close(dataChan)
}()
go func() {
defer wg.Wait()
for chunk := range dataChan {
if _, err := w.Write(chunk); err != nil {
http.Error(w, "Error writing response", http.StatusInternalServerError)
return
}
}
}()
// Skip the bytes until the start position
io.CopyN(io.Discard, resp.Body, start)
return nil
}
在上面的代碼中,我們設(shè)置了必要的HTTP頭部以傳遞請求所需的信息,并處理了指定字節(jié)范圍的并發(fā)讀取與寫入操作。為了確保數(shù)據(jù)流的高效性,我們運用了goroutines來同步進行數(shù)據(jù)的緩沖與寫入。若在此過程中遭遇任何錯誤,這些錯誤將被捕獲并以HTTP錯誤的形式返回。此外,我們還實現(xiàn)了writePartialContent
函數(shù)來處理部分內(nèi)容的寫入。
現(xiàn)在,我們可以在流處理程序函數(shù)中使用這些函數(shù),并創(chuàng)建 API 服務(wù)器來為流式處理終結(jié)點提供服務(wù)。
// Handles streaming of the file via HTTP range requests
func streamHandler(w http.ResponseWriter, r *http.Request) {
id, err := getSongID(r)
if err != nil {
http.Error(w, "Invalid song ID", http.StatusBadRequest)
return
}
song, err := getSongFromDB(id)
if err != nil {
http.Error(w, "Song not found", http.StatusNotFound)
return
}
resp, err := fetchFile(song.File)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
defer resp.Body.Close()
fileSize := resp.ContentLength
rangeHeader := r.Header.Get("Range")
if rangeHeader == "" {
http.ServeFile(w, r, song.File)
return
}
start, end, err := parseRangeHeader(rangeHeader, fileSize)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := writePartialContent(w, start, end, fileSize, resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func main() {
initDB()
r := mux.NewRouter()
r.HandleFunc("/songs/listen/{id}", streamHandler).Methods("GET")
log.Println("Server is running on port 8005")
log.Fatal(http.ListenAndServe(":8005", r))
}
streamHandler
函數(shù)負責(zé)處理流文件的HTTP范圍請求。它首先通過提取歌曲ID來工作,接著從數(shù)據(jù)庫中檢索該歌曲的詳細信息,并從指定的URL獲取文件。之后,該函數(shù)會解析HTTP請求中的范圍標(biāo)頭(如果存在),以確定需要流式傳輸文件的哪一部分。一旦確定了要傳輸?shù)牟糠郑摵瘮?shù)就會將這部分內(nèi)容寫入HTTP響應(yīng)中。
在?main
?函數(shù)中,我們對數(shù)據(jù)庫進行了初始化設(shè)置,并配置了一個HTTP路由器來處理流媒體歌曲的請求。最后,我們在8005端口上啟動了服務(wù)器。要在流引擎目錄中啟動服務(wù)器,運行以下命令以啟動服務(wù)器。
go run .
我們現(xiàn)已編寫完成Golang服務(wù),允許客戶端通過 /songs/listen/{id}
路徑(其中 {id}
是歌曲的 ID)來流式傳輸歌曲。
現(xiàn)在服務(wù)已經(jīng)編寫完畢,我們必須對 Django 后端和前端進行一些調(diào)整。
在Django后端,為了控制通過API暴露的數(shù)據(jù),我們對API響應(yīng)進行了更新,排除了某些字段。具體來說,我們更新了 fileSongSerializer
配置,故意省略了某個字段,以確保其不會暴露給客戶端。
# music/serializers.py
class SongSerializer(serializers.ModelSerializer):
class Meta:
model = Song
fields = ['id', 'name', 'artist', 'duration', 'thumbnail']
前端方面,我們調(diào)整了邏輯以利用新創(chuàng)建的流式處理終結(jié)點。該前端包能夠自動處理流,因此無需手動管理。我們使用了 react-h5-audio-player
組件。
此外,我們還更新了 playSongapp.js
中的函數(shù),以正確設(shè)置歌曲的URL。
// app.js
...
const playSong = (song) => {
setCurrentSong(http://localhost:8005/songs/listen/${song.id}
);
};
...
通過此更新,前端使用更新的 API 自動流式傳輸音頻,為用戶提供無縫體驗。
在編寫完應(yīng)用程序的最終版本后,我們來探討一些可能的增強措施。
在構(gòu)建此應(yīng)用程序及規(guī)劃其架構(gòu)時,我們已確保架構(gòu)能夠支持大量請求,從而實現(xiàn)可擴展性和可靠性。鑒于我們選擇了較為簡潔的編碼方案,因此有必要說明在架構(gòu)和編碼方面可以進行的一些增強。
當(dāng)前架構(gòu)中,流式處理服務(wù)組件位于服務(wù)器上。盡管流式傳輸是通過HTTP范圍請求實現(xiàn)的,但我們也必須考慮帶寬的使用情況。以下是對架構(gòu)可能的增強措施:
下面是這些更改后的體系結(jié)構(gòu)的新關(guān)系圖。
現(xiàn)在我們有了一個更好的架構(gòu)建議,讓我們來討論編碼增強。
許多流媒體服務(wù)通過使用加密來保護傳輸期間的數(shù)據(jù),這可以保護內(nèi)容免受未經(jīng)授權(quán)的訪問和篡改。該過程通常包括兩個主要步驟:
這是一個可以考慮添加到流媒體引擎中的有趣步驟,以確保流媒體傳輸?shù)陌踩院涂煽啃浴?/p>
在本文中,我們利用Golang和HTTP范圍請求開發(fā)了一個流媒體服務(wù),并深入探討了關(guān)鍵的架構(gòu)改進方案,旨在提升應(yīng)用程序的安全性和效率。
本系列的這一部分內(nèi)容至此告一段落,但請您持續(xù)關(guān)注我們的下一期。在接下來的一期中,我們將把這里所提及的所有概念融入一個全球性的架構(gòu)體系中。我們將詳細闡述如何構(gòu)建一個能夠向全球用戶提供服務(wù)的流媒體平臺,同時確保其高性能表現(xiàn)。
如果您喜歡本文,請考慮訂閱我們的時事通訊,以便您不會錯過后續(xù)的更新內(nèi)容。
您的反饋對我們來說至關(guān)重要!如果您有任何建議、批評或問題,請在下方留言。
讓我們共同期待更多精彩內(nèi)容的呈現(xiàn)!??