【声明】
非完全原创,主要思路来自于B站视频。如果有侵权,请联系我,可以立即删除掉。
二、实现二级菜单1、客户端登录成功时显示在线用户列表
1.1、服务端维护在线列表
mapusr_idkeymap
usr_online.gomapinitusr_id
1.2、服务端返回结构体中添加在线用户id列表
LoginReturnLoginReturnusr_id
1.3、修改点
utils/msg_def.go 服务端返回结构体中添加在线用户切片
var (
UsrNotExist = LoginReturn{403, "user not exist", nil}
UsrAlreadyExist = LoginReturn{402, "user already exist", nil}
PwdNotMatch = LoginReturn{401, "password not match", nil}
HandleSuccess = LoginReturn{200, "handle success", nil}
)
type LoginReturn struct {
ErrCode int
ErrInfo string
OnlineIds []int
}
server/proc/usr_online.go 新增文件,用以维护在线用户列表
package proc
import (
"Test0/IMS/utils"
"errors"
"net"
)
//记录成功登录的客户端id和con
var onlineUsrs map[int]*net.Conn
func init() {
onlineUsrs = make(map[int]*net.Conn, 1024)
}
func AddOnlineUsr(usrId int, con *net.Conn) {
onlineUsrs[usrId] = con
}
func DelOnlineUsr(usrId int) {
delete(onlineUsrs, usrId)
}
func GetAllOnlineUsrs() map[int]*net.Conn {
return onlineUsrs
}
func GetOnlineUsrById(usrId int) (con *net.Conn, err error) {
con, ok := onlineUsrs[usrId]
if !ok {
err = errors.New(utils.UsrNotExist.ErrInfo)
}
return
}
server/proc/deal_msg.go 在服务端验证客户登录成功时,将当前客户id、连接添加到在线列表中,同时将在线列表中的用户id加到服务端返回消息结构体中
func parseMsgLogin_or_Register(con net.Conn, msg *utils.Message) (err error) {
//1. 获取客户端发送的登录消息结构体
var login utils.LoginMsg
err = json.Unmarshal([]byte(msg.MsgData), &login)
if err != nil {
fmt.Println("Server received login message unmarshal failed, err = ", err)
return
}
fmt.Println("Server received login message unmarshal success, login = ", login)
var returnMsg utils.LoginReturn
if msg.MsgType == utils.ClientLoginMsg {
//2. 验证用户名和密码,并且服务端返回的状态消息
if err = model.LoginRedisCheck(login.UsrId, login.UsrPwd); err != nil {
if err.Error() == utils.UsrNotExist.ErrInfo {
returnMsg = utils.UsrNotExist
} else if err.Error() == utils.PwdNotMatch.ErrInfo {
returnMsg = utils.PwdNotMatch
} else {
returnMsg = utils.LoginReturn{ErrCode: 404, ErrInfo: err.Error()}
}
} else {
//将当前成功登录的客户端添加到服务端的在线列表中
onlineUsrs[login.UsrId] = &con
returnMsg = utils.HandleSuccess
for id := range onlineUsrs {
returnMsg.OnlineIds = append(returnMsg.OnlineIds, id)
}
}
} else {
//2. 注册用户
if err = model.UsrRegister(login.UsrId, login.UsrPwd); err != nil {
if err.Error() == utils.UsrAlreadyExist.ErrInfo {
returnMsg = utils.UsrAlreadyExist
} else {
returnMsg = utils.LoginReturn{ErrCode: 404, ErrInfo: err.Error()}
}
} else {
returnMsg = utils.HandleSuccess
}
}
//3. 将服务端返回的状态消息序列化为消息结构体
buf, err := json.Marshal(returnMsg)
if err != nil {
fmt.Println("Server return status data marshal failed, err = ", err)
return
}
return utils.SendMsg(con, buf, utils.ServerReturnMsg)
}
1.4、运行结果
//客户端1
D:\WorkSpace\Golang\src\Test0\IMS>client.exe
-------------欢迎来到简易及时通讯系统-------------
1. 用户登录
2. 用户注册
3. 注销用户
4. 退出系统
请选择(1~4): 1
请输入用户ID: 1234
请输入用户密码: root
当前在线用户列表如下:
用户id 1234
-------------恭喜xxx登录成功-------------
//客户端2
D:\WorkSpace\Golang\src\Test0\IMS>client.exe
-------------欢迎来到简易及时通讯系统-------------
1. 用户登录
2. 用户注册
3. 注销用户
4. 退出系统
请选择(1~4): 1
请输入用户ID: 5678
请输入用户密码: root
当前在线用户列表如下:
用户id 1234
用户id 5678
-------------恭喜xxx登录成功-------------
2、新用户上线通知/客户端获取在线用户列表
2.1、思路分析
当新用户上线时,其他已经在线的用户能够获取最新的在线用户列表。主要有几种实现的思路:
(1)当用户上线后,服务端马上把自身维护的在线列表整体推送
(2)服务端新建自己的策略,每隔一段时间把自身维护的在线列表整体推送
(3)当用户上线/离线后,用户通知服务器,服务器把该用户的上下线信息推送给所有在线用户
考虑到用户在线人数量很大的时候,服务端推送耗费的资源会很大;当用户群基数很大时,上下线人数必然会很多,将会造成服务器频繁推送上下线消息给所有在线用户。因此,服务端只需要推送上下线消息给该用户的好友列表即可
另外,前两种思路,都是需要服务端主动对客户端的在线状态进行检测,比较耗费资源;后一种思路,客户端主动告知服务端自己的状态,从而触发服务端的被动响应,该思路可以在一定程度上缓解服务端的资源压力
最后一个思路,需要客户端自己维护一个好友在线列表(目前是所有人),当其上下线时,通知服务器将其上下线消息推送给其好友列表即可。该思路的前提是,每一个成功登录的客户端,都必须保持有服务端的通讯,确保能实时收到服务端的推送消息
2.2、客户端登录成功后的处理
map[usr_id]*notify_msg{usr_id, usr_status}
2.3、服务端验证客户登录成功时的处理
map[usr_id]*usr_con
2.4、客户端接收到服务端推送的上线消息时
解析服务端的群发上线消息,更新客户端的在线好友列表,并显示当前所有的好友列表和状态
2.5、相关代码
2.5.1、客户端
parseStruct
package proc
import (
"Test0/IMS/utils"
"encoding/json"
"fmt"
"net"
)
func Login_or_Register(usrId int, usrPwd string, op_type string) (err error) {
//1. 连接到服务器
con, err := net.Dial("tcp", "localhost:8088")
if err != nil {
fmt.Println("Client connect Server[localhost:8088] failed, err = ", err)
return
}
defer con.Close()
//2. 创建LoginMsg结构体对象并序列化
loginbuf, err := json.Marshal(&utils.UserInfoMsg{UsrId: usrId, UsrPwd: usrPwd})
if err != nil {
fmt.Println("Client login data marshal failed, err = ", err)
return
}
//3. 根据LoginMsg序列化后的切片,构造Message结构体,并将数据发给服务端
err = utils.SendMsg(&con, loginbuf, op_type)
if err != nil {
fmt.Printf("Client send login message to Server[%s] failed, err = %v\n", con.RemoteAddr(), err)
return
}
//4. 接收服务端返回的消息
msg, err := utils.ReadMsg(&con)
if err != nil {
fmt.Printf("Client receive login returned message from Server[%s] failed, err = %v\n", con.RemoteAddr(), err)
return
}
return parseStruct(&con, msg)
}
消息解析:client/proc/msg_parse.go 解析来自服务器的数据,根据消息的类型,选择不同的函数去解析:
(1)服务器返回的注册验证,则直接返回验证结果给一级菜单,显示注册成功或失败
(2)服务端返回的登录验证,登录失败则直接返回失败信息给一级菜单;登录成功,则需要为聊天通讯做准备:初始化当前客户端的在线好友列表;开启协程保持与服务端的连接,用以接收服务端的消息;显示二级菜单(群聊、显示好友列表等功能)
(3)服务端返回的是其他客户端的状态变化通知,则将最新的用户状态记录于客户端的在线列表中,并打印当前好友列表id及状态
package proc
import (
"Test0/IMS/client/menu"
"Test0/IMS/client/online"
"Test0/IMS/utils"
"encoding/json"
"errors"
"fmt"
"net"
)
func parseServerNotifyMsg(con *net.Conn, msg *utils.Message) (err error) {
//1. 获取服务端返回的推送消息:用户状态变化
var notifyMsg utils.NotifyMsg
err = json.Unmarshal([]byte(msg.MsgData), ¬ifyMsg)
if err != nil {
fmt.Println("Client received notify message unmarshal failed, err = ", err)
return
}
//2. 将最新的用户状态记录于客户端的在线列表中
online.UpdateUsrStatus(¬ifyMsg)
online.ShowAllOnlineUsr()
return
}
func parseServerReturnMsg(con *net.Conn, msg *utils.Message) (err error) {
//1. 获取服务端返回的登录返回消息结构体
var loginReturn utils.LoginReturn
err = json.Unmarshal([]byte(msg.MsgData), &loginReturn)
if err != nil {
fmt.Println("Client received login returned message unmarshal failed, err = ", err)
return
}
/* 2. 登录成功后,需要触发以下事件:
(1)显示当前在线用户
(2)初始化当前客户端的在线好友列表
(3)开启协程保持于服务端的连接
(4)显示二级菜单*/
if loginReturn.ErrCode == utils.LoginSuccess.ErrCode {
fmt.Println("当前在线用户列表如下:")
for _, v := range loginReturn.OnlineIds {
fmt.Println("用户id\t", v)
}
fmt.Print("\n")
online.InitOnlineList()
go keepServerCon(con) //登录成功后,开启协程保持与服务端的连接
menu.ShowMenu() //显示二级菜单
return nil
} else if loginReturn.ErrCode == utils.RegisterSuccess.ErrCode {
return nil
} else {
return errors.New(loginReturn.ErrInfo)
}
}
func parseStruct(con *net.Conn, msg *utils.Message) (err error) {
switch msg.MsgType {
case utils.ServerReturnMsg:
return parseServerReturnMsg(con, msg)
case utils.ServerNotifyMsg:
return parseServerNotifyMsg(con, msg)
default:
return errors.New("message format error")
}
}
parseStruct
package proc
import (
"Test0/IMS/utils"
"fmt"
"net"
)
func keepServerCon(con *net.Conn) {
for {
//1. 获取服务端的消息
msg, err := utils.ReadMsg(con)
if err != nil {
fmt.Printf("Client receive login returned message from Server[%s] failed, err = %v\n", (*con).RemoteAddr(), err)
return
}
//2. 解析服务端的消息,并处理
err = parseStruct(con, msg)
if err != nil {
fmt.Printf("Client receive login returned message from Server[%s] failed, err = %v\n", (*con).RemoteAddr(), err)
return
}
}
}
好友列表及增删查改:client/online/friend_online.go 提供好友列表的定义和方法
package online
import (
"Test0/IMS/utils"
"fmt"
)
var clientOnlineList map[int]*utils.NotifyMsg
func InitOnlineList() {
clientOnlineList = make(map[int]*utils.NotifyMsg, 10)
}
func UpdateUsrStatus(notify *utils.NotifyMsg) {
clientOnlineList[notify.UsrId] = notify
}
func ShowAllOnlineUsr() {
fmt.Println("当前在线的用户列表:")
for id, usr := range clientOnlineList {
fmt.Println("用户id: ", id, "状态: ", usr.UsrStatus)
}
}
二级菜单:client/menu/menu.go 循环显示二级菜单的页面,并根据用户输入选择对应的函数
package menu
import (
"Test0/IMS/client/online"
"fmt"
)
func ShowMenu() {
var key int
for {
fmt.Println("-----------------恭喜xxx登录成功-----------------")
fmt.Println("\t\t 1. 显示在线用户列表")
fmt.Println("\t\t 2. 发送群聊消息")
fmt.Println("\t\t 3. 显示消息列表")
fmt.Println("\t\t 4. 退 出 系 统")
fmt.Println("\t\t请选择(1~4): ")
fmt.Scanln(&key)
switch key {
case 1:
online.ShowAllOnlineUsr()
case 2:
case 3:
case 4:
default:
fmt.Println("序号输入有误,请重新输入!")
}
if key == 4 {
break
}
}
}
客户端主函数及一级菜单:client/main/client.go
package main
import (
"Test0/IMS/client/proc"
"Test0/IMS/utils"
"fmt"
)
func main() {
//定义全局变量接收用户的序号选择、用户ID、密码
var key, id int
var pwd string
for {
fmt.Println("-------------欢迎来到简易及时通讯系统-------------")
fmt.Println("\t\t 1. 用户登录")
fmt.Println("\t\t 2. 用户注册")
fmt.Println("\t\t 3. 注销用户")
fmt.Println("\t\t 4. 退出系统")
fmt.Printf("\t\t请选择(1~4): ")
fmt.Scanln(&key)
switch key {
case 1:
fmt.Printf("请输入用户ID: ")
fmt.Scanln(&id)
fmt.Printf("请输入用户密码: ")
fmt.Scanln(&pwd)
err := proc.Login_or_Register(id, pwd, utils.ClientLoginMsg)
if err != nil {
fmt.Printf("%s", err.Error())
if err.Error() == utils.UsrNotExist.ErrInfo {
fmt.Println(", 请先注册")
} else if err.Error() == utils.PwdNotMatch.ErrInfo {
fmt.Println(", 请先重新输入")
} else {
fmt.Println()
}
} else {
fmt.Println("login success")
}
case 2:
fmt.Printf("请输入用户ID: ")
fmt.Scanln(&id)
fmt.Printf("请输入用户密码: ")
fmt.Scanln(&pwd)
err := proc.Login_or_Register(id, pwd, utils.UsrRegisterMsg)
if err != nil {
fmt.Println(err.Error())
} else {
fmt.Println("register success")
}
case 3:
case 4:
default:
fmt.Println("序号输入有误,请重新输入!")
}
if key == 1 || key == 2 || key == 3 {
break
}
}
}
2.5.2、服务端
server/main/server.goserver/model/redis_dao.go
package proc
import (
"Test0/IMS/utils"
"encoding/json"
"errors"
"fmt"
"net"
)
//记录成功登录的客户端id和con
var onlineUsrs map[int]*net.Conn
func init() {
onlineUsrs = make(map[int]*net.Conn, 1024)
}
func AddOnlineUsr(usrId int, con *net.Conn) {
onlineUsrs[usrId] = con
}
func DelOnlineUsr(usrId int) {
delete(onlineUsrs, usrId)
}
func GetAllOnlineUsrs() map[int]*net.Conn {
return onlineUsrs
}
func GetOnlineUsrById(usrId int) (con *net.Conn, err error) {
con, ok := onlineUsrs[usrId]
if !ok {
err = errors.New(utils.UsrNotExist.ErrInfo)
}
return
}
func NotifyUsrId_Others(usrId int, usrStaus string) (err error) {
for id, con := range onlineUsrs {
if id == usrId {
continue
}
dataSlice, err := json.Marshal(&utils.NotifyMsg{
UsrId: usrId,
UsrStatus: usrStaus,
})
if err != nil {
fmt.Printf("Server's notify-message who will be send to Clent[%s] marshal failed, err = %v\n",
(*con).LocalAddr(), err)
return err
}
err = utils.SendMsg(con, dataSlice, utils.ServerNotifyMsg)
if err != nil {
return err
}
}
return err
}
消息解析:server/proc/deal_msg.go 将客户端发送过来的数据进行解析并返回验证结果(现在只添加了注册、登录的解析,后续还有群聊消息的解析等等)。如果是注册消息,则直接返回redis添加数据的结果;如果是登录消息,失败直接返回,成功则需要:
(1)将当前成功登录的客户端添加到服务端的在线列表中
(2)将登录成功的用户上线消息推送给其他所有在线用户
(3)将登录成功的结果返回给客户端
package proc
import (
"Test0/IMS/server/model"
"Test0/IMS/utils"
"encoding/json"
"errors"
"fmt"
"net"
)
func parseMsgLogin_or_Register(con *net.Conn, msg *utils.Message) (err error) {
//1. 获取客户端发送的登录消息结构体
var login utils.UserInfoMsg
err = json.Unmarshal([]byte(msg.MsgData), &login)
if err != nil {
fmt.Println("Server received login message unmarshal failed, err = ", err)
return
}
fmt.Println("Server received login message unmarshal success, login = ", login)
var returnMsg utils.LoginReturn
if msg.MsgType == utils.ClientLoginMsg {
//2. 验证用户名和密码,并且服务端返回的状态消息
if err = model.LoginRedisCheck(login.UsrId, login.UsrPwd); err != nil {
if err.Error() == utils.UsrNotExist.ErrInfo {
returnMsg = utils.UsrNotExist
} else if err.Error() == utils.PwdNotMatch.ErrInfo {
returnMsg = utils.PwdNotMatch
} else {
returnMsg = utils.LoginReturn{ErrCode: 404, ErrInfo: err.Error()}
}
} else { //登录成功
// 将当前成功登录的客户端添加到服务端的在线列表中
onlineUsrs[login.UsrId] = con
// 将登录成功的用户上线消息推送给所有在线用户
NotifyUsrId_Others(login.UsrId, utils.USER_ONLINE)
// 将登录成功的结果返回给客户端
returnMsg = utils.LoginSuccess
for id := range onlineUsrs {
returnMsg.OnlineIds = append(returnMsg.OnlineIds, id)
}
}
} else {
//2. 注册用户
if err = model.UsrRegister(login.UsrId, login.UsrPwd); err != nil {
if err.Error() == utils.UsrAlreadyExist.ErrInfo {
returnMsg = utils.UsrAlreadyExist
} else {
returnMsg = utils.LoginReturn{ErrCode: 404, ErrInfo: err.Error()}
}
} else {
returnMsg = utils.RegisterSuccess
}
}
//3. 将服务端返回的状态消息序列化为消息结构体
buf, err := json.Marshal(returnMsg)
if err != nil {
fmt.Println("Server return status data marshal failed, err = ", err)
return
}
return utils.SendMsg(con, buf, utils.ServerReturnMsg)
}
func ParseStruct(con *net.Conn, msg *utils.Message) (err error) {
switch msg.MsgType {
case utils.ClientLoginMsg, utils.UsrRegisterMsg:
err = parseMsgLogin_or_Register(con, msg)
default:
fmt.Println("message format error!")
err = errors.New("message format error")
}
return
}
2.5.3、工具包
notifyMsg
package utils
const (
ClientLoginMsg = "LoginMsg"
ServerReturnMsg = "LoginReturn"
UsrRegisterMsg = "UsrRegister"
ServerNotifyMsg = "NotifyMsg"
)
const (
USER_ONLINE = "online"
USER_OFFLINE = "offline"
USER_BUSY = "busy"
USER_STUDY = "study"
)
var (
UsrNotExist = LoginReturn{403, "user not exist", nil}
UsrAlreadyExist = LoginReturn{402, "user already exist", nil}
PwdNotMatch = LoginReturn{401, "password not match", nil}
HandleSuccess = LoginReturn{200, "handle success", nil}
RegisterSuccess = LoginReturn{201, "register success", nil}
LoginSuccess = LoginReturn{202, "login success", nil}
)
type Message struct {
MsgType string
MsgData string
}
type UserInfoMsg struct {
UsrId int
UsrPwd string
}
type LoginReturn struct {
ErrCode int
ErrInfo string
OnlineIds []int
}
type NotifyMsg struct {
UsrId int
UsrStatus string
}
utils/msg_utils.go
package utils
import (
"encoding/binary"
"encoding/json"
"fmt"
"net"
)
func ReadMsg(con *net.Conn) (msg *Message, err error) {
//1. 读取前4个字节,即数据长度
buf := make([]byte, 8096)
_, err = (*con).Read(buf[:4])
if err != nil {
fmt.Printf("[%s] receive message length from [%s] failed, err = %v\n", (*con).LocalAddr(), (*con).RemoteAddr(), err)
return
}
pkgLens := binary.BigEndian.Uint32(buf[:4])
//2. 再读pkgLens个字节到buf中
lens, err := (*con).Read(buf[:pkgLens])
if lens != int(pkgLens) || err != nil {
fmt.Printf("[%s] receive message data from [%s] failed, err = %v\n", (*con).LocalAddr(), (*con).RemoteAddr(), err)
return
}
msg = &Message{}
//3. 读到的数据反序列化为Message结构体
err = json.Unmarshal(buf[:pkgLens], &msg)
if err != nil {
fmt.Printf("[%s] received message unmarshal failed, err = %v\n", (*con).LocalAddr(), err)
}
return
}
func SendMsg(con *net.Conn, buf []byte, msgType string) (err error) {
//1. 根据服务端返回消息/客户端登录消息(如LoginReturn、LoginMsg)序列化的切片来创建消息Message
var msg Message
msg.MsgData = string(buf)
msg.MsgType = msgType
data, err := json.Marshal(&msg)
if err != nil {
fmt.Printf("[%s] message data unmarshal failed, err = %v\n", (*con).LocalAddr(), err)
return
}
//2. 将Message序列化后的数据长度、内容发送给客户端
//2.1 发送数据长度
bytebuf := make([]byte, 4)
binary.BigEndian.PutUint32(bytebuf, uint32(len(data)))
lens, err := (*con).Write(bytebuf)
//fmt.Printf("[%s] send message length to [%s] ", con.LocalAddr(), con.RemoteAddr())
if lens != 4 || err != nil {
fmt.Printf("failed, len = %d, err = %v\n", lens, err)
return
}
//fmt.Printf("successful, len = %d, content = %+v\n", lens, bytebuf)
//2.2 发送数据内容
lens, err = (*con).Write(data)
//fmt.Printf("[%s] send message data to [%s] ", con.LocalAddr(), con.RemoteAddr())
if err != nil {
fmt.Println("failed, len = ", lens, "err = ", err)
return
}
//fmt.Printf("sucessful, len = %d, content = %+v\n", lens, string(data))
return
}
2.6、运行结果
依次运行3个客户端,得到结果如下: