go聊天系统项目-2 redis 验证用户id和密码
一、前言
敬告:本文不讲解代码,只是把代码展示出来。
该代码之前的代码见
go 聊天系统项目-1
注意:go 聊天系统项目-1 未使用 go mod 管理代码,本文改成使用 go mod 管理代码。详情见go 包相关知识
二、redis 端操作
1、安装redis
2、手动在redis添加数据
127.0.0.1:6379> hset user 100 "{\"UserId\":100,\"UserPwd\":\"123456\",\"UserName\":\"scott\"}"
三、使用mod管理项目
go mod init redis
go get github.com/gomodule/redigo/redis
四、代码
4.1、代码目录结构
pwd
/Users/zld/Go-project/src
tree
.
├── client
├── day8
│ └── chatroom
│ ├── client
│ │ ├── main
│ │ │ └── main.go
│ │ ├── model
│ │ ├── process
│ │ │ ├── server.go
│ │ │ ├── smsProcess.go
│ │ │ └── userProcess.go
│ │ └── utils
│ │ └── utils.go
│ ├── common
│ │ └── message
│ │ └── message.go
│ └── server
│ ├── model
│ │ ├── error.go
│ │ ├── user.go
│ │ └── userDao.go
│ ├── process
│ │ ├── smsProcess.go
│ │ └── userProcess.go
│ ├── processor
│ │ ├── processor.go
│ │ └── redis.go
│ └── utils
│ └── utils.go
├── go.mod
├── go.sum
├── main
└── main.go15 directories, 19 files
4.2、代码
4.2.1、服务端代码
4.2.1.1、day8/chatroom/server/model/error.go
package modelimport ("errors"
)// 根据业务逻辑的需要自定义一些错误.
var (ERROR_USER_NOTEXISTS = errors.New("用户不存在...")ERROR_USER_EXISTS = errors.New("用户已经存在...")ERROR_USER_PWD = errors.New("密码不正确")
)
4.2.1.2、day8/chatroom/server/model/user.go
package model//先定义一个用户的结构体type User struct {UserId int `json:"userId"`UserPwd string `json:"userPwd"`UserName string `json:"userName"`
}
4.2.1.3、day8/chatroom/server/model/userDao.go
package modelimport ("encoding/json""fmt""github.com/gomodule/redigo/redis"//"github.com/garyburd/redisgo/redis"//"go"//"github.com/go-redis/redis/v8"
)// 我们在服务器启动后,就初始化一个UserDao实例,
// 把它做成全局的变量,在需要和redis操作时,就直接使用即可
var (MyUserDao *UserDao
)// 定义一个UserDaoo 结构体
// 完成对 User 结构体的各种操作
type UserDao struct {pool *redis.Pool
}// 使用工厂模式创建一个UserDao实例
func NewUserDao(pool *redis.Pool) (userDao *UserDao) {userDao = &UserDao{pool: pool,}return
}// 思考一下在UserDao应该提供哪些方法
// 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", "user", id))if err != nil {//错误!if err == redis.ErrNil { //表示在users哈希中,没有找到对应iderr = ERROR_USER_NOTEXISTS}return}user = &User{}//这里我们需要把res反序列化成User实例err = json.Unmarshal([]byte(res), user)if err != nil {fmt.Println("json.Unmarshal err=", err)return}return
}// 完成登录的校验
// 1.Login 完成对用户的验证
// 2.如果用户的id和密码都正确,则返回一个user实例
// 3.如果用户的id和密码有错误,则返回对应的错误信息
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.2.1.4、day8/chatroom/server/process/smsProcess.go
package process2
4.2.1.5、day8/chatroom/server/process/userProcess.go
package process2import ("encoding/json""fmt""net""redis/day8/chatroom/common/message""redis/day8/chatroom/server/model""redis/day8/chatroom/server/utils"
)type UserProcess struct {//Conn net.Conn
}func (this *UserProcess) ServerProcessLogin(mes *message.Message) (err error) {//核心代码//先从mes 中取出 mes.Data,并直接反序列化成 LoginMesvar loginMes message.LoginMeserr = json.Unmarshal([]byte(mes.Data), &loginMes)if err != nil {fmt.Println("json.Unmarshal fail err=", err)return}// 先声明一个 resMesvar resMes message.MessageresMes.Type = message.LoginResMesType//再声明一个 LoginResMesvar loginResMes message.LoginResMes//我们需要到redis数据库去完成验证//1.使用model.MyUserDao 到redis 去验证user, err := model.MyUserDao.Login(loginMes.UserId, loginMes.UserPwd)if err != nil {loginResMes.Code = 500loginResMes.Error = "该用户不存在,请注册使用..."//这里我们先测试成功,然后我们在可以根据返回具体错误信息} else {loginResMes.Code = 200fmt.Println(user, "登录成功")}//如果用户id=100,密码=123456,认为合法,否则不合法// if loginMes.UserId == 100 && loginMes.UserPwd == "123456" {// //合法// loginResMes.Code = 200// } else {// //不合法// loginResMes.Code = 500// loginResMes.Error = "该用户不存在,请注册再使用..."// }//将 loginResMes 序列化data, err := json.Marshal(loginResMes)if err != nil {fmt.Println("json.Marshal fail", err)return}//将data赋值给resMesresMes.Data = string(data)//对 resMes 进行序列化,准备发送data, err = json.Marshal(resMes)if err != nil {fmt.Println("json.Marshal fail", err)return}//发送 data,将其封装到函数中//因为使用分层模式(mvc),我们先创建一个 Transfer 实例,然后读取tf := &utils.Transfer{Conn: this.Conn,}err = tf.WritePkg(data)return
}
4.2.1.6、day8/chatroom/server/processor/processor.go
package processorimport ("fmt""io""net""redis/day8/chatroom/common/message""redis/day8/chatroom/server/process""redis/day8/chatroom/server/utils"
)// 先创建一个processor的结构体
type Processor struct {Conn net.Conn
}func (this *Processor) serverProcessMes(mes *message.Message) (err error) {switch mes.Type {case message.LoginMesType://处理登录up := &process2.UserProcess{Conn: this.Conn,}err = up.ServerProcessLogin(mes)case message.RegisterMesType://处理注册default:fmt.Printf("消息类型不存在,无法处理")}return
}func (this *Processor) Process2() (err error) {//循环客户端发送信息for {//创建一个 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}}//fmt.Println("mes=", mes)err = this.serverProcessMes(&mes)if err != nil {return err}}
}
4.2.1.7、day8/chatroom/server/processor/redis.go
package processorimport (//"github.com/garyburd/redisgo/redis"//"github.com/go-redis/redis/v8""github.com/gomodule/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) { //初始化连接的代码,连接哪个ip的redisreturn redis.Dial("tcp", address)},}
}
4.2.1.8、day8/chatroom/server/utils/utils.go
package utilsimport ("encoding/binary""encoding/json""errors""fmt""net""redis/day8/chatroom/common/message"
)// 这里将这些方法关联到结构体中
type Transfer struct {//分析它应该有哪些字段Conn net.ConnBuf [8096]byte // 这是传输时,使用缓冲
}func (this *Transfer) ReadPkg() (mes message.Message, err error) {//buf := make([]byte, 8096)// conn.Read 在 conn 没有被关闭的情况下,才会阻塞//如果客户端关闭了conn 就不会阻塞_, err = this.Conn.Read(this.Buf[:4])if err != nil {//err = errors.New("read pkg header error")return}//根据读到的 buf[:4] 转成一个 unit32 类型var pkgLen uint32pkgLen = binary.BigEndian.Uint32(this.Buf[0:4])n, err := this.Conn.Read(this.Buf[:pkgLen])if n != int(pkgLen) || err != nil {//err = errors.New("read pkg body error")return}//把 pkgLen 反序列化成 -> message.Messageerr = json.Unmarshal(this.Buf[:pkgLen], &mes)if err != nil {err = errors.New("json.Unmarshal error")return}return}func (this *Transfer) WritePkg(data []byte) (err error) {//先发送一个长度给对方//data是 我们要发送的消息,先发送 data 长度//由于 conn 接口的 Write 方法参数要求是 bytes 切片var pkgLen uint32pkgLen = uint32(len(data))//var buf [4]bytebinary.BigEndian.PutUint32(this.Buf[0:4], pkgLen)//发送长度n, err := this.Conn.Write(this.Buf[:4])if n != 4 || err != nil {fmt.Println("conn.Write(bytes) fail", err)return}//发送 data本身n, err = this.Conn.Write(data)if n != int(pkgLen) || err != nil {fmt.Println("conn.Write(bytes) fail", err)return}return
}
4.2.2、common 代码
4.2.2.1、day8/chatroom/common/message/message.go
package messageconst (LoginMesType = "LoginMes"LoginResMesType = "LoginResMes"RegisterMesType = "RegisterMes"
)type Message struct {Type string `josn: "type"`Data string `json: "Data"`
}
type LoginMes struct {UserId int `json: "userId"`UserPwd string `json: "userPwd"`UserName string `json: "userName"`
}
type LoginResMes struct {Code int `json: "code"`Error string `json: "error"`
}type RegisterMes struct{//
}
4.2.3、客户端代码
4.2.3.1、day8/chatroom/client/main/main.go
package mainimport ("fmt""redis/day8/chatroom/client/process"
)var userId int
var userPwd stringfunc main() {var key intvar 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("登录聊天室")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 = falsedefault:fmt.Println("你的输入有误,请重新输入")}}
}
4.2.3.2、day8/chatroom/client/process/server.go
package processimport ("fmt""net""os""redis/day8/chatroom/client/utils"
)func ShowMenu() {fmt.Println("----------恭喜xxx登录成功--------")fmt.Println("--------1、显示在线用户列表--------")fmt.Println("--------2、发送消息--------")fmt.Println("--------3、信息列表--------")fmt.Println("--------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.Println("客户端%s正在等待读取服务器发送的消息")mes, err := tf.ReadPkg()if err != nil {fmt.Println("tf.ReadPkg err=", err)return}//如果读取到消息,又是下一步处理逻辑fmt.Printf("mes=%v", mes)}
}
4.2.3.3、day8/chatroom/client/process/smsProcess.go
package process
4.2.3.4、day8/chatroom/client/process/userProcess.go
package processimport ("encoding/binary""encoding/json""fmt""net""redis/day8/chatroom/client/utils""redis/day8/chatroom/common/message"
)type UserProcess struct {
}func (this *UserProcess) Login(userId int, userPwd string) (err error) {//fmt.Printf("userId = %d userPwd = %s\n", userId, userPwd)//return nil//连接到服务器conn, err := net.Dial("tcp", "localhost:8889")if err != nil {fmt.Println("net.Dial err=", err)return}//延时关闭defer conn.Close()//准备通过 conn 发送消息给服务器var mes message.Messagemes.Type = message.LoginMesTypevar loginMes message.LoginMesloginMes.UserId = userIdloginMes.UserPwd = userPwd//将 loginMes 序列化data, err := json.Marshal(loginMes)if err != nil {fmt.Println("json.Marshal err=", err)return}//将data赋值给 message 结构体 Data 字段mes.Data = string(data)//将 mes 进行序列化data, err = json.Marshal(mes)if err != nil {fmt.Println("json.Marshal err=", err)return}//data是 我们要发送的消息,先发送 data 长度//由于 conn 接口的 Write 方法参数要求是 bytes 切片var pkgLen uint32pkgLen = uint32(len(data))var buf [4]bytebinary.BigEndian.PutUint32(buf[0:4], pkgLen)//发送长度n, err := conn.Write(buf[0:4])if n != 4 || err != nil {fmt.Println("conn.Write(bytes) fail", err)return}fmt.Printf("客户端,发送消息的长度=%d,消息内容为: %s\n", len(data), string(data))_, err = conn.Write(data)if err != nil {fmt.Printf("conn.Write(data) fail", err)return}//time.sleep(20*time.Second)//fmt.Println("休眠了20S")//这里还需要处理服务器返回的消息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
}
4.2.3.5、day8/chatroom/client/utils/utils.go
package utilsimport ("encoding/binary""encoding/json""errors""fmt""net""redis/day8/chatroom/common/message"
)// 这里将这些方法关联到结构体中
type Transfer struct {//分析它应该有哪些字段Conn net.ConnBuf [8096]byte // 这是传输时,使用缓冲
}func (this *Transfer) ReadPkg() (mes message.Message, err error) {//buf := make([]byte, 8096)// conn.Read 在 conn 没有被关闭的情况下,才会阻塞//如果客户端关闭了conn 就不会阻塞_, err = this.Conn.Read(this.Buf[:4])if err != nil {//err = errors.New("read pkg header error")return}//根据读到的 buf[:4] 转成一个 unit32 类型var pkgLen uint32pkgLen = binary.BigEndian.Uint32(this.Buf[0:4])n, err := this.Conn.Read(this.Buf[:pkgLen])if n != int(pkgLen) || err != nil {//err = errors.New("read pkg body error")return}//把 pkgLen 反序列化成 -> message.Messageerr = json.Unmarshal(this.Buf[:pkgLen], &mes)if err != nil {err = errors.New("json.Unmarshal error")return}return}func (this *Transfer) WritePkg(data []byte) (err error) {//先发送一个长度给对方//data是 我们要发送的消息,先发送 data 长度//由于 conn 接口的 Write 方法参数要求是 bytes 切片var pkgLen uint32pkgLen = uint32(len(data))//var buf [4]bytebinary.BigEndian.PutUint32(this.Buf[0:4], pkgLen)//发送长度n, err := this.Conn.Write(this.Buf[:4])if n != 4 || err != nil {fmt.Println("conn.Write(bytes) fail", err)return}//发送 data本身n, err = this.Conn.Write(data)if n != int(pkgLen) || err != nil {fmt.Println("conn.Write(bytes) fail", err)return}return
}
4.2.4、main代码
main 代码跟 go.mod 在同级目录下
cat main.go
package mainimport ("fmt""net"//"redis/day8/chatroom/server""redis/day8/chatroom/server/model""redis/day8/chatroom/server/processor""time"
)// 处理和客户端的通信
func process(conn net.Conn) {//这里需要延时关闭conndefer conn.Close()//这里调用总控,创建一个processor := &processor.Processor{Conn: conn,}err := processor.Process2()if err != nil {fmt.Println("客户端和服务器端通讯的协程错误=err", err)return}
}// 这里我们编写一个函数,完成对 UserDao 的初始化任务
func initUserDao() {model.MyUserDao = model.NewUserDao(processor.Pool)
}
func main() {//当服务器启动时就初始化redis的连接池processor.InitPool("127.0.0.1:6379", 16, 0, 300*time.Second)initUserDao()//提示信息fmt.Println("服务器[新的结构]在 8889 端口监听......")listen, err := net.Listen("tcp", "0.0.0.0:8889")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)}
}
4.2.5、编译项目代码
4.2.5.1、编译服务端代码
go build main main.go
4.2.5.2、编译客户端代码
go build -o client day8/chatroom/client/main/main.go
4.2.6、演示代码
说明:本文代码是去redis数据库验证,redis数据库中存放的就一个用户id 100,密码 123456 的用户信息。所以只有这个才是正确的登录输入。另外如果登录失败,代码暂时没做详细的错误处理,统一输出 该用户不存在,请注册使用...
4.2.6.1、验证登录成功的情况
go run main.go
服务器[新的结构]在 8889 端口监听......
等待客户端连接服务器......
go run day8/chatroom/client/main/main.go
---------------欢迎登录多人聊天系统-------------------1、登录聊天室2、注册用户3、退出系统请选择(1-3):
client
1
登录聊天室
请输入用户的id
100
请输入用户密码
123456
客户端,发送消息的长度=86,消息内容为: {"Type":"LoginMes","Data":"{\"UserId\":100,\"UserPwd\":\"123456\",\"UserName\":\"\"}"}
----------恭喜xxx登录成功--------
--------1、显示在线用户列表--------
--------2、发送消息--------
--------3、信息列表--------
--------4、退出系统--------
客户端%s正在等待读取服务器发送的消息
server
等待客户端连接服务器......
&{100 123456 scott} 登录成功
客户端退出,服务器端也退出..
客户端和服务器端通讯的协程错误=err EOF
4.2.6.2、验证密码错误的情况
说明:由于没对登录错误做详细处理,统一输出 该用户不存在,请注册使用...
所以,这里只验证密码错误的情况,不再验证用户 id 错误的情况。
服务端不退出
重新执行客户端代码
./client
---------------欢迎登录多人聊天系统-------------------1、登录聊天室2、注册用户3、退出系统请选择(1-3):
client
1
登录聊天室
请输入用户的id
100
请输入用户密码
123
客户端,发送消息的长度=83,消息内容为: {"Type":"LoginMes","Data":"{\"UserId\":100,\"UserPwd\":\"123\",\"UserName\":\"\"}"}
该用户不存在,请注册使用...
---------------欢迎登录多人聊天系统-------------------1、登录聊天室2、注册用户3、退出系统请选择(1-3):
server
等待客户端连接服务器......
&{100 123456 scott} 登录失败
客户端退出,服务器端也退出..
客户端和服务器端通讯的协程错误=err EOF
4.3、错误处理
4.3.1、处理详细的错误输出
修改 day8/chatroom/server/process/userProcess.go 文件的代码
if err != nil {if err == model.ERROR_USER_NOTEXISTS {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, "登录成功")}
修改day8/chatroom/client/process/userProcess.go文件的代码
if loginResMes.Code == 200 {//fmt.Println("登录成功")//这里我们还需要在客户端启动一个协程//该协程保持和服务器端的通讯,如果服务器有数据推送给客户端//则接收并显示在客户端的终端go serverProcessMes(conn)//1.显示登录成功后的菜单for {ShowMenu()}
} else {fmt.Println(loginResMes.Error)
}
4.3.2、演示代码
./main
服务器[新的结构]在 8889 端口监听......
等待客户端连接服务器......
等待客户端连接服务器......
客户端退出,服务器端也退出..
客户端和服务器端通讯的协程错误=err EOF
等待客户端连接服务器......
客户端退出,服务器端也退出..
客户端和服务器端通讯的协程错误=err EOF
./client
---------------欢迎登录多人聊天系统-------------------1、登录聊天室2、注册用户3、退出系统请选择(1-3):
1
登录聊天室
请输入用户的id
100
请输入用户密码
123
客户端,发送消息的长度=83,消息内容为: {"Type":"LoginMes","Data":"{\"UserId\":100,\"UserPwd\":\"123\",\"UserName\":\"\"}"}
密码不正确
---------------欢迎登录多人聊天系统-------------------1、登录聊天室2、注册用户3、退出系统请选择(1-3):
1
登录聊天室
请输入用户的id
900
请输入用户密码
123
客户端,发送消息的长度=83,消息内容为: {"Type":"LoginMes","Data":"{\"UserId\":900,\"UserPwd\":\"123\",\"UserName\":\"\"}"}
用户不存在...
---------------欢迎登录多人聊天系统-------------------1、登录聊天室2、注册用户3、退出系统请选择(1-3):