一、项目前准备
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)
}
}
运行效果: