【声明】
非完全原创,主要思路来自于B站视频。如果有侵权,请联系我,可以立即删除掉。
一、其他功能的实现思路
1、实现群发、私发消息
为了方便,可以将群发、私聊消息的结构体共用,主要需要包含的字段应当有:发送用户id、用户状态、发送的消息、接收用户id。前面已经定义了notify
结构体(包含用户id、用户状态),此时定义ChatMsg
可以继承自notify
。在服务端,通过接收用户id来区分群发还是私聊,接收用户id为0时表示群聊,其他值表示私聊。
注意:
(1)为了减轻服务端的压力,客户端在私聊消息前,需要先根据好友在线列表来检测用户是否在线,不在线则不发送
(2)服务端在解析时,群发是直接将消息发送给所有在线用户;私聊则根据用户id单独发给指定id的用户
2、设置当前用户状态
在工具包中定义几种特定的状态
0: USER_OFFLINE, 1: USER_BUSY, 2: USER_STUDY, 3: USER_WORKING, 9: USER_ONLINE
用户设置状态后,发送notify
结构体的消息给服务端;服务端被动响应,将当前用户的最新状态群发给所有在线用户(包括当前用户),同时需要返回操作结果给该用户
其他用户收到该用户状态变化的通知消息之后,如果其维护的好友列表中没有该用户,则发送提醒用户xxx已上线,将其添加到在线好友列表中;如果该用户是下线,则发送提醒用户xxx已下线,并将其从在线好友列表中删除;其他情况则直接更新在线好友列表中该用户的状态
3、注销当前用户
使用该功能后,当前用户通知服务器自己即将注销,服务器收到消息后:
(1)调用redis函数将用户从数据库中删除
(2)将其从服务端的在线用户列表中删除
(3)将该用户下线的状态群发给其他在线用户
(4)返回注销的结果给客户端
二、整体的实现代码
1、工具包
1.1、结构体/变量/常量定义:utils/msg_def.go
package utils
const (
ClientLoginMsg = "LoginMsg"
ServerReturnMsg = "LoginReturn"
UsrRegisterMsg = "UsrRegister"
ClientNotifyMsg = "NotifyMsg"
ClientGroupChatMsg = "GroupChatMsg"
ClientPrivateChat = "PrivateChatMsg"
UsrLogoutMsg = "UsrLogout"
)
const (
USER_ONLINE = "online"
USER_OFFLINE = "offline"
USER_BUSY = "busy"
USER_STUDY = "study"
USER_WORKING = "working"
)
var (
GetUsrStatusByNo = map[int]string{
0: USER_OFFLINE, 1: USER_BUSY, 2: USER_STUDY,
3: USER_WORKING, 9: USER_ONLINE}
)
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}
LogoutSuccess = LoginReturn{
203, "logout 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
}
type ChatMsg struct {
NotifyMsg
Content string
DstUsrId int
}
1.2、发送消息的序列化/接收消息的反序列: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、客户端
2.1、主函数和一级目录: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.Printf("\t\t请选择(1~3): ")
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()
}
}
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:
default:
fmt.Println("序号输入有误,请重新输入!")
}
if key == 1 || key == 2 || key == 3 {
break
}
}
}
2.2、客户端的好友列表及方法:client/online/friend_online.go
package online
import (
"Test0/IMS/utils"
"errors"
"fmt"
)
var clientOnlineList map[int]*utils.NotifyMsg
func InitOnlineList() {
if clientOnlineList == nil {
clientOnlineList = make(map[int]*utils.NotifyMsg, 10)
}
}
func UpdateUsrStatus(notify *utils.NotifyMsg) {
InitOnlineList()
if notify.UsrStatus != utils.USER_OFFLINE {
clientOnlineList[notify.UsrId] = notify
} else {
delete(clientOnlineList, notify.UsrId)
}
}
func ShowAllClientOnlineUsr() {
fmt.Println("当前在线的用户列表:")
for id, usr := range clientOnlineList {
fmt.Println("用户id: ", id, "状态: ", usr.UsrStatus)
}
}