建立网站专栏vi设计公司
文章目录
- 二十三、海量用户即时通讯系统
- 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 stringfunc main() {//接收用户的选择var key int//判断是否还继续显示菜单var loop = truefor 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=falsecase 2 :fmt.Println("注册用户") loop=falsecase 3 :fmt.Println("退出系统") //loop=falseos.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.goerr := 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 messageconst (LoginMesType = "LoginMes"LoginResMesType = "LoginResMes"
)type Message struct {Type string `json:"type"`//消息的类型Data string `json:"data"`//消息的数据
}//定义两个消息。。后面需要再添加
type LoginMes struct {UserId int `json:"userId"`//用户IdUserPwd 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.Messagemes.Type = message.LoginMesType //3.创建一个LoginMes 结构体var loginMes message.LoginMesloginMes.UserId = userIdloginMes.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 uint32pkgLen = uint32(len(data))var buf [4]bytebinary.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 stringfunc main() {//接收用户的选择var key int//判断是否还继续显示菜单var loop = truefor 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=falsecase 2 :fmt.Println("注册用户") loop=falsecase 3 :fmt.Println("退出系统") //loop=falseos.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.goerr := 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 uint32pkgLen=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,Errmes, 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 uint32pkgLen = uint32(len(data))var buf [4]bytebinary.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,并直接反序列化成LoginMesvar loginMes message.LoginMeserr =json.Unmarshal([]byte(mes.Data),&loginMes)if err != nil {fmt.Println("json.Unmarshal fail err=",err)return}//1.先声明一个resMesvar resMes message.MessageresMes.Type=message.LoginResMesType//2.再声明一个LoginResMesvar 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赋值给resMesresMes.Data = string(data) //5.对resMes进行序列化,准备发送data, err = json.Marshal(resMes)if err != nil {fmt.Println("Marshal fail err=",err)return}//6.发送data 我们将其封装为writePkgerr = 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,Errmes, 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 uint32pkgLen=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 uint32pkgLen = uint32(len(data))var buf [4]bytebinary.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部分反序列化为LoginResMesvar loginResMes message.LoginResMeserr = 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 utilsimport ("fmt""net""encoding/json""encoding/binary""go_code/chatroom/common/message")//将这些方法关联到结构体当中type Transfer struct {//分析应该有哪些字段Conn net.ConnBuf [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 uint32pkgLen=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 uint32pkgLen = 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,并直接反序列化成LoginMesvar loginMes message.LoginMeserr =json.Unmarshal([]byte(mes.Data),&loginMes)if err != nil {fmt.Println("json.Unmarshal fail err=",err)return}//1.先声明一个resMesvar resMes message.MessageresMes.Type=message.LoginResMesType//2.再声明一个LoginResMesvar 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赋值给resMesresMes.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.Messagemes.Type = message.LoginMesType //3.创建一个LoginMes 结构体var loginMes message.LoginMesloginMes.UserId = userIdloginMes.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 uint32pkgLen = uint32(len(data))var buf [4]bytebinary.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部分反序列化为LoginResMesvar loginResMes message.LoginResMeserr = 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 intfmt.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 stringfunc main() {//接收用户的选择var key int//判断是否还继续显示菜单// loop = truefor 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=falsecase 2 :fmt.Println("注册用户") //loop=falsecase 3 :fmt.Println("退出系统") //loop=falseos.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中没有找到对应的iderr= 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_PWDreturn}return
}
(4)编写了server/main.redis.go
package main
import ("github.com/garyburd/redigo/redis""time")//定义一个全局的pool
var pool *redis.Poolfunc initPool(address string,maxIdle,maxActive int,idleTimeout time.Duration) {pool = &redis.Pool{MaxIdle: maxIdle, //最大空闲连接数MaxActive: maxActive,//表示和数据库的最大连接数,0表示没有限制IdleTimeout: idleTimeout,//最大空闲时间Dial:func()(redis.Conn,error){//初始化连接的代码。连接哪个ipreturn 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 = 500loginResMes.Error = err.Error()}else if err ==model.ERROR_USER_PWD {loginResMes.Code = 403loginResMes.Error = err.Error()}else {loginResMes.Code = 505loginResMes.Error = "服务器内部错误..."}//这里我们先测试成功,然后再返回具体的错误信息}else{loginResMes.Code = 200fmt.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,在initUserDaomodel.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,并直接反序列化成RegisterMesvar registerMes message.RegisterMeserr = json.Unmarshal([]byte(mes.Data), ®isterMes)if err != nil {fmt.Println("json.Unmarshal fail err=", err)return}//1.先声明一个resMesvar resMes message.MessageresMes.Type = message.RegisterResMesType//2.再声明一个RegisterMesvar registerResMes message.RegisterResMes//我们需要到redis数据库去完成注册//1.使用model.MyUserDao到redis去注册err= model.MyUserDao.Register(®isterMes.User)if err !=nil {if err == model.ERROR_USER_EXISTS {registerResMes.Code = 505registerResMes.Error = model.ERROR_USER_EXISTS.Error()} else {registerResMes.Code = 506registerResMes.Error = "注册时发生未知错误"}} else {registerResMes.Code = 200}//3.将loginResMes 序列化data, err := json.Marshal(registerResMes)if err != nil {fmt.Println("Marshal fail err=", err)}//4.将data赋值给resMesresMes.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_EXISTSreturn}//这时说明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.Messagemes.Type = message.RegisterMesType//3.创建一个RegisterMes 结构体var registerMes message.RegisterMesregisterMes.User.UserId = userIdregisterMes.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 uint32pkgLen = uint32(len(data))var buf [4]bytebinary.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 就是RegisterResMesif err != nil {fmt.Println("readPkg(conn) err=",err)return}//将mes的Data部分反序列化为RegisterResMesvar registerResMes message.RegisterResMeserr = json.Unmarshal([]byte(mes.Data),®isterResMes)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赋给thisthis.UserId = loginMes.UserIduserMgr.AddOnlinesUser(this)//将当前在线用户的id放入到loginResMes.UsersId//遍历userMgr.onlineUsersfor id, _ := range userMgr.onlineUsers{loginResMes.UsersId = append(loginResMes.UsersId,id)}fmt.Println(user, "登录成功")}
(3)client
/process/userProcess.go(在login成功的地方加入代码)
//现在可以显示当前在线的列表 遍历loginResMes.UsersIdfmt.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() {//遍历一把onlineUsersfmt.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] = useroutputOnlineUser()
}
(2)server/process/userProcess.go
//这里我们编写通知所有在线用户的方法
//这个id要通知其他的在线用户,我上线
func (this *UserProcess) NotifyOthersOnlineUser(userId int) {//遍历 onlineUsers ,然后一个一个的发送 NotifyUserStatusMesfor id, up := range userMgr.onlineUsers {//过滤掉自己if id == userId {continue}//开始通知【单独的写一个方法】up.NotifyMeOnline(userId)}}func (this *UserProcess) NotifyMeOnline(userId int){//组装我们的NotifyUserStatusMesvar mes message.Messagemes.Type = message.NotifyUserStatusMesTypevar notifyUserStatusMes message.NotifyUserStatusMesnotifyUserStatusMes.UserId = userIdnotifyUserStatusMes.Status = message.UserOnline//将notifyUserStatusMes序列化data, err := json.Marshal(notifyUserStatusMes)if err != nil {fmt.Println("json.Marshal err",err)return}//将序列化后的notifyUserStatusMes赋值给mes.Datames.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"` //用户idStatus 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() {//遍历一把onlineUsersfmt.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] = useroutputOnlineUser()
}
(5)client/main/server.go
case 1://fmt.Println("显示用户在线列表")outputOnlineUser()case 2://如果读取到消息,又是下一步处理逻辑switch mes.Type {case message.NotifyUserStatusMesType : //有人上线了//1.取出 NotifyUserStatusMesvar notifyUserStatusMes message.NotifyUserStatusMesjson.Unmarshal([]byte(mes.Data),¬ifyUserStatusMes)//2.把这个用户的信息,状态保存在客户端的map[int]User中updateUserStatus(¬ifyUserStatusMes)//处理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.Connmessage.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.创建一个Mesvar mes message.Messagemes.Type = message.SmsMesType//2.创建一个SmsMes 实例var smsMes message.SmsMessmsMes.Content = content //内容smsMes.UserId = CurUser.UserIdsmsMes.UserStatus = CurUser.UserStatus//3.序列化smsMesdata, 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.创建一个Mesvar mes message.Messagemes.Type = message.SmsMesType//2.创建一个SmsMes 实例var smsMes message.SmsMessmsMes.Content = content //内容smsMes.UserId = CurUser.UserIdsmsMes.UserStatus = CurUser.UserStatus//3.序列化smsMesdata, 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.Datavar 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.实现离线留言,在群聊时,如果某个用户没有在线,当登录后,可以接受到离线的消息