cd golang-react-music-streaming
make setup

完成項目設置需要克隆所需的分支并安裝所有包和依賴項。

一旦項目設置完畢,我們就可以著手討論架構決策及其增強了。

建筑

下圖表示項目當前狀態下的體系結構。

此體系結構是一個簡單的整體,它將所有后端功能(例如緩存、歌曲管理和數據庫連接)整合到單個服務器或域中。為了確保服務器的帶寬不被異常使用,我們將文件服務委托給了外部存儲,這在架構中體現為將存儲組件置于服務器域之外。

面對這種新構建的可擴展架構,我們遇到了一個挑戰:用戶能夠獲取直接下載鏈接,可能會繞過流媒體服務,從而違背了通過我們的應用程序進行內容流式傳輸的初衷。

為了優化系統,我們將通過整合流服務組件來重新規劃架構。

在更新后的架構中,我們在服務器上新增了一個名為Streaming Service的組件。此組件負責與數據庫交互,以檢索歌曲的相關信息。值得注意的是,存儲組件不再直接與客戶端進行通信,而是改為與Streaming Service進行通信。

那么,Streaming Service是如何運作的呢?當用戶發起歌曲請求時,API Gateway會將該請求重定向至Streaming Service。隨后,Streaming Service會查詢數據庫以獲取歌曲信息,并利用存儲文件的URL進行下載。下載完成后,文件會被分割成多個字節塊,并通過HTTP范圍請求向客戶端進行緩沖傳輸。

盡管惡意用戶可能會嘗試尋找下載歌曲的途徑,但我們可以通過加密緩沖的塊來增強安全性,加密密鑰由客戶端和服務器共同持有。不過,這一加密選項我們將在后文中詳細討論。

我們計劃使用Golang來編寫Streaming Service。之所以選擇Golang,是因為我們打算構建一個流式處理引擎,并充分利用其強大的并發處理能力,以應對每分鐘可能需要處理的數千個請求。盡管Python也可用于構建流式處理服務,特別是在開發便捷性更為關鍵且性能要求相對寬松的場景下,但對于需要高效處理大量并發請求且延遲要求低的服務來說,Go通常是更優的選擇。

現在,我們對架構的變更有了更深入的理解,接下來,讓我們著手添加流式處理引擎。

添加 Golang 流引擎

Streaming Engine 將用 Golang 編寫,并將作為單獨的服務運行。以下是這項服務的要求:

在明確這些要求后,我們接下來討論 HTTP 范圍請求的相關知識。

解釋 HTTP 范圍請求

HTTP 范圍請求是一種技術,它使客戶端能夠請求資源的特定部分,這一功能在處理大文件(如視頻或音頻流)時尤為重要。流式處理服務普遍采用這一方法,以確保內容能夠幾乎立即開始播放,而無需用戶等待整個文件的預先下載。

為了深入理解HTTP范圍請求的工作機制,讓我們逐步剖析這一過程。

HTTP 范圍請求的工作原理

當客戶端請求資源時,服務器通常會使用整個文件進行響應。但是,在處理大文件時,服務器可能會表明它支持范圍請求,從而允許客戶端分批下載文件。

  1. 初始請求
* 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.

接下來,客戶端可以使用范圍請求功能僅下載文件的必要部分。

  1. 客戶端發送范圍請求
* 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.

收到此請求后,服務器將僅使用文件的請求部分進行響應。

  1. 服務器使用部分內容進行響應
