顶点云(应用)用户结构设计

介绍顶点云应用程序服务器中用户 ADT 的设计、结构,实现用户模型框架。

用户 ADT 设计

  • 项目简介 中,我们已经设计好了数据库中用户表结构,下面设计保存在系统内存中的活动用户的数据结构。
  • 保存在系统内存的用户数据结构中应当包括如下信息,以使系统能够区分用户状态、验证用户身份、接收用户指令、给予用户反馈:
    • 用户 Id:数据库中用户表的主键,自增,可区分用户身份,可用于检索用户
    • 用户名:用于区分用户身份,方便用户记忆,在用户表中不可重复
    • 接收指令模块:用于接收用户发送的指令
    • 推送消息模块:用于向用户推送消息
    • 执行模块:用于执行接收到的合法命令
    • 其它:包括其它需要存储的零碎信息
  • 用户接口分析:用户 ADT 需要能提供至少如下大致的接口(参数可能和最终实现的代码不同,但函数意义相同)。
    • GetUsername() string:获取用户用户名
    • GetId() int64:获取用户 Id
    • DealWithRequests():接收、分析并处理用户发送的指令
    • DealWithTransmission():处理用户请求的文件传输
    • Logout():登出用户
  • 在工程目录下新建 cstruct 目录,要编写的模块名为 cstruct。在该目录下创建文件 cuser.go, 以下代码均在该文件中编辑。

用户数据结构

  • 用户结构需要保存自己的 Id、用户名。
  • 用户结构需要保存至少两个活动连接,根据最初设计的协议,其中一个用来接收用户发送的文本请求指令并反馈给用户可用文本表达的信息,另一个用来向用户推送请求。
  • 根据协议,文件传输请求需要启动一个新的活动连接,该连接的建立需要验证用户的 token ,因此用户数据结构需要保存登录时使用的 token
  • 用户还需要保存当前活动的文件传输连接。
  • 综上,用户 struct 应当维持以下基本内容:
    • idint64 类型
    • usernamestring 类型,用户名
    • listen:命令传输、文本传输连接,Transmitable 类型
    • infos:向用户推送信息的连接,Transmitable 类型
    • token:用户登录时保存的 token 值,string 类型
    • worklist []Transmitable:用户当前的文件传输连接列表
  • 拓展基本用户接口,增加了如下方法:
    • GetToken() string :获取用户登录时保存的 token 值(此方法破坏了数据的隐藏性,应当使用如 TokenVerify(token_to_check) 这样的函数来验证)
    • SetToken(string) bool:为用户设置新 token 值,此方法在用户登录时调用
    • SetListener(Transmitable) bool:为用户设置命令、文本交互连接
    • SetInfos(Transmitable) bool:为用户设置消息推送连接
    • AddTransmit(Transmitable) bool:用户启动一个新的文件传输连接
    • RemoveTransmit(Transmitable) bool:用户移除一个文件传输连接

用户接口实现

  • 以上接口均可根据函数名称获知其含义,实现均非常简单,其中 Set()Get() 系列函数和普通的获取、设置值的函数相似,不在此展示。
  • Logout() 实现如下,断开所有活动连接:
1
2
3
4
5
6
7
8
9
10
11
// cuser.go
func (u *cuser) Logout() {
if u.listen != nil {
u.listen.Destroy()
}
for _, ut := range u.worklist {
if ut != nil {
ut.Destroy()
}
}
}
  • AddTransmit(Transmitable) bool 实现如下,向用户的 worklist 中添加一个活动连接,以下代码中将 transmit 包导入为 trans,因此参数为 trans.Transmitable 类型。函数实现中 AppendTransmitable([]Transmitable, Transmitable) 的功能是向传输列表中附加一项,该函数将在 ulist.go 文件中实现:
1
2
3
4
5
6
7
8
9
// cuser.go
func (u *cuser) AddTransmit(t trans.Transmitable) bool {
if u.worklist == nil {
u.worklist = make([]trans.Transmitable, 0, 2)
}
tempLen := len(u.worklist)
u.worklist = AppendTransmitable(u.worklist, t)
return len(u.worklist) != tempLen
}
  • RemoveTransmit(Transmitable) bool 实现如下,它将参数和用户活动连接挨个比对:
1
2
3
4
5
6
7
8
9
10
11
// cuser.go
func (u *cuser) RemoveTransmit(t trans.Transmitable) bool {
for i, ut := range u.worklist {
if ut == t {
u.worklist = append(u.worklist[:i], u.worklist[i+1:]...)
t.Destroy()
return true
}
}
return false
}
  • DealWithRequests() 的实现需要根据设计的命令协议,创建一个 cuser_operation.go 文件,将该函数保存在该文件中。

处理请求

  • DealWithRequests() 函数根据协议设计的命令实现,主体由 switch 选择命令对应的行为,switchfor 循环包裹,每次执行完命令后,由 RecvBytes() 阻塞等待用户发送新命令。以下实现的 DealWithRequests(*sql.DB) 包含了 rmcpmvlsforktouchchmod 以及 send 指令的选择和实现,对应这些命令的函数将分别编写。因为需要对数据库做增删查改,所以 DealWithRequests() 函数传入了一个数据库对象指针作为参数。 此处的实现破坏了内部细节的不可见性,数据库不应由用户 ADT 操作,可以采用 Proxy 模式为用户对数据库操作的请求做转发 ,但当初并未考虑到,重构代码时会修正采用的设计模式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// cuser_operation.go
package cstruct
func (u *cuser) DealWithRequests(db *sql.DB) {
u.curpath = "/"
fmt.Println(u.username + " Start deal with args")
for {
recvB, err := u.listen.RecvBytes()
if err != nil {
return
}
command := string(recvB)
fmt.Println(command)
switch {
case len(command) >= 2 && strings.ToUpper(command[:2]) == "RM":
u.rm(db, command)
case len(command) >= 2 && strings.ToUpper(command[:2]) == "CP":
u.cp(db, command)
case len(command) >= 2 && strings.ToUpper(command[:2]) == "MV":
u.mv(db, command)
case len(command) >= 2 && strings.ToUpper(command[:2]) == "LS":
u.ls(db, command)
case len(command) >= 4 && strings.ToUpper(command[:4]) == "SEND":
u.send(db, command)
case len(command) >= 4 && strings.ToUpper(command[:4]) == "FORK":
u.fork(db, command)
case len(command) >= 5 && strings.ToUpper(command[:5]) == "TOUCH":
u.touch(db, command)
case len(command) >= 5 && strings.ToUpper(command[:5]) == "CHMOD":
u.chmod(db, command)
default:
u.listen.SendBytes([]byte("Invalid Command"))
}
}
}
  • 对于 DealWithRequests(db *sql.DB) 中可选的每个指令执行函数,用户 ADT 需要将它们分别实现为私有方法。例如,根据此前设计的用户功能,我们要实现的顶点云的 touch 函数实现如下,注意, 此函数的实现是非常失败的 !事实上,要实现的每个指令执行函数都非常复杂, touch 函数仅仅是这些函数中最简单的一个。因为指令函数需要通过对数据库的修改实现指令逻辑意义,并且不断检查是否有错误发生,所以 频繁出现的 nil == 验证扰乱了代码的正常逻辑 。一个可行的解决方案是, 此类函数运行时错误通过异常来触发,在函数执行结束后由 defer 指令统一 recover 并做出对应反馈 ,这一点在重构时应当涉及。可以看到,touch 函数的执行代码中,大部分都是涉及数据库的查询、更新操作,每次查询、更新都需要检查是否反馈了错误,所以这部分业务逻辑非常繁琐。此外,touch 代码中使用到了 goto 关键字,因此所有的变量都需要在 goto 语句之前声明,这也 破坏了就近原则 。作为一个设计失败的初版代码,放置在这里对我自己起到警醒作用,希望对后来者也能避免类似的错误:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// cuser_operation.go
type path_name struct {
u_path string
u_name string
}
func (u *cuser) touch(db *sql.DB, command string) {
var valid bool = true
var execString string
var subpaths []path_name
var queryRow *sql.Row
var isdir, recordCount int
var err error
args := generateArgs(command, 4)
if args == nil {
fmt.Println("args format wrong")
valid = false
goto TOUCH_VERIFY
}
isdir, err = strconv.Atoi(args[3])
if err != nil || strings.ToUpper(args[0]) != "TOUCH" || !isFilenameValid(args[1]) ||
!isPathFormatValid(args[2]) || isdir != 0 && isdir != 1 {
if err != nil {
fmt.Println(err.Error())
}
valid = false
goto TOUCH_VERIFY
}
subpaths = generateSubpaths(args[2])
for _, subpath := range subpaths {
queryRow = db.QueryRow(fmt.Sprintf(`select count(*) from ufile where ownerid=%d and
filename='%s' and path='%s' and isdir=1`, u.id, subpath.u_name, subpath.u_path))

if queryRow == nil {
valid = false
goto TOUCH_VERIFY
}
err = queryRow.Scan(&recordCount)
if err != nil {
fmt.Println(err.Error())
valid = false
goto TOUCH_VERIFY
}
if recordCount <= 0 {
_, err = db.Exec(fmt.Sprintf(`insert into ufile values(null, %d, -1, '%s', '', '%s', 0, 0,
'%s', 1, '', 1)`, u.id, subpath.u_path, time.Now().Format("2006-01-02 15:04:05"), subpath.u_name))

if err != nil {
fmt.Println(err.Error())
valid = false
}
}
}

execString = fmt.Sprintf(`insert into ufile values(null, %d, -1, '%s',
'', '%s', 0, 0, '%s', 1, '', %d)`,

u.id, args[2], time.Now().Format("2006-01-02 15:04:05"), args[1], isdir)
fmt.Println(execString)
_, err = db.Exec(execString)
if err != nil {
fmt.Println(err.Error())
valid = false
}
TOUCH_VERIFY:
if !valid {
u.listen.SendBytes(auth.Int64ToBytes(int64(400)))
} else {
u.listen.SendBytes(auth.Int64ToBytes(int64(200)))
}
}
  • 同样,当系统需要为用户添加新指令时,需要修改用户 ADT 中的 DealWithRequsts() 共有方法。这也破坏了 对功能扩展开放,对修改封闭 的原则,一个可行的替代方法是 使用 Strategy 模式封装指令,由其代理用户 ADT 中对指令的执行方案 。我们可以创建一个表驱动的 Strategy 基类,当添加新方法时,向 Strategy 类注册该方法并在具体子类中实现。

用户结构的列表操作

  • 此前在 AddTransmit() 函数中使用到的 AppendTransmitable([]Transmitable, Transmitable)ulist.go 文件中的函数,该文件专门用来处理涉及 cstruct 中各类结构的列表操作。其中 AppendTransmitable 函数实现如下,该函数判断当前活动列表是否已经超出设置的最大长度,若仍可加入,则扩展列表:
1
2
3
4
5
6
7
8
9
// ulist.go
func AppendTransmitable(slice []trans.Transmitable, data ...trans.Transmitable) []trans.Transmitable {
if len(slice)+len(data) >= conf.MAXTRANSMITTER {
slice = append(slice, data[:conf.MAXTRANSMITTER-len(slice)]...)
} else {
slice = append(slice, data...)
}
return slice
}
  • ulist.go 中还存在一个 AppendUser() 函数,该函数用于向一个用户列表附加新用户,可以猜测到,该函数将在服务器处理登录事件时使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
// ulist.go
func AppendUser(slice []User, data ...User) []User {
m := len(slice)
n := m + len(data)
if n > cap(slice) {
newSlice := make([]User, (n+1)*2)
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:n]
copy(slice[m:n], data)
return slice
}
  • 服务器需要根据用户名检索用户是否已登录,使用 UserIndexByName 可从一个用户列表中选择某一用户名的用户,该函数实现如下:
1
2
3
4
5
6
7
8
9
// ulist.go
func UserIndexByName(slice []User, name string) User {
for _, uc := range slice {
if uc.GetUsername() == name {
return uc
}
}
return nil
}

专栏目录:顶点云设计与实现
此专栏的上一篇文章:顶点云(应用)传输、认证单元测试
此专栏的下一篇文章:顶点云(应用)服务器逻辑实现

原创作品,允许转载,转载时无需告知,但请务必以超链接形式标明文章原始出处(http://blog.forec.cn/2016/11/19/zenith-cloud-5/) 、作者信息(Forec)和本声明。

分享到