基于go语言开发的海量用户及时通讯系统

文章目录

    • 二十三、海量用户即时通讯系统
      • 1、项目开发前技术准备
      • 2.实现功能-显示客户端登录菜单
      • 3.实现功能-完成用户登录
        • -1.完成客户端可以该长度值发送消息长度,服务器端可以正常接收到
        • -2.完成客户端可以发送消息,服务器端可以接收到消息并根据客户端发送的消息判断用户的合法性并返回相应的消息
        • -3.能够完成登录,并提示信息
        • -4.程序结构的改进
          • 1)画出程序框架图
          • 2)步骤
            • server层后端项目结构图
            • client层后端项目结构图
        • -5.应用redis
          • 1)在Redis手动添加测试用户,并画图+说明注意(后面通过程序注册用户)
          • 2)如输入的用户名密码正确在Redis中存在则登录,否则退出系统,并给出相应的提示信息
          • 3)代码实现
            • (1)先编写了server/model/user.go
            • (2)先编写了server/model/error.go
            • (3)编写了server/model/userDao.go
            • (4)编写了server/main.redis.go
            • (5)编写了server/process/userProcess.go改进登录方式以及错误类型
            • (6)改进server/main/main.go(加了一个初始化redis连接池的函数)
      • 4.完成用户注册操作
        • 1)要求
        • 2)具体代码
          • (1)common/message/user.go(从server/model下复制过来的。记住要复制而不是剪切还要改包名)
          • (2)common/message/message.go增加了关于注册消息的代码
          • (3)server/process/userProcess(增加了一个方法)
          • (4)server/model/userDao(增加了一个Register方法对数据库进行添加的操作)
          • (5)在client/main/main.go进行了调用操作
          • (6)client/process/userProcess.go(添加一个Register的方法)
      • 5.实现功能-完成登录时能返回当前在线用户
        • 3)代码实现
          • (1)编写了server/process/userMgr.go
          • (2)server/process/userProcess.go(在login成功的地方加入代码)
        • 4)当一个新的用户上线后,其他已经登录的用户也能获取最新在新用户列表
      • 6.完成登录可以完成群聊操作
        • -1.步骤1 :
          • 1)思路分析
          • 2)代码实现
        • -2.步骤2.
          • 1)思路分析
          • 2)代码实现
          • 3)拓展功能要求

二十三、海量用户即时通讯系统

项目展示
在这里插入图片描述
开始此项目之前请确保安装好redis,golang
源码下载:https://github.com/BeAlrightc/go-study.git

1、项目开发前技术准备

项目要保存用户信息和信息数据,因此我们需要学习数据(redis或者mysql),这里我们选择redis

2.实现功能-显示客户端登录菜单

在这里插入图片描述

代码编写

clien包下的main.go

package main
import (
	"fmt"
	"os"
)

//定义两个变量,一个表示用户的id,一个表示用户的密码
var userId int
var userPwd string

func main() {
	//接收用户的选择
    var key int
	//判断是否还继续显示菜单
	var loop = true
    
	for loop{
		fmt.Println("-----------欢迎登录多人聊天系统------")
		fmt.Println("\t\t\t 1 登录聊天室")
		fmt.Println("\t\t\t 2 注册用户")
		fmt.Println("\t\t\t 3 退出系统")
		fmt.Println("\t\t\t 请选择 1-3:")

		fmt.Scanf("%d\n",&key)
		switch key {
		case 1 :
			fmt.Println("登录聊天室")
			loop=false
		case 2 :
			fmt.Println("注册用户")	
			loop=false
		case 3 :
			fmt.Println("退出系统")	
			//loop=false
			os.Exit(0)
		default:
			fmt.Println("输入有误,请输入1-3")	
		}
	}

	//根据用户的输入,显示新的提示信息
	if key ==1 {
		//说明用户要登录了
		fmt.Println("请输入用户的id")
		fmt.Scanf("%d\n",&userId)
		fmt.Println("请输入用户的密码")
		fmt.Scanf("%s\n",&userPwd)
		//先把登录函数,写到另外一个文件,先写login.go
		err := login(userId,userPwd)
		if err != nil {
			fmt.Println("登录失败")
		}else {
			fmt.Println("登录成功")
		}
	}else if key ==2 {
		fmt.Println("进行用户注册的逻辑....")

	}

}

clien包下的login.go

package main
import (
	"fmt"
)
//写一个函数,完成登录操作
func login(userId int,userPwd string) (err error) {

	//下一个就要开始定协议
	fmt.Printf("userId = %d userPwd = %s\n",userId,userPwd)
	return nil
}

3.实现功能-完成用户登录

要求:完成指定用户的验证,用户id=100,密码pwd=123456可以登录,其他用户不能登录

理解从client到server中的程序执行流程,如图所示【Message组成的示意图。并发送一个message的流程介绍】

在这里插入图片描述

-1.完成客户端可以该长度值发送消息长度,服务器端可以正常接收到

分析思路

1)先确定消息Message的格式

2)发送消息示意图

在这里插入图片描述

代码展示

sever

main.go

package main
import (
	"fmt"
	"net"
)

//处理和客户端的通讯
func process(conn net.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[:4])
	}

}
func main() {
	//提示信息
	fmt.Println("服务器在8889端口监听....")
	listen, err := net.Listen("tcp","0.0.0.0:8889")
	defer listen.Close()
	if err != nil {
		fmt.Println("net.Listen err=",err)
		return
	} 
	//一旦监听成功,就等待客户端来连接服务器
	for {
		fmt.Println("等待客户端来连接服务器")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("listen.Accept err=",err)
		} 

		//一旦连接成功,则则启动一个协程和客户端保持通讯。。
		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层

login.go

package main
import (
	"fmt"
	"net"
	"encoding/json"
	"encoding/binary"
	"go_code/chatroom/common/message"
)
//写一个函数,完成登录操作
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.Mashal err=",err)
		return
	}
	//5.将data赋给了mes.Data字段
	mes.Data = string(data)
	//6.将mes进行序列化
	data, err =json.Marshal(mes)
	if err != nil {
		fmt.Println("json.Mashal err=",err)
		return
	}
	//7.到这个时候,data就是我们要发送的消息
    //7.1先把data的长度发送给服务器
	//先获取data的长度->转成一个表示长度的byte切片
	var pkgLen uint32
	pkgLen = uint32(len(data))
	var buf [4]byte
	binary.BigEndian.PutUint32(buf[0:4],pkgLen) //将该、长度转成了byte类型是数据
	//发送长度
	n, err := conn.Write(buf[:4])
	if n != 4 || err !=nil {
		fmt.Println("connWrite(buf) fail ",err)
		return
	}

	fmt.Printf("客户端发送数据的消息长度=%d 内容是=%s",len(data),string(data))
	return


}

main.go

package main
import (
	"fmt"
	"os"
)

//定义两个变量,一个表示用户的id,一个表示用户的密码
var userId int
var userPwd string

func main() {
	//接收用户的选择
    var key int
	//判断是否还继续显示菜单
	var loop = true
    
	for loop{
		fmt.Println("-----------欢迎登录多人聊天系统------")
		fmt.Println("\t\t\t 1 登录聊天室")
		fmt.Println("\t\t\t 2 注册用户")
		fmt.Println("\t\t\t 3 退出系统")
		fmt.Println("\t\t\t 请选择 1-3:")

		fmt.Scanf("%d\n",&key)
		switch key {
		case 1 :
			fmt.Println("登录聊天室")
			loop=false
		case 2 :
			fmt.Println("注册用户")	
			loop=false
		case 3 :
			fmt.Println("退出系统")	
			//loop=false
			os.Exit(0)
		default:
			fmt.Println("输入有误,请输入1-3")	
		}
	}

	//根据用户的输入,显示新的提示信息
	if key ==1 {
		//说明用户要登录了
		fmt.Println("请输入用户的id")
		fmt.Scanf("%d\n",&userId)
		fmt.Println("请输入用户的密码")
		fmt.Scanf("%s\n",&userPwd)
		//先把登录函数,写到另外一个文件,先写login.go
		err := login(userId,userPwd)
		if err != nil {
			fmt.Println("登录失败")
		}else {
			fmt.Println("登录成功")
		}
	}else if key ==2 {
		fmt.Println("进行用户注册的逻辑....")

	}

}
-2.完成客户端可以发送消息,服务器端可以接收到消息并根据客户端发送的消息判断用户的合法性并返回相应的消息

思路分析

1)让客户端发送消息本身

2)服务器端接收到消息,然后反序列化成对应的消息结构体

3)服务器端根据反序列化的消息,判断是否登录用户是合法,返回LoginReMes

4)客户端解析返回的LoginReMes,显示对应界面

5)这里我们需要做一些函数的封装

cient/login.go在结尾添加这些coding

//发送消息本身
	_, err = conn.Write(data)
	if err !=nil {
		fmt.Println("connWrite(data) fail ",err)
		return
	}
	//休眠20秒
	time.Sleep(10 * time.Second)
	fmt.Println("休眠了20秒..")
	//这里还需要处理服务器端返回的消息
	return

在server/main.go中我们做了以下改动

将读数据的过程封装了一个函数

package main
import (
	"fmt"
	"net"
	"encoding/json"
	"encoding/binary"
	"go_code/chatroom/common/message"
	//"errors"
	"io"
)
func readPkg(conn net.Conn)(mes message.Message,err error){
	buf := make([]byte,8096)
		fmt.Println("读取客户端发送的数据...")
		//conn.Read()只有在conn没有被关闭的情况下,才会阻塞
		//如果客户端关闭conn则,就不会阻塞
		_, err =conn.Read(buf[:4]) //read出buf中的数据
		if err !=nil {
			//fmt.Println("conn.Read err=",err)
			//err = errors.New("read pkg header error")
			return
		}

		//根据buf[:4]转成uint32类型
		var pkgLen uint32
		pkgLen=binary.BigEndian.Uint32(buf[0:4])

		//根据pkgLen读取消息内容
		n, err :=conn.Read(buf[:pkgLen])
		if n != int(pkgLen) || err !=nil {
			//err = errors.New("read pkg body error")
			return
		}

		//把pkgLen 反序列化成 -->message.Message
		//技术就是一层窗户纸
		json.Unmarshal(buf[:pkgLen],&mes)
		if err != nil {
			fmt.Println("json.Unmarshal err=",err)
			return
		}

		return
}

//处理和客户端的通讯
func process(conn net.Conn) {
    //这里需要延时关闭
	defer conn.Close()
	
	//循环地读客户端发送的信息
	for {
		//这里我们将读取数据包,直接封装成一个函数readPkg(),返回Message,Err
		mes, err :=readPkg(conn)
		if err != nil {
			if err == io.EOF {
				fmt.Println("客户端退出,服务器端也退出...")
				return
			}else {
				fmt.Println("readpkg err=",err)
			}
			return
		}
		fmt.Println("mes=",mes)
	}
}
//main函数下的则没有改变
func main() {
	//提示信息
	fmt.Println("服务器在8889端口监听....")
	listen, err := net.Listen("tcp","0.0.0.0:8889")
	defer listen.Close()
	if err != nil {
		fmt.Println("net.Listen err=",err)
		return
	} 
	//一旦监听成功,就等待客户端来连接服务器
	for {
		fmt.Println("等待客户端来连接服务器")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("listen.Accept err=",err)
		} 

		//一旦连接成功,则则启动一个协程和客户端保持通讯。。
		go process(conn)
	}
}
-3.能够完成登录,并提示信息

server/main.go

添加了发送信息给客户端的代码
func writePkg(conn net.Conn,data []byte)(err error) {

	//先发送一个长度给对方
	var pkgLen uint32
	pkgLen = uint32(len(data))
	var buf [4]byte
	binary.BigEndian.PutUint32(buf[0:4],pkgLen) //将该、长度转成了byte类型是数据
	//发送长度
	n, err := conn.Write(buf[:4])
	if n != 4 || err !=nil {
		fmt.Println("connWrite(buf) fail ",err)
		return
	}

	//发送data本身
	n, err = conn.Write(data)
	if n != int(pkgLen) || err !=nil {
		fmt.Println("connWrite(data) fail ",err)
		return
	}
	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)
	if err != nil {
		fmt.Println("json.Unmarshal fail 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
	} else {
		//不合法
		loginResMes.Code = 500  //500状态码表示用户不存在
		loginResMes.Error = "该用户不存在,请注册再使用。。。"
	}

	//3.将loginResMes 序列化
	data, err := json.Marshal(loginResMes)
	if err != nil {
		fmt.Println("Marshal fail err=",err)
	}

	//4.将data赋值给resMes
	resMes.Data = string(data) 

	//5.对resMes进行序列化,准备发送
	data, err = json.Marshal(resMes)
	if err != nil {
		fmt.Println("Marshal fail err=",err)
		return
	}
	//6.发送data 我们将其封装为writePkg
    err = writePkg(conn,data)
	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("消息类型不存在,无法处理...")
	}
	return
}


在process进行了改定
//处理和客户端的通讯
func process(conn net.Conn) {
    //这里需要延时关闭
	defer conn.Close()
	
	//循环地读客户端发送的信息
	for {
		//这里我们将读取数据包,直接封装成一个函数readPkg(),返回Message,Err
		mes, err :=readPkg(conn)
		if err != nil {
			if err == io.EOF {
				fmt.Println("客户端退出,服务器端也退出...")
				return
			}else {
				fmt.Println("readpkg err=",err)
			}
			return
		}
        //增加了这段代码进行调用这个函数
		err = serverProcessMes(conn,&mes)
		if err != nil {
			return
		}
	}
}

client/utils(增加了一个utils.go用于read的write的操作)

package main
import (
	"fmt"
	"net"
	"encoding/json"
	"encoding/binary"
	"go_code/chatroom/common/message"
)

func readPkg(conn net.Conn)(mes message.Message,err error){
	buf := make([]byte,8096)
		fmt.Println("读取客户端发送的数据...")
		//conn.Read()只有在conn没有被关闭的情况下,才会阻塞
		//如果客户端关闭conn则,就不会阻塞
		_, err =conn.Read(buf[:4]) //先读取之前发送的数据长度
		if err !=nil {
			//fmt.Println("conn.Read err=",err)
			//err = errors.New("read pkg header error")
			return
		}

		//根据buf[:4]转成uint32类型
		var pkgLen uint32
		pkgLen=binary.BigEndian.Uint32(buf[0:4])

		//根据pkgLen(data数据的长度)读取消息内容
		n, err :=conn.Read(buf[:pkgLen])
		if n != int(pkgLen) || err !=nil {
			//err = errors.New("read pkg body error")
			return
		}

		//把pkgLen 反序列化成 -->message.Message
		//技术就是一层窗户纸
		json.Unmarshal(buf[:pkgLen],&mes)
		if err != nil {
			fmt.Println("json.Unmarshal err=",err)  //json的反序列化失败!
			return
		}

		return
}