* 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
    ```

客戶端收到此部分后,可以根據需要繼續請求文件的其他部分。

  1. 后續請求
* 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 范圍請求具有多種優勢,使其在涉及大型文件的情況下特別有用。

HTTP 范圍請求的好處

通過允許客戶端僅下載他們需要的文件部分,HTTP 范圍請求提供了幾個關鍵優勢。

為了說明此過程在實際場景中的工作原理,請考慮流式傳輸歌曲的示例。

示例流:流式傳輸歌曲

當用戶流式傳輸歌曲時,客戶端和服務器會通過一系列請求和響應進行通信。

  1. 客戶端請求音頻啟動:客戶端首先請求音頻文件的第一個塊:GET /audio.mp3 HTTP/1.1 Range: bytes=0-2047
  2. 服務器以部分內容響應:服務器發送文件的前 2048 個字節:HTTP/1.1 206 Partial Content Content-Range: bytes 0-2047/100000
  3. 客戶端請求下一個片段:當音頻播放時,客戶端請求下一個片段:GET /audio.mp3 HTTP/1.1 Range: bytes=2048-4095
  4. Server Responds with Next Chunk(服務器使用 Next Chunk 響應):服務器發送文件的下一部分:HTTP/1.1 206 Partial Content Content-Range: bytes 2048-4095/100000
  5. Client Seek to another part(客戶端查找另一部分):如果用戶向前跳,則 Client 請求文件的不同部分:GET /audio.mp3 HTTP/1.1 Range: bytes=8192-10239
  6. Server Responds with the New Range(服務器使用新范圍響應):服務器使用請求的部分進行響應:HTTP/1.1 206 Partial Content Content-Range: bytes 8192-10239/100000

此示例凸顯了 HTTP 范圍請求如何助力實現高效且用戶友好的流式傳輸,通過允許立即播放和更順暢的文件傳輸,從而提供更流暢的用戶體驗。

解釋了 HTTP Range 請求后,讓我們用 Golang 編寫實現。我們將構建一個 API 來為終端節點提供服務,然后編寫一個函數來處理通過 HTTP Range Requests 進行的流式處理。

使用 Golang 構建流式處理服務

在本節中,我們將使用 Golang 構建流式處理服務。在項目的根目錄中,創建一個名為 的新文件夾。此目錄將包含用 Golang 編寫的流式處理后端。

mkdir streaming-engine
cd streaming-engine

然后,在此目錄中,運行以下行以創建 Golang 項目。

go mod init streaming-engine

然后安裝所需的依賴項,例如 Mux、Sqlite3 驅動程序和 gorm 以與數據庫交互。

go get github.com/gorilla/mux
go get gorm.io/driver/sqlite
go get gorm.io/gorm

安裝完成后,創建一個名為?main.go?的文件(我們將在這個文件中放置后端邏輯內容)。

編寫流式處理引擎后端邏輯

現在項目已經設置完成,我們可以著手為流式處理引擎編寫代碼了。首先,我們從必要的導入和基本結構體定義開始。

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" }

在上面的代碼中,我們導入了所需的包,以幫助編寫流處理程序函數和設置 API。我們還定義了變量,例如用于數據庫初始化和跟蹤整個應用程序中的錯誤。該結構體被定義為表示表中的歌曲模型。我們覆蓋了 Gorm 使用的默認表名,以確保它正確映射到現有的數據庫表。

接下來,我們繼續編寫用于數據庫初始化的函數:

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)
}
}

在之前的步驟中,我們定義了使用特定包的函數來初始化數據庫連接。請確保 SQLite 數據庫文件的路徑與您的項目結構相匹配,如有需要,請進行相應調整。接下來,我們將編寫一個名為?initDBgorm?的函數來打開數據庫。

繼續進行,我們還將編寫一個將在流處理程序函數中使用的函數。

func getSongID(r *http.Request) (int, error) {
params := mux.Vars(r)
id, err := strconv.Atoi(params["id"])
return id, err
}

在上面的代碼中,我們使用 從 URL 參數中提取歌曲 ID。該函數將 ID 從字符串轉換為整數,并返回 ID 以及遇到的任何錯誤。

func getSongFromDB(id int) (Song, error) {
var song Song
err := db.First(&song, id).Error
return song, err
}

該函數查詢數據庫以檢索給定 ID 的歌曲詳細信息。它返回歌曲數據以及查詢期間出現的任何錯誤。

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 來構建媒體文件的完整 URL,并執行 HTTP GET 請求以檢索文件。如果未找到文件或出現問題,它將返回響應或錯誤。

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請求的頭,以確定客戶端想要接收的文件的開始和結束字節。我們處理范圍規范中的任何錯誤,并返回開始和結束字節位置。

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
}

在上面的代碼中,我們設置了必要的HTTP頭部以傳遞請求所需的信息,并處理了指定字節范圍的并發讀取與寫入操作。為了確保數據流的高效性,我們運用了goroutines來同步進行數據的緩沖與寫入。若在此過程中遭遇任何錯誤,這些錯誤將被捕獲并以HTTP錯誤的形式返回。此外,我們還實現了writePartialContent函數來處理部分內容的寫入。

現在,我們可以在流處理程序函數中使用這些函數,并創建 API 服務器來為流式處理終結點提供服務。

// 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 函數負責處理流文件的HTTP范圍請求。它首先通過提取歌曲ID來工作,接著從數據庫中檢索該歌曲的詳細信息,并從指定的URL獲取文件。之后,該函數會解析HTTP請求中的范圍標頭(如果存在),以確定需要流式傳輸文件的哪一部分。一旦確定了要傳輸的部分,該函數就會將這部分內容寫入HTTP響應中。

在?main?函數中,我們對數據庫進行了初始化設置,并配置了一個HTTP路由器來處理流媒體歌曲的請求。最后,我們在8005端口上啟動了服務器。要在流引擎目錄中啟動服務器,運行以下命令以啟動服務器。

go run .

我們現已編寫完成Golang服務,允許客戶端通過 /songs/listen/{id} 路徑(其中 {id} 是歌曲的 ID)來流式傳輸歌曲。

現在服務已經編寫完畢,我們必須對 Django 后端和前端進行一些調整。

修改 Backend 和 Frontend 以使用 Streaming 服務

后端修改:限制公開的字段

在Django后端,為了控制通過API暴露的數據,我們對API響應進行了更新,排除了某些字段。具體來說,我們更新了 fileSongSerializer 配置,故意省略了某個字段,以確保其不會暴露給客戶端。

# music/serializers.py

class SongSerializer(serializers.ModelSerializer):
class Meta:
model = Song
fields = ['id', 'name', 'artist', 'duration', 'thumbnail']

前端調整:利用流式處理終結點

前端方面,我們調整了邏輯以利用新創建的流式處理終結點。該前端包能夠自動處理流,因此無需手動管理。我們使用了 react-h5-audio-player 組件。

此外,我們還更新了 playSongapp.js 中的函數,以正確設置歌曲的URL。

// app.js
...
const playSong = (song) => {
setCurrentSong(http://localhost:8005/songs/listen/${song.id}); }; ...

通過此更新,前端使用更新的 API 自動流式傳輸音頻,為用戶提供無縫體驗。

在編寫完應用程序的最終版本后,我們來探討一些可能的增強措施。

增強

在構建此應用程序及規劃其架構時,我們已確保架構能夠支持大量請求,從而實現可擴展性和可靠性。鑒于我們選擇了較為簡潔的編碼方案,因此有必要說明在架構和編碼方面可以進行的一些增強。

架構增強功能

當前架構中,流式處理服務組件位于服務器上。盡管流式傳輸是通過HTTP范圍請求實現的,但我們也必須考慮帶寬的使用情況。以下是對架構可能的增強措施:

下面是這些更改后的體系結構的新關系圖。

現在我們有了一個更好的架構建議,讓我們來討論編碼增強。

編碼增強

許多流媒體服務通過使用加密來保護傳輸期間的數據,這可以保護內容免受未經授權的訪問和篡改。該過程通常包括兩個主要步驟:

加密流的示例

這是一個可以考慮添加到流媒體引擎中的有趣步驟,以確保流媒體傳輸的安全性和可靠性。

結論

在本文中,我們利用Golang和HTTP范圍請求開發了一個流媒體服務,并深入探討了關鍵的架構改進方案,旨在提升應用程序的安全性和效率。

本系列的這一部分內容至此告一段落,但請您持續關注我們的下一期。在接下來的一期中,我們將把這里所提及的所有概念融入一個全球性的架構體系中。我們將詳細闡述如何構建一個能夠向全球用戶提供服務的流媒體平臺,同時確保其高性能表現。

如果您喜歡本文,請考慮訂閱我們的時事通訊,以便您不會錯過后續的更新內容。

您的反饋對我們來說至關重要!如果您有任何建議、批評或問題,請在下方留言。

讓我們共同期待更多精彩內容的呈現!??

原文鏈接:https://dev.to/koladev/building-a-music-streaming-service-with-python-golang-and-react-from-system-design-to-coding-part-3-52pm

上一篇:

Golang Echo教程:PostgreSQL的REST API(通過)

下一篇:

使用 Auth0 向 Sinatra API 添加授權
#你可能也喜歡這些API文章!

我們有何不同?

API服務商零注冊

多API并行試用

數據驅動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內容創意新穎性、情感共鳴力、商業轉化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費