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

项目目录结构

image-20230530151749538

项目框架搭建

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,这时就会出现该问题

image-20230530150401975

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  // 用来阻塞主线程
}

模板渲染服务

目录结构

image-20230601100958457

请求转发(跨域解决方式)

前后端端口不同,会存在跨域,项目中采用了请求转发的形式来规避跨域
// 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)
}
posted @ 2023-05-16 16:35  河图s  阅读(165)  评论(0)    收藏  举报