3.海量用户即时通讯系统
3.1项目开发流程
需求分析——>设计阶段——>编码实现——>测试阶段——>实施
需求分析:
- 用户注册
- 用户登录
- 显示在线用户列表
- 群聊(广播)
- 点对点聊天
- 离线留言
3.2界面设计
3.3 项目开发前技术准备
项目需要保存用户信息和消息数据,因此需要运用到数据库(Redis或MySQL)的知识,先了解在Golang中运用Redis。
Redis 快速入门
3.4 实现功能-显示客户端登录菜单
代码演示:
由于login.go文件依然打包在main文件中,故编译时需要在src目录下再go build -o client.exe go_code\chapter17\chatroom\client
client文件main.go
package main
import (
"fmt"
"os"
)
var (
userId int
userPwd string
)
func main() {
// 接收用户的选择
var key int
// 判断是否还继续循环显示菜单
var loop = true
//1. 循显示主菜单
for loop {
fmt.Println("-----------欢迎登录多人聊天系统----------")
fmt.Println("\t 请选择(1-3):")
fmt.Println("\t 1 登录聊天室")
fmt.Println("\t 2 注册用户")
fmt.Println("\t 3 退出系统")
// fmt.Scanln(&key)//比较下
fmt.Scanf("%d\n", &key)
switch key {
case 1:
fmt.Println("登录聊天室")
case 2:
fmt.Println("注册用户")
case 3:
fmt.Println("退出系统")
os.Exit(0) //退出操作系统
default:
fmt.Println("输入选项有误,请重新输入(1-3)")
continue
}
loop = false
}
//2. 进入详细分支,根据用户输入,提示相关信息
if key == 1 {
// 登录得需要 Id 和 Pwd,该两者皆是全局变量
fmt.Println("-----------欢迎登录多人聊天系统----------")
fmt.Println("\t 输入Id账号:")
fmt.Scanf("%d\n", &userId)
fmt.Println("\t 输入密码:")
fmt.Scanf("%s\n", &userPwd)
// 登录函数,设计较为复杂,单独放入login.go文件中
err := login(userId, userPwd)
if err != nil {
fmt.Println("login err=", err)
// 失败原因,是否密码或账号有误,重新输入?
} else {
fmt.Println(" login success")
}
} else if key == 2 {
fmt.Println("进入用户注册逻辑")
}
}
login.go
package main
import (
"encoding/binary"
"encoding/json"
"fmt"
"go_code/chapter17/chatroom/common/message"
"net"
)
// 写一个函数,完成登录
func login(userId int, userPwd string) (err error) {
// 下一步开始定协议
fmt.Printf("UserId=%d,UserPwd=%s\n", userId, userPwd)
return nil
}
3.5 实现功能-完成用户登录
- 要求,先完成指定用户的验证,用户id=100,密码123456可以登录,其它用户不能登录
- 重点在于传输协议(Message[struct]的构建)
- 完成客户端可以发送消息长度,服务器端可以正常收到该长度
(1)先确定消息Message的格式和结构体
(2)然后根据上图分析完成代码
(3)示意图
代码实现:
server包,main.go:
package main
import (
"fmt"
"net"
)
// 处理和客户端的通讯
func process(conn net.Conn) {
// 这里也需要延时关闭conn
defer conn.Close()
// 循环读取客户端发送的信息
for {
buf := make([]byte, 8096)
fmt.Println("读取客户端发送的数据")
n, err := conn.Read(buf[:4])
if n != 4 || err != nil {
fmt.Println("conn.Read err=", err)
return
}
fmt.Println("读到的buf=", buf[0:4])
}
}
func main() {
//提示信息
fmt.Println("服务器在8889端口监听")
listener, err := net.Listen("tcp", "0.0.0.0:8889")
if err != nil {
fmt.Println("net.Listen err=", err)
return
}
defer listener.Close()
// 一旦监听成功,就等待客户端来链接服务器
for {
fmt.Println("等待客户端来链接服务器")
conn, err := listener.Accept()
if err != nil {
fmt.Println("Listen.Accept err=", err)
// return //可能出现某一个链接出错,但其它多数链接是正常运行的
}
//链接成功,则启动一个协程和客户端保持通讯。。。
go process(conn)
}
}
common包,message包中message.go:
package message
// 定义一些常量
const (
LoginMesType = "LoginMes"
LoginResMesType = "LoginResMes"
)
type Message struct {
Type string `json:"type"` //消息类型
Data string `json:"data"` //消息的内容
}
type LoginMes struct {
UserId int `json:"userId"` //用户ID
UserPwd string `json:"userPwd"` //用户密码
UserName string `json:"userName"` //用户名
}
type LoginResMes struct {
Code int `json:"code"` // 返回状态码,设定500表示用未注册, 200 表示客户登录成功
Error string `json:"error"`
}
client包中,client.go与上前面一致,未修改
client包中,login.go:
package main
import (
"encoding/binary"
"encoding/json"
"fmt"
"go_code/chapter17/chatroom/common/message"
"net"
)
// 写一个函数,完成登录
func login(userId int, userPwd string) (err error) {
// 下一步开始定协议
// fmt.Printf("UserId=%d,UserPwd=%s\n", userId, userPwd)
// return nil
// 1.链接到服务器
conn, err := net.Dial("tcp", "localhost:8889")
if err != nil {
fmt.Println("net.Dial err=", err)
return
}
defer conn.Close()
// 2.准备通过conn 发送信息给服务器
var mes message.Message
mes.Type = message.LoginMesType
// 3. 创建一个LoginMes 结构体
var loginMes message.LoginMes
loginMes.UserId = userId
loginMes.UserPwd = userPwd
// 4.将loginMes 序列化
data, err := json.Marshal(loginMes)
if err != nil {
fmt.Println("json.Marshal err=", err)
return
}
// 5. 把序列化后的[]byte切片类型,赋给mes.Data
mes.Data = string(data)
// 6. 将mes 进行序列化
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("json.Marshal err=", err)
return
}
//7. 此时6中的data就是需要发送的信息
// 7.1 先把data的长度发送给服务器,用于防止丢包
// 先获取data的长度 ->转换成一个表示长度的切片
var pkgLen = uint32(len(data))
// pkgLen = uint32(len(data))
// var bytes [4]byte //数组?
buf := make([]byte, 4) // 切片
binary.BigEndian.PutUint32(buf[0:4], pkgLen)
//发送信息长度
n, err := conn.Write(buf[:4])
if n != 4 || err != nil {
fmt.Println("conn.Write(buf) failed", err)
return
}
fmt.Printf("客户端,发送消息的长度=%d,内容为=%s\n", len(data), string(data))
return nil
}
- 完成客户端可以发送消息本身,服务器端可以正常接收到信息,并根据客户端发送的信息(LoginMes),判断用户的合法性,并返回相应的LoginResMes。
//优化效率使用缓存加算法,优化结构用分层。
(1)客户端发送消息本身
(2)服务器端可以接收到消息,能反序列化成对应的消息结构体(LoginRes)
(3)根据反序列化信息,判断是否登录用户合法,返回相应的信息(LoginResMes),并要序列化处理
(4)客户端解析返回的LoginResMes信息,并显示对应的界面
(5)注意,此处需要做函数的封装
代码:
client文件中,login.go中的改动:
//8. 发送消息本身
_, err = conn.Write(data)
if err != nil {
fmt.Println("conn.Write(Message) failed", err)
return
}
// 9.休眠20秒
time.Sleep(20 * time.Second)
fmt.Println("测试,休眠20S,延迟关闭conn")
server包中,将读取包的信息封装到了函数readPkg()
中:
func readPkg(conn net.Conn) (mes message.Message, err error) {
buf := make([]byte, 1024)
fmt.Println("读取客户端发送的数据")
_, err = conn.Read(buf[:4])
// 当链接没有关闭时conn.Read才会阻塞
// 客户端若关闭了链接conn,则就不会阻塞,可能会出现死循环错误
if err != nil {
// fmt.Println("conn.Read err=", err)
err = errors.New("read pkg header error")
return
}
// fmt.Println("读到的buf=", buf[0:4])
// 根据buf[:4]转换成一个uint32类型
var pkgLen = binary.BigEndian.Uint32(buf[:4])
// 根据pkgLen 传输数据的长度,取信息内容
n, err := conn.Read(buf[:pkgLen])
if uint32(n) != pkgLen || err != nil {
err = errors.New("read pkg body error")
return
}
// 将接收信息到的信息,反序列化成 message.Message
// 此处关键是&mes,需要用到引用类型,若如写成mes,则mes内容会是空的
err = json.Unmarshal(buf[:pkgLen], &mes)
if err != nil {
// fmt.Println("服务器端信息反序列化失败,err=", err)
err = errors.New("json.Umarshal error")
return
}
return
}
// 处理和客户端的通讯
func process(conn net.Conn) {
// 这里也需要延时关闭conn
defer conn.Close()
// 循环读取客户端发送的信息
for {
// 此处,将读取数据包,直接封装成一个函数readPkg,返回message.Message 和 err
mes, err := readPkg(conn)
if err != nil {
fmt.Println("readPkg err=", err)
}
fmt.Println("mes=", mes)
}
}
第(3)步功能实现代码:
serverr包中,main.go中的代码修改:
// 处理和客户端的通讯
func process(conn net.Conn) {
// 这里也需要延时关闭conn
defer conn.Close()
// 循环读取客户端发送的信息
for {
// 此处,将读取数据包,直接封装成一个函数readPkg,返回message.Message 和 err
mes, err := readPkg(conn)
if err != nil {
fmt.Println("readPkg err=", err)
return
}
// fmt.Println("mes=", mes)
// 功能:根据客户端发送的消息种类,决定调用哪个函数来处理
err = serverProcessMes(conn, &mes)
if err != nil {
return
}
}
}
/ /编写一个serverProcessMes 函数
// 功能:根据客户端发送的消息种类,决定调用哪个函数来处理
func serverProcessMes(conn net.Conn, mes *message.Message) (err error) {
switch mes.Type {
case message.LoginMesType:
// 处理登录的逻辑
err = serverProcessLogin(conn, mes)
case message.RegisterMesType:
// 处理注册的逻辑
default:
fmt.Println("消息类型不存在,无法处理")
err = errors.New("消息类型不存在,无法处理")
}
return
}
// 编写一个serverProcessLogin函数,专门处理,登录请求
func serverProcessLogin(conn net.Conn, mes *message.Message) (err error) {
// 核心代码
// 1. 从mes 中取出mes.data,并直接反序列化成LoginMes
var loginMes message.LoginMes
err = json.Unmarshal([]byte(mes.Data), &loginMes) //注意,使用引用类型接收 &struct
if err != nil {
fmt.Println("json.Unmarshal failed err=", err)
return
}
// 先简化再深入
//1. 先定义一个resMes,返回给客户端的信息
var resMes message.Message
resMes.Type = message.LoginResMesType
// 2. 再申明一个LoginResMes结构体,用于存放返回内容信息本身
var loginResMes message.LoginResMes
// 如果用户的id=100 ,密码123456,认为合法,否则不合法
if loginMes.UserId == 100 && loginMes.UserPwd == "123456" {
// 合法
loginResMes.Code = 200 //状态码 200 表示登录成功
} else {
// 不合法
loginResMes.Code = 500 //状态码 500 表示该用户不存在
loginResMes.Error = "该用户不存在,请先注册"
}
// 3. 将loginResMes序列化
data, err := json.Marshal(loginResMes)
if err != nil {
fmt.Println("loginResMes json.Marshal failed err=", err)
return
}
// 4. 将data赋值到resMes中的Data
resMes.Data = string(data)
// 5. 将resMes 序列化,准备发送个客户端
data, err = json.Marshal(resMes)
if err != nil {
fmt.Println("resMes json.Marshal failed err=", err)
return
}
//6.发送,防止丢包,同样原理,发送字节长度校验的方法
// 为了简化程序,将发送封装到writePkg函数中(与readPkg函数相对应)
err = writePkg(conn, data)
return
}
//发送,防止丢包,发送字节长度校验的方法。将发送封装到writePkg函数中(与readPkg函数相对应)
func writePkg(conn net.Conn, data []byte) (err error) {
// 先发送一个长度给对方
var pkgLen = uint32(len(data))
var buf [4]byte //该处buf 是数组
binary.BigEndian.PutUint32(buf[0:4], pkgLen)
// 发送信息长度
n, err := conn.Write(buf[0:4])
if n != 4 || err != nil {
fmt.Println("resMes conn.Write(bytes) failed err=", err)
return
}
//发送消息data本身
n, err = conn.Write(data[:pkgLen])
if uint32(n) != pkgLen || err != nil {
fmt.Println("resMes conn.Write(data) failed err=", err)
return
}
return
}
client包中,增添了一个utils.go文件,用于存放客户端的readPkg()和writePkg()函数
package main
import (
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"go_code/chapter17/chatroom/common/message"
"net"
)
func readPkg(conn net.Conn) (mes message.Message, err error) {
buf := make([]byte, 1024)
fmt.Println("读取客户端发送的数据")
_, err = conn.Read(buf[:4])
// 当链接没有关闭时conn.Read才会阻塞
// 客户端若关闭了链接conn,则就不会阻塞,可能会出现死循环错误
if err != nil {
// fmt.Println("conn.Read err=", err)
err = errors.New("read pkg header error")
return
}
// fmt.Println("读到的buf=", buf[0:4])
// 根据buf[:4]转换成一个uint32类型
var pkgLen = binary.BigEndian.Uint32(buf[:4])
// 根据pkgLen 传输数据的长度,取信息内容
n, err := conn.Read(buf[:pkgLen])
if uint32(n) != pkgLen || err != nil {
err = errors.New("read pkg body error")
return
}
// 将接收信息到的信息,反序列化成 message.Message
// 此处关键是&mes,需要用到引用类型,若如写成mes,则mes内容会是空的
err = json.Unmarshal(buf[:pkgLen], &mes)
if err != nil {
// fmt.Println("服务器端信息反序列化失败,err=", err)
err = errors.New("json.Umarshal error")
return
}
return
}
func writePkg(conn net.Conn, data []byte) (err error) {
// 先发送一个长度给对方
var pkgLen = uint32(len(data))
// var buf [4]byte //该处buf 是数组
buf := make([]byte, 4)
binary.BigEndian.PutUint32(buf[:4], pkgLen)
// 发送信息长度
n, err := conn.Write(buf[:4])
if n != 4 || err != nil {
fmt.Println("resMes conn.Write(bytes) failed err=", err)
return
}
//发送消息data本身
n, err = conn.Write(data[:pkgLen])
if uint32(n) != pkgLen || err != nil {
fmt.Println("resMes conn.Write(data) failed err=", err)
return
}
return
}
在client包中,login.go文件中增改内容:
//7. 此时6中的data就是需要发送的信息
// 7.1 先把data的长度发送给服务器,用于防止丢包
// 先获取data的长度 ->转换成一个表示长度的切片
// var pkgLen = uint32(len(data))
// // pkgLen = uint32(len(data))
// // var bytes [4]byte //数组?
// buf := make([]byte, 4) // 切片
// binary.BigEndian.PutUint32(buf[0:4], pkgLen)
// //发送信息长度
// n, err := conn.Write(buf[:4])
// if n != 4 || err != nil {
// fmt.Println("conn.Write(buf) failed", err)
// return
// }
// // fmt.Printf("客户端,发送消息的长度=%d,内容为=%s\n", len(data), string(data))
// //8. 发送消息本身
// _, err = conn.Write(data)
err = writePkg(conn, data)
if err != nil {
fmt.Println("conn.Write(Message) failed", err)
return
}
// 9.休眠20秒
// time.Sleep(20 * time.Second)
// fmt.Println("测试,休眠20S,延迟关闭conn")
//10. 处理服务器返回的消息
//10.1 先处理服务器返回的消息
mes, err = readPkg(conn)
if err != nil {
fmt.Println("服务端返回信息 readPkg(conn) err=", err)
return
}
// 将mes.Data反序列化成LoginResMes
var loginResMes message.LoginResMes
err = json.Unmarshal([]byte(mes.Data), &loginResMes)
switch loginResMes.Code {
case 200:
fmt.Println("登录成功")
case 500:
fmt.Println(loginResMes.Error)
default:
fmt.Println("未知错误")
}
return
3.6 程序结构改进
前述程序,只是完成了单机版的一些工呢,但是没有结构性,系统的可读性、扩展性和维护性都不好
3.6.1先改进服务端
程序框架图如下:
(2)步骤
【1】先把分析出来的文件创建好,然后存放在相应的文件夹(包)中
【2】根据各个文件完成的任务不同,将main.go的代码剥离到相应的文件中
【3】剥离从底层网上修改
【3.1】utils包中的utils.go文件:
package utils
import (
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"go_code/chapter17/chatroom/common/message"
"net"
)
// 将这些方法关联到结构体中
type Transfer struct {
// 需要链接
Conn net.Conn
// 一个缓存,用于传输
Buf [8096]byte
}
func (trans *Transfer) ReadPkg() (mes message.Message, err error) {
// buf := make([]byte, 1024)
// fmt.Println("读取客户端发送的数据")
_, err = trans.Conn.Read(trans.Buf[:4])
// 当链接没有关闭时conn.Read才会阻塞
// 客户端若关闭了链接conn,则就不会阻塞,可能会出现死循环错误
if err != nil {
// fmt.Println("conn.Read err=", err)
err = errors.New("read pkg header error")
return
}
// fmt.Println("读到的buf=", buf[0:4])
// 根据buf[:4]转换成一个uint32类型
var pkgLen = binary.BigEndian.Uint32(trans.Buf[:4])
// 根据pkgLen 传输数据的长度,取信息内容
n, err := trans.Conn.Read(trans.Buf[:pkgLen])
if uint32(n) != pkgLen || err != nil {
err = errors.New("read pkg body error")
return
}
// 将接收信息到的信息,反序列化成 message.Message
// 此处关键是&mes,需要用到引用类型,若如写成mes,则mes内容会是空的
err = json.Unmarshal(trans.Buf[:pkgLen], &mes)
if err != nil {
// fmt.Println("服务器端信息反序列化失败,err=", err)
err = errors.New("json.Umarshal error")
return
}
return
}
func (trans *Transfer) WritePkg(data []byte) (err error) {
// 先发送一个长度给对方
var pkgLen = uint32(len(data))
// var buf [4]byte //该处buf 是数组
binary.BigEndian.PutUint32(trans.Buf[:4], pkgLen)
// 发送信息长度
n, err := trans.Conn.Write(trans.Buf[:4])
if n != 4 || err != nil {
fmt.Println("resMes conn.Write(bytes) failed err=", err)
return
}
//发送消息data本身
n, err = trans.Conn.Write(data[:pkgLen])
if uint32(n) != pkgLen || err != nil {
fmt.Println("resMes conn.Write(data) failed err=", err)
return
}
return
}
【3.2】process包中的userProcess.go文件:
package process
import (
"encoding/json"
"fmt"
"go_code/chapter17/chatroom/common/message"
"go_code/chapter17/chatroom/server/utils"
"net"
)
type UserProcess struct {
// 需要链接
Conn net.Conn
}
// 编写一个serverProcessLogin函数,专门处理,登录请求
func (userPro *UserProcess) ServerProcessLogin(mes *message.Message) (err error) {
// 核心代码
// 1. 从mes 中取出mes.data,并直接反序列化成LoginMes
var loginMes message.LoginMes
err = json.Unmarshal([]byte(mes.Data), &loginMes) //注意,使用引用类型接收 &struct
if err != nil {
fmt.Println("json.Unmarshal failed err=", err)
return
}
// 先简化再深入
//1. 先定义一个resMes,返回给客户端的信息
var resMes message.Message
resMes.Type = message.LoginResMesType
// 2. 再申明一个LoginResMes结构体,用于存放返回内容信息本身
var loginResMes message.LoginResMes
// 如果用户的id=100 ,密码123456,认为合法,否则不合法
if loginMes.UserId == 100 && loginMes.UserPwd == "123456" {
// 合法
loginResMes.Code = 200 //状态码 200 表示登录成功
} else {
// 不合法
loginResMes.Code = 500 //状态码 500 表示该用户不存在
loginResMes.Error = "该用户不存在,请先注册"
}
// 3. 将loginResMes序列化
data, err := json.Marshal(loginResMes)
if err != nil {
fmt.Println("loginResMes json.Marshal failed err=", err)
return
}
// 4. 将data赋值到resMes中的Data
resMes.Data = string(data)
// 5. 将resMes 序列化,准备发送个客户端
data, err = json.Marshal(resMes)
if err != nil {
fmt.Println("resMes json.Marshal failed err=", err)
return
}
//6.发送,防止丢包,同样原理,发送字节长度校验的方法
// 为了简化程序,将发送封装到writePkg函数中(与readPkg函数相对应)
tf := &utils.Transfer{
Conn: userPro.Conn,
}
err = tf.WritePkg(data)
return
}
【3.3】main包中的processor.go文件:
package main
import (
"errors"
"fmt"
"go_code/chapter17/chatroom/common/message"
"go_code/chapter17/chatroom/server/process"
"go_code/chapter17/chatroom/server/utils"
"net"
)
// 先创建一个Processor结构体
type Processor struct {
Conn net.Conn
}
// 处理和客户端的通讯
func (p *Processor) Process() (err error) {
// 循环读取客户端发送的信息
for {
// 此处,将读取数据包,直接封装成一个函数readPkg,返回message.Message 和 err
// 先创建一个Transfer结构体,引用ReadPkg()方法,完成读包任务
tf := &utils.Transfer{
Conn: p.Conn,
}
mes, err := tf.ReadPkg()
if err != nil {
fmt.Println("ReadPkg err=", err)
return err
}
// fmt.Println("mes=", mes)
// 功能:根据客户端发送的消息种类,决定调用哪个函数来处理
err = p.ServerProcessMes(&mes)
if err != nil {
return err
}
}
}
// 编写一个serverProcessMes 函数
// 功能:根据客户端发送的消息种类,决定调用哪个函数来处理
func (p *Processor) ServerProcessMes(mes *message.Message) (err error) {
switch mes.Type {
case message.LoginMesType:
// 处理登录的逻辑
// 创建一个UserProcess实例
up := &process.UserProcess{
Conn: p.Conn,
}
err = up.ServerProcessLogin(mes)
case message.RegisterMesType:
// 处理注册的逻辑
default:
fmt.Println("消息类型不存在,无法处理")
err = errors.New("消息类型不存在,无法处理")
}
return
}
【3.4】main包中的main.go文件内容修改:
package main
import (
"fmt"
"net"
)
// 处理和客户端的通讯
func mainProcess(conn net.Conn) {
// 这里也需要延时关闭conn
defer conn.Close()
//这里调用总控,先创建一个Processor结构体实例
processor := &Processor{
Conn: conn,
}
err := processor.Process()
if err != nil {
fmt.Println("客户端和服务器端的协程错误err=", err)
return
}
}
func main() {
//提示信息
fmt.Println("服务器[新的结构]在8889端口监听")
listener, err := net.Listen("tcp", "0.0.0.0:8889")
if err != nil {
fmt.Println("net.Listen err=", err)
return
}
defer listener.Close()
// 一旦监听成功,就等待客户端来链接服务
for {
fmt.Println("等待客户端来链接服务器")
conn, err := listener.Accept()
if err != nil {
fmt.Println("Listen.Accept err=", err)
// return //可能出现某一个链接出错,但其它多数链接是正常运行的
}
//链接成功,则启动一个协程和客户端保持通讯。。。
go mainProcess(conn)
}
}
3.6.2 改进客户端
程序框架图如下:
(1)改进示意图
(2)步骤
【1】先把分析出来的文件创建好,然后存放在相应的文件夹(包)中
【2】根据各个文件完成的任务不同,将main.go的代码剥离到相应的文件中
【3】剥离从底层往上修改
【3.1】将server/utils包拷贝到client/utils。服务器端和客户端使用的utils包一致。
【3.2】创建了server/process/userProcess.go文件。主要是将原来的login.go的功能移植到该文件中,使得结构更加分层,利于管理。
package process
import (
"encoding/json"
"fmt"
"go_code/chapter17/chatroom/client/utils"
"go_code/chapter17/chatroom/common/message"
"net"
)
type UserProcess struct {
// 暂时不需要field
}
// 写一个函数,完成登录
func (userPro *UserProcess) Login(userId int, userPwd string) (err error) {
// 下一步开始定协议
// fmt.Printf("UserId=%d,UserPwd=%s\n", userId, userPwd)
// return nil
// 1.链接到服务器
conn, err := net.Dial("tcp", "localhost:8889")
if err != nil {
fmt.Println("net.Dial err=", err)
return
}
defer conn.Close()
// 2.准备通过conn 发送信息给服务器
var mes message.Message
mes.Type = message.LoginMesType
// 3. 创建一个LoginMes 结构体
var loginMes message.LoginMes
loginMes.UserId = userId
loginMes.UserPwd = userPwd
// 4.将loginMes 序列化
data, err := json.Marshal(loginMes)
if err != nil {
fmt.Println("json.Marshal err=", err)
return
}
// 5. 把序列化后的[]byte切片类型,赋给mes.Data
mes.Data = string(data)
// 6. 将mes 进行序列化
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("json.Marshal err=", err)
return
}
)
//8.发送消息
tf := &utils.Transfer{
Conn: conn,
}
err = tf.WritePkg(data)
if err != nil {
fmt.Println("conn.Write(Message) failed", err)
return
}
//10. 处理服务器返回的消息
//10.1 先处理服务器返回的消息
mes, err = tf.ReadPkg()
if err != nil {
fmt.Println("服务端返回信息 readPkg(conn) err=", err)
return
}
// 将mes.Data反序列化成LoginResMes
var loginResMes message.LoginResMes
err = json.Unmarshal([]byte(mes.Data), &loginResMes)
switch loginResMes.Code {
case 200:
// fmt.Println("登录成功")
//此处还需启一个协程
// 该协程保持和服务器端的通讯,如果服务器有数据推送给客户端
// 则接收并显示在客户端终端
go serverProMes(conn)
//1.显示登陆成功后的菜单
ShowMenu()
case 500:
fmt.Println(loginResMes.Error)
default:
fmt.Println("未知错误")
}
return
}
【3.3】创建了server/process/server.go,用于实现显示登陆成功后的后续菜单;以及使得客户端一直和服务器端保持链接,若服务器端给客户端推送信息,会显示在客户端的终端上。
package process
import (
"fmt"
"go_code/chapter17/chatroom/client/utils"
"net"
"os"
)
// 显示登陆成功后的界面...
func ShowMenu() {
// 循环显示菜单
var loop = true
for loop {
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("------1.显示在线用户列表------")
case 2:
fmt.Println("------2.发送信息------")
case 3:
fmt.Println("------3.信息列表------")
case 4:
fmt.Println("------4.退出系统------")
// 最好的方式,在退出系统时,给服务器发出信号,服务器也关闭相关链接
os.Exit(0)
default:
fmt.Println("输入有误,重新输入")
}
}
}
// 和服务器保持通讯
func serverProMes(conn net.Conn) {
// 创建一个utils.Transfer结构体实例,不停的读取服务器发生的消息
tf := &utils.Transfer{
Conn: conn,
}
for {
fmt.Printf("客户端%s正在等待读取服务器发送的消息\n", conn.LocalAddr().String())
mes, err := tf.ReadPkg()
if err != nil {
fmt.Println("tf.ReadPkg err=", err)
return
}
//如果读到消息,下一步处理逻辑
fmt.Println("server send to client message = %v\n", mes)
}
}
【3.4】修改了client/main/main.go中的内容,将main(){}主体,减轻化。
package main
import (
"fmt"
"go_code/chapter17/chatroom/client/process"
"os"
)
var (
userId int
userPwd string
)
func main() {
// 接收用户的选择
var key int
// 判断是否还继续循环显示菜单
var loop = true
//1. 循显示主菜单
for loop {
fmt.Println("-----------欢迎登录多人聊天系统----------")
fmt.Println("\t 请选择(1-3):")
fmt.Println("\t 1 登录聊天室")
fmt.Println("\t 2 注册用户")
fmt.Println("\t 3 退出系统")
// fmt.Scanln(&key)//比较下
fmt.Scanf("%d\n", &key)
switch key {
case 1:
fmt.Println("登录聊天室")
fmt.Println("-----------欢迎登录多人聊天系统----------")
fmt.Println("\t 输入Id账号:")
fmt.Scanf("%d\n", &userId)
fmt.Println("\t 输入密码:")
fmt.Scanf("%s\n", &userPwd)
// 完成登陆
up := &process.UserProcess{}
err := up.Login(userId, userPwd)
if err != nil {
loop = false
}
case 2:
fmt.Println("注册用户")
case 3:
fmt.Println("退出系统")
os.Exit(0) //退出操作系统
default:
fmt.Println("输入选项有误,请重新输入(1-3)")
continue
}
loop = false
}
}
3.6.3增添Redis功能
结构更新示意图:
(1)在Redis中手动添加测试用户
(2)验证输入的用户信息(id、pwd等)在Redis中,则登录成功,否则退出系统,并给出相应的提示信息:
1)密码错误
2)用户不存在,确认已注册,再登录
代码实现:
代码实现:
【1】编写了model/user.go
package model
// 用于创建客户实例信息,用于与Redis交互使用
type User struct {
// 信息传输时采用json信息形式,故字段需是小写非非公开形式
// 为了序列化和反序列化成功,添加json的tag
// 要求用户信息的json字符串的key 和结构体的字段对应的tag名一致
UserId int `json:"userId"`
UserPwd string `json:"userPwd"`
UserName string `json:"userName"`
}
【2】编写model/error.go
package model
import "errors"
// 用于服务器端与Redis间信息传输时,自定义错误的文件
// 根据业务逻辑需要,自定义一些错误
var (
Error_User_NotExists = errors.New("用户不存在")
Error_User_Exists = errors.New("用户已存在(注册)")
Error_User_Pwd = errors.New("密码错误")
)
【3】编写model/userDao.go
package model
import (
"encoding/json"
"fmt"
"github.com/garyburd/redigo/redis"
)
// Dao:data access object
// 定义一个UserDao结构体
// 用于操作Uesr结构体的增删改查等功能
type UserDao struct {
pool *redis.Pool
}
// 在服务器启动后,就初始化一个UserDao实例,
// 做成全局变量,在需要和Redis操作时,就直接使用
var (
MyUserDao *UserDao
)
// 使用工厂模式,创建一个UserDao实例
func NewUserDao(pool *redis.Pool) (userDao *UserDao) {
userDao = &UserDao{
pool: pool,
}
return
}
// UserDao应该提供的功能:
// 1.登录时,根据用户输入的id 返回一个User实例+err
func (userDao *UserDao) getUserId(userId int) (user User, err error) {
// 通过给定的id去Redis查询该用户
conn := userDao.pool.Get()
defer conn.Close()
res, err := redis.String(conn.Do("hget", "users", userId))
if err != nil {
if err == redis.ErrNil { //表示在users 哈希中,没有找到对应的id
err = Error_User_NotExists
}
return
}
// 将res反序列化成User实例
err = json.Unmarshal([]byte(res), &user)
if err != nil {
fmt.Println("json.Unmarshal() err=", err)
return
}
return
}
// 2.完成登录的校验
// 2.1 Login 如果用户输入的id和pwd都正确,则返回一个user实例
// 2.2 如果用户的id或者pwd不正确,则返回对应的错误信息
func (userDao *UserDao) Login(userId int, userPwd string) (user User, err error) {
conn := userDao.pool.Get()
defer conn.Close()
user, err = userDao.getUserId(userId)
if err != nil {
return
}
//获取到id合法,校验密码
if user.UserPwd != userPwd {
err = Error_User_Pwd
}
return
}
【4】增加了main/redis.go,增添并初始化链接池
package main
import (
"time"
"github.com/garyburd/redigo/redis"
)
var pool *redis.Pool
func initPool(address string, maxIdle, maxActive int, idleTimeout time.Duration) {
pool = &redis.Pool{
MaxIdle: maxIdle, //最大空闲链接数
MaxActive: maxActive, //表示和数据库的最大链接数,0表示没有限制
IdleTimeout: idleTimeout, //最大空闲时间
Dial: func() (redis.Conn, error) { //初始化链接的代码,确定链接ip
return redis.Dial("tcp", address)
},
}
}
【5】在main/main.go中增添初始化函数;用于初始化链接池的调用,以及构建了用于初始化了一个全局变量(model.UserDao的结构体的)函数
func init() {
//当服务器启动时,就开始初始化Redis的链接池
initPool("127.0.0.1:6379", 16, 0, 300*time.Second)
// 初始化一个UserDao结构体,提高效率
initUserDao()
//提示信息
}
// 此处编写一个函数,完成对全局变量UserDao的初始化任务
func initUserDao() {
// 此处需要注意,初始化的顺序,initPool要在initUserDao之前
model.MyUserDao = model.NewUserDao(pool) // 此处的pool本身是在redis.go文件中定义的全局变量
}
func main() {
fmt.Println("服务器[新的结构]在8889端口监听")
listener, err := net.Listen("tcp", "0.0.0.0:8889")
if err != nil {
fmt.Println("net.Listen err=", err)
return
}
defer listener.Close()
......
【6】在process/userProcess.go 使用到Redis的验证功能。
// 2. 再申明一个LoginResMes结构体,用于存放返回内容信息本身
var loginResMes message.LoginResMes
......
......
// 改进:使用Redis数据去完成验证
// 【1】使用moedel.MyUserDao到Redis完成验证
user, err := model.MyUserDao.Login(loginMes.UserId, loginMes.UserPwd)
if err != nil {
// 根据错误信息,修改逻辑
if err == model.Error_User_NotExists {
loginResMes.Code = 500 //状态码 500 表示该用户不存在
loginResMes.Error = err.Error() //"该用户不存在,请先注册"
} else if err == model.Error_User_Pwd {
loginResMes.Code = 403 //状态码 403表示该用户密码错误
loginResMes.Error = err.Error()
} else {
loginResMes.Code = 505 //状态码 505表示该未知错误信息
loginResMes.Error = "未知错误,服务器内部错误"
}
} else {
loginResMes.Code = 200 //状态码 200 表示登录成功
fmt.Println(user, "登录成功")
}
// 3. 将loginResMes序列化
data, err := json.Marshal(loginResMes)
....
3.7实现功能-完成用户注册
1)完成注册功能 ,将用户信息录入到Redis中
2)思路分析:
3)实现功能-完成注册用户
【1】新增common/message/user.go
package message
// 用于创建客户实例信息,用于与Redis交互使用
type User struct {
// 信息传输时采用json信息形式,故字段需是小写非非公开形式
// 为了序列化和反序列化成功,添加json的tag
// 要求用户信息的json字符串的key 和结构体的字段对应的tag名一致
UserId int `json:"userId"`
UserPwd string `json:"userPwd"`
UserName string `json:"userName"`
}
【2】在common/message/message.go中新增两个结构体实例,用于注册
const (
LoginMesType = "LoginMes"
LoginResMesType = "LoginResMes"
RegisterMesType = "RegisterMes"
RegisterResMesType = "RegisterResMes"
)
...
type RegisterMes struct {
User User `json:"user"`
}
type RegisterResMes struct {
Code int `json:"code"` // 返回状态码,设定400表示该Id已经被占用, 200 表示客户注册成功
Error string `json:"error"`
}
【3】在client/process/userProcess.go中增加了Register方法
func (userPro *UserProcess) Register(userId int, userPwd string,
userName string) (err error) {
// 1.链接到服务器
conn, err := net.Dial("tcp", "localhost:8889")
if err != nil {
fmt.Println("net.Dial err=", err)
return
}
defer conn.Close()
// 2.准备通过conn 发送信息给服务器
var mes message.Message
mes.Type = message.RegisterMesType
// 3. 创建一个LoginMes 结构体
var registerMes message.RegisterMes
registerMes.User.UserId = userId
registerMes.User.UserPwd = userPwd
registerMes.User.UserName = userName
// 4.将registerMes 序列化
data, err := json.Marshal(registerMes)
if err != nil {
fmt.Println("json.Marshal err=", err)
return
}
// 5. 把序列化后的[]byte切片类型,赋给mes.Data
mes.Data = string(data)
// 6. 将mes 进行序列化
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("json.Marshal err=", err)
return
}
// 7. 发送消息
tf := &utils.Transfer{
Conn: conn,
}
err = tf.WritePkg(data)
if err != nil {
fmt.Println("conn.Write(Message) failed", err)
return
}
//9. 处理服务器返回的消息
//9.1 先处理服务器返回的消息
mes, err = tf.ReadPkg()
if err != nil {
fmt.Println("服务端返回信息 readPkg(conn) err=", err)
return
}
// 将mes.Data反序列化成RegisterResMes
var registerResMes message.RegisterResMes
err = json.Unmarshal([]byte(mes.Data), ®isterResMes)
switch registerResMes.Code {
case 200:
fmt.Println("注册成功,重新登录")
//此处还需启一个协程
// 该协程保持和服务器端的通讯,如果服务器有数据推送给客户端
// 则接收并显示在客户端终端
// go serverProMes(conn)
// //1.显示登陆成功后的菜单
// ShowMenu()
default:
fmt.Println(registerResMes.Error)
// fmt.Println("未知错误")
}
return
}
【4】在client/main/main.go增加了注册逻辑代码
....
case 2:
fmt.Println("注册用户")
fmt.Println("\t 输入Id账号:")
fmt.Scanf("%d\n", &userId)
fmt.Println("\t 输入密码:")
fmt.Scanf("%s\n", &userPwd)
fmt.Println("\t 输入用户名(nickname):")
fmt.Scanf("%s\n", &userName)
// 2.调用UserProcess,完成注册的请求
up := &process.UserProcess{}
err := up.Register(userId, userPwd, userName)
if err != nil {
loop = false
}
.....
【5】在server/model/userDao.go增加Register方法用于在Redis数据库注册(新增)客户信息
func (userDao *UserDao) Register(user message.User) (err error) {
//先从UserDao的链接池中取出一根链接
conn := userDao.pool.Get()
defer conn.Close()
_, err = userDao.getUserById(user.UserId)
if err == nil {
err = Error_User_Exists
return
} else if err == Error_User_NotExists {
//此时,该id在Redis中还没有,则可以完成注册
data, err := json.Marshal(user)
if err != nil {
fmt.Println("userDao register json.Marshal err=", err)
return err
}
//入库
// "users"最好做成常量
_, err = conn.Do("hset", "users", user.UserId, string(data))
if err != nil {
fmt.Println("userDao register hset err=", err)
return err
}
return err //调试时,若没此行return err , err==nil 返还不成功
} else {
fmt.Println("userDao register getById() 未知错误")
}
return
}
【6】在server/process/userProcess.go中增加了一个serverProcessRegister方法,用于处理注册
// 编写一个serverProcessRegister方法,专门处理,登录请求
func (userPro *UserProcess) ServerProcessRegister(mes *message.Message) (err error) {
// 核心代码
// 1. 从mes 中取出mes.data,并直接反序列化成RegisterMes
var registerMes message.RegisterMes
err = json.Unmarshal([]byte(mes.Data), ®isterMes) //注意,使用引用类型接收 &struct
if err != nil {
fmt.Println("json.Unmarshal failed err=", err)
return
}
// 先简化再深入
//1. 先定义一个resMes,返回给客户端的信息
var resMes message.Message
resMes.Type = message.RegisterResMesType
// 2. 再申明一个RegistrResMes结构体,用于存放返回内容信息本身
var registerResMes message.RegisterResMes
// 使用Redis数据去完成注册
// 【1】使用moedel.MyUserDao到Redis完成注册
err = model.MyUserDao.Register(registerMes.User) ///修改3.19
if err != nil {
// 根据错误信息,修改逻辑
if err == model.Error_User_Exists {
registerResMes.Code = 505 //状态码 505 表示该用户已存在
registerResMes.Error = err.Error() //"该用户不存在,请先注册"
} else {
registerResMes.Code = 506 //状态码 506表示该未知错误信息
registerResMes.Error = "未知错误,服务器内部错误"
}
} else {
registerResMes.Code = 200 //状态码 200 表示登录成功
fmt.Println("注册成功")
}
// 3. 将registerResMes序列化
data, err := json.Marshal(registerResMes)
if err != nil {
fmt.Println("registerResMes json.Marshal failed err=", err)
return
}
// 4. 将data赋值到resMes中的Data
resMes.Data = string(data)
// 5. 将resMes 序列化,准备发送个客户端
data, err = json.Marshal(resMes)
if err != nil {
fmt.Println("resMes json.Marshal failed err=", err)
return
}
//6.发送,防止丢包,同样原理,发送字节长度校验的方法
// 为了简化程序,将发送封装到writePkg函数中(与readPkg函数相对应)
tf := &utils.Transfer{
Conn: userPro.Conn,
}
err = tf.WritePkg(data)
return
}
【7】在总控server/main/processer.go调用注册业务
// 编写一个serverProcessMes 函数
// 功能:根据客户端发送的消息种类,决定调用哪个函数来处理
func (p *Processor) ServerProcessMes(mes *message.Message) (err error) {
switch mes.Type {
//case message.LoginMesType:
// 处理登录的逻辑
// 创建一个UserProcess实例
//up := &process.UserProcess{
//Conn: p.Conn,
//}
//err = up.ServerProcessLogin(mes)
case message.RegisterMesType:
// 处理注册的逻辑
up := &process.UserProcess{
Conn: p.Conn,
}
err = up.ServerProcessRegister(mes)
default:
fmt.Println("消息类型不存在,无法处理")
err = errors.New("消息类型不存在,无法处理")
}
return
}
3.8 实现功能-完成登录时能返回当前在线用户
1)用户登录后,可以得到当前在线用户列表
思路分析:
代码实现:
【1】新增了server/process/userMgr.go文件
用于维护在线客户端的链接
package process
import "fmt"
type UserMgr struct {
onlineUsers map[int]*UserProcess
}
// 由于UserMgr实例在服务器有且只有一个
// 并且在很多地方都会使用到,因此将其定义成一个全局变量
var (
MyUserMgr *UserMgr
)
// 完成对userMgr的初始化工作
func init() {
MyUserMgr = &UserMgr{ //缺少此步,userMgr为nil
onlineUsers: make(map[int]*UserProcess, 1024),
}
}
// 完成对onlineUser的添加/修改
func (um *UserMgr) AddlineUser(up *UserProcess) {
um.onlineUsers[up.UserId] = up
}
// 完成对onlineUser的删除
func (um *UserMgr) DeletelineUser(UserId int) {
delete(um.onlineUsers, UserId) //map的删除功能
}
// 完成对onlineUser的查询
// 返回当前在线的用户
func (um *UserMgr) GetAlllienUsers() map[int]*UserProcess {
return um.onlineUsers
}
// 根据id返回对应的链接/值
func (um *UserMgr) GetOnlineUserById(UserId int) (*UserProcess, error) {
// 如何从Map中取出一个值,带检测方式
up, ok := um.onlineUsers[UserId]
if !ok { //说明查找的用户不在线
err := fmt.Errorf("用户%v不在线", UserId)
return nil, err
}
return up, nil
}
【2】在server/process/userProcess.go 中ServerProcessLogin()方法中进行了功能新增。当客户登录成功,便增加了用户管理业务UserMgr的onlineUser切片的内容。
....
} else {
loginResMes.Code = 200 //状态码 200 表示登录成功
fmt.Println(user, "登录成功")
// 此处,用户登录成功,将该登录成功的用户放入到UserMgr中
// 由于此时的userPro中的UserId还是空值,需先对其进行赋值
userPro.UserId = loginMes.UserId
MyUserMgr.AddlineUser(userPro)
// 将当前在线用户的id 放入到loginResMes.UsersId中
// 遍历MyUserMgr.onlineUsers
for id, _ := range MyUserMgr.onlineUsers {
loginResMes.UsersId = append(loginResMes.UsersId, id) //是否不需要makeUsersId,或在别处make
}
}
....
【3】common/message/messag.go 中结构体LoginResMes中新增了一个字段UserId,用于实现在线用户列表功能:
type LoginResMes struct {
Code int `json:"code"` // 返回状态码,设定500表示用未注册, 200 表示客户登录成功
UsersId []int `json:"userIds"` //用于保存用户id的切片
Error string `json:"error"`
}
【4】client/process/userProcess.go 中登录Login()方法中,增加当登录成功后显示在线用户:
switch loginResMes.Code {
case 200:
// fmt.Println("登录成功")
// 显示在线用户的列表,遍历loginResMes.UsersId切片
fmt.Println("当前在线用户列表如下:")
for _, v := range loginResMes.UsersId {
// 要求不显示自己在线
if v == userId {
continue
}
fmt.Printf("用户id:%d\n", v)
}
2)当新的用户上线后,其他已经登录的用户也能获取最新的在线用户列表
【1】server/process/userProcess.go中增加了用户上线通知其他在线用户的方法
/ /编写一个通知所有用户,有用户新增在线的方法,服务器主动推送给客户端
func (userPro *UserProcess) NotifyOthersOnline(userId int) {
// 遍历onlineUsers,然后一个一个的发生NotifyUserStatusMes
for id, up := range MyUserMgr.onlineUsers { //this.onlineUsers
if id == userId {
// 过滤自己
continue
}
// 开始通知其在线用户
up.NotifyMeOnline(userId)
}
}
func (userPro *UserProcess) NotifyMeOnline(userId int) {
// 组装NotifyUserStatusMes
mes := message.Message{
Type: message.NotifyUserStatusMesType,
}
notifyUserStatusMes := message.NotifyUserStatusMes{
UserId: userId,
UserStatus: message.UserOnline,
}
// 将notifyUserStatusMes序列化
data, err := json.Marshal(notifyUserStatusMes)
if err != nil {
fmt.Println("NotifyMeOnline json.Marshal(notifyUserStatusMes) err=", err)
return
}
// 将序列化的notifyUserStatusMes信息赋给mes.Data
mes.Data = string(data)
// 将mes序列化
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("NotifyMeOnline json.Marshal(Message) err=", err)
return
}
//将消息发送给客户端
tf := utils.Transfer{
Conn: userPro.Conn,
}
err = tf.WritePkg(data)
if err != nil {
fmt.Println("NotifyMeOnline tf.WritePkg(data) err=", err)
}
}
【2】server/process/userProcess.go的ServerProcessLogin()方法中,当客户登录成功后通知其他在线用户
} else {
loginResMes.Code = 200 //状态码 200 表示登录成功
fmt.Println(user, "登录成功")
// 此处,用户登录成功,将该登录成功的用户放入到UserMgr中
// 由于此时的userPro中的UserId还是空值,需先对其进行赋值
userPro.UserId = loginMes.UserId
MyUserMgr.AddlineUser(userPro)
// 通知其他在线用户,新登客户上线了
userPro.NotifyOthersOnline(loginMes.UserId)
// 将当前在线用户的id 放入到loginResMes.UsersId切片中
// 遍历MyUserMgr.onlineUsers
for id := range MyUserMgr.onlineUsers {
loginResMes.UsersId = append(loginResMes.UsersId, id)
}
}
【3】为了支持正个通讯,以及将新登录客户的状态发送给其他在线客户的业务。在common/message中增添了新结构体和常量:
// 定义几个用户状态的常量
const (
UserOnline = iota
UserOffline
UserBusyline
)
// 为了配合服务器端推送用户的状态变化信息
type NotifyUserStatusMes struct {
UserId int `json:"userId"`
UserStatus int `json:"userStatus"`
}
【4】建立一个客户端需要维护的map,用于管理在线客户的信息:client/process/userMgr.go:
package process
import (
"fmt"
"go_code/chapter17/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.Printf("用户id:%d\n", id)
}
}
// 编写一个方法,处理返回的NotifyUserStatusMes
func updataUserStatus(notifyUserStatusMes *message.NotifyUserStatusMes) {
//适当优化,可onlineUsers Map中可能已经存在了该用户id
// 该id用户只是改变了状态
user, ok := onlineUsers[notifyUserStatusMes.UserId]
if !ok { //原来没有
user = &message.User{
UserId: notifyUserStatusMes.UserId,
UserStatus: notifyUserStatusMes.UserStatus,
}
} else {
user.UserStatus = notifyUserStatusMes.UserStatus
}
onlineUsers[notifyUserStatusMes.UserId] = user
outputOnlineUser()
}
【5】在client/process/userProcess.go 的Login()方法中,初始化onlineUsers(map):
case 200:
// fmt.Println("登录成功")
// 显示在线用户的列表,遍历loginResMes.UsersId切片
fmt.Println("当前在线用户列表如下:")
for _, v := range loginResMes.UsersId {
// 要求不显示自己在线
if v == userId {
continue
}
fmt.Printf("用户id:%d\n", v)
// 完成客户端的onlineUsers的初始化
user := &message.User{
UserId: v,
UserStatus: message.UserOnline,
}
onlineUsers[v] = user
}
【6】在client/proocess/server.go来处理服务器端发送的消息(在线用户状态更新)
// 显示登陆成功后的界面...
func ShowMenu() {
...
switch key {
case 1:
// fmt.Println("------1.显示在线用户列表------")
outputOnlineUser()
case 2:
....
}
// 和服务器保持通讯
func serverProMes(conn net.Conn) {
// 创建一个utils.Transfer结构体实例,不停的读取服务器发生的消息
tf := &utils.Transfer{
Conn: conn,
}
for {
fmt.Printf("客户端%s正在等待读取服务器发送的消息\n", conn.LocalAddr().String())
mes, err := tf.ReadPkg()
if err != nil {
fmt.Println("tf.ReadPkg err=", err)
return
}
//如果读到消息,下一步处理逻辑
switch mes.Type {
case message.NotifyUserStatusMesType: //有人上线了
// 处理
// 1.取出NotifyUserStatusMes
notifyUserStatusMes := &message.NotifyUserStatusMes{}
err := json.Unmarshal([]byte(mes.Data), notifyUserStatusMes)
if err != nil {
fmt.Println("serverProMes json.Unmarshal()err=", err)
return
}
// 2.把该用户的状态保存到客户的map[int]User中
updataUserStatus(notifyUserStatusMes)
default:
fmt.Println("f服务器端返回了未知的消息类型")
}
}
}
3.9 实现功能-群聊
1)当一个用户上线后,可以将群聊消息发送给服务器,服务器可以接收到
客户端结构新增/修改示意图:
代码实现:
【1】common/message/message.go中新增了消息发送结构体SmsMes
// 增加一个SmsMes 消息发送结构体
type SmsMes struct {
Content string `json:"content"`
User //匿名结构体,使用到继承的特性
}
【2为了维护当前客户的链接,新增了client/model/curUser.go文件
package model
import (
"go_code/chapter17/chatroom/common/message"
"net"
)
// 表示在线登录成功客户的信息,便于管理
type CurUser struct {
Conn net.Conn
message.User
}
var MyCurUser CurUser
// 由于CurUSer在许多处都会使用到,有且只用改一个变量
// 故将其定义成一个全局变量
// CurUser的作用实际上是用来管理和维护客户信息的
【3】在client/process/userProcess.go Login()方法中,当用户登录成功,初始化用于维护客户当前链接的结构体实例
// fmt.Println("登录成功")
// 完成客户端MyCurUser的初始化
user := message.User{
UserId: userId,
UserPwd: userPwd,
UserStatus: message.UserOnline,
}
model.MyCurUser = model.CurUser{
Conn: conn,
User: user, //继承
}
【4】在client/process/smsProcess.go中增加了发送群聊的方法
package process
import (
"encoding/json"
"fmt"
"go_code/chapter17/chatroom/client/model"
"go_code/chapter17/chatroom/client/utils"
"go_code/chapter17/chatroom/common/message"
)
type SmsProcess struct {
// Conn net.Conn
// message.User
// content string
}
// 发送群聊的消息
func (smsPro *SmsProcess) SendGroupMes(content string) (err error) {
// 1.创建一个mes
// var mes message.Message
// mes.Type = message.SmsMesType
// // 2.创建一个SmsMes 实例
// var smsMes message.SmsMes
// smsMes.Content = content
// smsMes.UserId = MyCurUser.UserId
// smsMes.UserStatus = MyCurUser.UserStatus
//1.1 创建一个smsMes实例
smsMes := message.SmsMes{
Content: content,
User: model.MyCurUser.User,
}
// 3/1.2序列化smsMes
data, err := json.Marshal(smsMes)
if err != nil {
fmt.Println("smsProcess.go SendGroupMes json.Marshal(sms) err=", err)
return
}
mes := message.Message{
Type: message.SmsMesType,
Data: string(data),
}
// 4/1.4对mes再次序列化
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("smsProcess.go SendGroupMes json.Marshal(mes) err=", err)
return
}
// 5发生给服务器
tf := &utils.Transfer{
Conn: model.MyCurUser.Conn,
}
// 6.发送
err = tf.WritePkg(data)
if err != nil {
fmt.Println("smsProcess.go SendGroupMes jtf.WritePkg() err=", err)
return
}
return
}
【5】在client/process/server.go中调用群发功能:
....
var key int
var content string
// 总会使用到SmsProcess 实例,因此将其定义在switch外部
smsProcess := &SmsProcess{}
fmt.Scanf("%d\n", &key)
switch key {
case 1:
// fmt.Println("------1.显示在线用户列表------")
outputOnlineUser()
case 2:
// fmt.Println("------2.发送信息------")
fmt.Println("请输入内容:")
fmt.Scanln(&content)
smsProcess.SendGroupMes(content)
....
2)服务器可以将接收到的消息,群发给所有在线用户(发送者除外)
代码实现:
在common/message/message.go中新增了消息回应结构体SmsResMes
// 定义一些常量
const (
LoginMesType = "LoginMes"
LoginResMesType = "LoginResMes"
RegisterMesType = "RegisterMes"
RegisterResMesType = "RegisterResMes"
NotifyUserStatusMesType = "NotifyUserStatusMes"
SmsMesType = "SmsMes"
SmsResMesType = "SmsResMes"
)
// SmsResMes 服务器消息回复结构体
type SmsResMes struct {
Content string `json:"content"`
User //匿名结构体,使用到继承的特性
}
【】server/process/smsProcess.go中新增了发送群发消息的方法
package process
import (
"encoding/json"
"fmt"
"go_code/chapter17/chatroom/common/message"
"go_code/chapter17/chatroom/server/utils"
"net"
)
type SmsProcess struct {
// 暂时不需要字段
}
func (smsPro *SmsProcess) SendGroupMes(mes *message.Message) (err error) {
//遍历服务器端的 onlineUsers map[int]*UserProcess
// 将消息转发取出
// 将mes的内容SmsMes 反序列化
var smsMes message.SmsMes
err = json.Unmarshal([]byte(mes.Data), &smsMes)
if err != nil {
fmt.Println("smsProcess SendGroupMes json.Unmarshal() err=", err)
return
}
// 为了后续更好的扩展性,实现离线留言等功能,此处还是传输mes本身更好
// 将mes 序列化
// data, err := json.Marshal(mes)
if err != nil {
fmt.Println("smsProcess SendGroupMes json.Marshal(mes) err=", err)
return
}
for id, up := range MyUserMgr.onlineUsers {
// 群发,排除本身
if id == smsMes.UserId {
continue
}
err = smsPro.SendMesToEachOnlineUsers(up.Conn, &smsMes)
if err != nil {
fmt.Printf("消息发送给id:%v失败\n,id")
}
}
return
}
func (smsPro *SmsProcess) SendMesToEachOnlineUsers(conn net.Conn, smsMes *message.SmsMes) (err error) {
smsResMes := &message.SmsResMes{
Content: smsMes.Content,
User: smsMes.User,
}
// 序列化 smsReMes
data, err := json.Marshal(smsResMes)
if err != nil {
fmt.Println("smsPro SendMesToEachOnlineUsersjson.Marshal(smsResMes) err=", err)
return
}
//创建一个mes
mes := message.Message{
Type: message.SmsResMesType,
Data: string(data),
}
// 序列化mes
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("smsPro SendMesToEachOnlineUsersjson.Marshal(mes) err=", err)
return
}
// 创建一个utils.Transfer 发送信息
tf := &utils.Transfer{
Conn: conn,
}
err = tf.WritePkg(data)
if err != nil {
fmt.Println("smsPro SendMesToEachOnlineUsers tf.WritePkg() err=", err)
return
}
return
}
【3】server/main/processor.go 中新增了群发消息的逻辑处理
...
case message.SmsMesType:
// fmt.Println("mes=", mes)
// 创建一个SmsProcess实例完成转发群消息的逻辑
smsProcess := &process.SmsProcess{}
smsProcess.SendGroupMes(mes)
default:
fmt.Println("消息类型不存在,无法处理")
err = errors.New("消息类型不存在,无法处理")
}
...
【4】新增client/process/smsMgr.go 用于输出接收到的群发消息的函数
package process
import (
"encoding/json"
"fmt"
"go_code/chapter17/chatroom/common/message"
)
func outputGroupMes(mes *message.Message) { //此处的mes.Data 内容是SmsResMes类型的消息
// 反序列化
smsResMes := &message.SmsResMes{}
err := json.Unmarshal([]byte(mes.Data), smsResMes)
if err != nil {
fmt.Println("smsMgr.go outputGroupMes json.Unmarshal() err=", err.Error())
return
}
// 显示信息
info := fmt.Sprintf("UserId %v:%s\n", smsResMes.UserId, smsResMes.Content)
fmt.Println(info)
fmt.Println()
}
【5】在client/process/server.go中调用用于输出接收到的群发消息的函数
switch mes.Type {
....
case message.SmsResMesType:
outputGroupMes(&mes)
....
3.10聊天的项目的扩展功能要求
1.实现私聊,点对点的聊天
【1】common/message/message.go 对于消息发送结构体进行修改,增加一个用于存放私聊客户id的字段
// 增加一个SmsMes 消息发送结构体
type SmsMes struct {
Content string `json:"content"`
User //匿名结构体,使用到继承的特性
ReceiverId int `json:"receiverId"` //私聊 对象的Id信息
}
// SmsResMes 服务器消息回复结构体
type SmsResMes struct {
Content string `json:"content"`
User //匿名结构体,使用到继承的特性
ReceiverId int `json:"receiverId"` //私聊 对象的Id信息
}
【2】client/process/smsProcess.go中增加私聊的方法
/ 发送私聊的消息
// 要有发送者的id、发送内容等信息,以及接收者的id信息
func (smsPro *SmsProcess) SendMesToOne(content string, receiverId int) (err error) {
//1.1 创建一个smsMes实例
smsMes := message.SmsMes{
Content: content,
User: model.MyCurUser.User,
ReceiverId: receiverId,
}
// 3/1.2序列化smsMes
data, err := json.Marshal(smsMes)
if err != nil {
fmt.Println("smsProcess.go SendGroupMes json.Marshal(sms) err=", err)
return
}
mes := message.Message{
Type: message.SmsMesType,
Data: string(data),
}
// 4/1.4对mes再次序列化
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("smsProcess.go SendGroupMes json.Marshal(mes) err=", err)
return
}
// 5发生给服务器
tf := &utils.Transfer{
Conn: model.MyCurUser.Conn,
}
// 6.发送
err = tf.WritePkg(data)
if err != nil {
fmt.Println("smsProcess.go SendGroupMes jtf.WritePkg() err=", err)
return
}
return
}
func (smsPro *SmsProcess) OutputMes(mes *message.Message) (err error) {
// 将mes的内容SmsMes 反序列化
var smsResMes message.SmsResMes
err = json.Unmarshal([]byte(mes.Data), &smsResMes)
if err != nil {
fmt.Println("smsProcess SendGroupMes json.Unmarshal() err=", err)
return
}
// 判断是群发还是私聊
// SmsMes中的Receiver.Id 若是0则为群发
// 非零则为私聊
// 群发
if smsResMes.ReceiverId == 0 {
outputGroupMes(mes)
} else { //私聊
outputOneMes(mes)
}
return
}
【3】修改登录成功后,client/process/server.go显示菜单中(2.发送消息)细分群发和私聊逻辑
//返回上一级,退出私聊或群聊
func reBackLastMenu(b *bool) {
fmt.Println("是否关闭聊天(y/n)")
var key string
fmt.Scanf("%s\n", &key)
if strings.ToLower(key) == "y" {
*b = false
// } else if strings.ToLower(key) == "n" {
// continue
// } else {
// fmt.Println("输入有误请输入(y/n)")
}
}
// 显示2.发送消息后私聊和群发显示菜单
func smsShowMenu(content string, smsProcess *SmsProcess) {
var loop = true
for loop {
fmt.Println("------信息发送菜单------")
fmt.Println("------1.群发------")
fmt.Println("------2.私聊------")
fmt.Println("------3.返回上一级------")
fmt.Println("请选择(1或2):")
var key int
fmt.Scanf("%d\n", &key)
switch key {
case 1: //群发
fmt.Println("----群发界面----")
var flag = true
for flag {
fmt.Println("请输入内容:")
fmt.Scanf("%s\n", &content)
smsProcess.SendGroupMes(content)
reBackLastMenu(&flag)
}
case 2: //私聊
// 显示当前在线客户
outputOnlineUser()
var receiverId int
fmt.Println("请输入用户id:")
fmt.Scanln(&receiverId)
fmt.Println("----私聊界面----")
var flag = true
for flag {
fmt.Println("请输入内容:")
fmt.Scanf("%s\n", &content)
smsProcess.SendMesToOne(content, receiverId)
reBackLastMenu(&flag) //另起一个协程,导致输入混乱,待解决
}
...
【4】在server/process/smsProcess.go中增加接收发送信息是群发还是私聊的逻辑区分方法,以及私聊功能实现的方法
//区分是群发还是私聊消息
func (smsPro *SmsProcess) SendMes(mes *message.Message) (err error) {
// 将mes的内容SmsMes 反序列化
var smsMes message.SmsMes
err = json.Unmarshal([]byte(mes.Data), &smsMes)
if err != nil {
fmt.Println("smsProcess SendGroupMes json.Unmarshal() err=", err)
return
}
// 判断是群发还是私聊
// SmsMes中的Receiver.Id 若是0则为群发
// 非零则为私聊
// 群发
if smsMes.ReceiverId == 0 {
smsPro.SendGroupMes(mes)
} else { //私聊
smsPro.SendMesToOne(&smsMes)
}
return
}
func (smsPro *SmsProcess) SendMesToOne(smsMes *message.SmsMes) (err error) {
up := MyUserMgr.onlineUsers[smsMes.ReceiverId] //&UserProcess{}
smsResMes := &message.SmsResMes{
Content: smsMes.Content,
User: smsMes.User,
ReceiverId: smsMes.ReceiverId,
}
// 序列化 smsReMes
data, err := json.Marshal(smsResMes)
if err != nil {
fmt.Println("smsPro SendMesToOne json.Marshal(smsResMes) err=", err)
return
}
//创建一个mes
mes := message.Message{
Type: message.SmsResMesType,
Data: string(data),
}
// 序列化mes
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("smsPro SendMesToOne json.Marshal(mes) err=", err)
return
}
// 创建一个utils.Transfer 发送信息
tf := &utils.Transfer{
Conn: up.Conn,
}
err = tf.WritePkg(data)
if err != nil {
fmt.Println("smsPro SendMesToOne tf.WritePkg() err=", err)
return
}
return
}
【5】在server/main/processor.go中调用信息发送功能
...
case message.SmsMesType:
// 区分是群发还是私聊消息
// fmt.Println("mes=", mes)
// 创建一个SmsProcess实例完成转发群消息的逻辑
smsProcess := &process.SmsProcess{}
smsProcess.SendMes(mes)
...
【6】client/process/smsProcess.go增添区分接收到的信息是群发还是私聊消息的功能:
func (smsPro *SmsProcess) OutputMes(mes *message.Message) (err error) {
// 将mes的内容SmsMes 反序列化
var smsResMes message.SmsResMes
err = json.Unmarshal([]byte(mes.Data), &smsResMes)
if err != nil {
fmt.Println("smsProcess SendGroupMes json.Unmarshal() err=", err)
return
}
// 判断是群发还是私聊
// SmsMes中的Receiver.Id 若是0则为群发
// 非零则为私聊
// 群发
if smsResMes.ReceiverId == 0 {
outputGroupMes(mes)
} else { //私聊
outputOneMes(mes)
}
return
}
【7】client/process/smsMgr.go中增添私聊信息显示功能的函数(暂时和群发信息显示函数是一致的)
func outputOneMes(mes *message.Message) { //此处的mes.Data 内容是SmsResMes类型的消息
// 反序列化
smsResMes := &message.SmsResMes{}
err := json.Unmarshal([]byte(mes.Data), smsResMes)
if err != nil {
fmt.Println("smsMgr.go outputGroupMes json.Unmarshal() err=", err.Error())
return
}
// 显示信息
info := fmt.Sprintf("UserId %v:%s\n", smsResMes.UserId, smsResMes.Content)
fmt.Println(info)
fmt.Println()
}
【8】client/process/server.go中调用区分是接收到的信息是群发还是私聊消息的方法
...
case message.SmsResMesType:
smsProcess := &SmsProcess{}
smsProcess.OutputMes(&mes)
...
2.如果用户离线,把该用户从在线列表中去掉
【思路分析】
用户离线,即用户的状态为UserOffline
,可能是自己将状态更换或退出系统两种情况。
- 用户状态的更改
- 用户退出系统时的逻辑
- 服务端维护的onlineUsers map[int]*UserProcess需能自动删减连线客户
【1】client/process/userProcess.go中新增用户登录成功后退出的方法
// 编写一个方法,完成登录成功后退出系统/离线,关闭客户端与服务器的链接
func (userPro *UserProcess) Login_Exit() {
// 将用户离线信息发送给服务器端
notifyUserStatusMes := &message.NotifyUserStatusMes{
UserId: model.MyCurUser.UserId,
UserStatus: message.UserOffline,
}
data, err := json.Marshal(notifyUserStatusMes)
if err != nil {
fmt.Println("userProcess Login_Exit json.Marshal(notifyUserStatusMes) err=", err)
return
}
mes := &message.Message{
Type: message.NotifyUserStatusMesType,
Data: string(data),
}
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("userProcess Login_Exit json.Marshal(mes) err=", err)
return
}
tf := &utils.Transfer{
Conn: model.MyCurUser.Conn,
}
err = tf.WritePkg(data)
if err != nil {
fmt.Println("userProcess Login_Exit tf.WritePkg(mes) err=", err)
return
}
os.Exit(0)
}
【2】client\process\server.go中调用登录成功后退出的方法
case 4:
fmt.Println("------4.退出系统------")
// 最好的方式,在退出系统时,给服务器发出信号,服务器也关闭相关链接
userProcee := &UserProcess{}
userProcee.Login_Exit()
【3】serve/process/userProcess.go中新增对用户状态的判定以及调用相关逻辑功能的函数
// 编写一个方法判断客户返回的状态
func (userPro *UserProcess) StatusJudge(mes *message.Message) {
notifyUserStatusMes := &message.NotifyUserStatusMes{}
err := json.Unmarshal([]byte(mes.Data), notifyUserStatusMes)
if err != nil {
fmt.Println("userProcess.go json.Unmarshal(notifyuserstatus) err=", err)
return
}
// 判断该用户是否离线
switch notifyUserStatusMes.UserStatus {
case message.UserOffline:
userPro.DeletelineUser(notifyUserStatusMes)
default:
fmt.Println("userprocess.go 暂时无法处理的状态")
}
}
//编写一个方法,当客户离线后,服务器自动删减在线map中该客户的信息
// 以及关闭相关链接
func (userPro *UserProcess) DeletelineUser(notifyUserStatusMes *message.NotifyUserStatusMes) {
// // 给userPro的UserIdf赋值
// userPro.UserId = notifyUserStatusMes.UserId
// 删除在线客户map中对应的该id
MyUserMgr.DeletelineUser(notifyUserStatusMes.UserId)
userPro.NotifyOthersOffline(notifyUserStatusMes)
}
func (userPro *UserProcess) NotifyOthersOffline(notifyUserStatusMes *message.NotifyUserStatusMes) {
// 遍历onlineUsers,然后一个一个的发生NotifyUserStatusMes
for _, up := range MyUserMgr.onlineUsers { //this.onlineUsers
// 开始通知其在线用户
// 组装NotifyUserStatusMes
mes := message.Message{
Type: message.NotifyUserStatusMesType,
}
data, err := json.Marshal(notifyUserStatusMes)
if err != nil {
fmt.Println("NotifyOthersOffline json.Marshal(notifyUserStatusMes) err=", err)
return
}
// 将序列化的notifyUserStatusMes信息赋给mes.Data
mes.Data = string(data)
// 将mes序列化
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("NotifyOthersOffline json.Marshal(Message) err=", err)
return
}
//将消息发送给客户端
tf := utils.Transfer{
Conn: up.Conn,
}
err = tf.WritePkg(data)
if err != nil {
fmt.Println("NotifyOthersOffline tf.WritePkg(data) err=", err)
}
}
}
【4】server/main/processor.go/serverProcessMes()方法中增加新的消息处理类型:用于客户离线后关闭相关链接和更新在线列表
case message.NotifyUserStatusMesType:
// 用于客户离线后关闭相关链接和更新在线列表
up := &process.UserProcess{
Conn: p.Conn,
}
up.StatusJudge(mes)
【5】client\process\userMgr.go中扩展updataUserStatus()方法的功能,不仅是用于用户上线通知,用户状态更改也会进行通知。
并修改了显示当前客户的方法
// 在客户端显示当前在线用户
func outputOnlineUser() {
// 遍历onlineUsers
fmt.Println("当前在线用户列表:")
for id, v := range onlineUsers {
if v.UserStatus == message.UserOffline {
continue
}
fmt.Printf("用户id:%d\n", id)
}
}
// 编写一个方法,处理返回的NotifyUserStatusMes
func updataUserStatus(notifyUserStatusMes *message.NotifyUserStatusMes) {
//适当优化,可onlineUsers Map中可能已经存在了该用户id
// 该id用户只是改变了状态
user, ok := onlineUsers[notifyUserStatusMes.UserId]
if !ok { //原来没有
user = &message.User{
UserId: notifyUserStatusMes.UserId,
UserStatus: notifyUserStatusMes.UserStatus,
}
} else {
user.UserStatus = notifyUserStatusMes.UserStatus
}
onlineUsers[notifyUserStatusMes.UserId] = user
outputOnlineUser()
}
【6】client\process\server.go中 serverProMes(conn net.Conn) 方法结构不变,但扩展其调用的updataUserStatus()方法
case message.NotifyUserStatusMesType: //有人状态改变了
// 处理
// 1.取出NotifyUserStatusMes
notifyUserStatusMes := &message.NotifyUserStatusMes{}
err := json.Unmarshal([]byte(mes.Data), notifyUserStatusMes)
if err != nil {
fmt.Println("serverProMes json.Unmarshal()err=", err)
return
}
// 2.把该用户的状态保存到客户的map[int]User中
updataUserStatus(notifyUserStatusMes)
case message.SmsResMesType:
3.实现离线留言,群聊时,如果某个用户没有在线,当登录后,可以接收离线的消息
【思路分析】:
- 群发时确保信息发送给所有注册用户(Redis数据库中Users(hash类型)的所有 key(UserId))
- 信息本身要有存放处,使得当离线用户重新在线时,能接收到消息
- 对于离线用户重新登录后,客户端与服务器建立好链接,数据缓存的消息发送给客户端
【1】为了存放发送给离线用户的数据,新建smsDao.go文件来管理信息存放在Redis中的操作。同理定义一个SmsDao结构体,绑定相关方法用于实现信息的存放删除等功能
server/model/smsDao.go
package model
import (
"encoding/json"
"fmt"
"go_code/chapter17/chatroom/common/message"
"github.com/garyburd/redigo/redis"
)
// Dao:data access object
// 定义一个UserDao结构体
// 用于操作Uesr结构体的增删改查等功能
type SmsDao struct {
pool *redis.Pool
}
// 在服务器启动后,就初始化一个UserDao实例,
// 做成全局变量,在需要和Redis操作时,就直接使用
var (
MySmsDao *SmsDao
)
// 使用工厂模式,创建一个UserDao实例
func NewSmsrDao(pool *redis.Pool) (smsDao *SmsDao) {
smsDao = &SmsDao{
pool: pool,
}
return
}
// SmsDao应该提供的功能:
// 1.信息发送时,对方离线,信息存放在数据库中
// 信息存放类型hash
func (smsDao *SmsDao) SaveMes(smsMes *message.SmsMes) {
conn := smsDao.pool.Get()
defer conn.Close()
data, err := json.Marshal(smsMes)
if err != nil {
fmt.Println("smsDao SaveMes json.Marshal() err=", err)
return
}
_, err = conn.Do("hset", "smses", smsMes.UserId, string(data))
if err != nil {
fmt.Println("smsDao SaveMes conn.Do(hset) err=", err)
return
}
}
// 当群发/私聊消息全都被接收后,删除数据中的缓存信息内容
func (smsDao *SmsDao) DeleteMes(userId int) {
conn := smsDao.pool.Get()
defer conn.Close()
_, err := conn.Do("hdel", "smses", userId) //信息发送者的UserId
if err != nil {
fmt.Println("smsDao DeleteMes conn.Do(hdel) err=", err)
return
}
}
// 缓存数据取出
func (smsDao *SmsDao) GetMes(userId int) *message.SmsMes {
conn := smsDao.pool.Get()
defer conn.Close()
res, err := redis.String(conn.Do("hget", "smses", userId)) //信息发送者的UserId
if err != nil {
fmt.Println("smsDao GetMes conn.Do(hget) err=", err)
return nil
}
sms := &message.SmsMes{}
err = json.Unmarshal([]byte(res), sms)
if err != nil {
fmt.Println("smsDao GetMes json.Unmarshal() err=", err)
return nil
}
return sms
}
【2】为了管理离线用户,在UserMgr结构体中新增offlineUserIds map[int][]int
字段
server/process/userMgr.go
type UserMgr struct {
onlineUsers map[int]*UserProcess
// 增加当有用户发送群发消息时,保存离线客户的id的map
offlineUserIds map[int][]int
}
【3】为了知道Redis中的所用注册客户的数量和Id,在UserDao结构体中新增UserIds()
方法
server/model/userDao.go
/ 4.取出数据库中所有的用户id,返回一个切片
func (userDao *UserDao) UserIds() []int {
conn := userDao.pool.Get()
defer conn.Close()
sliceint := make([]int, 0)
res, err := redis.Ints(conn.Do("hkeys", "users"))
if err != nil {
fmt.Println("userDao UserIds conn.Do(hkeys) err=", err)
return nil
}
// 返回的res 是[]int切片
sliceint = append(sliceint, res...)
return sliceint
【4】当服务端接收到客户发送的群发信息要求时,在处理群发逻辑块,增添群发信息的存放在Redis和对在线用户和离线用户的判断。对SendGroupMes
进行了修改
server/process/smsProcess.go
func (smsPro *SmsProcess) SendGroupMes(mes *message.Message) (err error) {
//遍历服务器端的 onlineUsers map[int]*UserProcess
// 将消息转发取出
// 将mes的内容SmsMes 反序列化
var smsMes message.SmsMes
err = json.Unmarshal([]byte(mes.Data), &smsMes)
if err != nil {
fmt.Println("smsProcess SendGroupMes json.Unmarshal() err=", err)
return
}
//将接收到的信息保存到数据库中
model.MySmsDao.SaveMes(&smsMes)
// 数据库中所有注册了的用户id切片
sliceIntUserIds := model.MyUserDao.UserIds()
// 在线人数和数据中的人数相比较(有问题:MyUserMgr.onlineUsers大小已经定死为1024了)
// 调出MyUserMgr.onlineUsers中的有在线客户的数量
onlinenum := 0
for i := range MyUserMgr.onlineUsers {
if i == 0 {
continue
} else {
onlinenum += 1
}
}
if len(sliceIntUserIds) == onlinenum { //数据库中的用户全在线
for id, up := range MyUserMgr.onlineUsers {
// 群发,排除本身
if id == smsMes.UserId {
continue
}
err = smsPro.SendMesToEachOnlineUsers(up.Conn, &smsMes)
if err != nil {
fmt.Printf("消息发送给id:%v失败\n", id)
}
}
// 全员都接收到了消息,删除数据中缓存的信息
model.MySmsDao.DeleteMes(smsMes.UserId) //未执行
} else { //数据库中的用户有不在线的
for _, id := range sliceIntUserIds {
up, ok := MyUserMgr.onlineUsers[id]
if ok {
if id == smsMes.UserId {
continue
}
err = smsPro.SendMesToEachOnlineUsers(up.Conn, &smsMes)
if err != nil {
fmt.Printf("消息发送给id:%v失败\n", id)
}
} else { //该id用户离线了
//此处需要添加当用户上线后,可以接受到消息的逻辑标识
//UserMgr 中增加offlineUserIds map[int][]int字段
// 用于保存 当有用户发送群发消息时,此时离线客户的id的map
// 初始化offlineUserIds 的切片
MyUserMgr.offlineUserIds[smsMes.UserId] = make([]int, 0)
MyUserMgr.offlineUserIds[smsMes.UserId] = append(MyUserMgr.offlineUserIds[smsMes.UserId], id)
}
}
// 离线人数数量
offlinenum := len(sliceIntUserIds) - onlinenum
// 在MyUserMgr.offlineUserIds[smsMes.UserId] 中最后再增加一个标识位
// 该标志位,标识还有多少用户离线未收到群发消息
MyUserMgr.offlineUserIds[smsMes.UserId] = append(MyUserMgr.offlineUserIds[smsMes.UserId], offlinenum)
}
return
}
【5】对新上线的用户,在其登录成功后,判断其是否有未接收到的离线信息,并完成信息发送
对ServerProcessLogin
方法进行了新增
server/process/userprocess.go
func (userPro *UserProcess) ServerProcessLogin(mes *message.Message) (err error) {
......
//6.发送,防止丢包,同样原理,发送字节长度校验的方法
// 为了简化程序,将发送封装到writePkg函数中(与readPkg函数相对应)
tf := &utils.Transfer{
Conn: userPro.Conn,
}
err = tf.WritePkg(data)
//登录成功后
if loginResMes.Code == 200 {
//当登录成功后,遍历MyUserMgr.offlineUserIds切片的内容
// 判断上线用户是否有离线信息
for sendUserId, intslice := range MyUserMgr.offlineUserIds {
for index, offlineId := range intslice {
if offlineId != loginMes.UserId {
continue
} else {
// 存在离线留言
// sms := &message.SmsMes{}
sms := model.MySmsDao.GetMes(sendUserId)
//此时sms中的ReceiverId是空的,需将接收者即登录客户的id赋值进去
sms.ReceiverId = loginMes.UserId
// 创建一个SmsProcess实例
smsProcess := &SmsProcess{}
err = smsProcess.SendMesToOne(sms)
if err == nil { //信息发送成功
// 将offlineUserIds中的该id删除
// 由于在切片中删除较困难,选择将其值设置为-1
intslice[index] = -1
} else {
fmt.Println("用户", loginMes.UserId, "留言信息发送失败")
// log.Fatal(err) //测试
}
// 测试
// fmt.Println("intslice 长度:", len(intslice))
// for _, v := range intslice {
// fmt.Println("v=", v)
// }
// fmt.Println("intslice 标志位:", intslice[len(intslice)-1])
// MyUserMgr.offlineUserIds 的 intslice 标识位自动减一
intslice[len(intslice)-1] = intslice[len(intslice)-1] - 1
// 跳出该轮遍历
break
// 调用信息发送功能
}
}
// 判断 MyUserMgr.offlineUserIds 的 intslice 标识位是否为0
if intslice[len(intslice)-1] == 0 {
// 删除数据库中缓存的信息
model.MySmsDao.DeleteMes(sendUserId)
// fmt.Println("缓存删除")
}
}
}
return
}