开发者学堂课程【Go 语言核心编程 - 面向对象、文件、单元测试、反射、TCP 编程:海量用户通讯系统-显示在线用户列表(6)】学习笔记,与课程紧密联系,让用户快速学习知识。
课程地址:https://developer.aliyun.com/learning/course/626/detail/9818
海量用户通讯系统-显示在线用户列表(6)
内容简介:
一、思路分析
二、代码实现
三、思考问题
一、思路分析
1. 代码实现步骤分析
写代码首先从服务器开始走,因为最关键的点便是服务器推送消息给所有在线客户,所以肯定要在一定时候(即当有一个用户登录时),这时是最容易的,也就是当一个用户登录的时候,就调出推送机制,再告诉”我上线了”,这个要从服务器开始写。
2. 代码实现思路分析
代码很多,首先分析一下:第一个首先会去原先 user 结构体里面增加一个状态将其称之为 UserStatus ,这个 UserStatus 代表不同用户的状态,这是第一个;第二个去增加一个新的一个通讯的消息类型,因为服务器端要推送一个消息给客户端必须定一个协议,不能发一段字符串过来,所以这里定的协议是 UserStatusNotifyMes ,其中 Mes是什么?因为是推送给一个人的,所以很简单,即告诉是谁、状态是什么就可以了,相当于就是我把我的状态告诉你。写完这个以后写 NotifyOtherUserOnline ,此方法写在 UserProcess 里,即你把 ID给我,我便去遍历所有 GetAllOnlineUsers ,这里很重要。在服务器蹭的一下遍历 map 里的所有人,然后把当前登录的人推送给所有人,这个代码就写完了,写完此地方大体思路就完成了。服务器推送完成之后,即当一个用户登录成功以后去通知所有人”我上线了“。客户端这边的变化体现在推送过去的消息,应该在哪儿?就是在不停的推送消息,即 NotifyMesOnline ,上线会有一个 WritePkg ,那么 write 类型应该是 userStatusNotifyMes ,在哪里去接收?在客户端一头会维护一个 map ,再会去另一头写一个类似于 userMgr ,在这里会去显示在线列表,在 updateUserStatus 更新状态用户,会接收整个在线用户状态,如果传过来的类型是server 端的协程,发现过后就会去更新状态,更新一下即可,思路便是这样,虽然讲起来简单但是实现起来很难。
二、代码实现
1. 在common/message/user.go文件内编写的代码
首先打开 common/message/user.go 文件,在其内添加一个新的字段,如下:
type User struct {
//确定字段信息
//为了序列文化和反序列化成功,我们必须保证
//用户信息的 json 字符串的 key 和结构体的字段对应的 tag
UserId int `json:”userId”`
UserPwd string `json:”userPwd`
UserName string `json:”userName”`
UserStatus int `json:”userStatus”` //用户状态。。
}
2.在common/message/message.go文件内编写的代码
再打开 common/message/message.go ,在其内定义一个新的消息类型,如下:
//为了配合服务器端推送用户状态变化的消息
type NotifyUserStatusMes struct {
UserId int `json:”userId”` //用户id
Status int `json:”status”` //用户的状态
}
3. 在common/message/user.go文件内编写代码
有了以上两个东西过后既然有消息了,就得定义一个对应的类型,想一想推送过后是否会有响应?有可能有,但是现在并不需要,因为现在发起的是主动的推送,其不像之前注册和登录是客户端发起的请求说”我要干什么”,服务器再说结果是什么,现在是服务器主动推送东西给客户端,这便是主动推送,可能之前听过此概念,现在终于见到了,这个称之为主推,即服务器推个东西给你,为什么有些人上微信和 QQ 会看到有些广告,因为那些都是服务器推给你的,现在相当于推送一个用户的状态的 message ,所以在common/message/user.go 中将部分代码修改或增加为:
const (
LoginMesType = “LoginMes”
LoginResMesType = “LoginResMes”
RegisterMesType = “RegisterMes”
RegisterResMesType = “RegisterResMes”
NotifyUserStatusMesType = “NotifyUserStatusMes”
)
4. 在common/message/message.go文件内定义一个常量
下一步写两个特别重要的方法,找到服务器端的server/process/userProcess.go ,因为它是专门处理用户相关的,曾经讲过将来 userProcess 是处理登录、注册、注销、包括用户列表管理的,将来用户列表管理、用户状态是跟用户相关的。写点对点聊天和群聊就写 smsProcess.go ,若还想写发送图片、文件、视频的,可以再写一个称为 viop 的,可以用此东西来处理这些,即各司其职。但首先在 common/message/message.go 文件内定义一个常量,代码如下:
//这里我们定义几个用户状态的常量
const (
UserOnline = iota
UserOffline
UserBusyStatus
)
5. 在server/process/userProcess.go文件内编写代码
接着在 server/process/userProcess.go 文件内编写代码,如下:
//这里我们编写通知所有在线用户的方法
// userId 要通知其它的在线用户,我上线
func (this *UserProcess) NotifyOthersOnlineUser(userId int){
//遍历onlineUsers,然后一个一个的发送NotifyUserStatusMes
for id, up := range this.onlineUsers {
//过滤到自己
if id == userId {
continue
}
//开始通知【单独的写一个方法】
}
}
func (this *UserProcess) NotifyMeOnline(userId int) {
//组装我们的 NotifyUserStatusMes
var mes message.Message
mes.Type = message.NotifyUserStatusMesType
var notifyUserStatusMes message.NotifyUserStatusMes
notifyUserStatusMes.UserId = userId
notifyUserStatusMes.Status = message.UserOnline
//将 notifyUserStatusMes 序列化
data, err := json.Marshal(notifyUserStatusMes)
if err != nil {
fmt.Println(“json.Marshal err=”, err)
return
}
//将序列化后的 notifyUserStatusMes 赋值给 mes.Data
mes.Data = string(data)
//对 mes 再次序列化,准备发送。
data, err = json.Marshal(mes)
if err != nil {
fmt.Println(“json.Marshal err=”, err)
return
}
//发送,创建我们 Transfer 实例,发送
tf := &utils.Transfer{
Conn : this.Conn,
}
err = tf.WritePkg(data)
if err != nil {
fmt.Println(“NotifyMeOnline err=”, err)
return
}
}
三、思考问题
1. 关于”提出来”的问题
请思考一个问题,在上文 server/process/userProcess.go 文件中添加的代码它实际上永远都是 data ,相当于说有一份 data 发给10个人、20个人、100个人,其中这个 data 可以将其提出去,那么 Transfer 能不能提出去?有可能会想统一用一个 Transfer 就可以了,写那么多 Transfer 有什么用,但是如果将 Transfer 提出去了,就只能发给一个人了,且永远只能发给一个人,因为其 Conn 不一样,每次 this 传进去的 up 不一样会导致 this 也不一样,因为最终实现的目的是把 userProcess Conn 的 Conn 拿出来并一个一个的发,所以绝对不能把 Transfer 提出去,所以不能提出以下这段代码:
//发送,创建我们Transfer实例,发送
tf := &utils.Transfer{
Conn : this.Conn,
}
提出去会发现只有一个人能收到最终消息,其他人全部一个消息都收不到,但是可以提出以下这段代码:
//这里我们编写通知所有在线用户的方法
// userId 要通知其它的在线用户,我上线
func (this *UserProcess) NotifyOthersOnlineUser(userId int){
//遍历onlineUsers,然后一个一个的发送NotifyUserStatusMes
for id, up := range this.onlineUsers {
//过滤到自己
if id == userId {
continue
}
//开始通知【单独的写一个方法】
}
}
func (this *UserProcess) NotifyMeOnline(userId int) {
//组装我们的 NotifyUserStatusMes
var mes message.Message
mes.Type = message.NotifyUserStatusMesType
var notifyUserStatusMes message.NotifyUserStatusMes
notifyUserStatusMes.UserId = userId
notifyUserStatusMes.Status = message.UserOnline
//将 notifyUserStatusMes 序列化
data, err := json.Marshal(notifyUserStatusMes)
if err != nil {
fmt.Println(“json.Marshal err=”, err)
return
}
//将序列化后的 notifyUserStatusMes 赋值给 mes.Data
mes.Data = string(data)
//对 mes 再次序列化,准备发送。
data, err = json.Marshal(mes)
if err != nil {
fmt.Println(“json.Marshal err=”, err)
return
}
2. 关于调用方法位置的问题
历经千辛万苦把发送代码写完,那么写完过后在哪里去调用此方法比较合理?因为现在已经编写了一个用户上线过后就通知他,那显然调用就应该放在用户登录成功的位置,登录成功或者用户状态发生变化的时候去调用,现在因为只编写了在线的状态,还没有编写离线,离线很简单就是当某人要注销或者退出之前或者检测到没有连接的时候,再去把状态改成离线即可。现在只有上线的状态,所以此函数的调用应该放在登录成功的位置,在 serve/process/userProcess.go中找到登录成功的位置,并添加一段代码,如下:
} else {
loginResMes.Code = 200
//这里,因为用户登录成功,我们就把该登录成功的用放入到userMgr
//将登录成功的用户的 userId 赋给 this
this.UserId = loginMes.UserId
userMgr.AddOnlineUser(this)
//通知其他的在线用户,我上线了
this.NotifyOthersOnlineUser(loginMes.UserId)
//将当前在线用户的 id 放入到 loginResMes.UsersId
//遍历 userMgr.onlineUsers
for id, _ := range userMgr.onlineUsers {
loginResMes.UsersId = append(loginResMes.UsersId,id)
}
fmt.Println(user,””)
}
说明:因为此 NotifyOthersOnlineUser 是属于 this 管理的,所以直接用 this 调用就可以了,然后把当前登录的 ID 传进去,即loginMes.UserId,虽然增加的代码很简单就一小段,但是背后写了很多代码。
3. 关于上线通知的问题
可以想象这样一个场景,有一个 A 客户端上线了,假设还有 B 客户端、 C 客户端,假设 A 客户端和 B 客户端已经上线了, C 客户端一登录成功过后,他把自己也加进去了,相当于就是 map 里面已经有 A 客户端和 B 客户端了, C 客户端一上线服务器就遍历了整个map ,然后把 map 里面的 Conn 拿到过后通知 A 客户端和 B 客户端说” C 客户端上线了”,服务器端的代码显然调一下就完成了,这样服务器端的代码便写完了。