一、基本配置
1、配置
config.go 使用viper读取config.yml的配置
package config
import (
"log"
"github.com/spf13/viper"
)
type Config struct {
App struct {
Name string
Port string
}
Streamer struct {
Name string
Port string
}
Scheduler struct {
Name string
Port string
}
Web struct {
Name string
Port string
}
Database struct {
Host string
Port string
User string
Password string
Name string
}
}
var AppConfig *Config
func InitConfig() {
viper.SetConfigName("config")
viper.SetConfigType("yml")
viper.AddConfigPath("E:/goproject/src/video_server/config")
if err := viper.ReadInConfig(); err != nil {
log.Fatalf("Error reading config file :%v", err)
}//根据设置的配置路径查找文件
AppConfig = &Config{}
if err := viper.Unmarshal(AppConfig); err != nil {
log.Fatalf("Unable to decode into struct:%v", err)
}//将配置文件中的内容解码到指定的结构体中将配置文件的内容解码到 AppConfig 结构体指针中。
InitDB()
InitRedis()
}
config.yml
App:
name: video_server
port: :3001
Streamer:
name: stream_server
port: :3007
Scheduler:
name: scheduler
port: :3009
Web:
name: web
port: :3010
Database:
host: 192.168.1.177
port: 3306
user: root
password: 1234
name: video_server
datebase.go 用于初始化数据库
package config
import (
"fmt"
"log"
"time"
"video_server/global"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
// InitDB 初始化数据库连接
func InitDB() {
// 如果已经存在数据库连接,直接返回,避免重复连接
if global.DB != nil {
fmt.Println("Database connection already initialized.")
return
}
// 从配置中获取数据库连接参数
user := AppConfig.Database.User
pwd := AppConfig.Database.Password
ip := AppConfig.Database.Host
port := AppConfig.Database.Port
database := AppConfig.Database.Name
// 格式化连接字符串
dsn := fmt.Sprintf("%v:%v@tcp(%v:%v)/%v?charset=utf8mb4&parseTime=True&loc=Local", user, pwd, ip, port, database)
// 打印连接字符串(可以去掉生产环境中)
fmt.Println(dsn)
// 尝试连接数据库
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
//SkipDefaultTransaction: true, // 禁用事务(根据需要)
})
if err != nil {
log.Fatal("Database connection failed:", err)
}
// 获取底层的 sql.DB 实例
sqlDB, err := db.DB()
if err != nil {
log.Fatal("Failed to get SQL DB instance:", err)
}
// 配置数据库连接池
sqlDB.SetMaxIdleConns(10) // 设置最大空闲连接数
sqlDB.SetMaxOpenConns(100) // 设置最大打开连接数
sqlDB.SetConnMaxLifetime(time.Hour) // 设置连接最大生命周期
// 将数据库连接赋值给 global.DB
global.DB = db
fmt.Println("Database connected successfully.")
}
全局函数
package global
import (
"github.com/go-redis/redis"
"gorm.io/gorm"
)
var (
DB *gorm.DB
RedisDB *redis.Client
)
2、数据库内容和模型
package models
type VideoInfo struct {
Id string `json:"id"`
AuthorId int `json:"author_id"`
Name string `json:"name"`
DisplayCtime string
}
type VideoDelRec struct {
VideoId string `json:"video_id"`
}
type User struct {
gorm.Model // 自动包含 ID、CreatedAt、UpdatedAt、DeletedAt
Pwd string `json:"pwd"`
Id int `json:"id"`
LoginName string `json:"login_name" gorm:"unique;not null"` // 唯一且不为空
}
type Comment struct {
Id string
VideoId string
Author string
Content string
}
二、api设置
实现注册,登录,注销,删除视频,添加视频,添加评论功能
1、路由设置
这里面的路由都上传和删除视频的信息,并不是上传本身
api中的main函数 设置了api的路由
package main
import (
"video_server/config"
"video_server/api/router"
)
func main() {
config.InitConfig()
r := router.SetupRouter()
port := config.AppConfig.App.Port
r.Run(port)
}
router.go api中的路由,使用了中间件来实现验证是否登录
package router
import (
"video_server/controllers"
"video_server/middlewares"
"github.com/gin-gonic/gin"
)
func SetupRouter() *gin.Engine {
r := gin.Default()
auth := r.Group("/api/auth")
{
auth.POST("/register", controllers.Register)
auth.POST("/login", controllers.Login)
auth.DELETE("/delete/:id", controllers.DeleteUser)
}
api := r.Group("/api")
{
// 将需要保护的路由放在 api 路由组中,并使用中间件
api.Use(middlewares.AuthMiddleWare()) //配置中间件
{
api.POST("/addnewvideo", controllers.AddNewVideo)
auth.DELETE("/deletevideo/:id", controllers.DeleteVideo)
auth.DELETE("/comment/:id", controllers.Deletecomment)
auth.POST("/addcoment", controllers.Addcomnet)
}
}
return r
}
2、中间件配置
配置中间件
新建一个utils文件 实现一些基本的功能
产生jwt用于验证
func GenerateJWT(username string) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"username": username,
"exp": time.Now().Add(time.Hour * 144).Unix(),
})
signedToken, err := token.SignedString([]byte("secret"))
return "Bearer " + signedToken, err
}
解析jwt 获得username
func ParseJWT(tokenString string) (string, error) {
if len(tokenString) > 7 && tokenString[:7] == "Bearer " {
tokenString = tokenString[7:]
}
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("unexpected Signing Method")
}
return []byte("secret"), nil
})
if err != nil {
return "", err
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
username, ok := claims["username"].(string)
if !ok {
return "", errors.New("username claim is not a string")
}
return username, nil
}
return "", err
}
生成hash密码 和验证密码函数
func HashPassword(pwd string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(pwd), 12)
return string(hash), err
}
func CheckPassword(password string, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
建立控制器,实现路由的功能
3、路由配置
(1)注册
func Register(ctx *gin.Context) { //POST
var user models.User
if err := ctx.ShouldBindJSON(&user); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
hashedPwd, err := utils.HashPassword(user.Pwd)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
user.Pwd = hashedPwd
token, err := utils.GenerateJWT(user.LoginName)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := global.DB.AutoMigrate(&user); err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := global.DB.Create(&user).Error; err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ctx.JSON(http.StatusOK, gin.H{"token": token})
}
(2)登录
func Login(ctx *gin.Context) {
var input struct {
Id int `json:"id"`
Pwd string `json:"pwd"`
}
if err := ctx.ShouldBindJSON(&input); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{
"error7": err.Error(),
})
return
}
var user models.User
if err := global.DB.Where("id=?", input.Id).First(&user).Error; err != nil {
ctx.JSON(http.StatusUnauthorized, gin.H{"error8": "wrong creditinals"})
return
}
if !utils.CheckPassword(input.Pwd, user.Pwd) {
ctx.JSON(http.StatusUnauthorized, gin.H{"error9": "wrong creditinals"})
return
}
token, err := utils.GenerateJWT(user.LoginName)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{
"error8": err.Error(),
})
return
}
ctx.JSON(http.StatusOK, gin.H{
"token": token,
})
}
(3)注销
func DeleteUser(ctx *gin.Context) {
id := ctx.Param("id")
global.DB.Exec("delete from users where id=?", id)
// 成功删除时返回消息
ctx.JSON(http.StatusOK, gin.H{"message": "User deleted", "id": id})
}
(4)添加视频
func AddNewVideo(ctx *gin.Context) {
var videoinfo models.VideoInfo
// 绑定请求体中的 JSON 数据
if err := ctx.ShouldBindJSON(&videoinfo); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON"})
return
}
if err := global.DB.AutoMigrate(&videoinfo); err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := global.DB.Create(&videoinfo).Error; err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ctx.JSON(200, "success")
}
(5)删除视频
func DeleteVideo(ctx *gin.Context) {
id := ctx.Param("id")
global.DB.Exec("delete from video_infos where id=?", id)
// 成功删除时返回消息
ctx.JSON(http.StatusOK, gin.H{"message": "Video deleted", "id": id})
}
(6)添加评论
func Addcomnet(ctx *gin.Context) {
var comment models.Comment
if err := ctx.ShouldBindJSON(&comment); err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON"})
return
}
if err := global.DB.AutoMigrate(&comment); err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := global.DB.Create(&comment).Error; err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
(6)删除评论
func Deletecomment(ctx *gin.Context) {
id := ctx.Param("id")
global.DB.Exec("delete from comments where id=?", id)
}
三、stream设置
main函数都是一样的,把使用的端口改一下就可以,
1、路由设置
实现更新视频和上传视频的功能
package router
import (
"time"
"video_server/controllers"
"video_server/middlewares"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
func SetupRouter() *gin.Engine {
r := gin.Default()
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"http://localhost:5175"},
AllowMethods: []string{"GET", "POST", "OPTIONS", "DELETE"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
connliomiter := middlewares.NewCoonLimiter(10)
api := r.Group("/stream")
{
// 将需要保护的路由放在 api 路由组中,并使用中间件
api.Use(connliomiter.Middleware()) //配置中间件
{
api.GET("/videos/:vid-id", controllers.StreamHandler)
api.POST("/update/:vid-id", controllers.UploadVideoHandler)
}
}
return r
}
2、中间件配置
实现一个连接限制中间件 ConnLimiter
,用于限制并发连接数。
package middlewares
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
)
type ConnLimiter struct {
concurrentCoon int
bucket chan int
}
func NewCoonLimiter(cc int) *ConnLimiter {
return &ConnLimiter{
concurrentCoon: cc,
bucket: make(chan int, cc), // 初始化缓冲通道,容量为并发连接数 cc
}
}
func (cl *ConnLimiter) GetCoon() bool {
if len(cl.bucket) >= cl.concurrentCoon {
log.Printf("reached the rate limitation")
return false
}
cl.bucket <- 1 //给通道发放如一个值
return true
}
func (cl *ConnLimiter) ReleaseCoon() {
c := <-cl.bucket
log.Printf("new connction coming:%d", c)
}
func (cl *ConnLimiter) Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 尝试获取一个连接
if !cl.GetCoon() {
// 如果获取连接失败,返回 429 错误
c.JSON(http.StatusTooManyRequests, gin.H{
"message": "Too many requests. Please try again later.",
})
c.Abort() // 阻止进一步处理请求
return
}
// 如果成功获取连接,继续处理请求
c.Next()
// 请求处理完后释放连接
cl.ReleaseCoon()
}
}
3、路由配置
(1)获取视频
func StreamHandler(ctx *gin.Context) {
// 获取视频ID参数
vid := ctx.Param("vid-id")
vl := "E:/goproject/src/video_server/videos/" + vid + ".mp4"
// 打开视频文件
video, err := os.Open(vl)
if err != nil {
// 如果文件打开失败,返回 500 错误
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 获取文件信息
fileInfo, err := video.Stat()
if err != nil {
// 如果无法获取文件信息,返回 500 错误
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 获取文件的最后修改时间
lastModified := fileInfo.ModTime()
// 设置响应头,表明返回的是 MP4 格式的视频
ctx.Header("Content-Type", "video/mp4")
// 使用 http.ServeContent 来流式传输视频文件
http.ServeContent(ctx.Writer, ctx.Request, vid, lastModified, video)
//http.ServeContent(ctx.Writer, ctx.Request, vid, lastModified, video)
defer video.Close()
}
(2)更新视频
func UploadVideoHandler(ctx *gin.Context) {
// 限制上传文件的最大大小为 100 MB
const maxSize = 100 * 1024 * 1024 // 100 MB
// 使用 http.MaxBytesReader 限制上传请求体的大小
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, maxSize)
// 获取上传的文件
file, err := ctx.FormFile("file")
if err != nil {
// 处理上传错误
if err.Error() == "http: request body too large" {
ctx.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "文件过大,最大支持 100MB"})
} else {
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
return
}
// 限制文件类型为 .mp4 和 .avi
ext := filepath.Ext(file.Filename)
if ext != ".mp4" && ext != ".avi" {
ctx.JSON(http.StatusBadRequest, gin.H{"error": "只支持 .mp4 和 .avi 格式的文件"})
return
}
// 指定视频保存的路径
videoDir := "E:/goproject/src/video_server/videos"
// 确保目录存在,如果不存在则创建它
if err := os.MkdirAll(videoDir, os.ModePerm); err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
vid := ctx.Param("vid-id")
if vid == "" {
ctx.JSON(http.StatusInternalServerError, "Missing video ID")
return
}
// 生成保存文件的路径,使用文件的原始名称
dst := filepath.Join(videoDir, vid)
// 保存文件到服务器
if err := ctx.SaveUploadedFile(file, dst); err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 返回上传成功的响应
ctx.JSON(http.StatusOK, gin.H{
"message": "文件上传成功",
"file": dst,
})
}
标签:流媒体,return,string,err,ctx,http,gin,gorm
From: https://blog.csdn.net/2302_77013343/article/details/144078587