首先,新建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)
}
视频中间件用于控制播放视频的连接数。这里我们通过管道对用户连接数进行控制。
第一步:在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)
}
在项目根路径下新建videos文件夹,该文件夹存放要播放的视频文件。
第一步:注册路由;
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,运行效果如下图所示:
在videos目录下新建一个upload.html文件,文件内容如下所示:
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
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>
第一步:配置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)
}
第一步:配置路由;
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。
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)
}
在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
}
在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表添加记录。
在scheduler目录下新建taskrunner子目录,然后在taskrunner目录下新建defs.go和task.go文件。defs.go存储常量和类型变量的定义。task.go存储要执行的任务代码。
下面是defs.go文件的完整代码:
const (
VIDEO_PATH = "./video/"
)
// 定义一个管道,用于存储待删除的视频id
type dataChan chan interface{}
在task.go文件中定义三个方法:
下面是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
}
上面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
}
}
}
}
在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
}*/