流媒体网站开发(一)

一、项目前准备

1.1 数据库设计

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for comments
-- ----------------------------
DROP TABLE IF EXISTS `comments`;
CREATE TABLE `comments` (
  `id` varchar(64) NOT NULL,
  `video_id` varchar(64) DEFAULT NULL,
  `author_id` int(10) unsigned DEFAULT NULL,
  `content` text,
  `time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for sessions
-- ----------------------------
DROP TABLE IF EXISTS `sessions`;
CREATE TABLE `sessions` (
  `session_id` varchar(255) NOT NULL,
  `ttl` tinytext,
  `login_name` varchar(64) DEFAULT NULL,
  PRIMARY KEY (`session_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `login_name` varchar(64) DEFAULT NULL,
  `pwd` text,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for video_del_rec
-- ----------------------------
DROP TABLE IF EXISTS `video_del_rec`;
CREATE TABLE `video_del_rec` (
  `video_id` varchar(64) NOT NULL,
  PRIMARY KEY (`video_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for video_info
-- ----------------------------
DROP TABLE IF EXISTS `video_info`;
CREATE TABLE `video_info` (
  `id` varchar(64) NOT NULL,
  `author_id` int(10) unsigned DEFAULT NULL,
  `name` text,
  `display_ctime` text,
  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

1.2 模块划分

api:处理核心业务,如用户登录、用户注册、视频管理、发表评论等;
scheduler:定义任务;
streamserver:视频处理,如上传视频、播放视频等;

1.3 系统设计图

在这里插入图片描述

二、api模块设计

api模块划分:

  • dbops:数据库操作,如链接数据库、添加Session、查询Session等
  • defs:定义结构体
  • session:用户登录信息管理
  • util:系统工具

项目目录结构:
在这里插入图片描述

三、公共代码实现

3.1 conn实现

在dbops目录下新建conn.go文件,然后定义一个init方法,用于初始化数据库链接。

package dbops

import "database/sql"
import _ "github.com/go-sql-driver/mysql"

var (
	dbConn *sql.DB
	err error
)

func init()  {
	dbConn, err = sql.Open("mysql", "root:root@tcp(localhost:3306)/video_server?charset=utf8")
	if err != nil {
		panic(err)
	}
}

3.2 session_dao实现

在dbops目录下新建session_dao.go文件,然后提供session的crud方法。

package dbops

import (
	"database/sql"
	"strconv"
	"sync"
	"video_server_demo/api/defs"
)

// 向session表添加数据
func InsertSession(sid string, ttl int64, loginName string) error {
	// 将ttl转换成10进制的字符串
	ttlStr := strconv.FormatInt(ttl, 10)
	// 通过dbConn准备要执行的sql
	stmt, err := dbConn.Prepare("insert into session values(?, ?, ?)")
	defer stmt.Close()
	if err != nil {
		return err
	}
	// 执行插入操作,如果成功则返回nil,否则返回err
	if _, err = stmt.Exec(sid, ttlStr, loginName); err != nil {
		return err
	}
	return nil
}

// 删除session表数据
func DeleteSession(sid string) error {
	// 通过dbConn准备要执行的sql
	stmt, err := dbConn.Prepare("delete from sessions where session_id = ?")
	defer stmt.Close()
	if err != nil {
		return err
	}
	// 执行删除操作,如果成功则返回nil,否则返回err
	if _, err = stmt.Exec(sid); err != nil {
		return err
	}
	return nil
}

// 根据session的id查询
func GetSession(sid string) (*defs.SimpleSession, error) {
	// 通过dbConn准备要执行的sql
	stmt, err := dbConn.Prepare("select * from sessions where sid = ?")
	defer stmt.Close()
	if err != nil {
		return nil, err
	}
	var ttl string      // session时间戳
	var username string // session关联的用户名
	// 执行查询
	err = stmt.QueryRow(sid).Scan(&ttl, &username)
	// 如果查询过程中出现异常,或者没有查询结果,则返回nil, err
	// 如果查询到session,那么就把ttl和username封装到SimpleSession中并返回
	if err != nil && err != sql.ErrNoRows {

		return nil, err
	}
	ss := &defs.SimpleSession{}
	// 将字符串转换成int64类型
	if res, err := strconv.ParseInt(ttl, 10, 64); err == nil {
		ss.TTL = res
		ss.Username = username
		return ss, nil
	}
	return nil, err;
}

// 查询session表所有数据,返回存储所有session的Map
func GetAllSessions() (*sync.Map, error) {
	// 定义一个map,存储所有session
	sessionMap := &sync.Map{}
	// 通过dbConn准备要执行的sql
	stmt, err := dbConn.Prepare("select * from sessions")
	defer stmt.Close()
	if err != nil {
		return nil, err
	}
	// 执行查询
	rows, err := stmt.Query()
	if err != nil {
		return nil, err
	}
	// 遍历查询结果
	for rows.Next() {
		// 定义变量接收每一列数据
		var sid string
		var ttlStr string
		var login_name string

		if err = rows.Scan(&sid, &ttlStr, &login_name); err != nil {
			return nil, err
		}

		// 先将ttl转换成int64后,构建SimpleSession对象,在存储到map中
		if ttl, err := strconv.ParseInt(ttlStr, 10, 64); err == nil {
			ss := &defs.SimpleSession{login_name, ttl}
			// 把session存储到map中,为了保证key的唯一性,使用sid作为key的值
			sessionMap.Store(sid, &ss)
		}
	}
	return sessionMap, nil;
}

3.3 定义结构体

在defs目录下新建api_def.go文件,然后在文件中定义结构体SimpleSession,该结构体用于封装用户的session信息。

type SimpleSession struct {
	Username string
	TTL      int64 
}

Username代表session指向的用户名,TTL代表session有效时间戳。

3.4 session操作部分

在session目录下新建ops.go文件,该文件提供了session以下方法:

  • NewSession:新建一个Session,该Session存储到内存的map中,以及sessions表中;
  • DeleteExpiredSession:删除过期的session;
  • IsSessionExpired:判断session是否过期,如果过期则从map和sessions表中删除该session记录;、
  • LoadSessionFromDb:从sessions表中查询所有session记录;
package session

import (
	"time"
	"video_server_demo/api/dbops"
	"video_server_demo/api/defs"
	"video_server_demo/api/util"
	"sync"
)

// 定义一个Map变量,用于存储用户登录的session信息
var sessionMap *sync.Map

func init() {
	// 初始化Map
	sessionMap = &sync.Map{}
}

/*
	生成session,用于记录用户登录状态
*/
func NewSession(username string) string {
	ttl := NowInMillis() + 30 * 60 * 1000  // 预设有效时间为30分钟
	sid, _ := util.NewUUID()
	ss := &defs.SimpleSession{username, ttl}
	// 将session存储到map中
	sessionMap.Store(sid, ss)
	// 保存到sessions表中
	dbops.InsertSession(sid, ttl, username)
	// 返回session id
	return sid
}

// 获取系统当前时间的时间戳
func NowInMillis() int64 {
	return time.Now().UnixNano() / 100000
}

/*
	删除session
		1. 从map中删除
		2. 删除session表
*/
func DeleteExpiredSession(sid string) {
	// 从map中删除指定id的session
	sessionMap.Delete(sid)
	// 根据sid删除sessions表记录
	dbops.DeleteSession(sid)
}

/*
	判断session是否有效,
	如果session没有过期,则返回用户名和状态false
	如果session已过期或者没有session数据,则返回""和状态true
*/
func IsSessionExpired(sid string) (string, bool) {
	ss, ok := sessionMap.Load(sid)
	if ok {
		ct := NowInMillis()
		if ss.(defs.SimpleSession).TTL < ct {
			// 如果时间戳小于当前时间的时间戳,代表session过期
			// 分别从map和sessions表中删除指定id的session记录
			DeleteExpiredSession(sid)
			return "", true
		}
		return ss.(defs.SimpleSession).Username, false
	}
	return "", true
}

/*
	从数据库中读取所有session,并存储到map中,以便后期的索引和删除操作
 */
 func LoadSessionFromDb() {
	 sessions, err := dbops.GetAllSessions()
	 if err != nil {
	 	return
	 }
	 sessionMap = sessions
 }

3.5 util

在util目录下新建util.go文件,提供一个用于生成session id的方法。

// 生成session id
func NewUUID()	(string, error) {
	// 创建一个长度为16的byte切片
	uuid := make([]byte, 16)
	n, err := io.ReadFull(rand.Reader, uuid)
	if n != len(uuid) || err != nil {
		return "", err
	}
	uuid[8] = uuid[8]&^0xc0 | 0x80
	uuid[6] = uuid[6]&^0xf0 | 0x40
	return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:]), nil
}

四、服务器

4.1 服务器基本功能实现

在api/main.go文件中实现服务端功能。

package main

import (
	"fmt"
	"github.com/julienschmidt/httprouter"
	"net/http"
)
/*
	服务器搭建
*/
func main() {
	// 创建router对象
	router := RegisterHandler()
	// 启动服务
	http.ListenAndServe(":8080", newRouter)
}

func RegisterHandler() *httprouter.Router {
	router := httprouter.New()
	router.POST("/createUser", CreateUser)
	return router
}

// 处理路由的方法
func CreateUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
	fmt.Println("CreateUser...")
}

4.2 增加路由中间件

为了能够让router对象能够实现对用户登录状态的过滤。这里使用中间件扩展了router对象的功能。

实现步骤:

  • 第一步:定义一个方法,该方法返回http.Handler对象(这里之所以返回http.Handler,是因为http.Router实现了这个http.Handler接口);
  • 第二步:定义一个结构体,该结构体实现了http.Handler接口;
  • 第三步:让结构体实现http.Handler接口的ServeHTTP方法。该方法对用户登录状态进行校验,并调用原生router对象的ServeHTTP方法处理用户请求;

完整代码实现:

// 中间件方法,该方法对http.Router进行增强处理
// 因http.Router实现http.Handler接口,因此,该方法也返回一个实现http.Handler接口的对象
func NewMiddleWareHandler(r *httprouter.Router) http.Handler {
	// 创建中间件Handler对象
	m := &MiddleWareHandler{}
	// 把router对象传入到中间件里面
	m.router = r
	// 返回增强处理后的Handler
	return m
}

// 定义中间件结构体,该结构体实现http.Handler接口
type MiddleWareHandler struct {
	router *httprouter.Router
}

// 实现http.Handler接口的ServeHTTP方法
func (m MiddleWareHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// 检查session是否存在
	validateUserSession(r)
	// 保留原有处理请求的功能
	m.router.ServeHTTP(w, r)
}

在auth.go文件中定义validateUserSession方法,该方法从请求头中获取用户的session id。然后再根据该id判断用户Session是否存在。如果存在则返回true,否则返回false。

package main

import (
	"net/http"
	"video_server_demo/api/session"
)

// 该常量代表cookie存储session id的名称
const HEADER_FILED_SESSION = "sid"
const HEADER_FILED_USERNAME = "username"

// 校验用户session是否存在,如果存在则返回true,否则返回false
func validateUserSession(r *http.Request) bool {
	// 从cookie中获取用户登录的session id
	sid := r.Header.Get(HEADER_FILED_SESSION)
	// 判断sid是否存在,如果不存则返回false
	if len(sid) == 0 {
		return false
	}
	// 判断session是否存在
	uname, st := session.IsSessionExpired(sid)
	if st {
		return false
	}
	// 将uname存储在请求头中,让后续处理请求方法使用
	r.Header.Add(HEADER_FILED_USERNAME, uname)
	return true
}

五、用户注册

5.1 数据库操作

在dbops目录下新建user_dao.go文件,该文件提供用户相关的数据库方法。

package dbops

import (
	"database/sql"
)

// 添加用户
func AddUser(loginName string, pwd string) error {
	stmt, err := dbConn.Prepare("insert into users(login_name, pwd) values(?, ?)")
	defer stmt.Close()
	if err != nil {
		return err
	}
	// 执行插入操作,如果成功则返回nil,否则返回err
	if _, err = stmt.Exec(loginName, pwd); err != nil {
		return err
	}
	return nil
}

// 根据用户名获取登录凭证
func GetUserCredential(loginName string) (string, error) {
	stmt, err := dbConn.Prepare("select pwd from users where login_name = ?")
	defer stmt.Close()
	if err != nil {
		return "", err
	}
	var pwd string
	err = stmt.QueryRow(loginName).Scan(&pwd)
	// 如果查询过程中出现异常,或者没有查询结果,则返回"", err
	if err != nil && err != sql.ErrNoRows {
		return "", err
	}
	return pwd, nil;
}

// 删除用户
func DeleteUser(loginName string, pwd string) error {
	stmt, err := dbConn.Prepare("delete from users where login_name = ? and pwd = ?")
	defer stmt.Close()
	if err != nil {
		return err
	}
	// 执行删除操作,如果成功则返回nil,否则返回err
	if _, err = stmt.Exec(loginName, pwd); err != nil {
		return err
	}
	return nil
}

5.2 定义结构体

修改defs/api_def.go文件:

// 该结构体保存用户信息
type UserCredential struct {
	User_name string `json:"user_name"` // 登录用户名
	Pwd      string `json:"pwd"`       // 密码
}

// 该结构体保存用户的登录状态
type SignedUp struct {
	Success   bool   `json:"success"` // TRUE代表已登录,false代表未登录
	SessionId string `json:"session_id"`
}

新增defs/errs.go文件,该文件提供了封装错误消息的结构体。

package defs

// 保存错误信息的结构体
type Err struct {
	Error     string `json:"error"`      // 错误原因
	ErrorCode string  `json:"error_code"` // 错误代码
}

// 定义一个响应错误的结构体
type ErrResponse struct {
	HttpSc int // 响应	码
	Error  Err // 具体错误
}

var (
	// 服务器解析用户数据失败
	ErrorRequestBodyParseFailed = ErrResponse{400, Err{"请求体参数格式不正确", "001"}}
	// 用户认证失败
	ErrorNotAuthUser = ErrResponse{401, Err{"用户认证失败", "002"}}
	// 数据库访问失败
	ErrorDbError = ErrResponse{500, Err{"数据库访问失败", "003"}}
	// 服务器内部错误
	ErrorInternalFaults = ErrResponse{500, Err{"用户认证失败", "004"}}
)

5.3 业务操作

新建user目录,然后在该目录下新建ops.go文件,该文件提供用户相关的业务方法。

package user

import "video_server_demo/api/dbops"

// 删除用户
func DeleteUser(loginName string, pwd string) error {
	error := dbops.DeleteUser(loginName, pwd)
	return error
}

// 添加用户
func AddUser(loginName string, pwd string) error {
	error := dbops.AddUser(loginName, pwd)
	return error
}

5.4 发送响应消息

在api目录下新建response.go文件,该文件提供发送响应消息的方法。

package main

import (
	"encoding/json"
	"io"
	"net/http"
	"video_server_demo/api/defs"
)

// 发送错误响应
func sendErrorResponse(w http.ResponseWriter, errResp defs.ErrResponse) {
	// 输出响应码
	w.WriteHeader(errResp.HttpSc)
	// 将Error对象解析成json格式字符串
	bytes, _ := json.Marshal(&errResp.Error)
	// 向客户端输出错误消息
	io.WriteString(w, string(bytes))
}

// 发送正常的响应
// 参数一:responseWriter对象
// 参数二:响应内容
// 参数三:响应码
func sendNormalResponse(w http.ResponseWriter, msg string, sc int) {
	w.WriteHeader(sc)
	io.WriteString(w, msg)
}

5.5 创建用户

实现步骤:

  • 第一步:获取用户名和密码;
  • 第二步:向user表添加一条新的记录;
  • 第三步:生成session;

修改api/main.go,实现createUser方法。

// 处理路由的方法
func CreateUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
	// 1.从Post请求体中获取参数,然后封装到UserCredential对象中
	body := &defs.UserCredential{}
	res, _ := ioutil.ReadAll(r.Body)
	if err := json.Unmarshal(res, body); err != nil {
		sendErrorResponse(w, defs.ErrorRequestBodyParseFailed)
		return
	}

	// 2.保存数据库
	if err := user.AddUser(body.User_name, body.Pwd); err != nil {
		sendErrorResponse(w, defs.ErrorDbError)
		return
	}

	// 3.生成session
	sid := session.NewSession(body.User_name)
	// 创建SignedUp对象,该对象记录了用户登录状态
	su := defs.SignedUp{true, sid}
	// 完成注册的操作
	if res, err := json.Marshal(su); err != nil {
		sendErrorResponse(w, defs.ErrorInternalFaults)
	} else {
		sendNormalResponse(w, string(res), 201)
	}
}

运行效果:
在这里插入图片描述

六、用户登录

6.1 定义路由

// 用户登录
router.POST("/user/:username", Login)

上面路由指定username参数是为了以后能够方便获取登录用户的名称。

6.2 用户登录处理

func Login(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
	// 1.获取登录的用户名和密码
	body := &defs.UserCredential{}
	res, _ := ioutil.ReadAll(r.Body)
	if err := json.Unmarshal(res, body); err != nil {
		sendErrorResponse(w, defs.ErrorRequestBodyParseFailed)
		return
	}
	// 2.判断用户名和密码是否正确
	pwd, err := dbops.GetUserCredential(body.User_name)
	if err != nil || len(pwd) == 0 || pwd != body.Pwd {
		sendErrorResponse(w, defs.ErrorNotAuthUser)
		return
	}
	// 3.生成session
	sid := session.NewSession(body.User_name)
	// 创建SignedUp对象,该对象记录了用户登录状态
	su := defs.SignedUp{true, sid}
	// 完成注册的操作
	if res, err := json.Marshal(su); err != nil {
		sendErrorResponse(w, defs.ErrorInternalFaults)
	} else {
		sendNormalResponse(w, string(res), 201)
	}
}

// 处理路由的方法
func CreateUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
	/*query := r.URL.Query()
	name := query.Get("user_name")
	pwd := query.Get("pwd")
	fmt.Println("name = ", name)
	fmt.Println("pwd = ", pwd)

	// 获取REST风格的url参数
	name := p.ByName("user_name")
	// 向浏览器输出内容
	io.WriteString(w, name)*/

	// 从Post请求体中获取参数,然后封装到UserCredential对象中
	body := &defs.UserCredential{}
	res, _ := ioutil.ReadAll(r.Body)
	if err := json.Unmarshal(res, body); err != nil {
		sendErrorResponse(w, defs.ErrorRequestBodyParseFailed)
		return
	}

	// 保存数据库
	if err := user.AddUser(body.User_name, body.Pwd); err != nil {
		sendErrorResponse(w, defs.ErrorDbError)
		return
	}

	// 生成session
	sid := session.NewSession(body.User_name)
	// 创建SignedUp对象,该对象记录了用户登录状态
	su := defs.SignedUp{true, sid}
	// 完成注册的操作
	if res, err := json.Marshal(su); err != nil {
		sendErrorResponse(w, defs.ErrorInternalFaults)
	} else {
		sendNormalResponse(w, string(res), 201)
	}
}

运行效果:
在这里插入图片描述

发布了111 篇原创文章 · 获赞 41 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/zhongliwen1981/article/details/103473297
今日推荐