开发者学堂课程【Go 语言核心编程 - 面向对象、文件、单元测试、反射、TCP 编程:海量用户通讯系统-显示在线用户列表(7)】学习笔记,与课程紧密联系,让用户快速学习知识。
课程地址:https://developer.aliyun.com/learning/course/626/detail/9819
海量用户通讯系统-显示在线用户列表(7)
内容简介:
一、思路分析
二、代码测试
三、代码整理
一、思路分析
1. 代码整体思路分析
现在开始写客户端,客户端这块要想:它已经把东西给你了,那应该将东西要送到哪个位置?它把结构送到哪儿了?比如上线了之后,通过服务器端之后遍历了,上来过后调用了一下NotifyOthersOnlineUser, 然后它里面就去遍历所有在线用户,然后再将其一个发回去,最后它的关键点是WritePkg ,这时需要想write到哪儿去了?把这个NotifyUser Message包会送到什么地方去?这个地方一定要分析出来,因为都知道在客户端这边,其实它们早就一直在等待,终于发现有人发东西回来了,且在chatroom/process/server.go内启了一个跟服务器保持通讯的一个协程,一直在这儿等即客户端正在等待读取服务器发送的消息,终于等到了一个类型,那么这个类型现在干什么?把message拿到后直接跑路了,其中写了一句话:如果读到消息,下一步处理逻辑,这是曾经编写代码时留的逻辑口在这里, 会发现这地方都是有思考的,并不是乱七八糟去写的。
2.代码实现处理方法分析
现在去处理,处理最好的方式肯定就是用先看返回的消息的类型,这就又是一个套路了,所以返回什么类型就处理什么类型,那代码应该这样去写,如下:
for {
fmt.Println(“客户端正在等待读取服务器发送的消息”)
mes, err := tf.ReadPkg()
if err != nil {
fmt.Println(“tf.ReadPkg err=”, err)
return
}
//如果读取到消息,又是下一步处理逻辑
switch mes.Type {
case message.NotifyUserStatusMesType
: //有人上线了
//1.取出NotifyUserStatusMesType
//2.把这个用户的信息,状态保存到客户map[int]User中
//处理
default :
fmt.Println(“服务器端返回了未知的消息类型”)
}
//fmt.Println(“mes=%v\n”,mes)
}
说明:其中方法1因为NotifyUserStatusMes里面包含了一个用户的ID和其状态,就是common/message/message.go中的UserId和Status,有Id和Status就说明知道有个人来了,但是这个人来了过后怎么处理?不能直接显示,这个地方就意味着第二步得把这个加入到本身客户端内维护的map里面去,因为它既然来了,肯定要把它加进去,比如它的状态是什么,之前讲过一个重要的思路就是让客户端也维护一个map,若不维护它,那服务器只能硬生生的把所有的都返回,代价太高了,所以现在还得有一个事情,就是上文代码中的方法2,有个问题即没有map,map内还是空的,分析到这个地方会马上觉得map还没有写完,所以现在还不能着急,还得先把map写出来,所以当看这个代码就感觉有点跳跃,就是突然一下会感觉怎么到这边还得写这个东西,还得又回头写,还得管服务器那边,所以说写网络通讯时如果是两个人写,那么这个项目经理最主要的就是先订一份协议,定好他们给编写代码人员什么编写人员再给其公司带来什么,实际上给的message便是这样的,返回一个东西就可以了,其他不用管,之前在做近看协同执行的时候便是这样订的,就是老大就说了一句话:我给你一个这样的包,你返回的格式是什么就行了,至于你怎么返回都是你的事情。现在没有别的办法,就是还得搞一个map,这个map刚才已经分析出来了,它是长这个样子的:map【int】User,那此时把它放在哪里比较合适呢?
3.map的放置位置分析
现在处理这个东西也需要写一个专门来管理此东西的文件会比较好,所以在chatroom内的client内的process中新建一个文件为userMgr.go,这个跟服务器不一样,这是它管理(维护)客户端的User,这个是一直在的。首先在chatroom/client/process/userMgr.go中编写代码,写package,它的包是process,把这个写完之后再写一个聊天,基本就通了,即通讯这块基本上以后就可以没问题了,假设你将来做通讯,因为Go语言最核心的是做服务器的,而做服务器里面最核心的功能就是做数据通道的,通讯、协程、协议、后台的服务器全部都在这里,Go语言学的精华就是这里,比如学Java时,Java想学好学到位,面向对象它整体的东西都在网络这块全部体现出来,面向对象、文件的操作、数组的操作、机制、包括思维的锻炼全部在这里,所以为什么很多老师在讲这个Java时他们一定是把这个最重要的放在这里,就是全部都讲完了,包括多线程,多线程学完了才去讲综合的东西,包括文件、数据库等全部都在这里面了,所以此案例很重要,不要觉得这个是不是没用,这个已经是核心了,那些天天写界面的,那个没有用,那是前端的人该做的事情。继续在chatroom/client/process/userMgr.go将代码编写完成,如下:
package process
import (
“fmt”
“go_code/chatroom/common/message”
)
//客户端要维护的map
var onlineUsers map[int]*message.User = make(map[int]*message.User, 10)
4.关于map初始化工作位置的分析
那问题来了,如果客户端要维护map,那它初始化的工作是在哪里发生的?这个后面是来一个加一个,但是它初始化的工作在哪里完成是比较合理的?之前在客户端登录的时候即它第一次登录的时候,它会首先把服务器所有在线的关心的ID整个拉下来,在那里面有UserId的切片,即它初始化的工作应该是在用户登录的时候给它搞进去的,因为它第一次登录时会把所有的在线的人一股脑全部扯下来, 所以说这个初始化的工作我们肯定是要在chatroom/client/process/userProcess.go中输入以下代码:
//fmt.Println(“登录成功”)
//可以显示当前在线用户列表,遍历loginResMes.UsersId
fmt.Println(“当前在线用户列表如下”)
for _, v := range loginResMes.UsersId {
//如果我们要求不显示自己在线,下面我们增加一个代码
if v == userId {
continue
}
fmt.Println(“用户id:\t”, v)
//完成客户端的onlineUsers完成初始化
user := &message.User{
UserId : v,
UserStatus : message.UserOnline,
}
onlineUsers[v]= user
}
fmt.Print(“\n\n”)
5.如何将用户状态保存到map
上面已经有两个方法了,现在就差client/process/server.go中 客户端在这里有个server 怎么去处理?把用户状态保存到map中去,那怎么保存?这就需要专门写一个方法再做这个事情,它不是那么简单的,因为它返回的是消息结构体,并不是一个简单的User,所以说还要对它进行一个解析来处理,现在这个代码呢就开始写了 在chatroom/client/process/userMgr.go中继续编写代码,如下:
//在客户端显示当前在线用户
func outpuOnlineUser() {
//遍历一把onlineUsers
fmt.Println(“当前在线用户列表:”)
for id, _ := range onlineUsers{
//如果不显示自己
fmt.Println(用户id:\t”, id)
}
}
//编写一个方法,处理返回的NotifyUserStatusMes
func updateUserStatus(notifyUserStatusMes *message.NotifyUserStatusMes) {
//适当优化
user, ok := onlineUsers[notifyUserStatusMes.UserId]
if lok { //原来没有
user := &message.User{
UserId : notifyUserStatusMes.UserId,
}
}
UserStatus : notifyUserStatusMes.Status,
onlineUsers[notifyUserStatusMes.UserId] = user
outputOnlineUser()
}
6.调用问题的分析
这文件内代码就写完了,写完以后回头到chatroom/client/process/server.go中,这里还等着调用,在这地方拿到NotifyUserStatusMes过后,要更新的时候是要传一个NotifyUserStatusMes,所以需要将其反序列化一下,因为message已经拿到了,再去取出其中的NotifyUserStatusMes就可以,在chatroom/client/process/server.go中做一个简单的处理,如下:
//如果读取到消息,又是下一步处理逻辑
switch mes.Type {
case message.NotifyUserStatusMesType
: //有人上线了
//1.取出NotifyUserStatusMesType
var notifyUserStatusMes message.NotifyUserStatusMes
json.Unmarshal([]byte(mes.Data), ¬ifyUserStatusMes)
//2.把这个用户的信息,状态保存到客户map[int]User中
updateUserStatus(¬ifyUserStatusMes)
//处理
default :
fmt.Println(“服务器端返回了未知的消息类型”)
}
//fmt.Println(“mes=%v\n”,mes)
}
全部保存过后会发现message和json包没引,把message和json包引一下,如下:
package process
import (
“fmt”
“os”
“go_code/chatroom/client/utils”
“go_code/chatroom/common/message”
“encoding/json”
“net”
)
二、代码测试
1.编译服务器端
将代码全部保存,保存之后会出现这样一个效果,就是一个客户登录以后,另外一个客户一旦出来登录过后,这边客户会马上看到有一个新的用户上线了,显示谁上线了,即新的用户列表能够看到,现在测试一下,现在能看到的效果是当有一个用户上线后,比如A客户端上线、B客户端上线,于是C客户端一上线,A客户端和B客户端能够看到当前所有在线用户,现在是不是就能达到效果?显然还不知道,还需要走代码,因为这个并不好说,所以来走一把代码,先将服务器端和客户端全部重新编译一下,首先编译服务器端,代码显示在服务器端即server/process/userProcess.go中有错误,这时需要打开server/process/userProcess.go文件并找到23行的位置,其中是当时定的onlineUsers出错了,将其修改为:
//遍历一把onlineUsers,然后一个一个的发送NotifyUserStatusMes
for id, up := range userMgr.onlineUsers{
修改完之后保存,再重新编译,服务器端显示无错误,如下:
D:\goproject>go build -o server.exc go_code/chatroom/server/main
2.编译客户端
再把客户端也重新编译一下,客户端编译显示:
D:\goproject>go build -o client.exc go_code/chatroom/client/main
显示没有错误。再把另一个客户端也显示出来,此客户端就不需要再重新编译了。
(3)代码测试结果及分析
①先在第一个客户端登录,测试结果如下:
net.Dial err= dial tcp 127.0.0.1:8889: conncctex: No conncction could be made because the target machine activel
②显示这地方有个错误,即没有办法连接到8889,因为服务器端还没启动 ,启动服务器端后服务器端显示:
服务器[新的结构]在8889端口监听。。。。
等待客户端来链接服务器。。。。
③现在再接着在第一个客户端登录,检测结果显示:
D:\goproject>client.exe
-----------------欢迎登录多人聊天系统--------------------
1 登陆聊天室
2 注册用户
3 退出系统
请选择<1-3>:
1
登陆聊天室
请输入用户的id
100
请输入用户的密码
123456
客户端,发送消息的长度=86 内容=(“type ”=“LoginMe”)
当前在线用户列表如下:
-------------恭喜xxx登录成功----------
-------------1.显示在线用户列表--------------
-------------2.发送消息-----------------
-------------3.信息列表-----------------
-------------4.退出系统-----------------
请选择<1-4>:
客户端正在等待读取服务器发送的消息
读取客户端发送的数据。。。
④可以看出检测结果成功了,可以看到目前在线用户列表
(4)增添新功能
第二个客户端没有看到新的消息,他登录过后是不是看到100号也在登录,这时要加一个小功能,即输入一个1,便能看到除了自己以外的所有在线用户,这个将其快速加进去就完成了,很简单,就是显示在线用户列表工作怎么做,这个代码非常简单只需要做到在client/process/server.go 文件中加一个逻辑进去就可以,即:
switch key {
case 1:
//fmt.Println(“显示在线用户列表”)
outputOnlineUser()
case 2:
fmt.Println(“发送消息”)
case 3:
fmt.Println(“信息列表”)
case 4:
fmt.Println(“你选择了退出系统。。。”)
os.Exit(0)
default :
fmt.Println(“你输入的选项不正确。。”)
}
(5)增添新功能过后的代码测试结果
①再来走一把代码,将客户端重新编译,编译过后再来重新启动,在第一个客户端显示:
D:\goproject>client.exe
-----------------欢迎登录多人聊天系统--------------------
1 登陆聊天室
2 注册用户
3 退出系统
请选择<1-3>:
1
登陆聊天室
请输入用户的id
100
请输入用户的密码
123456
客户端,发送消息的长度=86 内容=(“type ”=“LoginMe”)
当前在线用户列表如下:
用户id: 200
②为什么第一个客户端能看到200?因为服务器没有关闭过,服务器没有关闭说明服务器列表还在工作中,所以把服务器关闭才能看到完整的东西,再重新测试一遍,会发现写网络编程恐怖的地方在一个人要照顾两头,所以难度肯定是要大一点的,接着在第一个客户端测试,结果显示为:
D:\goproject>client.exe
-----------------欢迎登录多人聊天系统--------------------
1 登陆聊天室
2 注册用户
3 退出系统
请选择<1-3>:
1
登陆聊天室
请输入用户的id
100
请输入用户的密码
123456
客户端,发送消息的长度=86 内容=(“type ”=“LoginMe”)
当前在线用户列表如下:
-------------恭喜xxx登录成功----------
-------------1.显示在线用户列表--------------
-------------2.发送消息-----------------
-------------3.信息列表-----------------
-------------4.退出系统-----------------
请选择<1-4>:
客户端正在等待读取服务器发送的消息
读取客户端发送的数据。。。
③这时什么都看不到,这时再登录第二个客户端登录200号
④可以看到第二个客户端是200号登录,第一个客户端是100号登录,那现在假设再想看输入1 时还看到100号,在第二个客户端中测试,结果显示为:
请选择<1-4>:
客户端正在等待读取服务器发送的消息
读取客户端发送的数据。。。
1
当前在线用户列表如下:
用户id: 100
-------------恭喜xxx登录成功----------
-------------1.显示在线用户列表--------------
-------------2.发送消息-----------------
-------------3.信息列表-----------------
-------------4.退出系统-----------------
请选择<1-4>:
⑤因为列表里面已经维护了这个信息,所以同样在第一个客户端输入1 也还是能看到200号,因为map里面也维护了这个信息
(6)新添一个客户端代码测试
①再来最后一个用户,只要三个人能够将代码走起来,那就没有代码上的逻辑错误了,输入cmd,在其客户端内再来登录这个人,因为现在只有两个用户,所以需要先注册一个用户,假设此用户为300号
②与此同时第一个客户端也更新了在线用户列表,结果显示:
当前在线用户列表:
用户id: 200
用户id: 300
客户端正在等待读取服务器发送的消息
读取客户端发送的数据。。。
同时第二个客户端也更新了在线用户列表 显示:
当前在线用户列表:
用户id: 100
用户id: 300
客户端正在等待读取服务器发送的消息
读取客户端发送的数据。。。
③那么在第三个客户端中输入1后,它的测试结果显示:
1
当前在线用户列表:
用户id: 100
用户id: 300
-------------恭喜xxx登录成功----------
-------------1.显示在线用户列表--------------
-------------2.发送消息-----------------
-------------3.信息列表-----------------
-------------4.退出系统-----------------
请选择<1-4>:
(7)小结
代码就测试完成,当把这条线打通过后,可以做很多事情了,因为现在可以看到用户在线了,如果用户离线了,他离线的时候发一个包说“我要走了”, 只需要在服务器端把这个人找到就行,现在接口就全都写完了,可以看到代码写的比较多。为了完成登录时能返回当前在线用户,写了很多代码,将来做数据通道、做Go语言的服务器肯定要用数据通讯,因为Go语言它本质有一部分C语言的特性,如果是做过C语言的都知道,C语言其实就是做服务器,那谁做界面呢?这时肯定要和网络、通讯打交道,所以此机制一定要去深刻理解。
四、代码整理
1.server/process/userProcess.go
//这里我们编写通知所有在线用户的方法
//userId要通知其他的在线用户,我上线
func (this *UserProcess) NotifyOthersOnlineUser(userId int) {
//遍历onlineUsers,然后一个一个的发送NotifyUserStatusMes
for id, up := range userMgr.onlineUsers {
//过滤到自己
if id == userId {
continue
}
//开始通知【单独写一个方法】
up.NotifyMeOnline(userId)
}
}
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
}
}
2.server/process/userProcess.go[的Login]
} else {
loginResMes.Code = 200
//这里,因为用户登录成功,我们就把该登录成功的用放入到userMgr中
//将登录成功的用户的userId赋给this
this.UserId = loginMes.UserId
userMgr.AddOnlineUser(this)
//通知其他的在线用户,我上线了
this.NotifyOthersOnlineUser(loginMes.UserId)
//将当前在线用户的id 放入到loginMes.UserId
//遍历userMgr.onlineUsers
3.common/message/message.go
//为了配合服务器端推送用户状态变化的消息
type NotifyUserStatusMes struct {
UserId int `json:”userId”` //用户id
Status int `json:”status”` //用户的状态
}
4.client/process/userMgr.go
package process
import (
“fmt”
”go_code/chatroom/common/message”
)
//客户端要维护的map
var onlineUsers map[int]*message.User = make(map[int]*message.User, 10)
//在客户端显示当前在线的用户
func outputOnlineUser(){
//遍历一把onlineUsers
fmt.Println(“当前在线用户列表:”)
for id, _ := range onlineUsers{
//如果不显示自己.
fmt.Println(“用户id:\t”,id)
}
}
//编写一个方法,处理返回的NotifyUserStatusMes
func updateUserStatus(notifyUserStatusMes *message.NotifyUserStatusMes) {
//适当优化
user, ok := onlineUsers[notifyUserStatusMes.UserId]
if !ok { //原来没有
user = &message.User{
UserId : notifyUserStatusMes.UserId,
}
}
user.UserStatus = notifyUserStatusMes.Status
onlineUsers[notifyUserStatusMes.UserId] = user
OutputOnlineUser()
}
5.client/process/server.go
//如果读取到消息,又是下一步处理逻辑
switch mes.Type {
case message.NotifyUserStatusMesType :
//有人上线了
//1.取出NotifyUserStatusMesType
var notifyUserStatusMes message.NotifyUserStatusMes
json.Unmarshal([]byte(mes.Data), ¬ifyUserStatusMes)
//2.把这个用户的信息,状态保存到客户map[int]User中
updateUserStatus(¬ifyUserStatusMes)
//处理
default :
fmt.Println(“服务器端返回了未知的消息类型”)
}
//fmt.Println(“mes=%v\n”,mes)
}
//fmt.Println(“mes=%v\n”, mes)
client/process/server.go
fmt ShowMenu() {
fmt.Println(“----------恭喜xxx登录成功----------”)
fmt.Println(“----------1.显示在线用户列表----------”)
fmt.Println(“----------2.发送消息----------”)
fmt.Println(“----------3.信息列表----------”)
fmt.Println(“----------4.退出系统----------”)
fmt.Println(“请选择(1-4):”)
var key int
fmt.Scanf(“%d\n”, &key)
switch key {
case 1:
//fmt.Println(“显示在线用户列表”)
outputOnlineUser()
case 2:
fmt.Println(“发送消息”)