【声明】

非完全原创,主要思路来自于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), &notifyMsg)
	if err != nil {
		fmt.Println("Client received notify message unmarshal failed, err = ", err)
		return
	}

	//2. 将最新的用户状态记录于客户端的在线列表中
	online.UpdateUsrStatus(&notifyMsg)
	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个客户端,得到结果如下:
打开客户端1
打开客户端2
打开客户端3