一、視頻服務搭建
1.1 準備工作
首先,新建streamserver目錄,然後定義main.go文件。
package streamserver
import (
"github.com/julienschmidt/httprouter"
"net/http"
)
func main() {
router := RegisterHandler()
newRouter := NewMiddleWareHandler(router)
http.ListenAndServe(":9000", newRouter)
}
func RegisterHandler() *httprouter.Router {
router := httprouter.New()
//router.POST("/", RegistHandler)
return router
}
func RegistHandler(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
}
// 中間件方法,該方法對http.Router進行增強處理
// 因http.Router實現http.Handler接口,因此,該方法也返回一個實現http.Handler接口的對象
func NewMiddleWareHandler(r *httprouter.Router) http.Handler {
// 創建中間件Handler對象
m := &MiddleWareHandler{}
// 把router對象傳入到中間件裏面
m.router = r
// 返回增強處理後的Handler
return m
}
// 定義中間件結構體,該結構體實現http.Handler接口
type MiddleWareHandler struct {
router *httprouter.Router
}
// 實現http.Handler接口的ServeHTTP方法
func (m MiddleWareHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// TODO 顯示同時觀看視頻的用戶數
// 保留原有處理請求的功能
m.router.ServeHTTP(w, r)
}
2.2 視頻中間件
視頻中間件用於控制播放視頻的連接數。這裏我們通過管道對用戶連接數進行控制。
第一步:在streamserver目錄下新建limiter.go文件,然後定義一個結構體,該結構體記錄了最大連接數和當前連接數。並且當前連接數使用管道進行控制。
type ConnLimiter struct {
maxConn int // 最大連接數
bucket chan int // 使用管道控制當前連接數
}
第二步:定義一個方法,該方法返回ConnLimiter對象。
func NewConnLimiter(maxSize int) *ConnLimiter {
return &ConnLimiter{
maxSize,
make(chan int, maxSize),
}
}
第三步:修改中間件結構體,增加ConnLimiter屬性。
type MiddleWareHandler struct {
router *httprouter.Router
connLimiter *ConnLimiter // 連接數限制
}
第四步:NewMiddleWareHandler方法增加一個參數,用於記錄最大連接數。
func main() {
router := RegisterHandler()
newRouter := NewMiddleWareHandler(router, 2)
http.ListenAndServe(":9000", newRouter)
}
func NewMiddleWareHandler(r *httprouter.Router, maxsize int) http.Handler {
// 創建中間件Handler對象
m := &MiddleWareHandler{}
// 把router對象傳入到中間件裏面
m.router = r
// 初始化connLimiter
m.connLimiter = NewConnLimiter(maxsize)
// 返回增強處理後的Handler
return m
}
第五步:修改main方法,創建NewMiddleWareHandler方法時候指定最大連接數。
func main() {
router := RegisterHandler()
newRouter := NewMiddleWareHandler(router, 2)
http.ListenAndServe(":9000", newRouter)
}
第六步:在ServeHTTP方法中實現訪問用戶數的控制。
func (m MiddleWareHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 判斷當前連接數是否超過最大連接數的限制,如果沒有超過,則正常訪問;否則返回失敗原因
if !m.connLimiter.GetConn() {
sendErrorResponse(w, http.StatusTooManyRequests, "已超過最大連接數!")
return
}
// 保留原有處理請求的功能
m.router.ServeHTTP(w, r)
// 釋放連接
defer m.connLimiter.ReleaseConn()
}
第七步:新建response.go文件,定義sendErrorResponse方法,該方法用於向客戶端輸出錯誤信息。
package main
import (
"io"
"net/http"
)
// 發送錯誤響應
func sendErrorResponse(w http.ResponseWriter, sc int, errMsg string) {
// 輸出響應碼
w.WriteHeader(sc)
// 向客戶端輸出錯誤消息
io.WriteString(w, errMsg)
}
二、播放視頻
2.1 準備工作
在項目根路徑下新建videos文件夾,該文件夾存放要播放的視頻文件。
2.2 功能實現
第一步:註冊路由;
func RegisterHandler() *httprouter.Router {
router := httprouter.New()
router.GET("/videos/:vid-id", StreamHandler)
return router
}
第二步:實現請求處理的方法;
func StreamHandler(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
// 獲取請求參數
vid := p.ByName("vid-id")
// 獲取視頻文件路徑
videoPath := VIDEO_DIR + vid
// 打開視頻文件
video, err := os.Open(videoPath)
if err != nil {
sendErrorResponse(w, http.StatusInternalServerError, "視頻不存在!")
return
}
// 設置響應頭
w.Header().Set("Content-Type", "video/mp4")
// 把視頻輸出給客戶端
// 參數三:播放文件的文件名,如果沒有設置,則輸出文件的名字是未知的
// 參數四:響應客戶端的當前時間
// 參數五:視頻文件
http.ServeContent(w, r, "", time.Now(), video)
}
最後運行程序,然後在瀏覽器上輸入localhost:9000/videos/1.mp4,運行效果如下圖所示:
三、上傳視頻
3.1 頁面搭建
在videos目錄下新建一個upload.html文件,文件內容如下所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="http://localhost:9000/upload/ddd" method="post" enctype="multipart/form-data">
<input type="file" name="file"/>
<input type="submit" value="上傳"/>
</form>
</body>
</html>
3.2 路由配置
第一步:配置upload.html頁面的路由;
router.GET("/testpage", TestPageHandler)
第二步:定義處理函數;
func TestPageHandler(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
// 創建Template模版,該模版指向upload.html文件
t, _ := template.ParseFiles("./videos/upload.html")
// 把模版輸出到瀏覽器上
t.Execute(w, nil)
}
測試:
四、上傳視頻
4.1 配置上傳視頻的路由
第一步:配置路由;
router.POST("/upload/:vid-id", UploadHandler)
第二步:定義處理函數;
func UploadHandler(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
// 設置上傳文件的上限
// http.MaxBytesReader方法用於限制body請求體的大小
r.Body = http.MaxBytesReader(w, r.Body, MAX_UPLOAD_SIZE)
// r.ParseMultipartForm方法用於將請求的主體作爲multipart/form-data解析
// 請求的整個主體都會被解析,得到的文件記錄最多maxMemery字節保存在內存,
// 其餘部分保存在硬盤的temp文件裏
// 獲取上傳文件
file, _, err := r.FormFile("file")
if err != nil {
sendErrorResponse(w, http.StatusBadRequest, "上傳文件失敗!")
return
}
// 讀取上傳文件內容
data, err := ioutil.ReadAll(file)
if err != nil {
sendErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
// 把上傳文件保存在videos目錄下
fileName := p.ByName("vid-id")
err = ioutil.WriteFile(VIDEO_DIR + fileName, data, 0666)
if err != nil {
sendErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
// 輸出上傳結果
w.WriteHeader(http.StatusCreated)
io.WriteString(w, "上傳成功")
}
測試:
點擊上傳按鈕後:
五、刪除視頻
刪除視頻的表video_del_rec。
這個表只有一個字段video_id,該字段記錄了要刪除的視頻ID。
5.1 準備工作
scheduler模塊負責維護定時任務,以及提供定時刪除視頻的功能。
第一步:在項目根路徑下創建scheduler目錄;
第二步:創建dbops子目錄;
第三步:在dbops目錄下新建conn.go文件,該文件可以從api模塊下的conn.go文件拷貝過來即可,不需要重複編寫;
第四步:在scheduler目錄下新建response.go文件,該文件提供了向客戶端發送響應的方法。
package main
import (
"io"
"net/http"
)
// 發送響應
func sendResponse(w http.ResponseWriter, sc int, msg string) {
// 輸出響應碼
w.WriteHeader(sc)
// 向客戶端輸出錯誤消息
io.WriteString(w, msg)
}
5.2 定義video_dao
在dbops目錄下新建video_dao.go文件,該文件提供了操作video_del_rec表的相關方法。
package dbops
// 向video_del_rec表中添加待刪除的視頻id
func AddVideoDelRec(vid string) error {
stmt, err := dbConn.Prepare("insert into video_del_rec values(?)")
if err != nil {
return err
}
defer stmt.Close()
_, err = stmt.Exec(vid)
if err != nil {
return err
}
return nil
}
// 刪除video_del_rec表記錄
func DelVideoDelRec(vid string) error {
stmt, err := dbConn.Prepare("delete from video_del_rec where video_id = ?")
if err != nil {
return err
}
defer stmt.Close()
_, err = stmt.Exec(vid)
if err != nil {
return err
}
return nil
}
// 按照參數查詢video_del_rec表的n條記錄
func ReadVideoDelRec(n int) ([]string, error) {
// 定義一個切片,存儲查詢到的所有video_id字段值
var ids []string
stmt, err := dbConn.Prepare("select video_id from video_del_rec limit ?")
if err != nil {
return ids, err
}
defer stmt.Close()
rows, err := stmt.Query(n)
if err != nil {
return ids, err
}
for rows.Next() {
var id string
if err := rows.Scan(&id); err != nil {
return ids, err
}
ids = append(ids, id)
}
return ids, nil
}
5.3 服務器
在scheduler目錄下新建main.go文件,文件內容如下:
package main
import (
"github.com/julienschmidt/httprouter"
"net/http"
"video_server_demo/scheduler/dbops"
)
func main() {
// 創建router對象
router := RegisterHandler()
// 使用中間件判斷當前請求是否是要登錄後才能夠訪問
// 啓動服務
http.ListenAndServe(":8989", router)
}
func RegisterHandler() *httprouter.Router {
router := httprouter.New()
router.GET("/video-delete-record/:vid-id", VideoDeleteRecordHandler)
return router
}
func VideoDeleteRecordHandler(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
// 獲取vid-id參數
vid := p.ByName("vid-id")
if len(vid) == 0 {
sendResponse(w, 400, "參數vid-id爲空!")
return
}
// 執行插入操作
err := dbops.AddVideoDelRec(vid)
if err != nil {
sendResponse(w, 500, "數據庫操作失敗!")
return
}
sendResponse(w, 200, "添加成功!")
}
上面代碼配置了一個路由,該路由用於往video_del_del表添加記錄。
5.4 刪除視頻的定時任務
在scheduler目錄下新建taskrunner子目錄,然後在taskrunner目錄下新建defs.go和task.go文件。defs.go存儲常量和類型變量的定義。task.go存儲要執行的任務代碼。
下面是defs.go文件的完整代碼:
const (
VIDEO_PATH = "./video/"
)
// 定義一個管道,用於存儲待刪除的視頻id
type dataChan chan interface{}
在task.go文件中定義三個方法:
- DeleteVideo:從磁盤上刪除指定id的視頻文件;
- VideoClearDispatcher:從video_del_del表中讀取要刪除的視頻id,然後先存儲到管道中;
- VideoClearExecutor:從管道中讀取要刪除的視頻id,然後執行刪除操作(先調用DeleteVideo方法刪除磁盤上的視頻文件,然後再刪除video_del_del表的記錄);
下面是task.go文件的完整代碼:
package taskrunner
import (
"errors"
"os"
"video_server_demo/scheduler/dbops"
"sync"
)
// 根據id刪除視頻文件
func DeleteVideo(vid string) error {
err := os.Remove(VIDEO_PATH + vid)
return err
}
// 從數據庫中一次性讀取多條數據
func VideoClearDispatcher(dc dataChan) error {
res, err := dbops.ReadVideoDelRec(3)
if err != nil {
return err
}
if len(res) == 0 {
return errors.New("任務結束!")
}
for _, id := range res {
dc <- id
}
return nil
}
// 刪除視頻的執行函數
func VideoClearExecutor(dc dataChan) error {
// 定義一個map,用於記錄錯誤消息,視頻id作爲key,err作爲value
errMap := &sync.Map{}
var err error
forloop:
for {
select {
case id := <- dc:
// 啓動go程執行刪除操作
go func(vid interface{}){
// 在磁盤上刪除視頻
if err := DeleteVideo(vid.(string)); err != nil {
// 將刪除的異常原因記錄到map中
errMap.Store(vid, err)
return
}
// 刪除video_del_rec表記錄
if err := dbops.DelVideoDelRec(vid.(string)); err != nil {
errMap.Store(vid, err)
return
}
}(id)
default:
// 管道中的數據已經消費完,循環終止
break forloop
}
}
// 循環遍歷errMap,將error異常返回
errMap.Range(func(key, value interface{}) bool {
// 如果回調函數返回false,代表循環結束,否則繼續循環遍歷map
err = value.(error)
return err == nil
})
return err
}
5.5 生產和消費狀態切換
上面VideoClearDispatcher方法負責生產數據(待刪除的視頻ID),VideoClearExecutor方法負責消費數據。生產和消費的操作應該是輪流執行。
首先在defs.go文件中定義一個管道,用於存儲待刪除的視頻id,並且定義兩個常量,用於標識當前任務的狀態。
const (
...
READY_TO_DISPATH = "c" // 任務狀態:生產狀
READY_TO_EXECUTE = "e" // 任務狀態:消費
CLOSE = "cl" // 錯誤狀態
)
// 通過管道記錄生產、消費的狀態
type controlChan chan string
// 定義一個函數類型,需要和生產和消費方法的結構相同
type fn func(dc dataChan) error
然後,在taskrunner目錄下新建runner.go文件,負責生產數據、消費數據,以及生產和消費狀態的切換。
package taskrunner
/*
該結構體用於管理狀生產數據、消費數據的狀態
*/
type Runner struct {
Controller controlChan // 用於狀態管理的通道
Error controlChan // 管理異常的通道
Dispatcher fn // 生產數據函數
Executor fn // 消費數據函數
ch dataChan // 調用上面函數需要的參數
}
func NewRunner(size int, dispatcher fn, executor fn) *Runner {
return &Runner{
make(chan string, 1),
make(chan string, 1),
dispatcher,
executor,
make(chan interface{}, size),
}
}
// 開啓生產任務
func (r *Runner) Start() {
// 把任務狀態設置爲“生產”
r.Controller <- READY_TO_DISPATH
// 開始生產流程
r.StartDispatcher()
}
// 開始生產
func (r *Runner) StartDispatcher() {
// 由於生產和消費的流程是輪流執行,生產完成後,將任務狀態設置爲消費,消費完成後,將任務狀態設置爲生產,因此這裏放在一個死循環中來實現
for {
select {
// 從Controller管道中讀取任務狀態
case c := <- r.Controller:
if c == READY_TO_DISPATH {
// 如果是生產狀態,則調用生產方法
if err := r.Dispatcher(r.ch); err != nil {
// 如果生產過程發生錯誤,則返回錯誤結果
r.Error <- CLOSE
} else {
// 如果生產完後,修改任務狀態爲“消費”
r.Controller <- READY_TO_EXECUTE
}
}
if c == READY_TO_EXECUTE {
// 如果是消費狀態,則調用消費方法
if err := r.Executor(r.ch); err != nil {
// 如果生產過程發生錯誤,則返回錯誤結果
r.Error <- CLOSE
} else {
// 如果生產完後,修改任務狀態爲“生產”
r.Controller <- READY_TO_DISPATH
}
}
// 讀取Error管道中的數據
case e := <- r.Error:
// 如果讀取到的數據等於CLOSE常量的值,則執行退出操作
if e == CLOSE {
return
}
}
}
}
5.6 開啓定時任務
在taskrunner目錄下新建start_task.go文件,該文件存儲了啓動定時任務的方法。
package taskrunner
import (
"fmt"
"time"
)
// 啓動定時器
func Start() {
// 創建任務對象
runner := NewRunner(3, VideoClearDispatcher, VideoClearExecutor)
work := NewWork(5, runner)
go work.Start()
}
type Work struct {
ticket *time.Ticker // 定時器
runner *Runner // 定時執行的任務
}
func NewWork(interval time.Duration, runner *Runner) *Work {
return &Work {
time.NewTicker(interval * time.Second), // 每個interval秒執行定時任務一次
runner,
}
}
// 執行定時任務
func (w *Work) Start() {
fmt.Println("開啓定時器...")
for {
select {
case <- w.ticket.C:
fmt.Println("執行任務...")
// 如果定時器中可以讀取到數據,則代表定時時間到了,開啓定時器
w.runner.Start()
}
}
}
在dbops目錄中新建video_dao_test.go文件,定義一個測試方法,用於向video_del_rec表中插入一些數據。
package dbops
import (
"fmt"
"testing"
)
func TestAddVideoDelRec(t *testing.T) {
for i := 0; i< 100; i++ {
vid := fmt.Sprintf("vid-%d", i)
AddVideoDelRec(vid)
}
}
由於vid對應的文件在磁盤上不存在,所以測試前先把task.go文件中刪除視頻的代碼註釋掉。
/*TODO 在磁盤上刪除視頻
if err := DeleteVideo(vid.(string)); err != nil {
// 將刪除的異常原因記錄到map中
errMap.Store(vid, err)
return
}*/
先運行測試文件,然後在運行schduler模塊,執行結果如下: