2-1 流媒体 - API模块

 

0x0 基础概念

API是什么

后端对外形成Service的接口

RESTful API特点

REST设计原则

0x1 API模块设计 (RESTful风格)

分析实体及关系

三个实体

  1. 用户
  2. 资源
    1. 用户上传的视频
  3. 视频下面的评论
    1. 用户和资源发生的互动

实体间的关系


之后:把关系映射到刚才说的RESTful method里对应起来即可

用户

用户资源

评论

评论和Videos是强的从属关系,所以在设计的时候,它是属于Video的

RESTful API设计小结

三个实体之间,是一个美好的树形结构

使用URL设计的API,符合一种自然的描述

如果没有按照标准化设计API

0x2 API分层设计

逻辑流程分层

根据Request处理流程,分为四个主要层

处理流程:request进来交给routine导航到正确的handler,校验鉴权,处理业务逻辑、返回response

  1. handler层
  2. validation层
    1. 校验request是否合法
    2. 校验user是否合法
    3. 基础设施
      1. 数据结构定义
      2. 返回的错误信息
  3. 逻辑处理层(business logic)
    1. 可以把business logic放在handlerl里面写
    2. 然后通过handler调db_ops
    3. (说明)整个API的逻辑处理流程:访问数据库、拿到信息、处理信息、经过校验
    4. (说明)一般API的操作,都是数据库的增删改查
  4. response层

代码层级设计

代码层级设计需要遵循上述分层结构

代码调用流程

  1. 从main进到handler
  2. handler会调用dbops,从dbops里拿到想要的信息做进一步处理
    1. 进一步处理过程中会用到defs中的一些东西 - 消息定义等
  3. 组装成response,调取response.go中提供的方法返回

分层设计 vs 所有逻辑写在同一个函数中

写在一个函数中

  1. 没有分层,不符合REST的建议
  2. 可读性差
  3. 扩展性差
    1. 比如:对所有请求增加一个校验步骤
    2. 如果没做分层,所有handler函数都需要逐一修改

分层架构

  1. 编写test case非常容易
  2. 工程效率高

0x3 User API代码框架

公共文件

api/defs

放一些配置和定义

  1. api的消息体结构(前台会发什么样的消息体进来)
  2. 返回的错误信息

apidef.go

用于声明model

type UserCredential struct {
    Username string `json:"user_name"`
    Pwd string `json:"pwd"`
}
知识点

Golang使用打tag的方式声明序列化/反序列化

代码中:tag名称为json,tag值为user_name

errs.go

定义各种错误

type Err struct {
    Error string `json:"error"`
    ErrorCode string `json:"error_code"`  
}

type ErrResponse struct {
    HttpSC int
    Error Err
}

var (
    ErrorRequestBodyParseFailed = ErrResponse{HttpSC: 400, Error: Err{Error: "Request body is not correct", ErrorCode: "001"}}
    ErrorNotAuthUser = ErrResponse{HttpSC: 401, Error: Err{Error: "User authentication failed.", ErrorCode: "002"}}
    ErrorDBError = ErrResponse{HttpSC: 500, Error: Err{Error: "DB ops failed", ErrorCode: "003"}}
    ErrorInternalFaults = ErrResponse{HttpSC: 500, Error: Err{Error: "Internal service error", ErrorCode: "004"}}
)
知识点

error_code是系统内部用来查问题的error编码,和http的status_code不是一个东西

api/dbops

处理和数据库交互的相关逻辑

api/response.go

把它抽象出来,让API的分层处理更加清晰

方法定义

入口文件

api/main.go

main放一些简单的定义性的东西,逻辑处理的东西分发给别的文件

库们


初始化Routine

func RegisterHandlers() *httprouter.Router {
    router := httprouter.New()
    router.POST("/user", CreateUser)
    router.POST("/user/:user_name", Login)
    return router
}

func main() {
    r := RegisterHandlers()
    http.ListenAndServe(":8000", r)
}

Handler实现

需要重新开一个文件handlers.go

func CreateUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
    io.WriteString(w, "Create User Handler")
}

func Login(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
    uname := p.ByName("user_name")
    io.WriteString(w, uname)
}

小结Golang处理HTTP请求

http.ListenAndServer(":8000", handler)

一个Request进来之后



0x4 数据库层设计和实现

数据库设计

数据模型的设计、数据表的设计

对照API模型,需要三个表:用户、视频、评论

建表参考:http://www.runoob.com/mysql/mysql-create-tables.html

用户

mysql> CREATE TABLE IF NOT EXISTS users(
-> id INT UNSIGNED AUTO_INCREMENT,
-> login_name VARCHAR(64) UNIQUE KEY,
-> pwd TEXT,
-> PRIMARY KEY ( id )
-> );


资源

CREATE TABLE video_info( id VARCHAR(64) NOT NULL, author_id INT UNSIGNED, name TEXT, display_ctime TEXT, create_time DATETIME, PRIMARY KEY( id ) );

id的类型为varchar,防止id溢出

author_id 和用户表的id映射

name:资源的名字

display_ctime TEXT:显示在页面上的创建时间

create_time DATETIME:video入库的时间


评论

CREATE TABLE comments( id VARCHAR(64) NOT NULL, video_id VARCHAR(64), author_id INT UNSIGNED, content TEXT, time DATETIME, PRIMARY KEY( id ) );

id类型为varchar

video_id 就是上一张表的id,应该起到「外键」的作用。

author_id 通过这个找user


这三个表的设计:


sessions

什么是sessions?

在登录网站的时候,只需要登录一次就好,之后再打开,还是登录状态。

session:标识用户还在validate状态下的标识符


session表 - 专门用来记录session的信息

CREATE TABLE sessions(session_id VARCHAR(64) NOT NULL, TTL TINYTEXT, login_name VARCHAR(64), PRIMARY KEY( session_id ) );

session_id:varchar也可以,因为登录的次数是无限的,所以要用一个范围非常大的类型表示它

TTL(time to live):sessionID的过期时间戳,如果过期,需要返回一个错误

login_name:这里没有用户ID,因为想做的简单一点。有了login_name后,就知道用户是谁了,证明它是合法的。


session的主要问题:在机制的处理上


数据库设计小结

任意两张表,不会出现除了id之外的冗余信息。

当你在表中没有重复信息出现的时候,你的表的扩展性才是最大的

mysql操作

课程mysql环境: 5.7.21

mysql指令大全: 

当然,为了偷懒不想记指令,可以使用Navicat的呀!

Golang操纵数据库

mysql的Driver:github.com/go-sql-driver/mysql

使用示例:https://github.com/go-sql-driver/mysql/wiki/Examples

Golang-SQL文档:https://github.com/golang/go/wiki/SQLInterface

Database Usage

打开数据库

sql.Open(driver, dataSourName)

db, err = sql.Open("mysql", "root:密码@tcp(localhost:3306)/库名?charset=utf8")

db.Ping():检测连接是否正常

操纵数据库

不需要返回值:db.Exec(SQL语句, 参数)

需要返回值:db.Query(SQL语句, 参数)

rows, err := db.Query("SELECT name FROM users WHERE age = $1", age)
if err != nil {
	log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
	var name string
	if err := rows.Scan(&name); err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s is %d\n", name, age)
}
if err := rows.Err(); err != nil {
	log.Fatal(err)
}

只返回一个Row:db.QueryRow

var age int64
row := db.QueryRow("SELECT age FROM users WHERE name = $1", name)
err := row.Scan(&age)

声明(statements)一个SQL:db.Prepare (推荐,这样的查询方式会有预编译机制,更安全)

age := 27
stmt, err := db.Prepare("SELECT name FROM users WHERE age = $1")
defer stmt.Close() // 需要关闭
if err != nil {
	log.Fatal(err)
}
rows, err := stmt.Query(age) // stmt.Exec // stmt.QueryRow
// process rows

代码

连数据库

func openConn() *sql.DB {
    dbConn, err = sql.Open("mysql", "root:123!@#@tcp(localhost:3306)/video_server?charset=utf8")
    if err != nil {
        panic(err.Error()) // 只有在确实无法正常完成业务逻辑的时候,才panic
    }
    return dbConn
}

说明:

OpenDSN的这个地方,实际上需要抽出来,作为配置文件。

生产环境都会这样做的,把数据库的配置、Cache连接的配置都抽出来,作为配置文件。

部署的时候,有类似CMDB的配置数据库,把配置下发过去。代码会根据配置,连它应该连的数据库,这样配置和业务可以解耦。

我们为了简便起见,抽为配置的这一步省略掉了。

操作数据库的常规思路:

func AddUserCredential(loginName string, pwd string) error {
    db := openConn()
}

这样写有问题:

  1. Golang的sql Interface是为长期存在的连接而创建的,每次openConn会造成connection的浪费
  2. 如果每个查询请求都open一次,查询完成之后需要加一个db.Close
    1. 当并发量非常大的时候,会出现「半连接」和「半关闭」的状态
    2. Server端会出现很多个Close Wait
  3. 不能每次都open一个Connection,而是「应该复用起来」

复用Connection

添加 api/dbops/conn.go

package dbops

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


var (
    dbConn *sql.DB
    err error
)


func init() {
        dbConn, err = sql.Open("mysql", "root:123!@#@tcp(localhost:3306)/video_server?charset=utf8")
    if err != nil {
        panic(err.Error()) // 只有在确实无法正常完成业务逻辑的时候,才panic
    }
}

init方法:包一旦被加载,Init方法会第一个就执行。

执行之后,开启一个连接,赋给包内的全局变量dbConn

User API逻辑处理

增加用户

func AddUserCredential(loginName string, pwd string) error {
    // 先写一个SQL
	SQLQuery := `INSERT INTO users (login_name, pwd) VALUES (?, ?)`
	stmtIns, err := dbConn.Prepare(SQLQuery) // *2
	if err != nil {
		retuen err
	}
	
	_, err := stmtIns.Exec(loginName, pwd)
	if err != nil {
		return err
	}
	defer stmtIns.Close()
	return nil
}

提示:

  1. 不要用拼接的方式(如「+」)连接query的各个部分,非常不安全,容易被撞库攻击。
  2. Prepare:所有Driver都有的好用的功能「预编译」,预编译之后不会出现安全上的数据库攻击

获取用户密码

func GetUserCredential(loginName string) (string, error) {
	pwdQuery := `SELECT pwd FROM users WHERE login_name = ?`
	stmtOut, err := dbConn.Prepare(pwdQuery)
	defer stmtOut.Close() // *3
	if err != nil {
		log.Printf("%s", err)
		return "", err
	}
	
	// 先声明需要放的东西
	var pwd string
	err = stmtOut.QueryRow(loginName).Scan(&pwd) // *1、2
	if err != nil && err != sql.ErrNoRows { // *4
		retuen "", err
	}

	retuen pwd, nil
}

提示:

  1. QueryRow: 只Queyr一个数据
  2. Scan: 写入
  3. stmtOut.Close()方法实际上可以放在defer里面
    1. 但是defer会在栈退出的时候才会调用,对性能有些许损耗
    2. 这里由于我们的函数有多个退出的口子,不可能每次退出都写一次Close,所以即使有一些性能损耗,我们也要用defer统一处理之
  4. 如果第一个QueryRow取不到东西(返回空值)的时候,一样会返回一个Object叫Raw;Object下面的scan会把这个Object的Error带出来,就是ErrNoRaws

删除用户

func DeleteUser(loginName string, pwd string) error {
	deleteQuery := `DELETE FROM users WHERE login_name = ? AND pwd = ?`
	stmtDel, err := dbConn.Prepare(deleteQuery)
	if err != nil {
		log.Printf("Delete User error: %s", err)
		return err
	}

	_, err = stmtDel.Exec(loginName, pwd)
	if err != nil {
		return err
	}
	stmtDel.Close()

	return nil
}

测试

基本的逻辑弄完了,下面,写几个UT测试业务函数

api/dbops/api_test.go

业务处理都在api里面,为了更清楚的区分其他逻辑和api,所以test统一写在api里面

package dbops

import (
	"testing"
)

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) { // *1
	t.Run("Add", testAddUser)
	t.Run("Get", testGetUser)
	t.Run("Del", testDeleteUser)
	t.Run("Reget", testRegetUser)
}

func testAddUser(t *testing.T) {
	err := AddUserCredential("azen", "123")
	if err != nil {
		t.Errorf("Error of AddUser: %v", err)
	}
}

func testGetUser(t *testing.T) {
	pwd, err := GetUserCredential("azen")
	if pwd != "123" || err != nil {
		t.Errorf("Get User Error)
	}
}

func testDeleteUser(t *testing.T) {
	err := DeleteUser("azen", "123")
	if err != nil {
		t.Errorf("Delete User Error : %v", err)
	}
}

func testRegetUser(t *testing.T) { // Delete完之后check是否正确
	pwd, err := GetUserCredential("azen")
	if err != nil {
		t.Errorf("Get User Error)
	}
	if pwd != "" {
		t.Errorf("Delete User Error")
	}
}

写UT的最佳实践

  1. init一下(由于我们的包已经有init方法了,所以不需要在test文件中做Init)
  2. truncate tables:保证每次跑测试表都是空的,初始状态
  3. run test
  4. clear data(truncate tables)

提示:

  1. 因为我们的测试顺序是固定的,为了保证顺序,必须用subTest
  2. 这样写的好处:
    1. 代码规整
    2. 可读性非常好
  3. mock data本来是应该写在Fixture里面的(表格驱动测试吗...)

每次修改完代码,在提交之前先跑Test

Video API逻辑处理

定义VideoModel

type VideoInfo struct {
	Id string
	AuthorId int // 上传者ID
	Name string
	DisplayCtime
}

添加Video

func AddNewVideo(aid int, name string) (*defs.VideoInfo, error) {
	// create uuid
	vid, err := utils.NewUUID()
	if err != nil {
		return nil, err
	}

	// creatime: 进到这个函数里的时间 - 以这个作为display ctime
	// 把creatime写到db里面,还有一个写库的时间戳,用来以后做排序之类的事情
	t := time.Now()
	ctime := t.Format("Jan 02 2006, 15:04:05") // *2

	stmtIns, err := dbConn.Prepare(`INSERT INTO video_info (id, author_id, name, display_ctime) VALUES(?, ?, ?, ?)`)
	defer stmtIns.Close()
	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}

	return res, nil
}

提示:

  1. 有现成的算法可以生成uuid
  2. f.Format函数,通过示例描述时间格式
    1. 时间原点:这串字符必须使用这个,不然就没办法正确格式化,但是可以添加自己的各种符号
    2. 没人知道这个时间原点是什么鬼...可能是Golang的彩蛋

附件:

生成uuid

package utils

import (
    "crypto/rand"
    "io"
    "fmt"
)

func NewUUID() (string, error) {
    uuid := make([]byte, 16)
    n, err := io.ReadFull(rand.Reader, uuid)
    if n != len(uuid) || err != nil {
        return "", err
    }
    // variant bits; see section 4.1.1
    uuid[8] = uuid[8]&^0xc0 | 0x80
    // version 4 (pseudo-random); see section 4.1.3
    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
}

提示:

创建video、comment的时候都需要用到相关方法,所以抽成一个包

Video的其他操作

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} // 不能把struct直接传进去吗...封个方法吧

    return res, nil
}

func DeleteVideoInfo(vid string) error {
    stmtDel, err := dbConn.Prepare("DELETE FROM video_info WHERE id=?")
    defer stmtDel.Close()
    if err != nil {
        return err
    }

    _, err = stmtDel.Exec(vid)
    if err != nil {
        return err
    }

    return nil
}

Video的相关测试

var tempvid string // 这个参数是在AddNewVideo函数的返回值里的,但是测试中没办法拿到返回值,所以用一个全局变量引着它

func TestVideoWorkFlow(t *testing.T) {
    clearTables()
    t.Run("PrepareUser", testAddUser) // 需要把用户准备好才能测Video
    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)
    }
}

Comments API逻辑处理

添加评论

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
}

展示评论

type Comment struct {
    Id string
    VideoId string
    Author string
    Content string
}
func ListComments(vid string, from, to int) ([]*defs.Comment, error) {

	query := `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(?)`

	stmtOut, err := dbConn.Prepare(query)
	defer stmtOut.Close()

	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)
	}


	return res, nil
}

评论测试

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 API逻辑处理

概念

Session不是业务entity,而是系统的entity

在使用RESTful API的时候,状态是不会保持的。为了记录用户在服务端的状态,需要有个东西保存它。

为什么用session

如果不存状态,用户之前操作的东西就会丢掉。如果每次刷网页都要用户登录一遍,是不可用的。

session vs cookie

session是一种在服务端为用户保存状态的机制

cookie是在客户端保存用户状态的机制

使用session的时候需要sessionID,当客户端为了方便访问session,会把sessionID放到cookie里。

我们今天的课程,session不会存复杂的东西,只负责用户的登录状态

流程图

当系统初始化的时候,Cache会load所有sessionID

当更新用户session的时候,需要往Cache和DB里面写两次

写两次原因:

代码实现

session是系统逻辑,需要单独拎出来 api/session

session的操作:

  1. 服务起来的时候load所有session
  2. 新用户登录的时候,需要分配一个sessionID,需要一个产生sessionID的方法
  3. 校验的时候session过期,需要返回一个是否过期的状态
package session

import (
	"time"	// *1
	"sync"	// *2
	"xxxxxx/dbops" // 自己写的数据库操作文件
	"xxxxxx/utils"	//	生成uuid用的
)

// type SimpleSession struct {
//  	Username string	// login name
//  	TTL int64 // session过期时间戳,检查用户是否过期
// }
// 放到dbops的def里

var sessionMap *sync.Map // *3

func init() {
	sessionMap = &sync.Map{}
}

//	从数据库拿出来所有的session,放入Cache中
func loadSessionsFromDB() {
	r := 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) (sessionID string) {
	id, _ := utils.NewUUID()
	ct := time.Now().UnixNano()/1000000 // 毫秒表示
	ttl := ct + 30 * 60 * 1000 // serverside session valid time : 30 min

	ss := &defs.SimpleSession{Username: un, TTL: ttl}
	sessionMap.Store(id, ss)

	dbops.InsertSession(id, ttl, un)

	return id
}

func IsSessionExpried(sid string) (un string, bool) {
	ss, ok := sessionMap.Load(sid)
	if ok {
		ct := time.Now().UnixNano() / 100000 // 毫秒级表示
		if ss.(*defs.SimpleSession).TTL < ct {
			// 删除session
			deleteExpiredSession(sid)
			return "", true
		}
		
		return ss.(*defs.SimpleSession).Username, false
	}

	return "", true
}


func deleteExpireSession(sid string) {
	sessionMap.Delete(sid)
	dbops.DeleteSession(sid)
}

提示:

  1. session是否过期 - 需要time
  2. 需要一个存session的地方 - sync
  3. Cache:为什么不用一个web的Redis这种类型的Cache?
    1. 当一个系统中增加一个模块的时候,系统的复杂度必然增加。
    2. 当系统复杂度超出了业务增量、能带给你的好处之后,会增加你的不必要负担。综合考虑,没必要
    3. 使用Go的map做内部缓存,在每个节点上都会缓存全量数据
      1. 有关用户的数据量非常有限,不会特别多,使用内建Cache - Sync.map就完全足够了
    4. sync.Map vs 内建map
      1. sync.Map是Golang 1.9之后加入的,是一个线程安全的map
        1. 自己实现了一套线程安全的map
        2. 在「读」上面,优化的非常极致
        3. 「写」上有一些问题,内部每次写都要加一个全局锁
      2. 内建map:不能支持并发的读写
        1. 两个以上的协程读写的话,会panic

dbops中session交互的操作,放在/api/dbops/internal.go

package dbops

func InsertSession(sid string, ttl int64, uname string) error {
	ttlstr := strconv.FormatInt(ttl, 10)
	stmtIns, err := dbConn.Prepare("INSERT INTO session (session_id, TTL, login_name) VALUES (?, ?, ?)")
	defer stmtIns.Close()
	if err != nil {
		return err
	}
		
	_, err := stmtIns.Exec(sid, ttlstr, login_name)
	if err != nil {
		return err
	}

	return nil
}

func RetrieveSession(sid string) (*defs.SimpleSession, error) {
	ss := &defs.SimpleSession({}
	stmt, err := dbConn.Prepare(`SELECT TTL, login_name FROM sessions WHERE session_id=?`)
	defer stmt.Close()

	if err != nil {
		return nil, err
	}

	var ttl string
	var unane string
	stmt.QueryRow(sid).Scan(&ttl, &uname)
	if err != nil && err != sql.ErrNoRows {
		return nil, err
	}
	
	if res, ttlint := strconv.ParseInt(ttl, 10, 64); err == nil {
		ss.TTL = res
		ss.Username = uname
	} else {
		return nil, err
	}
	return ss, nil
}

func RetrieveAllSessions() (*sync.Map, err) {

}

func DeleteSession(sid string) error {

}

API前端部分

API前端部分包括:

HTTP中间件

HTTP 中间件提供了一个方便的机制来过滤进入应用程序的 HTTP 请求,例如,Auth 中间件验证用户的身份,如果用户未通过身份验证,中间件将会把用户导向登录页面,反之,当用户通过了身份验证,中间件将会通过此请求并接着往下执行。

当然,除了身份验证之外,中间件也可以被用来运行各式各样的任务,如:CORS 中间件负责替所有即将离开程序的响应加入适当的标头;而日志中间件则可以记录所有传入应用程序的请求。

实现MiddleWare

原理:duck type - 组合

httprouter.Router,实际实现的是httpHandler中的ServeHTTP函数。我们先劫持这个函数,然后自己实现一下httpHandler,实现的时候先写自己的逻辑,再调用一下它的默认实现就好

type middleWareHandler struct {
	r *httprouter.Router
}

func NewMiddleWareHandler(r *httprouter.Router) http.Handler {
	m := middleWareHandler{}
	m.r = r
	return m
}

// 实现协议方法
func (m middleWareHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	//	劫持它本身的这个函数,自己实现一些通用的功能
	validateUserSession(r)  //	添加自定义鉴权
	m.r.ServeHTTP(w,r)
}

func RegisterHandlers() *httprouter.Router {
	router := httprouter.New()
	router.POST("/user", CreateUser)
	router.POST("/user/:user_name", Login)
	return router
}


func main() {
	r := RegisterHandlers()
	//	http.ListenAndServe(":8000", r) // 旧写法

	mh := NewMiddleWareHandler()
	http.ListenAndServe(":8000", mh)  // 实现MiddleWare的写法
}

MiddleWare功能 - 鉴权

添加api/auth.go文件

package main

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

var HEADER_FIELD_SESSION = "X-Session-Id"
var HEADER_FIELD_UNAME = "X-User-Name" // *1


//	检查用户是否合法
func validateUserSession(r *http.Request) bool {
	sid := r.Header.Get(HEADER_FIELD_SESSION)
	if len(sid) == 0 {
		return false
	}

	uname, ok := session.IsSessionExpired(sid)
	if ok {
		return false
	}

	r.Header.Add(HEADER_FIELD_UNAME, uname) // 如果鉴权成功,则把username加入到Header中,方便请求的后续处理

	return true
}

func ValidateUser(w http.ResponseWriter, r *http.Response) bool {
	uname := r.Header.Get(HEADER_FIELD_UNAME)
	if len(uname) == 0 {
		sendErrorResponse()
		return false
	}
	return true
}

提示:

  1. X开头的字段,都是HTTP协议中的自定义Header
    1. 我们将这两种Header加入到HTTP的原生headers中,构成鉴权过程


添加类型定义

type SignedUp struct {
	Success bool `json:"success"`
	SessionId string `json:"session_id"`
}


继续完成Handler部分

func CreateUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
    res, _ := ioutil.ReadAll(r.Body) // 从request里面拿出Body - POST方法嘛,必有body
	ubody := &defs.UserCredential{}

	if err := json.Unmarshal(res, ubody); err != nil {  // *1
		sendErrorResponse(w, defs.ErrorRequestBodyParseFailed)
		return
	}

	if err := dbops.AddUserCredential(ubody.Username, ubody.Pwd); err != nil {
		sendErrorResponse(w, defs.ErrorDBError)
	}

	//	写入正确,创建session
	id := session.GenerateNewSessionId(udody.Username)
	su := &defs.SignUp{Success: true, SessionId: id}

	if resp, err := json.Marshal(su); err != nil {
		sendErrorResponse(w, defs.ErrorInternalFaluts)
	} 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)
}


实现sendErrorResponse

func sendErrorResponse(w http.ResponseWriter, errResp defs.ErrResponse) {
	w.WriteHeader(errResp.HttpSC)

	resStr, _ := json.Marshal(&errResp.Error)
	io.WriteString(w, string(resStr))
}

func sendNormalResponse(w http.ResonseWriter, resp string, sc int) {
	w.WriterHeader(sc)
	io.WriteString(w, resp)
}

提示:

  1. json.Unmarshal(xxx, yyy) - 把xxxjson序列化为yyy结构体


以上,一套API已经完成了。可以编译一下,测下能不能跑通。

其他API和这个相同,自己实现之即可

TODOs

  1. 数据库DSN作为配置文件下发