基于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)拓展功能要求

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

项目展示
基于go语言开发的海量用户及时通讯系统_第1张图片
开始此项目之前请确保安装好redis,golang
源码下载:https://github.com/BeAlrightc/go-study.git

1、项目开发前技术准备

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

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

基于go语言开发的海量用户及时通讯系统_第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的流程介绍】

基于go语言开发的海量用户及时通讯系统_第3张图片

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

分析思路

1)先确定消息Message的格式

2)发送消息示意图

基于go语言开发的海量用户及时通讯系统_第4张图片

代码展示

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)画出程序框架图

基于go语言开发的海量用户及时通讯系统_第5张图片

2)步骤

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

server层后端项目结构图

基于go语言开发的海量用户及时通讯系统_第6张图片

(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层后端项目结构图

基于go语言开发的海量用户及时通讯系统_第7张图片

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

基于go语言开发的海量用户及时通讯系统_第8张图片

(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手动添加测试用户,并画图+说明注意(后面通过程序注册用户)

基于go语言开发的海量用户及时通讯系统_第9张图片

手动直接在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)示意图

基于go语言开发的海量用户及时通讯系统_第10张图片

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)思路分析

基于go语言开发的海量用户及时通讯系统_第11张图片

(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)测试

基于go语言开发的海量用户及时通讯系统_第12张图片

-2.步骤2.

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

1)思路分析

基于go语言开发的海量用户及时通讯系统_第13张图片

(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.实现离线留言,在群聊时,如果某个用户没有在线,当登录后,可以接受到离线的消息

你可能感兴趣的:(golang学习,学习日记,golang,开发语言,后端)