func writePkg(conn net.Conn,data []byte)(err error) {

	//先发送一个长度给对方
	var pkgLen uint32
	pkgLen = uint32(len(data))
	var buf [4]byte
	binary.BigEndian.PutUint32(buf[0:4],pkgLen) //将该、长度转成了byte类型是数据
	//发送长度
	n, err := conn.Write(buf[:4])
	if n != 4 || err !=nil {
		fmt.Println("connWrite(buf) fail ",err)
		return
	}

	//发送data本身
	n, err = conn.Write(data)
	if n != int(pkgLen) || err !=nil {
		fmt.Println("connWrite(data) fail ",err)
		return
	}
	return

}

client/login.go

//在末尾加入了如下的代码
//这里还需要处理服务器端返回的消息
	mes, err = readPkg(conn) //mes 就是

	if err != nil {
		fmt.Println("readPkg(conn) err=",err)
		return
	}
	
	//将mes的Data部分反序列化为LoginResMes
	var loginResMes message.LoginResMes
	err = json.Unmarshal([]byte(mes.Data),&loginResMes)
	if loginResMes.Code == 200 {
		fmt.Println("登录成功")
	}else if loginResMes.Code == 500 {
		fmt.Println(loginResMes.Error)
	}
	return


}
-4.程序结构的改进

说明:前面的程序虽然完成了功能,但是没有结构,系统的可读性、拓展性和维护性都不好,因此需要对程序的结构进行改进

1)画出程序框架图

在这里插入图片描述

2)步骤

(1)先把分析出来的文件,创建好,然后放到相应的文件夹中

server层后端项目结构图

在这里插入图片描述

(2)现在根据各个文件完成的任务和作用不同,将main.go的代码剥离到对应的文件即可

(3)先修改了utils.go

package utils
 import (
	"fmt"
	"net"
	"encoding/json"
	"encoding/binary"
	"go_code/chatroom/common/message"
 )

 //将这些方法关联到结构体当中
 type Transfer struct {
	//分析应该有哪些字段
	Conn net.Conn
	Buf [8096]byte  //这是传输时使用缓冲
 }

 func (this *Transfer) ReadPkg()(mes message.Message,err error){
		fmt.Println("读取客户端发送的数据...")
		//conn.Read()只有在conn没有被关闭的情况下,才会阻塞
		//如果客户端关闭conn则,就不会阻塞
		_, err =this.Conn.Read(this.Buf[:4]) //先读取之前发送的数据长度
		if err !=nil {
			//fmt.Println("conn.Read err=",err)
			//err = errors.New("read pkg header error")
			return
		}

		//根据buf[:4]转成uint32类型
		var pkgLen uint32
		pkgLen=binary.BigEndian.Uint32(this.Buf[0:4])

		//根据pkgLen(data数据的长度)读取消息内容
		n, err :=this.Conn.Read(this.Buf[:pkgLen])
		if n != int(pkgLen) || err !=nil {
			//err = errors.New("read pkg body error")
			return
		}

		//把pkgLen 反序列化成 -->message.Message
		//技术就是一层窗户纸
		json.Unmarshal(this.Buf[:pkgLen],&mes)
		if err != nil {
			fmt.Println("json.Unmarshal err=",err)  //json的反序列化失败!
			return
		}

		return
}

func (this *Transfer) WritePkg(data []byte)(err error) {

	//先发送一个长度给对方
	var pkgLen uint32
	pkgLen = uint32(len(data))
	binary.BigEndian.PutUint32(this.Buf[0:4],pkgLen) //将该、长度转成了byte类型是数据
	//发送长度
	n, err := this.Conn.Write(this.Buf[:4])
	if n != 4 || err !=nil {
		fmt.Println("connWrite(this.Buf) fail ",err)
		return
	}

	//发送data本身
	n, err = this.Conn.Write(data)
	if n != int(pkgLen) || err !=nil {
		fmt.Println("connWrite(data) fail ",err)
		return
	}
	return

}

(4)修改了process2/userProcess.go

package process2
import (
	"fmt"
	"net"
	"encoding/json"
	"go_code/chatroom/common/message"
	"go_code/chatroom/server/utils"
)

type UserProcess struct {
	//字段
	Conn net.Conn
}

//编写一个函数serverProcessLogin函数,专门处理登录请求
func (this *UserProcess) ServerProcessLogin(mes *message.Message)(err error){
	//核心代码
	//1.先从mes中取出mes.Data,并直接反序列化成LoginMes
	var loginMes message.LoginMes
	err =json.Unmarshal([]byte(mes.Data),&loginMes)
	if err != nil {
		fmt.Println("json.Unmarshal fail 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
	} else {
		//不合法
		loginResMes.Code = 500  //500状态码表示用户不存在
		loginResMes.Error = "该用户不存在,请注册再使用。。。"
	}

	//3.将loginResMes 序列化
	data, err := json.Marshal(loginResMes)
	if err != nil {
		fmt.Println("Marshal fail err=",err)
	}

	//4.将data赋值给resMes
	resMes.Data = string(data) 

	//5.对resMes进行序列化,准备发送
	data, err = json.Marshal(resMes)
	if err != nil {
		fmt.Println("Marshal fail err=",err)
		return
	}
	//6.发送data 我们将其封装为writePkg
	//因为使用了分层模式(mvc),我们先创建一个Transfer实例,然后读取
	tf := &utils.Transfer{
		Conn : this.Conn,
	}
    err = tf.WritePkg(data)
	return
}

(5)修改了main/processor.go

package main
import (
	"fmt"
	"net"
	"go_code/chatroom/common/message"
	"go_code/chatroom/server/utils"
	"go_code/chatroom/server/process"
	"io"
)

//先创建一个Processor的结构体
type Processor struct {
	Conn net.Conn
}

//编写一个ServerProcessMes函数
//功能 :根据客户端发送的消息种类不同,决定调用哪个函数处理
func (this *Processor) serverProcessMes(mes *message.Message)(err error) {
	switch mes.Type {
	case message.LoginMesType :
		//处理登录的逻辑
		//创建一个UserProcess实例
		up := &process2.UserProcess{
			Conn : this.Conn,
		}
		err = up.ServerProcessLogin(mes)
	case message.RegisterMesType :
		//处理注册
	default :
		fmt.Println("消息类型不存在,无法处理...")
	}
	return
}

func (this *Processor) process2()(err error){
		//循环地读客户端发送的信息
		for {
			//这里我们将读取数据包,直接封装成一个函数readPkg(),返回Message,Err
			//创建一个Transfer实例完成读包任务
			tf := &utils.Transfer{
				Conn : this.Conn,
			}
			mes, err :=tf.ReadPkg()
			if err != nil {
				if err == io.EOF {
					fmt.Println("客户端退出,服务器端也退出...")
					return err
				}else {
					fmt.Println("readpkg err=",err)
				}
				return err
			}
			err = this.serverProcessMes(&mes)
			if err != nil {
				return err
			}
		}
}

修改了main/main.go

package main
import (
	"fmt"
	"net"
)

//处理和客户端的通讯
func process(conn net.Conn) {
    //这里需要延时关闭
	defer conn.Close()
	
	//这里调用总控,创建一个processor实例
	processor := &Processor{
		Conn : conn,
	}
	err := processor.process2()
	if err != nil {
		fmt.Println("客户端和服务器通讯的协程错误=err",err)
		return 
	}
}
func main() {
	//提示信息
	fmt.Println("服务器[新的结构]在8889端口监听....")
	listen, err := net.Listen("tcp","0.0.0.0:8889")
	defer listen.Close()
	if err != nil {
		fmt.Println("net.Listen err=",err)
		return
	} 
	//一旦监听成功,就等待客户端来连接服务器
	for {
		fmt.Println("等待客户端来连接服务器")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("listen.Accept err=",err)
		} 

		//一旦连接成功,则则启动一个协程和客户端保持通讯。。
		go process(conn)
	}
}

修改客户端。先画出程序的框架图,再写代码

client层后端项目结构图

在这里插入图片描述

(2)先把各个文件放到对应的文件夹[包]

在这里插入图片描述

(3)将server/utils.go拷贝到client/utils/utils.go

(4)创建了client/process/userProcess.go

package process
import (
	"fmt"
	"net"
	"encoding/json"
	"encoding/binary"
	"go_code/chatroom/common/message"
	"go_code/chatroom/client/utils"
)
type UserProcess struct {
	//暂时不需要字段
}
//给关联一个用户登录的方法
//写一个函数,完成登录操作
func (this *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.Mashal err=",err)
		return
	}
	//5.将data赋给了mes.Data字段
	mes.Data = string(data)
	//6.将mes进行序列化
	data, err =json.Marshal(mes)
	if err != nil {
		fmt.Println("json.Mashal err=",err)
		return
	}
	//7.到这个时候,data就是我们要发送的消息
    //7.1先把data的长度发送给服务器
	//先获取data的长度->转成一个表示长度的byte切片
	var pkgLen uint32
	pkgLen = uint32(len(data))
	var buf [4]byte
	binary.BigEndian.PutUint32(buf[0:4],pkgLen) //将该、长度转成了byte类型是数据
	//发送长度
	n, err := conn.Write(buf[:4])
	if n != 4 || err !=nil {
		fmt.Println("connWrite(buf) fail ",err)
		return
	}

	//fmt.Printf("客户端发送数据的消息长度=%d 内容是=%s",len(data),string(data))
	//发送消息本身
	_, err = conn.Write(data)
	if err !=nil {
		fmt.Println("connWrite(data) fail ",err)
		return
	}
	//休眠20秒
	// time.Sleep(10 * time.Second)
	// fmt.Println("休眠了20秒..")
	//这里还需要处理服务器端返回的消息
	//创建一个Transfer实例
	tf := &utils.Transfer{
		Conn : conn,
	}
	mes, err = tf.ReadPkg() //mes 就是

	if err != nil {
		fmt.Println("readPkg(conn) err=",err)
		return
	}
	
	//将mes的Data部分反序列化为LoginResMes
	var loginResMes message.LoginResMes
	err = json.Unmarshal([]byte(mes.Data),&loginResMes)
	if loginResMes.Code == 200 {
		//fmt.Println("登录成功")

		//这里我们还需要再客户端启动一个协程
		//该协程保持和服务器端的通讯,如果服务器有数据推送给客户端
		//则可以接受并显示在客户端的终端
		go serverProcessMes(conn)
		//1.显示登录成功后的菜单[循环显示]
		for {
			ShowMenu()
		}
	}else if loginResMes.Code == 500 {
		fmt.Println(loginResMes.Error)
	}
	return
}

说明:该文件就是在原来login.go做了一个改进,封装到userProcess结构体

(5)创建了server/process/server.go

package process
import (
	"fmt"
	"os"
	"go_code/chatroom/client/utils"
	"net"
)

//显示登录后的界面..
func ShowMenu(){
	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("显示用户在线列表")
		case 2:
			fmt.Println("发送消息")
		case 3:
			fmt.Println("信息列表")
		case 4:
			fmt.Println("你选择退出系统 ")	
			os.Exit(0)	
		default:
			fmt.Println("你输入的选项不正确")		
	}
}
//和服务器保持通讯
func serverProcessMes(conn net.Conn) {
	//创建一个transfer实例,不停的读取服务器发送的消息
	tf := &utils.Transfer{
		Conn : conn,
	}
	for {
		fmt.Printf("客户端正在等待读取服务器发送的消息")
		mes, err:=tf.ReadPkg()
		if err != nil {
			fmt.Println("tf.ReadPkg err=",err)
			return
		}
		//如果读取到消息,又是下一步处理逻辑
		fmt.Printf("mes=%v",mes)


	}

}

(6)client/main/main.go

package main
import (
	"fmt"
	"os"
	"go_code/chatroom/client/process"
)

//定义两个变量,一个表示用户的id,一个表示用户的密码
var userId int
var userPwd string

func main() {
	//接收用户的选择
    var key int
	//判断是否还继续显示菜单
	// loop = true
    
	for true{
		fmt.Println("-----------欢迎登录多人聊天系统------")
		fmt.Println("\t\t\t 1 登录聊天室")
		fmt.Println("\t\t\t 2 注册用户")
		fmt.Println("\t\t\t 3 退出系统")
		fmt.Println("\t\t\t 请选择 1-3:")

		fmt.Scanf("%d\n",&key)
		switch key {
		case 1 :
			fmt.Println("登录聊天室")
			fmt.Println("请输入用户的id")
			fmt.Scanf("%d\n",&userId)
			fmt.Println("请输入用户的密码")
			fmt.Scanf("%s\n",&userPwd)
			//完成登录
			//1.创建一个UserProcess的实例
			up :=&process.UserProcess{}
			up.Login(userId,userPwd)
			//loop=false
		case 2 :
			fmt.Println("注册用户")	
			//loop=false
		case 3 :
			fmt.Println("退出系统")	
			//loop=false
			os.Exit(0)
		default:
			fmt.Println("输入有误,请输入1-3")	
		}
	}
}
-5.应用redis
1)在Redis手动添加测试用户,并画图+说明注意(后面通过程序注册用户)

在这里插入图片描述

手动直接在redis增加一个用户信息

在这里插入图片描述

2)如输入的用户名密码正确在Redis中存在则登录,否则退出系统,并给出相应的提示信息
  • 1.用户不存在,你也可以重新注册,再登录
  • 2.你的密码不正确
3)代码实现
(1)先编写了server/model/user.go
package model

//定义一个用户的结构体
type User struct {
	//确定字段信息
	//为了序列化和反序列化成功
	//用户信息的json字符串与结构体字段对应的Tag名字一致
	UserId int `json:"userId"`
	UserPwd string `json:"userPwd"`
	UserName string `json:"userName"`
}
(2)先编写了server/model/error.go
package model
import (
	"errors"
)

//根据业务逻辑的需要,自定义一些错误

var (
	ERROR_USER_NOTEXIST = errors.New("用户不存在。。")
	ERROR_USER_EXIST = errors.New("用户已存在。。")
	ERROR_USER_PWD = errors.New("密码错误")

)
(3)编写了server/model/userDao.go
package model
import (
	"fmt"
	"github.com/garyburd/redigo/redis"
	"encoding/json"
)

//我们在服务器启动后,就初始化一个UserDao实例
//把它做成全局的变量,在需要和redis操作时,就直接使用即可
var (
	MyUserDao *UserDao
)
//定义一个UserDao结构体
//完成对User 结构体的各种操作

type UserDao struct {
	pool *redis.Pool 
}

//使用工厂模式创建一个UserDao实例
func NewUserDao(pool *redis.Pool) (userDao *UserDao){
	userDao = &UserDao{
	pool:pool,
  }
  return
}

//写方法,应该提供哪个方法呢
//1,根据用户id返回一个User实例+err
func (this *UserDao) getUserById(conn redis.Conn,id int) (user *User,err error) {

	//通过给定的id去redis去查询用户
	res,err := redis.String(conn.Do("HGet","users",id))
	if err != nil {
		//错误
		if err == redis.ErrNil {//表示在users中没有找到对应的id
			err= ERROR_USER_NOTEXIST
		}
		return
	}
	user = &User{}

	//这里我们需要反序列化成一个User实例
	err = json.Unmarshal([]byte(res),user)
	if err != nil {
		fmt.Println("json.Unmarshal Err=",err)
		return
	}
	return

}

//完成登录的校验 Login
//1.Login 完成对用户的验证
//2.如果用户的id和pwd都正确,则返回一个User实例
//3.如果用户的id和pwd有错误,则返回对应的错误信息

func (this *UserDao)Login(userId int,userPwd string)(user *User,err error){

	//先从UserDao链接池中取出一根连接
	conn := this.pool.Get()
	defer conn.Close()
	user,err = this.getUserById(conn,userId)
	if err != nil {
		return
	}
	//这时证明用户是获取到了
	if user.UserPwd != userPwd {
		err = ERROR_USER_PWD
		return
	}
	return
}

(4)编写了server/main.redis.go
package main
import (
	"github.com/garyburd/redigo/redis"
	"time"

)

//定义一个全局的pool
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)编写了server/process/userProcess.go改进登录方式以及错误类型
//我们需要到redis数据库去完成验证
	//1.使用model.MyUserDao到redis去验证
	user, err := model.MyUserDao.Login(loginMes.UserId,loginMes.UserPwd)
	
	if err != nil {
		if err ==model.ERROR_USER_NOTEXIST {
			loginResMes.Code = 500
		    loginResMes.Error = err.Error()
		}else if err ==model.ERROR_USER_PWD {
			loginResMes.Code = 403
		    loginResMes.Error = err.Error()
		}else {
			loginResMes.Code = 505
		    loginResMes.Error = "服务器内部错误..."
		}
		
		//这里我们先测试成功,然后再返回具体的错误信息
	}else{
		loginResMes.Code = 200
		fmt.Println(user,"登录成功")
	}
(6)改进server/main/main.go(加了一个初始化redis连接池的函数)
func init(){
	//当服务器启动时,我们就去初始化我们的redis的连接池
	initPool("localhost:6379",16,0,300 * time.Second)
    initUserDao()
}

//这里我们编写一个函数完成对UserDao的初始化任务
func initUserDao() {
	//这里的pool本身就是一个全局的变量
	//这里需要注意一个初始化的顺序问题
	//initPool,在initUserDao
	model.MyUserDao =model.NewUserDao(pool)
}

4.完成用户注册操作

1)要求

完成注册功能,将用户信息录入到Redis中

思路分析,并完成代码

思路分析的示意图

2)具体代码
(1)common/message/user.go(从server/model下复制过来的。记住要复制而不是剪切还要改包名)
package message

// User 定义一个用户的结构体
type User struct {
	//确定字段信息
	//为了序列化和反序列化成功
	//用户信息的json字符串与结构体字段对应的Tag名字一致
	UserId   int    `json:"userId"`
	UserPwd  string `json:"userPwd"`
	UserName string `json:"userName"`
}
(2)common/message/message.go增加了关于注册消息的代码
type RegisterMes struct {
	User User `json:"user"` //类型就是User结构体

 }
type RegisterResMes struct {
	Code int `json:"code"` //返回状态码400表示该用户已经占用 200表示登录注册成功
	Error string `json` //返回错误信息
}
(3)server/process/userProcess(增加了一个方法)
func (this *UserProcess) ServerProcessRegister(mes *message.Message) (err error){
	//1.先从mes中取出mes.Data,并直接反序列化成RegisterMes
	var registerMes message.RegisterMes
	err = json.Unmarshal([]byte(mes.Data), &registerMes)
	if err != nil {
		fmt.Println("json.Unmarshal fail err=", err)
		return
	}

	//1.先声明一个resMes
	var resMes message.Message
	resMes.Type = message.RegisterResMesType
	//2.再声明一个RegisterMes
	var registerResMes message.RegisterResMes

	//我们需要到redis数据库去完成注册
	//1.使用model.MyUserDao到redis去注册
	err= model.MyUserDao.Register(&registerMes.User)
	if err !=nil {
		if err == model.ERROR_USER_EXISTS {
			registerResMes.Code = 505
			registerResMes.Error = model.ERROR_USER_EXISTS.Error()
		} else {
			registerResMes.Code = 506
			registerResMes.Error = "注册时发生未知错误"
		}
	} else {
		registerResMes.Code = 200
	}

	//3.将loginResMes 序列化
	data, err := json.Marshal(registerResMes)
	if err != nil {
		fmt.Println("Marshal fail err=", err)
	}

	//4.将data赋值给resMes
	resMes.Data = string(data)

	//5.对resMes进行序列化,准备发送
	data, err = json.Marshal(resMes)
	if err != nil {
		fmt.Println("Marshal fail err=", err)
		return
	}
	//6.发送data 我们将其封装为writePkg
	//因为使用了分层模式(mvc),我们先创建一个Transfer实例,然后读取
	tf := &utils.Transfer{
		Conn: this.Conn,
	}
	err = tf.WritePkg(data)
	return

}
(4)server/model/userDao(增加了一个Register方法对数据库进行添加的操作)
func (this *UserDao)Register(user *message.User)(err error){

	//先从UserDao链接池中取出一根连接
	conn := this.pool.Get()
	defer conn.Close()
	_,err = this.getUserById(conn,user.UserId)
	if err == nil {
		err = ERROR_USER_EXISTS
		return
	}
	//这时说明id在redis还没有,则可以完成注册
	data, err :=json.Marshal(user) //序列化
	if err != nil {
		return
	}
	//入库
	_,err = conn.Do("HSet","users",user.UserId,string(data))
	if err != nil {
		fmt.Println("保存注册用户错误 err=",err)
		return
	}
	return

}
(5)在client/main/main.go进行了调用操作
case 2 :
			fmt.Println("注册用户")	
			fmt.Println("请输入用户id")	
			fmt.Scanf("%d\n",&userId)
			fmt.Println("请输入用户的密码")
			fmt.Scanf("%s\n",&userPwd)
			fmt.Println("请输入用户的名字(昵称)")
			fmt.Scanf("%s\n",&userName)
			//2.调用UserProcess,完成注册的请求
			up :=&process.UserProcess{}
			up.Register(userId,userPwd,userName)
