海量用户即时通讯系统

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. 完成客户端可以发送消息长度,服务器端可以正常收到该长度
    (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
}


  1. 完成客户端可以发送消息本身,服务器端可以正常接收到信息,并根据客户端发送的信息(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), &registerResMes)
	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), &registerMes) //注意,使用引用类型接收 &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
}

猜你喜欢

转载自blog.csdn.net/ALEX_CYL/article/details/123465851