Go项目---流媒体视频网站
整体架构
为什么选择Golang
1.Golang语言层面的设计在网络I/O上相比其他语言有很大的优势(优秀的并发模型,任务处理和调度)
2.Go在这个项目中的绝大部分技能要点(任务调度,上传下载,视频播放)
3.优良的native http库以及模版引擎(无需任何第三方框架)
项目依赖
# 1.路由系统:https://github.com/julienschmidt/httprouter
go get github.com/julienschmidt/httprouter
# 2.MysqlDriver:https://github.com/go-sql-driver/mysql
go get github.com/go-sql-driver/mysql
# 3.UUID:https://github.com/google/uuid
go get -u -v github.com/google/uuid
# 或
go get -u github.com/satori/go.uuid
项目目录结构
项目框架搭建
scoket
main.go 路由系统和socket搭建
package main
import (
"github.com/julienschmidt/httprouter"
"net/http"
"projectdemos/handlers"
)
func RegisterHandlers() *httprouter.Router{
router := httprouter.New()
router.POST("/user", handlers.UserHandler)
router.POST("/user/:user_name", handlers.LoginHandler)
return router
}
func main() {
r := RegisterHandlers()
http.ListenAndServe(":9005",r) // socket监听8000端口
}
mysql
db/conn.go
package db
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
// 提前声明变量,否则未使用的变量会报错
var (
dbConn *sql.DB
err error
)
func init() {
dbConn,err = sql.Open("mysql","root:vansing2022@tcp(192.168.0.11:3306)/videos?charset=utf8")
if err != nil{
fmt.Println(err)
}
}
请求响应数据
data/request_data.go
package data
type UserRequest struct {
UserName string `json:"user_name"`
Password string `json:"password"`
}
type VideoInfo struct {
Id string
AuthorId int
Name string
DisplayTime string
}
data/response_data.go
package data
type ErrorData struct {
Error string `json:"error"`
ErrorCode string `json:"error_code"`
}
type ErrorRespose struct {
HttpCode int
ErrorData ErrorData
}
var (
ErrorRequestBodyParseFailed = ErrorRespose{400,ErrorData{"无法解析请求参数","001"}}
ErrorAuthUser = ErrorRespose{401,ErrorData{"用户验证错误","001"}}
)
测试用例
package db
import (
"fmt"
"strconv"
"testing"
"time"
)
func clearTables(){
dbConn.Exec("truncate users")
dbConn.Exec("truncate videos")
dbConn.Exec("truncate comments")
}
func TestMain(m *testing.M) {
clearTables()
m.Run()
clearTables()
}
func TestUserWorkFlow(t *testing.T){
clearTables()
t.Run("ADD",testAddUser)
t.Run("GET",testGetUser)
t.Run("DELETE",testDelUser)
t.Run("REGET",testRegetUser)
}
功能模块划分
middleware
注册middleware
main.go
package main
import (
"github.com/julienschmidt/httprouter"
"net/http"
"projectdemos/handlers"
)
type middleWareHandler struct {
r *httprouter.Router
}
func NewMiddleWareHandler(r *httprouter.Router) http.Handler {
m := middleWareHandler{}
m.r = r
return m
}
// 实现Handler的接口
func (m middleWareHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 检查session
validateUserSession(r)
m.r.ServeHTTP(w, r)
}
func RegisterHandlers() *httprouter.Router {
router := httprouter.New()
router.POST("/user", handlers.UserHandler)
router.POST("/user/:user_name", handlers.LoginHandler)
return router
}
func main() {
r := RegisterHandlers()
mh := NewMiddleWareHandler(r)
http.ListenAndServe(":9005", mh) // socket监听8000端口
}
auth.go
package main
import (
"net/http"
"projectdemos/session"
)
var HEADER_FIELD_SESSION = "X-Session-Id"
var HEADER_FIELD_USERNAME = "X-User-Name"
func validateUserSession(r *http.Request) bool {
sessionID := r.Header.Get(HEADER_FIELD_SESSION)
if len(sessionID) == 0 {
return false
}
userName,ok := session.IsSessionExpired(sessionID)
if ok{
return false
}
r.Header.Add(HEADER_FIELD_USERNAME,userName)
return true
}
func validateUser(r *http.Request) bool {
userName := r.Header.Get(HEADER_FIELD_USERNAME)
if len(userName) == 0 {
return false
}
userName,ok := session.IsSessionExpired(sessionID)
if ok{
return false
}
r.Header.Add(HEADER_FIELD_USERNAME,userName)
return true
}
session
功能介绍
主要是解决用户登录状态问题,本例采用的是cache存一份,sql存一份,这里的cache选择的是程序缓存,用的golang中的线程安全sync.Map
data
data/response_data.go
type SimpleSession struct {
UserName string
TTL int64
}
数据库操作
db/sql_session
package db
import (
"database/sql"
"projectdemos/data"
"strconv"
"sync"
)
func CreateSession(sessionID string, ttl int64, userName string) error {
ttlstr := strconv.FormatInt(ttl, 10)
stmtIns, err := dbConn.Prepare("insert into sessions(session_id,user_name,ttl) values(?, ?, ?)")
if err != nil {
return err
}
_, err = stmtIns.Exec(sessionID, userName, ttlstr)
if err != nil {
return err
}
defer stmtIns.Close()
return nil
}
func GetSession(sessionID string) (*data.SimpleSession, error) {
stmtOut, err := dbConn.Prepare("select ttl,user_name from sessions where session_id=?")
if err != nil {
return nil, err
}
sessionMap := &data.SimpleSession{}
var userName, ttl string
err = stmtOut.QueryRow(sessionID).Scan(&userName, &ttl)
if err != nil && err != sql.ErrNoRows {
return nil, err
}
if res, err := strconv.ParseInt(ttl, 10, 64); err != nil {
sessionMap.TTL = res
sessionMap.UserName = userName
}else{
return nil,err
}
defer stmtOut.Close()
return sessionMap,nil
}
func RetrieveAllSession()(*sync.Map,error){
sessionMap := &sync.Map{}
stmtOut,err := dbConn.Prepare("select * from sessions")
if err != nil{
return nil, err
}
rows,err := stmtOut.Query()
if err != nil{
return sessionMap,nil
}
for rows.Next(){
var sessionID string
var ttlStr string
var userName string
if err := rows.Scan(&sessionID,ttlStr,userName);err !=nil{
break
}
if ttl,err := strconv.ParseInt(ttlStr,10,64);err != nil{
sessionStruct := &data.SimpleSession{UserName: userName,TTL: ttl}
sessionMap.Store(sessionID,sessionStruct)
}
}
return sessionMap,nil
}
func DeleteSession(sessionID string) error {
stmtDel,err := dbConn.Prepare("delete from session where session_id=?")
if err != nil{
return err
}
_,err = stmtDel.Exec(sessionID)
if err != nil{
return err
}
return nil
}
session操作
session/fetch_session.go
package session
import (
"github.com/google/uuid"
"projectdemos/data"
"projectdemos/db"
"sync"
"time"
)
// 由于golang的map不是线程安全的,所以用golang内置的线程安全mapsync.Map
// 这个线程安全Map在并发读的时候是很稳定的,但是并发写的时候会出现keyError,要加全局锁
var sessionMap *sync.Map
func init() {
// 初始化Map
sessionMap = &sync.Map{}
}
func LoadSessionsFromDB() {
res, err := db.RetrieveAllSession()
if err != nil {
return
}
res.Range(func(key, value interface{}) bool {
sessionStruct := value.(*data.SimpleSession)
sessionMap.Store(key, sessionStruct)
return true
})
}
func CreateNewSessionId(userName string) string {
sessionId := uuid.New().String()
createTime := time.Now().UnixNano() / 100000
ttl := createTime + 30*60*1000
sessionData := &data.SimpleSession{UserName: userName, TTL: ttl}
sessionMap.Store(sessionId, sessionData)
err:= db.CreateSession(sessionId,ttl,userName)
if err != nil{
return ""
}
return sessionId
}
func IsSessionExpired(sessionID string) (string, bool) {
sessionData,ok := sessionMap.Load(sessionID)
if !ok{
return "",true
}
createTime := time.Now().UnixNano() / 100000
if createTime < sessionData.(*data.SimpleSession).TTL{
return sessionData.(*data.SimpleSession).UserName,false
}
sessionMap.Delete(sessionID)
db.DeleteSession(sessionID)
return "",true
}
将sessionMap加载到内存
main.go
func Prepare() {
session.LoadSessionsFromDB()
}
func main(){
Prepare()
...
}
用户
API设计
// 1.创建(注册用户):
URL:/user
Method:POST
SC:201,400,500
// 2.用户登录:
URL:/user/:username
Method:POST
SC:200,400,500
// 3.获取用户的基本信息:
URL:/user/:username
Method:GET
SC:200,400,401,403,500
// 4.注销登录:
URL:/user/:username
Method:DELETE
SC:204,400,401,403,500
配置路由
main.go
package main
import (
"github.com/julienschmidt/httprouter"
"net/http"
"projectdemos/handlers"
)
func RegisterHandlers() *httprouter.Router{
router := httprouter.New()
router.POST("/user", handlers.UserHandler)
router.POST("/user/:user_name", handlers.LoginHandler)
return router
}
func main() {
r := RegisterHandlers()
http.ListenAndServe(":9005",r) // socket监听8000端口
}
请求/响应消息体
data/request_data.go
package data
type UserRequest struct {
UserName string `json:"user_name"`
Password string `json:"password"`
}
data/response_data.go响应数据
package data
type ErrorData struct {
Error string `json:"error"`
ErrorCode string `json:"error_code"`
}
type ErrorRespose struct {
HttpCode int
ErrorData ErrorData
}
var (
ErrorRequestBodyParseFailed = ErrorRespose{400, ErrorData{"001", "无法解析请求参数"}}
ErrorAuthUser = ErrorRespose{401, ErrorData{"002", "用户验证错误"}}
ErrorDB = ErrorRespose{500, ErrorData{"003", "数据库错误"}}
ErrorMarshalOrUnmarshal = ErrorRespose{500, ErrorData{"004", "序列化错误"}}
)
type VideoInfo struct {
Id string
AuthorId int
Name string
DisplayTime string
}
type Comment struct {
Id string
VideoId string
UserName string
Content string
}
type SimpleSession struct {
UserName string
TTL int64
}
// 注册成功后返回的消息体
type SignedUp struct {
Success string `json:"success"`
SessiongId string `json:"sessiong_id"`
}
数据库操作
db/mysql_crud.go
package db
import (
"database/sql"
"log"
)
func AddUserCredential(user_name string, password string) error {
// 新建用户
stmtIns,err := dbConn.Prepare("INSERT INTO users (user_name,password) VALUES (?,?)")
defer stmtIns.Close()
if err != nil{return err}
_,err = stmtIns.Exec(user_name,password)
if err != nil{return err}
return nil
}
func GetUserCredential(user_name string) (string, error) {
// 查询用户
stmtOut,err := dbConn.Prepare("SELECT password FROM users WHERE user_name=?")
if err != nil{
log.Printf("%s",err)
return "", err
}
var password string
err = stmtOut.QueryRow(user_name).Scan(&password)
// 查不到数据会抛出NoRows异常
if err != nil && err != sql.ErrNoRows{
return "",err
}
defer stmtOut.Close()
return password,nil
}
func DeleteUser(user_name string,password string) error{
// 删除用户
stmtDel,err := dbConn.Prepare("DELETE FROM users WHERE user_name=? AND password=?")
if err != nil{
log.Printf("%s",err)
return err
}
_,err = stmtDel.Exec(user_name,password)
if err != nil{return err}
defer stmtDel.Close()
return nil
}
单元测试
db/mysql_test.go
package db
import "testing"
func clearTables(){
dbConn.Exec("TRUNCATE users")
}
func TestMain(m *testing.M) {
clearTables()
m.Run()
clearTables()
}
func TestUserWorkFlow(t *testing.T){
t.Run("ADD",testAddUser)
t.Run("GET",testGetUser)
t.Run("DELETE",testDelUser)
t.Run("REGET",testRegetUser)
}
func testAddUser(t *testing.T){
err := AddUserCredential("test01","123")
if err != nil{
t.Errorf("Error of AddUser:%v",err)
}
}
func testGetUser(t *testing.T){
password,err := GetUserCredential("test01")
if password != "123" || err != nil{
t.Errorf("Error of GetUser:%v",err)
}
}
func testDelUser(t *testing.T){
err := DeleteUser("test01","123")
if err != nil{
t.Errorf("Error of DelUser:%v",err)
}
}
func testRegetUser(t *testing.T){
password,err := GetUserCredential("test01")
if err != nil{
t.Errorf("Error of GetUser:%v",err)
}
if password != ""{
t.Errorf("Error of GetUser:%v",err)
}
}
执行单元测试
cd db
go test -v
功能函数
auth.go
package handlers
import (
"encoding/json"
"github.com/julienschmidt/httprouter"
"io"
"io/ioutil"
"log"
"net/http"
"projectdemos/data"
"projectdemos/db"
"projectdemos/responses"
"projectdemos/session"
)
func RegisterUserHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
// POST方法 读取请求体中的数据
req, _ := ioutil.ReadAll(request.Body)
userBody := &data.UserRequest{}
// 反序列化到结构体中
if err := json.Unmarshal(req, userBody); err != nil {
log.Println("反序列化请求体错误",err)
responses.SendErrorResponse(writer, data.ErrorRequestBodyParseFailed)
return
}
// 将用户创建到数据库中
if err := db.AddUser(userBody.UserName, userBody.Password); err != nil {
responses.SendErrorResponse(writer, data.ErrorDB)
return
}
// 注册成功,返回session
sessionId := session.CreateNewSessionId(userBody.UserName)
res := &data.SignedUp{Success: "ok", SessiongId: sessionId}
if jsonRes, err := json.Marshal(res); err != nil {
responses.SendErrorResponse(writer, data.ErrorMarshalOrUnmarshal)
return
} else {
responses.SendNormalResponse(writer, string(jsonRes), 200)
}
}
func LoginHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
userName := params.ByName("user_name")
io.WriteString(writer, userName)
}
handlers/handler.go
package handlers
import (
"github.com/julienschmidt/httprouter"
"io"
"net/http"
)
func CreateUser(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
io.WriteString(writer,"nihao",)
}
视频
API设计
// 1.添加视频:
URL:/video/
Method:POST
SC:201,400,500
配置路由
main.go
package main
import (
"github.com/julienschmidt/httprouter"
"net/http"
"projectdemos/handlers"
)
func RegisterHandlers() *httprouter.Router{
router := httprouter.New()
router.POST("/user", handlers.UserHandler)
router.POST("/user/:user_name", handlers.LoginHandler)
return router
}
func main() {
r := RegisterHandlers()
http.ListenAndServe(":9005",r) // socket监听8000端口
}
请求/响应消息体
data/response_data.go
package data
type UserRequest struct {
UserName string `json:"user_name"`
Password string `json:"password"`
}
数据库操作
db/mysql_crud.go
func AddVideo(authorId int, name string) (*data.VideoInfo, error) {
//添加视频
videoId := uuid.New().String()
t := time.Now()
// 这个displayTime是方便将来页面显示的时间格式
displayTime := t.Format("Jan 02 2006,15:04:05") //M D y,HH:MM:SS
stmIns, err := dbConn.Prepare(`INSERT INTO videos (id,author_id,video_name,display_time) VALUES (?,?,?,?)`)
if err != nil {
log.Printf("%s", err)
return nil, err
}
_, err = stmIns.Exec(videoId, authorId, name, displayTime)
if err != nil {
log.Printf("%s", err)
return nil, err
}
res := &data.VideoInfo{
Id: videoId,
AuthorId: authorId,
Name: name,
DisplayTime: displayTime,
}
defer dbConn.Close()
return res, nil
}
func GetVideo(videoId string) (*data.VideoInfo, error) {
fmt.Println("传进来的videoId:", videoId)
stmOut, err := dbConn.Prepare("SELECT author_id,video_name,display_time FROM videos WHERE id = ?")
if err != nil {
fmt.Println("查询到的对象:", stmOut, err)
return nil, err
}
var authorId int
var displayTime string
var name string
err = stmOut.QueryRow(videoId).Scan(&authorId, &displayTime, &name)
if err != nil && err != sql.ErrNoRows {
log.Printf("%s", err)
return nil, err
}
if err == sql.ErrNoRows {
log.Printf("%s", err)
return nil, nil
}
defer stmOut.Close()
res := &data.VideoInfo{videoId, authorId, name, displayTime}
return res, err
}
func DeleteVideo(videoId string) error {
stmtDel, err := dbConn.Prepare("DELETE FROM videos WHERE id = ?")
if err != nil {
return err
}
_, err = stmtDel.Exec(videoId)
if err != nil {
log.Printf("%s", err)
return err
}
defer stmtDel.Close()
return nil
}
单元测试
db/mysql_test.go
func TestVideoWorkFlow(t *testing.T){
clearTables()
t.Run("PrepareUser",testAddUser)
t.Run("ADD",testAddVideo)
t.Run("GET",testGetVideo)
t.Run("DELETE",testDelVideo)
t.Run("REGET",testRegetVideo)
}
func testAddVideo(t *testing.T){
videoObject,err := AddVideo(1,"张三的快乐生活")
if err != nil{
t.Errorf("Error of AddUser:%v",err)
}
fmt.Println(videoObject)
videoId = videoObject.Id
fmt.Println(videoId)
}
func testGetVideo(t *testing.T){
_,err := GetVideo(videoId)
if err != nil{
t.Errorf("Error of GetUser:%v",err)
}
}
func testDelVideo(t *testing.T){
err := DeleteVideo(videoId)
if err != nil{
t.Errorf("Error of DelUser:%v",err)
}
}
func testRegetVideo(t *testing.T){
_,err := GetVideo(videoId)
if err != nil{
t.Errorf("Error of GetUser:%v",err)
}
}
执行单元测试
cd db
go test -v
功能函数
auth.go
package handlers
import (
"github.com/julienschmidt/httprouter"
"io"
"net/http"
)
func UserHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
io.WriteString(writer, "nihao")
}
func LoginHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
userName := params.ByName("user_name")
io.WriteString(writer, userName)
}
评论
API设计
// 1.添加视频:
URL:/video/
Method:POST
SC:201,400,500
配置路由
main.go
package main
import (
"github.com/julienschmidt/httprouter"
"net/http"
"projectdemos/handlers"
)
func RegisterHandlers() *httprouter.Router{
router := httprouter.New()
router.POST("/user", handlers.UserHandler)
router.POST("/user/:user_name", handlers.LoginHandler)
return router
}
func main() {
r := RegisterHandlers()
http.ListenAndServe(":9005",r) // socket监听8000端口
}
请求/响应消息体
data/response.go
type Comment struct {
Id string
VideoId string
UserName string
Content string
}
数据库操作
db/mysql_crud.go
func AddComment(videoId string, authorId int, commentContent string) error {
commentId := uuid.New().String()
stmIns, err := dbConn.Prepare("INSERT INTO comments (id,video_id,author_id,content) VALUES (?,?,?,?)")
if err != nil {
return err
}
_, err = stmIns.Exec(commentId, videoId, authorId, commentContent)
if err != nil {
return err
}
defer stmIns.Close()
return nil
}
func ListComments(videoId string, from, to int) ([]*data.Comment, error) {
stmtOut, err := dbConn.Prepare("SELECT comments.id,users.user_name,comments.content from comments INNER JOIN users ON comments.author_id=users.id WHERE comments.video_id = ? AND comments.created_time > FROM_UNIXTIME(?) AND comments.created_time <= FROM_UNIXTIME(?)")
var res []*data.Comment
commentObjs, err := stmtOut.Query(videoId, from, to)
if err != nil{
return res,err
}
for commentObjs.Next(){
var id,user_name,content string
if err := commentObjs.Scan(&id,&video_name,&content);err !=nil{
return res,err
}
comment := &data.Comment{
id,
videoId,
user_name,
content,
}
res = append(res,comment)
}
defer stmtOut.Close()
return res,nil
}
单元测试
db/mysql_test.go
func TestVideoWorkFlow(t *testing.T){
clearTables()
t.Run("PrepareUser",testAddUser)
t.Run("ADD",testAddVideo)
t.Run("GET",testGetVideo)
t.Run("DELETE",testDelVideo)
t.Run("REGET",testRegetVideo)
}
func testAddVideo(t *testing.T){
videoObject,err := AddVideo(1,"张三的快乐生活")
if err != nil{
t.Errorf("Error of AddUser:%v",err)
}
fmt.Println(videoObject)
videoId = videoObject.Id
fmt.Println(videoId)
}
func testGetVideo(t *testing.T){
_,err := GetVideo(videoId)
if err != nil{
t.Errorf("Error of GetUser:%v",err)
}
}
func testDelVideo(t *testing.T){
err := DeleteVideo(videoId)
if err != nil{
t.Errorf("Error of DelUser:%v",err)
}
}
func testRegetVideo(t *testing.T){
_,err := GetVideo(videoId)
if err != nil{
t.Errorf("Error of GetUser:%v",err)
}
}
执行单元测试
cd db
go test -v
功能函数
auth.go
package handlers
import (
"github.com/julienschmidt/httprouter"
"io"
"net/http"
)
func UserHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
io.WriteString(writer, "nihao")
}
func LoginHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
userName := params.ByName("user_name")
io.WriteString(writer, userName)
}
handler
定义消息体
请求消息体
package data
type UserRequest struct {
UserName string `json:"user_name"`
Password string `json:"password"`
}
响应消息体
package data
type ErrorData struct {
Error string `json:"error"`
ErrorCode string `json:"error_code"`
}
type ErrorRespose struct {
HttpCode int
ErrorData ErrorData
}
var (
ErrorRequestBodyParseFailed = ErrorRespose{400, ErrorData{"001", "无法解析请求参数"}}
ErrorAuthUser = ErrorRespose{401, ErrorData{"002", "用户验证错误"}}
ErrorDB = ErrorRespose{500, ErrorData{"003", "数据库错误"}}
ErrorMarshalOrUnmarshal = ErrorRespose{500, ErrorData{"004", "序列化错误"}}
)
type VideoInfo struct {
Id string
AuthorId int
Name string
DisplayTime string
}
type Comment struct {
Id string
VideoId string
UserName string
Content string
}
type SimpleSession struct {
UserName string
TTL int64
}
// 注册成功后返回的消息体
type SignedUp struct {
Success string `json:"success"`
SessiongId string `json:"sessiong_id"`
}
业务逻辑
handlers/auth.go
package handlers
import (
"encoding/json"
"github.com/julienschmidt/httprouter"
"io"
"io/ioutil"
"net/http"
"projectdemos/data"
"projectdemos/db"
"projectdemos/responses"
"projectdemos/session"
)
func RegisterUserHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
// POST方法 读取请求体中的数据
req, _ := ioutil.ReadAll(request.Body)
userBody := &data.UserRequest{}
// 反序列化到结构体中
if err := json.Unmarshal(req, userBody); err != nil {
responses.SendErrorResponse(writer, data.ErrorAuthUser)
return
}
// 将用户创建到数据库中
if err := db.AddUser(userBody.UserName, userBody.Password); err != nil {
responses.SendErrorResponse(writer, data.ErrorDB)
return
}
// 注册成功,返回session
sessionId := session.CreateNewSessionId(userBody.UserName)
res := &data.SignedUp{Success: "ok", SessiongId: sessionId}
if jsonRes, err := json.Marshal(res); err != nil {
responses.SendErrorResponse(writer, data.ErrorMarshalOrUnmarshal)
return
} else {
responses.SendNormalResponse(writer, string(jsonRes), 200)
}
}
func LoginHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
userName := params.ByName("user_name")
io.WriteString(writer, userName)
}
postman测试
出现的错误
invalid character '-' in numeric literal
原因
我在使用postmain测试接口时后端报错,该错误原因是因为使用请求头使用的是form-data/或x-www....
而后端接受使用的json,这时就会出现该问题
Streaming
REMP协议
简单介绍
RTMP协议是流媒体协议,RTMP是Adobe的私有协议,没有完全公开,一般传输的是flv、f4v格式流
应用场景
直播
RTSP协议
简单介绍
RTSP协议是流媒体协议,RTSP协议是公有协议,有专门的机构维护RTSP协议一般传输的是ts、mp4格式流
流量控制(bocket token算法)
由于视频流的周期和请求的周期是一致的,当多个流媒体同时请求,会占用大量资源,这里会加入流量控制组件
bocket 相当于一个箱子,这个箱子里有一定数量的token,每当来一个请求,就给这个请求办法一个token,当箱子中的token没有了,则阻塞请求,当请求处理完成后,将token还到箱子中,保证一段时间内,server只处理这些数量的请求,这里用channel实现,而不用共享内存实现,因为会发生线程安全问题
实现bocket token算法
limit/limiter
package limit
import "log"
type ConnLimiter struct {
concurrentConn int
bucket chan int
}
func NewConnLimiter(connNum int) *ConnLimiter {
return &ConnLimiter{
connNum,
make(chan int, connNum),
}
}
// 获取token
func (c *ConnLimiter) GetConn() bool {
if len(c.bucket) >= c.concurrentConn {
return false
}
c.bucket <- 1
return true
}
// 释放token
func (c *ConnLimiter) ReleaseConn() {
conn := <-c.bucket
log.Printf("释放了一个链接,%d", conn)
}
将流量控制组件加到项目中
data/response_data.go
// 请求过多的官方错误码为429
ErrorTooManyConnections = ErrorRespose{http.StatusTooManyRequests, ErrorData{"005", "请求过多"}}
main.go
package main
import (
"github.com/julienschmidt/httprouter"
"net/http"
"projectdemos/data"
"projectdemos/handlers"
"projectdemos/limit"
"projectdemos/responses"
"projectdemos/session"
)
type middleWareHandler struct {
r *httprouter.Router
l *limit.ConnLimiter
}
func NewMiddleWareHandler(r *httprouter.Router, cc int) http.Handler {
m := middleWareHandler{}
m.r = r
m.l = limit.NewConnLimiter(cc)
return m
}
func (m middleWareHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 流量控制组件
if !m.l.GetConn() {
responses.SendErrorResponse(w, data.ErrorTooManyConnections)
return
}
// 把token还到盒子里
defer m.l.ReleaseConn()
// 检查session
session.ValidateUserSession(r)
m.r.ServeHTTP(w, r)
}
func RegisterHandlers() *httprouter.Router {
router := httprouter.New()
router.POST("/user", handlers.RegisterUserHandler)
router.POST("/user/:user_name", handlers.LoginHandler)
router.GET("/video/:video_id", handlers.StreamingHandler)
router.POST("/upload/:video_id", handlers.UploadVideoHandler)
return router
}
func main() {
r := RegisterHandlers()
mh := NewMiddleWareHandler(r, 100)
http.ListenAndServe(":9005", mh) // socket监听9005端口
}
handler
保存文件
data/response_data.go
ErrorOpenVideo = ErrorRespose{500, ErrorData{"006", "打开视频文件错误"}}
ErrorSaveVideo = ErrorRespose{500, ErrorData{"007", "保存视频文件错误"}}
ErrorBadRequest = ErrorRespose{400, ErrorData{"008", "文件太大了"}}
ErrorNoVideoName = ErrorRespose{400, ErrorData{"009", "找不到提交的文件,表单名称错误"}}
const (
VIDEODIR = "./videos/"
MaxUploadLength = 1024 * 1024 * 50
)
handler/video.go
package handlers
import (
"fmt"
"github.com/julienschmidt/httprouter"
"io/ioutil"
"net/http"
"os"
"projectdemos/data"
"projectdemos/responses"
"time"
)
func StreamingHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
videoId := params.ByName("video_id")
fmt.Println(videoId)
// 视频的路径
videoDir := data.VIDEODIR + videoId
// 打开文件
video, err := os.Open(videoDir)
if err != nil {
responses.SendErrorResponse(writer, data.ErrorOpenVideo)
return
}
// 加响应头,浏览器会根据响应头来解析相应数据
writer.Header().Set("Content-Type", "video/mp4")
// 将文件作为二进制流传给client,并保证前段可以自由拖动进度条来请求数据
http.ServeContent(writer, request, "", time.Now(), video)
// 关闭文件
defer video.Close()
}
func UploadVideoHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
// 检查Json格式提交的文件大小
request.Body = http.MaxBytesReader(writer, request.Body, data.MaxUploadLength)
// 检查表单提交的文件的大小
if err := request.ParseMultipartForm(data.MaxUploadLength); err != nil {
responses.SendErrorResponse(writer, data.ErrorBadRequest)
return
}
// form的name值,这里固定为取file
file, fileHeader, err := request.FormFile("file")
if err != nil {
responses.SendErrorResponse(writer, data.ErrorNoVideoName)
return
}
acceptStr := fileHeader.Header.Get("accept")
// 检查请求头的accept是否是video/*
if acceptStr != "video/*" {
responses.SendErrorResponse(writer, data.ErrorBadRequest)
return
}
Streamdata, err := ioutil.ReadAll(file)
if err != nil {
responses.SendErrorResponse(writer, data.ErrorBadRequest)
return
}
video_id := params.ByName("video_id")
err = ioutil.WriteFile(data.VIDEODIR+video_id, Streamdata,0666) // 0666是权限
if err != nil{
responses.SendErrorResponse(writer, data.ErrorSaveVideo)
return
}
responses.SendNormalResponse(writer,"文件上传成功",200)
}
获取stream流
data/response_data.go
ErrorOpenVideo = ErrorRespose{500, ErrorData{"006", "打开视频文件错误"}}
const (
VIDEODIR = "./videos/"
)
handler/video.go
package handlers
import (
"github.com/julienschmidt/httprouter"
"net/http"
"os"
"projectdemos/data"
"projectdemos/responses"
"time"
)
func StreamingHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
videoId := params.ByName("video_id")
// 视频的路径
videoDir := data.VIDEODIR + videoId
// 打开文件
video, err := os.Open(videoDir)
if err != nil {
responses.SendErrorResponse(writer, data.ErrorOpenVideo)
return
}
// 加响应头,浏览器会根据响应头来解析相应数据
writer.Header().Set("Content-Type", "video/mp4")
// 将文件作为二进制流传给client,并保证前段可以自由拖动进度条来请求数据
http.ServeContent(writer, request, "", time.Now(), video)
// 关闭文件
defer video.Close()
}
func UploadVideoHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
}
测试功能
http://127.0.0.1:9005/video/video64181e784baa4.mp4
Scheduler(异步任务调度)
基本介绍
异步任务调度框架,如删除视频时并非是实时删除,首先修改视频的状态为逻辑删除,然后下发任务到Scheduler,Scheduler会进行周期性的执行用来真实删除视频
在项目中的作用
在项目中是一个单独的服务,可以单独运行,也可以加到项目中
runner的生产/消费者模型
data.go
package taskrunner
const (
READY_TO_DISPATCH = "d"
READY_TO_EXECUTE = "e"
CLOSE = "c"
)
// 控制channel,用来做任务调度
type controlChan chan string
// 任务内容
type dataChan chan interface{}
//
type fn func(dc dataChan) error
runner.go
package taskrunner
type Runner struct {
Control controlChan // 存放
Error controlChan // 存放关闭指令,如果有数据,则关闭所有channel
Data dataChan // 存放数据的channel,size为自定义size
dataSize int // 决定DataChan的大小
loogLived bool // 是否是常驻的runner,如果不是会被回收
Dispatcher fn // 派发任务函数
Executor fn // 调度任务函数
}
func NewRunner(size int, loogLived bool, d fn, e fn) *Runner {
return &Runner{
Control: make(chan string, 1),
Error: make(chan string, 1),
Data: make(chan interface{}, size),
loogLived: loogLived,
Dispatcher: d,
Executor: e,
}
}
func (r *Runner) startDispatch() {
defer func() {
if !r.loogLived {
close(r.Control)
close(r.Error)
close(r.Data)
}
}()
for {
select {
case c := <-r.Control:
if c == READY_TO_DISPATCH {
err := r.Dispatcher(r.Data)
if err != nil {
r.Error <- CLOSE
} else {
r.Control <- READY_TO_EXECUTE
}
}
if c == READY_TO_EXECUTE {
err := r.Executor(r.Data)
if err != nil {
r.Error <- CLOSE
} else {
r.Control <- READY_TO_DISPATCH
}
}
case e := <-r.Error:
if e == CLOSE {
return
}
default:
}
}
}
func (r *Runner) StartAll() {
r.Control <- READY_TO_DISPATCH
r.startDispatch()
}
测试runner
runner_test.go
package taskrunner
import (
"fmt"
"testing"
"time"
)
func TestNewRunner(t *testing.T) {
// 生产者
d := func(dc dataChan) error {
for i := 0; i < 30; i++ {
dc <- i
fmt.Println("派发任务内容:", i)
}
return nil
}
// 消费者
e := func(dc dataChan) error {
// 这里是死循环,为保证两个goroutine同时执行,当任务调度结束后,开始下一次循环,
// 那么就想到for range 了,但是for range会阻塞channel,如果没有新的任务进来会卡在这里
forLoop:
for {
select {
case d := <-dc:
fmt.Println("执行任务内容:", d)
default:
break forLoop
}
}
return nil
}
r := NewRunner(30, false, d, e)
go r.StartAll() // 开启goroutine执行任务,否则会死循环在任务中
// 等待子线程执行3s
time.Sleep(3 * time.Second)
}
主动报错,停止整个runner
package taskrunner
import (
"fmt"
"testing"
"time"
"errors"
)
func TestNewRunner(t *testing.T) {
// 生产者
d := func(dc dataChan) error {
for i := 0; i < 30; i++ {
dc <- i
fmt.Println("派发任务内容:", i)
}
return nil
}
// 消费者
e := func(dc dataChan) error {
// 这里是死循环,为保证两个goroutine同时执行,当任务调度结束后,开始下一次循环,
// 那么就想到for range 了,但是for range会阻塞channel,如果没有新的任务进来会卡在这里
forLoop:
for {
select {
case d := <-dc:
fmt.Println("执行任务内容:", d)
default:
break forLoop
}
}
// 主动报错终止runner
return errors.New("出错了")
}
r := NewRunner(30, false, d, e)
go r.StartAll() // 开启goroutine执行任务,否则会死循环在任务中
// 等待子线程执行3s
time.Sleep(3 * time.Second)
}
定时任务
tmain.go
package taskrunner
import "time"
type Worker struct {
ticker *time.Ticker // 定时任务的时间,多长时间后执行
runner *Runner
}
func NewWorker(interval time.Duration, r *Runner) *Worker {
return &Worker{
ticker: time.NewTicker(interval * time.Second),
runner: r,
}
}
func (w *Worker) startWorker() {
for {
select {
case <-w.ticker.C: // time内置的chnnel
go w.runner.StartAll()
}
}
}
func Start() {
r := NewRunner(3, true, VideoClearDispatcher, VideoClearExecutor)
w := NewWorker(2, r) // 这里设置的是2s后执行删除操作
w.startWorker()
}
项目中使用runner
data/response_data.go
const (
VIDEODIR = "./videos/"
)
taskrunner/tasks.go
package taskrunner
import (
"errors"
"sche/db"
"sync"
)
func VideoClearDispatcher(dc dataChan) error {
videos, err := db.ReadVideoDeletion(3)
if err != nil {
return err
}
if len(videos) == 0 {
return errors.New("没取到数据")
}
for _, id := range videos {
dc <- id
}
return nil
}
func VideoClearExecutor(dc dataChan) error {
// 用来存放所有的error
errMap := &sync.Map{}
forloop:
for {
select {
case vid := <-dc:
// 因为开启的是一个新的goroutine,有可能会发生重复读写的问题,可以使用waitgroup解决
go func(id interface{}) {
err := db.DeleteVideoFile(id.(string))
if err != nil {
errMap.Store(id, err)
return
}
err = db.DeleteVideo(id.(string))
if err != nil {
errMap.Store(id, err)
return
}
}(vid)
default:
break forloop
}
}
var err error
// 循环所有的err如果有一个err则立刻终止函数
errMap.Range(func(key, value any) bool {
err = value.(error)
if err != nil {
return false
}
return true
})
return err
}
main.go
package main
import (
"scheduler/taskrunner"
)
func main() {
// 用来阻塞主线程
c := make(chan int)
go taskrunner.Start()
<-c // 用来阻塞主线程
}
模板渲染服务
目录结构
请求转发(跨域解决方式)
前后端端口不同,会存在跨域,项目中采用了请求转发的形式来规避跨域
// proxy
如:前端访问的是http://127.0.0.1:8000/upload
// proxy 会转发到真实服务的端口
http://127.0.0.1:9000/upload
// api
{
url:""
method:""
message:""
}
包下载
go get github.com/julienschmidt/httprouter
api透传
1.添加路由
main.go
router.POST("/api", handlers.ApiHandler)
2.定义请求和响应消息体
data/response_data.go
type ApiBody struct {
Url string `json:"url"`
Method string `json:"method"`
RequestBody string `json:"request_body"`
}
type ErrorResponse struct {
Error string `json:"error"`
ErrorCode string `json:"error_code"`
}
var (
ErrorBadRequestMethod = ErrorResponse{Error: "错误的请求方法",ErrorCode: "001"}
ErrorBadRequestBody = ErrorResponse{Error: "请求数据错误",ErrorCode: "002"}
ErrorInternalFaults = ErrorResponse{Error: "后台错误",ErrorCode: "500"}
)
handler/homehandler
func ApiHandler(writer http.ResponseWriter, request *http.Request, p httprouter.Params) {
if request.Method != http.MethodPost {
re, _ := json.Marshal(data.ErrorBadRequestMethod)
io.WriteString(writer, string(re))
return
}
res, _ := ioutil.ReadAll(request.Body)
apiBody := &data.ApiBody{}
if err := json.Unmarshal(res, apiBody); err != nil {
re, _ := json.Marshal(data.ErrorBadRequestBody)
io.WriteString(writer, string(re))
return
}
// api透传逻辑函数
client.Request(apiBody, writer, request)
defer request.Body.Close()
}
client/client.go
package client
import (
"encoding/json"
"io"
"io/ioutil"
"net/http"
"web/data"
)
var httpClient *http.Client
func init() {
httpClient = &http.Client{}
}
func Request(apiBody *data.ApiBody, writer http.ResponseWriter, request *http.Request) {
var resp *http.Response
var err error
switch apiBody.Method {
case http.MethodGet:
// 新做一个请求
req, _ := http.NewRequest("GET", apiBody.Url, nil)
req.Header = request.Header
resp, err = httpClient.Do(req)
if err != nil {
return
}
normalResponse(writer, resp)
case http.MethodPost:
// 新做一个请求
req, _ := http.NewRequest("POST", apiBody.Url, nil)
req.Header = request.Header
resp, err = httpClient.Do(req)
if err != nil {
return
}
normalResponse(writer, resp)
case http.MethodDelete:
// 新做一个请求
req, _ := http.NewRequest("DELETE", apiBody.Url, nil)
req.Header = request.Header
resp, err = httpClient.Do(req)
if err != nil {
return
}
normalResponse(writer, resp)
default:
writer.WriteHeader(http.StatusBadRequest)
io.WriteString(writer, "请求错误")
}
}
func normalResponse(w http.ResponseWriter, r *http.Response) {
res, err := ioutil.ReadAll(r.Body)
if err != nil {
errRes, _ := json.Marshal(data.ErrorInternalFaults)
w.WriteHeader(500)
io.WriteString(w, string(errRes))
return
}
w.WriteHeader(r.StatusCode)
io.WriteString(w, string(res))
}
proxy(请求代理)
api透传解决不了一些原始请求,如上传文件时,文件流放不到body中
需要代理的url
router.POST("/upload/:video_id", handlers.ProxyHandler)
handler/homehandler.go
package main
import (
"github.com/julienschmidt/httprouter"
"net/http"
"net/http/httputil"
"net/url"
)
// 请求代理
func ProxyHandler(writer http.ResponseWriter, request *http.Request,p httprouter.Params) {
u, _ := url.Parse("http://127.0.0.1:9005/") // 真实服务器的IP地址和端口
proxy := httputil.NewSingleHostReverseProxy(u) // 只是很高效的将127.0.0.1:9000替换了127.0.0.1:9005
proxy.ServeHTTP(writer, request)
}
// 注册路由
func RegisterHandler() *httprouter.Router {
router := httprouter.New()
router.GET("/upload/:video_id", ProxyHandler)
return router
}
func main() {
r := RegisterHandler()
http.ListenAndServe(":9000", r)
}
模版渲染
main.go
package main
import (
"github.com/julienschmidt/httprouter"
"net/http"
"web/handlers"
)
func RegisterHandler() *httprouter.Router {
router := httprouter.New()
router.GET("/", handlers.HomeHandler)
router.POST("/", handlers.HomeHandler)
router.GET("/index", handlers.IndexHandler)
// 绑定静态文件
router.ServeFiles("/static/*filepath", http.Dir("./templates"))
return router
}
func main() {
r := RegisterHandler()
http.ListenAndServe(":9000", r)
}
data/response_data.go
package data
type HomePage struct {
Name string
}
handlers/homehandler.go
package handlers
import (
"fmt"
"github.com/julienschmidt/httprouter"
template2 "html/template"
"net/http"
)
func HomeHandler(writer http.ResponseWriter, request *http.Request, p httprouter.Params) {
homePage := &HomePage{Name: "小明"}
template, err := template2.ParseFiles("./templates/home.html")
if err != nil {
fmt.Println("出错了", err)
return
}
template.Execute(writer, homePage)
}
templates/home.html
<!DOCTYPE html>
<html lang="zh-Hans" dir="ltr">
<head>
<link rel="stylesheet" type="text/css" href="/static/admin/css/base.css">
<link rel="stylesheet" type="text/css" href="/static/admin/css/nav_sidebar.css">
<script src="/static/admin/js/nav_sidebar.js" defer></script>
<link rel="stylesheet" type="text/css" href="/static/admin/css/login.css">
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0">
<link rel="stylesheet" type="text/css" href="/static/admin/css/responsive.css">
<meta name="robots" content="NONE,NOARCHIVE">
</head>
<body class=" login"
data-admin-utc-offset="28800">
<h1>{{.Name}}</h1>
<!-- Container -->
<div id="container">
<!-- Header -->
<div id="header">
<div id="branding" style="display: contents">
</div>
</div>
<div class="main shifted" id="main">
<div class="content">
<!-- Content -->
<div id="content" class="colM">
<p class="errornote" id="error_info" style="display:none">请输入一个正确的 用户名 和密码,注意区分大小写</p>
<div id="content-main">
<form action="" method="post" id="login-form">
<div class="form-row">
<label class="required" for="userName">用户名:</label> <input type="text" name="userName" autofocus autocapitalize="none" autocomplete="username" maxlength="100%" required id="userName" style="width:338px;">
</div>
<div class="form-row">
<label class="required" for="password">密码:</label> <input type="password" name="password" autocomplete="current-password" maxlength="100%" required id="password" style="width:338px;">
</div>
<div class="submit-row">
<input type="submit" value="登录">
</div>
</form>
</div>
<br class="clear">
</div>
<!-- END Content -->
<div id="footer"></div>
</div>
</div>
</div>
</body>
</html>
build的shell脚本
#!/usr/bin/env bash
# Build web UI
cd C:\utils\Goproject\projectdemos\web
go install
cp -R C:\utils\Goproject\projectdemos\templates C:\Users\99283\go\bin
handler加入身份认证功能
package handlers
import (
"fmt"
"github.com/julienschmidt/httprouter"
template2 "html/template"
"net/http"
"web/data"
)
func HomeHandler(writer http.ResponseWriter, request *http.Request, p httprouter.Params) {
cname, err1 := request.Cookie("user_name")
sessionId, err2 := request.Cookie("session")
if err1 != nil || err2 != nil {
homePage := &data.HomePage{Name: "小明"}
t, err := template2.ParseFiles("./templates/home.html")
if err != nil {
fmt.Println("出错了", err)
return
}
t.Execute(writer, homePage)
return
}
if len(cname.Value) != 0 && len(sessionId.Value) != 0 {
http.Redirect(writer, request, "/user_home", http.StatusFound)
}
}
主页
data/response_data.go
type UserHomePage struct {
Name string
}
handlers/homehandler.go
func IndexHandler(writer http.ResponseWriter, request *http.Request, p httprouter.Params) {
cname, err1 := request.Cookie("user_name")
_, err2 := request.Cookie("session")
if err1 != nil || err2 != nil {
http.Redirect(writer, request, "/", http.StatusFound)
return
}
fname := request.FormValue("user_name")
var up *data.UserHomePage
if len(cname.Value) != 0 {
up = &data.UserHomePage{Name: cname.Value}
} else {
up = &data.UserHomePage{Name: fname}
}
t, e := template2.ParseFiles("./templates/index.html")
if e != nil {
fmt.Println("出错了", e)
return
}
t.Execute(writer, up)
return
}
config
公共配置
每一个服务下创建config文件夹,创建config.go文件
config.go
package config
import (
"encoding/json"
"os"
)
type Config struct {
LBAddr string `json:"lb_addr"` // 负载均衡地址
OssAddr string `json:"oss_addr"` // oss对象存储
}
var config *Config
func init() {
file,_:= os.Open("./conf.json")
defer file.Close()
decoder := json.NewDecoder(file)
config = &Config{}
err := decoder.Decode(config)
if err != nil{
panic(err.Error())
}
}
//
func GetLBArr() string {
return config.LBAddr
}
func GetOssArr() string {
return config.OssAddr
}
云原生部署
Cliud Native(云原生)
无状态
不保存任何数据
伸缩性
可以动态扩展/或收缩,即多服务器部署
冗余
主从备份
平台无关性
花费最少得代码,在不同的云平台部署
部署和发布
自动化部署
良好的迁移性
多云共生
云存储OSS
读取视频流
但是有视频安全的问题,这里用的是直接重定向到OSS,浏览器的地址栏会显示所有的秘钥之类的数据
func StreamingHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
videoId := params.ByName("video_id")
// 调用OSS对象存储
pubilicUrl := "https://ceshi0929.oss-cn-chengdu.aliyuncs.com/videos/ceshi/" + videoId + ".mp4"
http.Redirect(writer, request, pubilicUrl, 301)
}
上传视频
下载阿里云上传的SDK
go get github.com/aliyun/aliyun-oss-go-sdk@v2.2.7
如果发生错误
直接下载包github.com/aliyun/aliyun-oss-go-sdk/oss到GOPATH下
ossops/alioss.go
package ossops
import (
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/julienschmidt/httprouter"
"net/http"
"projectdemos/data"
"projectdemos/responses"
)
func UploadFileToOss(writer http.ResponseWriter, request *http.Request, p httprouter.Params) bool {
videoId := p.ByName("videoId")
// Endpoint以杭州为例,其它Region请按实际情况填写。
endpoint := "http://oss-cn-chengdu.aliyuncs.com"
// 阿里云主账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建RAM账号。
accessKeyId := "LTAI5t9kGEcZm1h8ZuviUJgD"
accessKeySecret := "rw5c78PgRDQiOxNHCarixd6Cv5eeL3"
bucketName := "ceshi0929"
// <yourObjectName>上传文件到OSS时需要指定包含文件后缀在内的完整路径,例如abc/efg/123.jpg。
osspath := "/videos/ceshi/" + videoId + ".mp4"
// <yourLocalFileName>由本地文件路径加文件名包括后缀组成,例如/users/local/myfile.txt。
localFileName := "./videos/" + videoId + ".mp4"
// 创建OSSClient实例。
client, err := oss.New(endpoint, accessKeyId, accessKeySecret)
if err != nil {
responses.SendErrorResponse(writer, data.ErrorSaveVideo)
return false
}
// 获取存储空间。
bucket, err := client.Bucket(bucketName)
if err != nil {
responses.SendErrorResponse(writer, data.ErrorSaveVideo)
return false
}
// 第一种方式:单文件上传
err = bucket.PutObjectFromFile(osspath, localFileName)
if err != nil {
responses.SendErrorResponse(writer, data.ErrorSaveVideo)
return false
}
// 第二种方式:多文件上传
err = bucket.UploadFile(localFileName, osspath, 500*1024, oss.Routines(3))
if err != nil {
responses.SendErrorResponse(writer, data.ErrorSaveVideo)
return false
}
return true
}
handler/video.go
func UploadVideoHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
// 检查Json格式提交的文件大小
request.Body = http.MaxBytesReader(writer, request.Body, data.MaxUploadLength)
// 检查表单提交的文件的大小
if err := request.ParseMultipartForm(data.MaxUploadLength); err != nil {
responses.SendErrorResponse(writer, data.ErrorBadRequest)
return
}
// form的name值,这里固定为取file
file, fileHeader, err := request.FormFile("file")
if err != nil {
responses.SendErrorResponse(writer, data.ErrorNoVideoName)
return
}
acceptStr := fileHeader.Header.Get("accept")
// 检查请求头的accept是否是video/*
if acceptStr != "video/*" {
responses.SendErrorResponse(writer, data.ErrorBadRequest)
return
}
// 将句柄读成二进制流
Streamdata, err := ioutil.ReadAll(file)
if err != nil {
responses.SendErrorResponse(writer, data.ErrorBadRequest)
return
}
video_id := params.ByName("video_id")
err = ioutil.WriteFile(data.VIDEODIR+video_id, Streamdata, 0666) // 0666是权限
if err != nil {
responses.SendErrorResponse(writer, data.ErrorSaveVideo)
return
}
// 调用oss上传到阿里
ossops.UploadFileToOss(writer,request,params)
responses.SendNormalResponse(writer, "文件上传成功", http.StatusCreated)
}