(6)client/process/userProcess.go(添加一个Register的方法)
func (this *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.创建一个RegisterMes 结构体
	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.Mashal err=",err)
		return
	}

	//5.将data赋给了mes.Data字段
	mes.Data = string(data)

	//6.将mes进行序列化
	data, err =json.Marshal(mes)
	if err != nil {
		fmt.Println("json.Mashal err=",err)
		return
	}
	
	//7.到这个时候,data就是我们要发送的消息
    //7.1先把data的长度发送给服务器
	//先获取data的长度->转成一个表示长度的byte切片
	var pkgLen uint32
	pkgLen = uint32(len(data))
	var buf [4]byte
	binary.BigEndian.PutUint32(buf[0:4],pkgLen) //将该、长度转成了byte类型是数据
	//发送长度
	n, err := conn.Write(buf[:4])
	if n != 4 || err !=nil {
		fmt.Println("connWrite(buf) fail ",err)
		return
	}

	fmt.Printf("客户端发送数据的消息长度=%d 内容是=%s",len(data),string(data))
	//发送消息本身
	_, err = conn.Write(data)
	if err !=nil {
		fmt.Println("connWrite(data) fail ",err)
		return
	}

	//创建一个Transfer实例
	tf := &utils.Transfer{
		Conn : conn,
	}

	//发送data给服务器端
	err = tf.WritePkg(data)
	if err != nil {
		fmt.Println("注册发送信息错误 err=",err)
	}

	mes, err = tf.ReadPkg() //mes 就是RegisterResMes

	if err != nil {
		fmt.Println("readPkg(conn) err=",err)
		return
	}

	//将mes的Data部分反序列化为RegisterResMes
	var registerResMes message.RegisterResMes
	err = json.Unmarshal([]byte(mes.Data),&registerResMes)
	if registerResMes.Code == 200 {
		fmt.Println("注册成功,你重新登录一把")
		os.Exit(0)
	}else {
		fmt.Println(registerResMes.Error)
		os.Exit(0)
	}
	return

}

5.实现功能-完成登录时能返回当前在线用户

1)用户登陆后,可以得到当前在线用户列表 思路分析、示意图代码实现

用户登陆后,可以得到当前在线用户列表

(1)在服务器端维护一个onlineUsers map[int] *UserProcess

(2)创建一个新的文件userMgr.go,完成功能,对onlineUsers这个map进行增删改查

(3)在loginResMes增加一个字段 User []int 将在线的用户ID返回

(4)当用户登陆后,可以显示当前在线用户列表

2)示意图

在这里插入图片描述

3)代码实现
(1)编写了server/process/userMgr.go
package process
import (
	"fmt"
)
//因为UserMge实例在服务其中有且只有一个
//因为在很多的地方,都会使用,因此,我们
//将其定义为全局变量
var (
	userMgr *UserMgr
)
type UserMgr struct {
	onlineUsers map[int]*UserProcess
}

//完成对userMge的初始化工作
func init() {
	userMgr = &UserMgr{
		onlineUsers : make(map[int]*UserProcess,1024),
	}
}

//完成对onlineUsers的添加
func (this *UserMgr) AddOnlinesUser(up *UserProcess) {
	this.onlineUsers[up.UserId] = up
}

//删除
func (this *UserMgr) DeleteOnlinesUser(userId int ) {
	delete(this.onlineUsers,userId)
}

//返回当前所有在线的用户
func (this *UserMgr)GetAllUsers() map[int]*UserProcess {
	return this.onlineUsers
}

//根据id返回对应的值
func(this *UserMgr) GetOnlineUserById(userId int) (up *UserProcess,err error){
	//如何从map中取出一个值,待检测的方式
	up, ok := this.onlineUsers[userId]
	if !ok { //说明你要查找的用户,当前不在线
		err = fmt.Errorf("用户id不存在",userId)
		return
	} 
	return
}
(2)server/process/userProcess.go(在login成功的地方加入代码)
} else {
		loginResMes.Code = 200
		//这里,因为用户登录成功,我们就把登录成功的用户放入到userMgr中
		//将登录成功的用户的userId赋给this
		this.UserId = loginMes.UserId
		userMgr.AddOnlinesUser(this)
		//将当前在线用户的id放入到loginResMes.UsersId
		//遍历userMgr.onlineUsers
		for id, _ := range userMgr.onlineUsers{
			loginResMes.UsersId = append(loginResMes.UsersId,id)
		}
		fmt.Println(user, "登录成功")
	}

(3)client

/process/userProcess.go(在login成功的地方加入代码)

//现在可以显示当前在线的列表 遍历loginResMes.UsersId
		fmt.Println("当前在线用户列表如下")
		for _, v := range loginResMes.UsersId {
			//如果我们要求不显示自己在线,下面我们增加一个代码
			if v == userId {
				continue
			}
			
			fmt.Println("用户id:\t",v)
		}
		fmt.Println("\n\n")
4)当一个新的用户上线后,其他已经登录的用户也能获取最新在新用户列表

思路1:

当有一个用户上线后,服务其就马上把维护的onlineUser map整体推送

思路2:

服务其有自己的策略,每隔一段时间,把维护的onlineUsers map整体推送

思路3:

(1)当一个用户上线后,服务器就把A用户的上线信息推送给所有在线用户即可

(2)客户端也要维护一个map,map中记录了他的好友(目前就是所有人)map[int]User

(3)客户端和服务器的通讯通道要依赖于serverProcess协程

代码实现

(1)在server/process/userMgr.go

package process
import (
	"fmt"
	"go_code/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.Println("用户id:\t\t",id)
	}
}

//编写一个方法,处理返回的NotifyUserStatusMes
func updateUserStatus(notifyUserStatusMes *message.NotifyUserStatusMes) {
	
	//适当的优化
	user,ok :=onlineUsers[notifyUserStatusMes.UserId]
	if !ok { //原来没有
		user = &message.User{
			UserId : notifyUserStatusMes.UserId,
		}	
	}

	user.UserStatus = string(notifyUserStatusMes.Status)
	onlineUsers[notifyUserStatusMes.UserId] = user

	outputOnlineUser()
}

(2)server/process/userProcess.go

//这里我们编写通知所有在线用户的方法
//这个id要通知其他的在线用户,我上线
func (this *UserProcess) NotifyOthersOnlineUser(userId int) {

	//遍历 onlineUsers ,然后一个一个的发送 NotifyUserStatusMes
	for id, up := range userMgr.onlineUsers {
		//过滤掉自己
		if id == userId {
			continue
		}
		//开始通知【单独的写一个方法】
		up.NotifyMeOnline(userId)
	}

}

func (this *UserProcess) NotifyMeOnline(userId int){

	//组装我们的NotifyUserStatusMes
	var mes message.Message
	mes.Type = message.NotifyUserStatusMesType

	var notifyUserStatusMes message.NotifyUserStatusMes
	notifyUserStatusMes.UserId = userId
	notifyUserStatusMes.Status = message.UserOnline

	//将notifyUserStatusMes序列化
	data, err := json.Marshal(notifyUserStatusMes)
	if err != nil {
		fmt.Println("json.Marshal err",err)
		return
	}
	//将序列化后的notifyUserStatusMes赋值给mes.Data
	mes.Data = string(data)

	//对message再次序列化
	data, err = json.Marshal(mes)
	if err != nil {
		fmt.Println("json.Marshal err",err)
		return
	}
	//发送,创建一个transfer实例发送
	tf := &utils.Transfer{
		Conn : this.Conn,
	}
	err = tf.WritePkg(data)
	if err != nil {
		fmt.Println("NotifyMeOline err=",err)
		return
	}

}

下面调用
//通知其他的用户我上线了
		this.NotifyOthersOnlineUser(loginMes.UserId)

(3)common/message/message.go

//为了配合服务器端推送用户状态变化类型
type NotifyUserStatusMes struct {
	UserId int `json:"userId"` //用户id
	Status int `json:"status"` //用户的状态
}

(4)客户端client/process/userMgr.go

package process
import (
	"fmt"
	"go_code/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.Println("用户id:\t\t",id)
	}
}

//编写一个方法,处理返回的NotifyUserStatusMes
func updateUserStatus(notifyUserStatusMes *message.NotifyUserStatusMes) {
	
	//适当的优化
	user,ok :=onlineUsers[notifyUserStatusMes.UserId]
	if !ok { //原来没有
		user = &message.User{
			UserId : notifyUserStatusMes.UserId,
		}	
	}

	user.UserStatus = string(notifyUserStatusMes.Status)
	onlineUsers[notifyUserStatusMes.UserId] = user

	outputOnlineUser()
}

(5)client/main/server.go

case 1:
			//fmt.Println("显示用户在线列表")
			outputOnlineUser()
		case 2:


//如果读取到消息,又是下一步处理逻辑
		switch mes.Type {
			case message.NotifyUserStatusMesType : //有人上线了
			//1.取出 NotifyUserStatusMes
			var notifyUserStatusMes message.NotifyUserStatusMes
			json.Unmarshal([]byte(mes.Data),&notifyUserStatusMes)
			//2.把这个用户的信息,状态保存在客户端的map[int]User中
			updateUserStatus(&notifyUserStatusMes)
			//处理

			default :
			fmt.Println("服务其端返回了未知的消息类型")	

		}

6.完成登录可以完成群聊操作

-1.步骤1 :

当一个用户上线后,可以将群聊消息发给服务器。服务器可以接收到

1)思路分析

在这里插入图片描述

(1)新增一个消息结构体

(2)新增一个model CurUser

(3)在smsProcess增加相应的方法 SendGroupMes,

2)代码实现

(1)common/message/message.go

//增加一个SmsMes //发送的
type SmsMes struct {
	Content string   `json:"content"` //内容
	User //匿名结构体,继承
}

(2)client/model/curUser.go

package model
import (
	"net"
	"go_code/chatroom/common/message"
)

//因为在客户端,我们很多地方会使用到curUser,我们将其作为一个全局的
type CurUser struct {
	Conn net.Conn
	message.User
}

(3)client/process/smsProcess.go

package process
import (
	"fmt"
	"encoding/json"
	"go_code/chatroom/common/message"
	"go_code/chatroom/client/utils"
)

type SmsProcess struct {

}

//发送群聊的消息
func (this *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 = CurUser.UserId
	smsMes.UserStatus = CurUser.UserStatus

	//3.序列化smsMes
	data, err := json.Marshal(smsMes)
	if err != nil {
		fmt.Println("SendGroupMes json.Marshal err=",err.Error())
		return
	}
	mes.Data = string(data)

	//4.对mes再次序列化
	data, err = json.Marshal(mes)
	if err != nil {
		fmt.Println(" json.Marshal err=",err.Error())
		return
	}

	//5.将mes发送给服务器
	tf := &utils.Transfer{
		Conn : CurUser.Conn,
	}

	//6.发送
	err = tf.WritePkg(data)
	if err != nil {
		fmt.Println("SendGroupsMes err=",err.Error())
		return
	}

	return
}

(4)测试

在这里插入图片描述

-2.步骤2.

服务器可以将接收到的消息,群发给所有在线用户(发送者除外)

1)思路分析

在这里插入图片描述

(1)在服务器端接收到SmsMes消息

(2)在server/process/SmsProcess.go文件增加群发消息的方法

(3)在客户端还要增加去处理服务器端转发的群发消息

2)代码实现

(1)server/main/processor.go[在server中调用转发消息的方法]

//处理注册
		up := &process2.UserProcess{
			Conn : this.Conn,
		}
		err = up.ServerProcessRegister(mes)
	case message.SmsMesType :
		//创建一个SmsProcess实例完成转发群聊消息。	
		smsProcess := &process2.SmsProcess{}
		smsProcess.SendGroupMes(mes)

(2)client/process/smsMes.go

package process
import (
	"fmt"
	"encoding/json"
	"go_code/chatroom/common/message"
	"go_code/chatroom/client/utils"
)

type SmsProcess struct {

}

//发送群聊的消息
func (this *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 = CurUser.UserId
	smsMes.UserStatus = CurUser.UserStatus

	//3.序列化smsMes
	data, err := json.Marshal(smsMes)
	if err != nil {
		fmt.Println("SendGroupMes json.Marshal err=",err.Error())
		return
	}
	mes.Data = string(data)

	//4.对mes再次序列化
	data, err = json.Marshal(mes)
	if err != nil {
		fmt.Println(" json.Marshal err=",err.Error())
		return
	}

	//5.将mes发送给服务器
	tf := &utils.Transfer{
		Conn : CurUser.Conn,
	}

	//6.发送
	err = tf.WritePkg(data)
	if err != nil {
		fmt.Println("SendGroupsMes err=",err.Error())
		return
	}

	return
}

(3)client/process/smsMgr.go

package process
import (
	"fmt"
	"encoding/json"
	"go_code/chatroom/common/message"

)

func outputGroupMes(mes *message.Message) {//这个地方一定是SmsMes
	//显示即可
	//1.反序列化mes.Data
	var smsMes message.SmsMes 
	err := json.Unmarshal([]byte(mes.Data),&smsMes)
	if err != nil {
		fmt.Println("json.Unmarshal err=",err.Error())
		return
	}

	//显示信息
	info := fmt.Sprintf("用户id:\t%d 对大家说:\t%s",smsMes.UserId,smsMes.Content)
	fmt.Println(info)
	fmt.Println()
}

(4)client/process/server.go

case message.SmsMesType : //有人群发消息了
				outputGroupMes(&mes)
3)拓展功能要求

1.可以实现私聊(点对点聊天)

2.如果一个登录用户离线,就把这个人从在线列表中去掉

3.实现离线留言,在群聊时,如果某个用户没有在线,当登录后,可以接受到离线的消息

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/251858.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

一键安装下载3ds Max!别墅还是宫殿?3ds Max助你建造梦幻般的艺术建筑

不再浪费时间在网上寻找3ds Max的安装包了!因为你所需要的一切都可以在这里找到!作为一款全球领先的3D设计工具,3ds Max为创作者们带来了前所未有的便利和创作灵感。无论是建筑设计、影视特效还是游戏开发,3ds Max都能帮助你实现想…

Keil新建STM32软件工程 - (详细步骤图文)

文章目录 1. 前言2. 下载芯片对应的Keil开发包3. 下载芯片对应的标准外设库 - STM32F10x_StdPeriph_Lib_Vx.x.x4. 新建工程文件夹 - Demo34.1 移植标准外设库4.2 启动文件介绍及如何选择 5. 新建软件工程 - Demo5.1 打开Keil → Project → New uVision Project5.2 选择芯片型号…

银行数字化转型导师坚鹏:银行数字化转型正在重塑您的工作

您好,我是银行数字化转型导师坚鹏。坚持知行果合一,赋能数字化转型!非常荣幸和您分享关于银行数字化转型如何影响老百姓工作的一些思考。 您知道吗?银行数字化转型给您的工作方式带来新变化、新趋势、新潮流啦!在这个…

SpringBoot配置线程池

SpringBoot配置线程池 1、线程池简介 1.1 什么是线程池 线程池是一种利用池化技术思想来实现的线程管理技术,主要是为了复用线程、便利地管理线程和任务、并将线程 的创建和任务的执行解耦开来。我们可以创建线程池来复用已经创建的线程来降低频繁创建和销毁线程…

STM32迪文屏图标控件保姆级教程

要主图的去末尾,末尾福利图在等着你~~~ 文章目录 前言 开发环境 二、使用步骤 1.添加图标控件 2.设置图标属性 3.图标库ICL文件生成 4.单片机程序编写 容易踩得坑 一、前言 本篇文章主要介绍了在DGBUS平台上使用图标变量的步骤。首先需要在DGBUS中添加一个图标变量控…

股票价格预测 | Python实现基于Stacked-LSTM的股票预测模型,可预测未来(keras)

文章目录 效果一览文章概述模型描述源码设计效果一览 文章概述 以股票价格预测为例,基于Stacked-LSTM的股票预测模型(keras),可预测未来。 模型描述 LSTM 用于处理序列数据,如时间序列、文本和音频。相对于传统的RNN,LSTM更擅长捕获长期依赖关系,

录制第一个jmeter性能测试脚本2(http协议)_图书管理系统

我们手工编写了一个测试计划,现在我们通过录制的方式来实现那个测试计划。也就是说‘’测试计划目标和上一节类似:让5个用户在2s内登录图书管理系统,然后进入 页面进行查看。 目录 欢迎访问我的免费课程 PPT、安装包、视频应有尽有&#xff…

【PHP入门】1.2-常量与变量

-常量与变量- PHP是一种动态网站开发的脚本语言,动态语言特点是交互性,会有数据的传递,而PHP作为“中间人”,需要进行数据的传递,传递的前提就是PHP能自己存储数据(临时存储) 1.2.1变量基本概…

​FL Studio2024最新版本好不好用?有哪些新功能

FL Studio2024版是一款在国内非常受欢迎的多功能音频处理软件,我们可以通过这款软件来对多种不同格式的音频文件来进行编辑处理。而且FL Studio 2024版还为用户们准备了超多的音乐乐器伴奏,我们可以直接一键调取自己需要的音调。 FL Studio 2024版不仅拥…

阿里 P7 三面凉凉,kafka Borker 日志持久化没答上来

👏作者简介:大家好,我是爱敲代码的小黄,阿里巴巴淘天Java开发工程师,CSDN博客专家📕系列专栏:Spring源码、Netty源码、Kafka源码、JUC源码、dubbo源码系列🔥如果感觉博主的文章还不错…

C语言指针4

1. #define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h>int main() {int a 10;int* p &a;//一级指针int** pp &p;//二级指针return 0; }上述代码中p的类型是int* pp的类型是int** 2.int* arr[5]; 数组arr的每个元素是整形指针 3.定义一个变量时,去掉变…

css 使用flex 完成瀑布流布局

瀑布流布局在商城类、文章类 app、网页中都是常用的&#xff0c;使用这样的形式&#xff0c;能过让整个页面更加的活波&#xff0c;也能让图片根据实际的大小来显示&#xff0c;更好的展示图片内容。那么代码如何实现呢 实现的效果 代码 <template><view class"…

系统架构设计师教程(七)系统架构设计基础知识

系统架构设计基础知识 7.1 软件架构概念7.1.1 软件架构的定义7.1.2 软件架构设计与生命周期需求分析阶段设计阶段实现阶段构件组装阶段部署阶段后开发阶段 7.1.3 软件架构的重要性 7.2 基于架构的软件开发方法7.2.1 体系结构的设计方法概述7.2.2 概念与术语7.2.3 基于体系结构的…

山西电力市场日前价格预测【2023-12-17】

日前价格预测 预测说明&#xff1a; 如上图所示&#xff0c;预测明日&#xff08;2023-12-17&#xff09;山西电力市场全天平均日前电价为467.13元/MWh。其中&#xff0c;最高日前电价为710.68元/MWh&#xff0c;预计出现在17:45。最低日前电价为316.35元/MWh&#xff0c;预计…

VMware----基于 VMware 玩转 CentOS 虚拟机创建、克隆以及配置后台运行

查看原文 文章目录 一、安装 Vmware二、创建 CentOS7 系统的虚拟机三、克隆虚拟机四、设置虚拟机后台运行 一、安装 Vmware &#xff08;1&#xff09;打开VMware下载地址页面&#xff0c;滑动页面&#xff0c;找到如下界面&#xff0c;点击【下载】 &#xff08;2&#xff…

UE4 Niagara学习笔记

需要在其他发射器的同一个粒子位置发射其他粒子就用Spawn Particles from other Emitter 把发射器名字填上去即可 这里Move to Nearest Distance Field Subface GPU&#xff0c;可以将生成的Niagara附着到最近的物体上 使用场景就是做的火苗附着到物体上

轻量封装WebGPU渲染系统示例<51>- 视差贴图(Parallax Map)(源码)

视差纹理是一种片段着色阶段增强材质表面凹凸细节的技术。 当前示例源码github地址: https://github.com/vilyLei/voxwebgpu/blob/feature/material/src/voxgpu/sample/ParallaxTexTest.ts 当前示例运行效果: 此示例基于此渲染系统实现&#xff0c;当前示例TypeScript源码如…

流量分析基础

定义&#xff1a; 流量分析&#xff08;Traffic Analysis&#xff09;是指对网络流量数据进行分析和解释&#xff0c;以获得有关网络中通信的信息和情报。这种技术可以用于网络安全、网络管理和网络优化等领域。 网络流量包含了许多有关网络通信的细节信息&#xff0c;如源IP地…

【漏洞复现】CVE-2023-36076:smanga漫画阅读系统 远程命令执行 漏洞复现 附POC 附SQL注入和任意文件读取

漏洞描述 无需配置,docker直装的漫画流媒体阅读工具。以emby plex为灵感,为解决漫画阅读需求而开发的漫画阅读器。在windows环境部署smanga安装环境面板,首先安装小皮面板,下载smanga项目,导入数据库,登录smanga,windows部署smanga。 /php/manga/delete.php接口处存在未…

分面中添加不同表格

简介 关于分面的推文&#xff0c;小编根据实际科研需求&#xff0c;已经分享了很多技巧。例如&#xff1a; 分面一页多图 基于分面的面积图绘制 分面中的细节调整汇总 分面中添加不同的直线 基于分面的折线图绘制 最近遇到了另一个需求&#xff1a;在分面中添加不同的表…