Go语言实战流媒体视频网站
文章目录
1. 知识要点
介绍这门课程大纲,技术堆栈以及环境
1-1 prestudy
2. golang常用工具链
通过一个简单的webservice具体从golang的工具链,到test,全面介绍golang在工程项目里需要掌握的知识点。
2-1 一个例子了解golang常用工具链
2-2 golang项目中test的写法
2-3 golang项目中benchmark的写法
2-4 章节总结
第三章
API设计与架构
restful api的特点
API设计原则
用户API设计
Model
User
location:video_server\api\dbops\api.go
package dbops
import (
"database/sql"
"log"
// "video_server/api/utils"
_ "github.com/go-sql-driver/mysql"
)
func AddUserCredential(loginName string, pwd string) error {
// DB交给内部的prepare方法负责查询。
// prepare首先调用DB内部的conn方法从连接池里面获得一个连接,
// 然后调用driverConn的prepareLocked方法负责查询。
stmtIns, err := dbConn.Prepare("INSERT INTO users (login_name, pwd) VALUES (?, ?)")
if err != nil {
return err
}
_, err = stmtIns.Exec(loginName, pwd)
if err != nil {
return err
}
defer stmtIns.Close()
return nil
}
func GetUserCredential(loginName string) (string, error) {
stmtOut, err := dbConn.Prepare("SELECT pwd FROM users WHERE login_name = ?")
if err != nil {
log.Printf("%s", err)
return "", err
}
var pwd string
err = stmtOut.QueryRow(loginName).Scan(&pwd)
if err != nil && err != sql.ErrNoRows {
return "", err
}
defer stmtOut.Close()
return pwd, nil
}
func DeleteUser(loginName string, pwd string) error {
stmtDel, err := dbConn.Prepare("DELETE FROM users WHERE login_name=? AND pwd=?")
if err != nil {
log.Printf("Delete User Error Found: %s", err)
return err
}
_, err = stmtDel.Exec(loginName, pwd)
if err != nil {
return err
}
defer stmtDel.Close()
return nil
}
api db user unittest case
user unittest
location: video_server\api\dbops\api_test.go
package dbops
import (
"testing"
)
var tempvid string
func clearTables() {
dbConn.Exec("truncate users")
dbConn.Exec("truncate video_info")
dbConn.Exec("truncate comments")
dbConn.Exec("truncate sessions")
}
func TestMain(m *testing.M) {
clearTables()
m.Run()
clearTables()
}
func TestUserWorkFlow(t *testing.T) {
t.Run("Add", testAddUser)
t.Run("Get", testGetUser)
t.Run("Del", testDeleteUser)
t.Run("Reget", testRegetUser)
}
func testAddUser(t *testing.T) {
err := AddUserCredential("avenssi", "123")
if err != nil {
t.Errorf("Error of AddUser: %v", err)
}
}
func testGetUser(t *testing.T) {
pwd, err := GetUserCredential("avenssi")
if pwd != "123" || err != nil {
t.Errorf("Error of GetUser")
}
}
func testDeleteUser(t *testing.T) {
err := DeleteUser("avenssi", "123")
if err != nil {
t.Errorf("Error of DeleteUser: %v", err)
}
}
func testRegetUser(t *testing.T) {
pwd, err := GetUserCredential("avenssi")
if err != nil {
t.Errorf("Error of RegetUser: %v", err)
}
if pwd != "" {
t.Errorf("Deleting user test failed")
}
}
go test -v
DB Movie
func AddNewVideo(aid int, name string) (*defs.VideoInfo, error) {
// create uuid
vid, err := utils.NewUUID()
if err != nil {
return nil, err
}
t := time.Now()
ctime := t.Format("Jan 02 2006, 15:04:05")
stmtIns, err := dbConn.Prepare(`INSERT INTO video_info
(id, author_id, name, display_ctime) VALUES(?, ?, ?, ?)`)
if err != nil {
return nil, err
}
_, err = stmtIns.Exec(vid, aid, name, ctime)
if err != nil {
return nil, err
}
res := &defs.VideoInfo{Id: vid, AuthorId: aid, Name: name, DisplayCtime: ctime}
defer stmtIns.Close()
return res, nil
}
func GetVideoInfo(vid string) (*defs.VideoInfo, error) {
stmtOut, err := dbConn.Prepare("SELECT author_id, name, display_ctime FROM video_info WHERE id=?")
var aid int
var dct string
var name string
err = stmtOut.QueryRow(vid).Scan(&aid, &name, &dct)
if err != nil && err != sql.ErrNoRows {
return nil, err
}
if err == sql.ErrNoRows {
return nil, nil
}
defer stmtOut.Close()
res := &defs.VideoInfo{Id: vid, AuthorId: aid, Name: name, DisplayCtime: dct}
return res, nil
}
func DeleteVideoInfo(vid string) error {
stmtDel, err := dbConn.Prepare("DELETE FROM video_info WHERE id=?")
if err != nil {
return err
}
_, err = stmtDel.Exec(vid)
if err != nil {
return err
}
defer stmtDel.Close()
return nil
}
DB comment
ListComments 是重点
func AddNewComments(vid string, aid int, content string) error {
id, err := utils.NewUUID()
if err != nil {
return err
}
stmtIns, err := dbConn.Prepare("INSERT INTO comments (id, video_id, author_id, content) values (?, ?, ?, ?)")
if err != nil {
return err
}
_, err = stmtIns.Exec(id, vid, aid, content)
if err != nil {
return err
}
defer stmtIns.Close()
return nil
}
func ListComments(vid string, from, to int) ([]*defs.Comment, error) {
stmtOut, err := dbConn.Prepare(`SELECT comments.id, users.Login_name, comments.content FROM comments
INNER JOIN users ON comments.author_id = users.id
WHERE comments.video_id = ? AND comments.time > FROM_UNIXTIME(?) AND comments.time <= FROM_UNIXTIME(?)`)
var res []*defs.Comment
rows, err := stmtOut.Query(vid, from, to)
if err != nil {
return res, err
}
for rows.Next() {
var id, name, content string
if err := rows.Scan(&id, &name, &content); err != nil {
return res, err
}
c := &defs.Comment{Id: id, VideoId: vid, Author: name, Content: content}
res = append(res, c)
}
defer stmtOut.Close()
return res, nil
}
DB Video and Comment testcase
func TestVideoWorkFlow(t *testing.T) {
clearTables()
t.Run("PrepareUser", testAddUser)
t.Run("AddVideo", testAddVideoInfo)
t.Run("GetVideo", testGetVideoInfo)
t.Run("DelVideo", testDeleteVideoInfo)
t.Run("RegetVideo", testRegetVideoInfo)
}
func testAddVideoInfo(t *testing.T) {
vi, err := AddNewVideo(1, "my-video")
if err != nil {
t.Errorf("Error of AddVideoInfo: %v", err)
}
tempvid = vi.Id
}
func testGetVideoInfo(t *testing.T) {
_, err := GetVideoInfo(tempvid)
if err != nil {
t.Errorf("Error of GetVideoInfo: %v", err)
}
}
func testDeleteVideoInfo(t *testing.T) {
err := DeleteVideoInfo(tempvid)
if err != nil {
t.Errorf("Error of DeleteVideoInfo: %v", err)
}
}
func testRegetVideoInfo(t *testing.T) {
vi, err := GetVideoInfo(tempvid)
if err != nil || vi != nil {
t.Errorf("Error of RegetVideoInfo: %v", err)
}
}
func TestComments(t *testing.T) {
clearTables()
t.Run("AddUser", testAddUser)
t.Run("AddCommnets", testAddComments)
t.Run("ListComments", testListComments)
}
func testAddComments(t *testing.T) {
vid := "12345"
aid := 1
content := "I like this video"
err := AddNewComments(vid, aid, content)
if err != nil {
t.Errorf("Error of AddComments: %v", err)
}
}
func testListComments(t *testing.T) {
vid := "12345"
from := 1514764800
to, _ := strconv.Atoi(strconv.FormatInt(time.Now().UnixNano()/1000000000, 10))
res, err := ListComments(vid, from, to)
if err != nil {
t.Errorf("Error of ListComments: %v", err)
}
for i, ele := range res {
fmt.Printf("comment: %d, %v \n", i, ele)
}
}
session
操作session,数据库为mysql,利用·sync.Map(在并发环境中使用的map)进行session的存取
sync.Map这个数据结构是线程安全的(基本类型Map结构体在并发读写时会panic严重错误),它填补了Map线程不安全的缺陷,不过最好只在需要的情况下使用。它一般用于并发模型中对同一类map结构体的读写,或其他适用于sync.Map的情况。
video_server\api\session\ops.go
package dbops
import (
"database/sql"
"log"
"strconv"
"sync"
"video_server/api/defs"
)
func InsertSession(sid string, ttl int64, uname string) error {
ttlstr := strconv.FormatInt(ttl, 10)
stmtIns, err := dbConn.Prepare("INSERT INTO sessions (session_id, TTL, login_name) VALUES (?, ?, ?)")
if err != nil {
return err
}
_, err = stmtIns.Exec(sid, ttlstr, uname)
if err != nil {
return err
}
defer stmtIns.Close()
return nil
}
func RetrieveSession(sid string) (*defs.SimpleSession, error) {
ss := &defs.SimpleSession{}
stmtOut, err := dbConn.Prepare("SELECT TTL, login_name FROM sessions WHERE session_id=?")
if err != nil {
return nil, err
}
var ttl string
var uname string
stmtOut.QueryRow(sid).Scan(&ttl, &uname)
if err != nil && err != sql.ErrNoRows {
return nil, err
}
if res, err := strconv.ParseInt(ttl, 10, 64); err == nil {
ss.TTL = res
ss.Username = uname
} else {
return nil, err
}
defer stmtOut.Close()
return ss, nil
}
func RetrieveAllSessions() (*sync.Map, error) {
m := &sync.Map{}
stmtOut, err := dbConn.Prepare("SELECT * FROM sessions")
if err != nil {
log.Printf("%s", err)
return nil, err
}
rows, err := stmtOut.Query()
if err != nil {
log.Printf("%s", err)
return nil, err
}
for rows.Next() {
var id string
var ttlstr string
var login_name string
if er := rows.Scan(&id, &ttlstr, &login_name); er != nil {
log.Printf("retrive sessions error: %s", er)
break
}
if ttl, err1 := strconv.ParseInt(ttlstr, 10, 64); err1 == nil {
ss := &defs.SimpleSession{Username: login_name, TTL: ttl}
// 存储
m.Store(id, ss)
log.Printf(" session id: %s, ttl: %d", id, ss.TTL)
}
}
return m, nil
}
func DeleteSession(sid string) error {
stmtOut, err := dbConn.Prepare("DELETE FROM sessions WHERE session_id = ?")
if err != nil {
log.Printf("%s", err)
return err
}
if _, err := stmtOut.Query(sid); err != nil {
return err
}
return nil
}
数据库 session表的增删改查
video_server\api\dbops\internal.go
package session
import (
"sync"
"time"
"video_server/api/dbops"
"video_server/api/defs"
"video_server/api/utils"
)
var sessionMap *sync.Map
func init() {
sessionMap = &sync.Map{}
}
func nowInMilli() int64 {
return time.Now().UnixNano() / 1000000
}
func deleteExpiredSession(sid string) {
sessionMap.Delete(sid)
dbops.DeleteSession(sid)
}
func LoadSessionsFromDB() {
r, err := dbops.RetrieveAllSessions()
if err != nil {
return
}
r.Range(func(k, v interface{}) bool {
ss := v.(*defs.SimpleSession)
sessionMap.Store(k, ss)
return true
})
}
func GenerateNewSessionId(username string) string {
id, _ := utils.NewUUID()
ct := nowInMilli()
ttl := ct + 30*60*1000 // Server-side session valid time: 30 min
ss := &defs.SimpleSession{Username: username, TTL: ttl}
sessionMap.Store(id, ss)
dbops.InsertSession(id, ttl, username)
return id
}
func IsSessionExpired(sid string) (string, bool) {
ss, ok := sessionMap.Load(sid)
if ok {
ct := nowInMilli()
if ss.(*defs.SimpleSession).TTL < ct {
deleteExpiredSession(sid)
return "", true
}
// 不过期,返回username
return ss.(*defs.SimpleSession).Username, false
}
return "", true
}
注册用户
package main
import (
"encoding/json"
"io"
"io/ioutil"
"net/http"
"video_server/api/dbops"
"video_server/api/defs"
"video_server/api/session"
"github.com/julienschmidt/httprouter"
)
func CreateUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
res, _ := ioutil.ReadAll(r.Body)
ubody := &defs.UserCredential{}
if err := json.Unmarshal(res, ubody); err != nil {
sendErrorResponse(w, defs.ErrorRequestBodyParseFailed)
return
}
if err := dbops.AddUserCredential(ubody.Username, ubody.Pwd); err != nil {
sendErrorResponse(w, defs.ErrorDBError)
return
}
// 生成json
id := session.GenerateNewSessionId(ubody.Username)
// 返回响应
su := &defs.SignedUp{Success: true, SessionId: id}
// 转为json格式
if resp, err := json.Marshal(su); err != nil {
sendErrorResponse(w, defs.ErrorInternalFaults)
return
} else {
sendNormalResponse(w, string(resp), 201)
}
}
func Login(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
uname := p.ByName("user_name")
io.WriteString(w, uname)
}