1.项目开发流程
开发流程
需求分析->设计阶段->编码实现->测试阶段->实施
需求分析
- 用户注册
- 用户登录
- 显示在线用户列表
- 群聊(广播)
- 点对点聊天
- 离线留言
项目开发前技术准备
项目要保存用户信息和消息数据,因此需要数据库(Mysql或Redis),这里使用Redis
2.Golang操作Redis
2.1 安装第三方开源Redis库
方法一
使用第三方开源的redis库:github.com/gomodule/redigo/redis
安装第三方Redis库,在GOPATH路径下执行安装命令:go get github.com/gomodule/redigo/redis
注意:先安装git
镜像:go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/
2.2 Set/Get接口
通过Golang添加和获取key-value
func Redis_test() {
c, err := redis.Dial("tcp", "localhost:6379")
if err != nil {
fmt.Println("conn redis faild,", err)
return
}
defer c.Close()
_, err = c.Do("set", "key1", 111)
if err != nil {
fmt.Println(err)
return
}
r, err := redis.Int(c.Do("Get", "key1"))
if err != nil {
fmt.Println("get key1 faild,", err)
return
}
fmt.Println(r)
}
2.3 Hash
通过Golang对Hash操作Hash数据类型
func Redis_test() {
c, err := redis.Dial("tcp", "localhost:6379")
if err != nil {
fmt.Println("conn redis faild,", err)
return
}
defer c.Close()
_, err = c.Do("Hset", "user01", "name", "Tom")
if err != nil {
fmt.Println(err)
return
}
_, err = c.Do("Hset", "user01", "age", 18)
if err != nil {
fmt.Println(err)
return
}
r1, err := redis.String(c.Do("HGet", "user01", "name"))
if err != nil {
fmt.Println(err)
return
}
r2, err := redis.Int(c.Do("HGet", "user01", "age"))
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("user01 name:%v get:%v", r1, r2)
}
批量Get/Set
func Redis_test() {
c, err := redis.Dial("tcp", "localhost:6379")
if err != nil {
fmt.Println("conn redis faild,", err)
return
}
defer c.Close()
_, err = c.Do("HMset", "user02", "name", "Jerry", "age", 15)
if err != nil {
fmt.Println(err)
return
}
r, err := redis.Strings(c.Do("HMGet", "user02", "name", "age"))
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("user01 name:%v get:%v", r[0], r[1])
}
给数据设置有效时间
//给name数据设置有效时间为10s
_, err = c.Do("expire", "name", 10)
2.4 操作List
_, err = c.Do("lpush", "personList", "Tom", 11, "Jerry", 15)
r, err = redis.Strings(c.Do("lrange", "personList", 0, -1))
2.5 Redis连接池
目的:节省临时获取Redis链接的时间,从而提高效率
var pool *redis.Pool
func init() {
pool = &redis.Pool{
MaxIdle: 8,
MaxActive: 0,
IdleTimeout: 100,
Dial: func() (redis.Conn, error) {
return redis.Dial("tcp", "localhost:6379")
},
}
}
func Redis_test() {
c := pool.Get()
_, err := c.Do("HMset", "user02", "name", "Jerry", "age", 15)
if err != nil {
fmt.Println(err)
return
}
r, err := redis.Strings(c.Do("HMGet", "user02", "name", "age"))
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("user01 name:%v get:%v\n", r[0], r[1])
_, err = c.Do("lpush", "personList", "Tom", 11, "Jerry", 15)
r, err = redis.Strings(c.Do("lrange", "personList", 0, -1))
fmt.Println(r)
c.Close()
}
3.实现功能-显示客户端登录菜单
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("退出系统")
os.Exit(0)
default:
fmt.Println("您的输入有误,请重新输入")
}
}
// 根据用户的输入,显示新的提示信息
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("显示用户注册界面")
}
}
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
}
4.实现功能-完成用户登录
要求:先完成指定用户的验证,用户id=100,密码pwd=123456可以登录,其他用户不能登录
这里需要先说明一个Message的组成,并发送与一个Message的流程
客户端:
- 接收输入的id和pwd
- 发送id和密码
- 判断是否成功,并显示对应页面
关键问题是怎么组织发送的数据 - 设计消息协议
发送的流程
- 先创建一个Message的结构体
- mes.Type = 登录消息类型
- mes.Data = 登录消息的内容(序列)
- 对mes进行序列化
- 在网络传输中,最麻烦丢包
(1) 先给服务器发送mes的长度[有多少个字节n]
(2) 在发送消息本身
服务器
- 接收用户id,pwd【goroutine】
- 比较
- 返回结果
接收数据流程
- 接收到客户端发送的长度.len
- 根据接收到的长度len,在接收消息本身
- 接收时要判断实际接收到的消息内容是否等于len
- 如果不相等,就有纠错协议
- 取到反序列化>Message
- 取出message.Data(string)-反序列化>LoginMes
- 取出loginMes.userid和loginMes.userPwd
- 比较
- 根据比较结果,返回Mess
- 发送给客户端
1.完成客户端可以发送消息长度,服务器端可以正常接收该长度值
代码实现:
server/main.go
package main
import (
"fmt"
"net"
)
// 处理和客户端的通讯
func process(conn net.Conn) {
//这里需要延时关闭conn
defer conn.Close()
//训话客户端发送的消息
for {
buf := make([]byte, 8096)
fmt.Println("读到客户端发送的数据...")
n, err := conn.Read(buf[:4])
if n != 4 || err != nil {
fmt.Println("conn.Read err = ", err)
return
}
fmt.Println("读到的buf = ", buf[:4])
}
}
func main() {
// 信息提示
fmt.Println("服务器在8889端口监听...")
listen, err := net.Listen("tcp", "0.0.0.0:8889")
if err != nil {
fmt.Println("net.Listen err = ", err)
return
}
//延时关闭
defer listen.Close()
// 一旦监听成功,就等待客户端来链接服务器
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 (
"encoding/binary"
"encoding/json"
"fmt"
"net"
"project/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 结构体
loginMes := message.LoginMes{}
loginMes.UserId = userId
loginMes.UserPwd = userPwd
//4. 将loginMes 序列化
data, err := json.Marshal(loginMes)
if err != nil {
fmt.Println("json.Marshal err = ", err)
return
}
//5. 把data赋给 mes.Data字段
mes.Data = string(data)
//6. 将 mes 进行序列化
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("json.Marshal err = ", err)
return
}
//7. 发送data消息
//7.1 把data的长度发送给服务器
// 获取data的长度->转成一个表示长度的byte切片
var pkgLen uint32
pkgLen = uint32(len(data))
var bytes [4]byte
binary.BigEndian.PutUint32(bytes[0:4], pkgLen)
// 发送长度
n, err := conn.Write(bytes[0:4])
if n != 4 || err != nil {
fmt.Println("conn.Write(bytes) fail ", err)
return
}
fmt.Println("客户端发送长度ok")
fmt.Printf("客户端,发送消息的长度 = %d 内容 = %s", len(data), string(data))
return
}
2.完成客户端可以发送消息本身,服务端可以正常收到消息,并根据客户端发送的消息(LoginMes)判断用户的合法性,并返回相应的LoginResMes
思路分析:
- 让客户发送消息本身
- 服务器端接收到消息,然后反序列化成对应的消息结构体
- 服务器端根据反序列化成对应的消息,判断是否登录用户是合法,返回LoginResMes
- 客户端解析返回的LoginResMes显示对应界面
代码改动:
client/login.go
最后添加发送data代码
_, err = conn.Write(data)
if err != nil {
fmt.Println("conn.Write(data) err = ", err)
return
}
server/main.go
func readpkg(conn net.Conn) (mes message.Message, err error) {
buf := make([]byte, 8096)
fmt.Println("读到客户端发送的数据...")
_, err = conn.Read(buf[:4])
if err != nil {
//fmt.Println("conn.Read err = ", err)
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 {
//fmt.Println("conn.Read fail err = ", err)
return
}
//吧pkgLen反序列化为 message.Message
err = json.Unmarshal(buf[:pkgLen], &mes)
if err != nil {
fmt.Println("json.Unmarshal err = ", err)
return
}
return
}
// 处理和客户端的通讯
func process(conn net.Conn) {
//这里需要延时关闭conn
defer conn.Close()
//训话客户端发送的消息
for {
//读取数据包,直接封装成一个函数readpkg(), 返回message, err
mes, err := readpkg(conn)
if err != nil {
if err == io.EOF {
fmt.Println("客户端关闭,服务端正常关闭...")
return
}
{
fmt.Println("readpkg err = ", err)
return
}
}
fmt.Println("mes = ", mes)
}
}
3.能够完成登录,并提示相应信息
新增common/utils/utils.go
package utils
import (
"encoding/binary"
"encoding/json"
"fmt"
"net"
"project/common/message"
)
func ReadPkg(conn net.Conn) (mes message.Message, err error) {
buf := make([]byte, 8096)
fmt.Println("读到对方发送的数据...")
_, err = conn.Read(buf[:4])
if err != nil {
//fmt.Println("conn.Read err = ", err)
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 {
//fmt.Println("conn.Read fail err = ", err)
return
}
//吧pkgLen反序列化为 message.Message
err = json.Unmarshal(buf[:pkgLen], &mes)
if err != nil {
fmt.Println("json.Unmarshal err = ", err)
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)
//发送长度
n, err := conn.Write(buf[:4])
if n != 4 || err != nil {
fmt.Println("conn.write(bytes) err = ", err)
return
}
//发送data本身
n, err = conn.Write(data)
if n != int(pkgLen) || err != nil {
fmt.Println("conn.Write(bytes) err = ", err)
return
}
return
}
server/main.go 修改
// 编写一个函数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.Marshal err = ", err)
return
}
//声明一个 resMes
var resMes message.Message
resMes.Type = message.LoginResMesType
//声明一个loginResMes 用于赋值
var loginResMes message.LoginResMes
//如果用户id=100 pwd=123456 认为合法
if loginMes.UserId == 100 && loginMes.UserPwd == "123456" {
//合法
loginResMes.Code = 200
} else {
loginResMes.Code = 500 //500状态码表示该用户不存在
loginResMes.Error = "该用户不存在,请先注册..."
}
//将loginResMes 反序列化
data, err := json.Marshal(loginResMes)
if err != nil {
fmt.Println("json.Marshal err = ", err)
return
}
// 将data 赋值给 resMes
resMes.Data = string(data)
// 对resMes及逆行序列化,准备发送
data, err = json.Marshal(resMes)
if err != nil {
fmt.Println("json.Marshal err = ", err)
return
}
// 发送data,将其封装到writePkg函数
err = utils.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)
default:
fmt.Println("消息类型不存在,无法处理...")
}
return
}
// 处理和客户端的通讯
func process(conn net.Conn) {
//这里需要延时关闭conn
defer conn.Close()
//训话客户端发送的消息
for {
//读取数据包,直接封装成一个函数readpkg(), 返回message, err
mes, err := utils.ReadPkg(conn)
if err != nil {
if err == io.EOF {
fmt.Println("客户端关闭,服务端正常关闭...")
return
}
{
fmt.Println("readpkg err = ", err)
return
}
}
serverProcessMes(conn, &mes)
if err != nil {
return
}
}
}
修改client/login.go
//处理服务器端返回的消息
mes, err = utils.ReadPkg(conn)
fmt.Println("----------------")
if err != nil {
fmt.Println("utils.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
修改client/main.go
// 先把登录的函数,写到另外一个文件,login.go
login(userId, userPwd)
//if err != nil {
// fmt.Println("登录失败")
//} else {
// fmt.Println("登录成功")
//}
4.程序结构的改进,前面完成了程序的功能,但是没有结构,系统的可读性、扩展性和维护性都不好,因此需要对程序结构进行改进
一. 改进服务端
1.结构划分
1)main.go
- 监听
- 等待客户端的链接
- 初始化的工作
2)processor.go(总的处理器)
- 根据客户端的请求,调用对应的处理器,完成相应的任务
3)smsProcess.go
- 处理和短消息相关的请求
- 群聊
- 点对点聊天
4)userPorcess.go
- 处理和用户相关的请求
- 登录
- 注册
- 注销
- 用户列表管理
5)utils.go
- 一些常用的工具,函数,结构体
- 提供常用的方法和函数
6)common/message
- 有服务端和客户端公用的文件,比如说message.go
2.结构目录
- common
- message
- message.go
- utils
- utils.go
- message
- server
- main
- main.go
- processor.go
- model
- process
- smsProcess.go
- userProcess.go
- main
将main.go的代码剥离到对应的文件中
common/utils/utils.go
package utils
import (
"encoding/binary"
"encoding/json"
"fmt"
"net"
"project/common/message"
)
// 将方法关联到结构体中
type Transfer struct {
//分析应有哪些字段
Conn net.Conn
Buf [8096]byte //传输时,使用缓冲
}
func (this *Transfer) ReadPkg() (mes message.Message, err error) {
fmt.Println("读到对方发送的数据...")
_, err = this.Conn.Read(this.Buf[:4])
if err != nil {
//fmt.Println("conn.Read err = ", err)
return
}
//根据buf[:4] 转成一个uint32类型
var pkgLen uint32
pkgLen = binary.BigEndian.Uint32(this.Buf[0:4])
//根据pkgLen读取消息内容
n, err := this.Conn.Read(this.Buf[:pkgLen])
if n != int(pkgLen) || err != nil {
//fmt.Println("conn.Read fail err = ", err)
return
}
//吧pkgLen反序列化为 message.Message
err = json.Unmarshal(this.Buf[:pkgLen], &mes)
if err != nil {
fmt.Println("json.Unmarshal err = ", err)
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)
//发送长度
n, err := this.Conn.Write(this.Buf[:4])
if n != 4 || err != nil {
fmt.Println("conn.write(bytes) err = ", err)
return
}
//发送data本身
n, err = this.Conn.Write(data)
if n != int(pkgLen) || err != nil {
fmt.Println("conn.Write(bytes) err = ", err)
return
}
return
}
server/process/userProcess.go
package process
import (
"encoding/json"
"fmt"
"net"
"project/common/message"
"project/common/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.Marshal err = ", err)
return
}
//声明一个 resMes
var resMes message.Message
resMes.Type = message.LoginResMesType
//声明一个loginResMes 用于赋值
var loginResMes message.LoginResMes
//如果用户id=100 pwd=123456 认为合法
if loginMes.UserId == 100 && loginMes.UserPwd == "123456" {
//合法
loginResMes.Code = 200
} else {
loginResMes.Code = 500 //500状态码表示该用户不存在
loginResMes.Error = "该用户不存在,请先注册..."
}
//将loginResMes 反序列化
data, err := json.Marshal(loginResMes)
if err != nil {
fmt.Println("json.Marshal err = ", err)
return
}
// 将data 赋值给 resMes
resMes.Data = string(data)
// 对resMes及逆行序列化,准备发送
data, err = json.Marshal(resMes)
if err != nil {
fmt.Println("json.Marshal err = ", err)
return
}
// 发送data,将其封装到writePkg函数
//因为使用分层模式(mvc)需要先创建一个Transfer实例,然后读取
tf := &utils.Transfer{
Conn: this.Conn,
}
err = tf.WritePkg(data)
return
}
server/mian/processor.go
package main
import (
"fmt"
"io"
"net"
"project/common/message"
"project/common/utils"
process2 "project/server/process"
)
type Processor struct {
Conn net.Conn
}
// 编写一个serverProcessMes 函数
// 功能:根据客户端发送消息种类不同,决定调用哪个函数来处理
func (this *Processor) ServerProcessMes(mes *message.Message) (err error) {
switch mes.Type {
case message.LoginMesType:
//处理登录
//创建UserPorcess实例
up := &process2.UserProcess{
Conn: this.Conn,
}
err = up.ServerProcessLogin(mes)
default:
fmt.Println("消息类型不存在,无法处理...")
}
return
}
func (this *Processor) process() (err error) {
for {
//读取数据包,直接封装成一个函数readpkg(), 返回message, err
tf := &utils.Transfer{
Conn: this.Conn,
}
mes, err := tf.ReadPkg()
if err != nil {
if err == io.EOF {
fmt.Println("客户端关闭,服务端正常关闭...")
return err
}
{
fmt.Println("readpkg err = ", err)
return err
}
}
err = this.ServerProcessMes(&mes)
if err != nil {
return err
}
}
}
server/main/main.go
package main
import (
"fmt"
"net"
)
// 处理和客户端的通讯
func process(conn net.Conn) {
//这里需要延时关闭conn
defer conn.Close()
//调用主控
processor := &Processor{
Conn: conn,
}
err := processor.process()
if err != nil {
fmt.Println("客户端和服务端通讯协程错误 err = ", err)
return
}
}
func main() {
// 信息提示
fmt.Println("服务器在8889端口监听...")
listen, err := net.Listen("tcp", "0.0.0.0:8889")
if err != nil {
fmt.Println("net.Listen err = ", err)
return
}
//延时关闭
defer listen.Close()
// 一旦监听成功,就等待客户端来链接服务器
for {
fmt.Println("等待客户端来链接服务器...")
conn, err := listen.Accept()
if err != nil {
fmt.Println("listen.Accept err = ", err)
}
// 一旦链接成功,则启动一个携程和客户端保持通讯
go process(conn)
}
}
二.修改客户端
1.结构划分
1)main.go
- 显示第一级菜单
- 根据用户的输入,去调用对应的处理器
2)smsProcess.go
- 处理和短消息相关逻辑
- 私聊
- 群发
3)userProcess.go
- 处理和用户相关的业务
- 登录
- 注册等
4)server.go
- 显示登录成功界面
- 保持和服务器通讯【启动协程】
- 当读取服务器发送的消息后,就会显示在界面
2.文件结构
- client
- main
- main.go
- model
- process
- server.go
- smsProcess.go
- userProcess.go
- main
3.代码修改
client/process/userProcess.go
package process
import (
"encoding/binary"
"encoding/json"
"fmt"
"net"
"project/common/message"
"project/common/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 结构体
loginMes := message.LoginMes{}
loginMes.UserId = userId
loginMes.UserPwd = userPwd
//4. 将loginMes 序列化
data, err := json.Marshal(loginMes)
if err != nil {
fmt.Println("json.Marshal err = ", err)
return
}
//5. 把data赋给 mes.Data字段
mes.Data = string(data)
//6. 将 mes 进行序列化
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("json.Marshal err = ", err)
return
}
//7. 发送data消息
//7.1 把data的长度发送给服务器
// 获取data的长度->转成一个表示长度的byte切片
var pkgLen uint32
pkgLen = uint32(len(data))
var bytes [4]byte
binary.BigEndian.PutUint32(bytes[0:4], pkgLen)
// 发送长度
n, err := conn.Write(bytes[0:4])
if n != 4 || err != nil {
fmt.Println("conn.Write(bytes) fail ", err)
return
}
//fmt.Printf("客户端,发送消息的长度 = %d 内容 = %s", len(data), string(data))
//发送消息本身
_, err = conn.Write(data)
if err != nil {
fmt.Println("conn.Write(data) err = ", err)
return
}
//处理服务器端返回的消息
tf := &utils.Transfer{
Conn: conn,
}
mes, err = tf.ReadPkg()
fmt.Println("----------------")
if err != nil {
fmt.Println("utils.ReadPkg(conn) err = ", err)
return
}
//将mes的Data部分反序列化为LoginResMes
var loginResMes message.LoginResMes
err = json.Unmarshal([]byte(mes.Data), &loginResMes)
if loginResMes.Code == 200 {
//需要在客户端启动一个携程
//该携程保持喝服务器端的通讯,如果服务器有数据推送给客户端
//则接收并显示在客户端的终端
go serverProcessMes(conn)
//显示登录成功界面
ShowMenu()
} else if loginResMes.Code == 500 {
fmt.Println(loginResMes.Error)
}
return
}
client/process/server.go
package process
import (
"fmt"
"net"
"os"
"project/common/utils"
)
// 显示登录成功后的界面..
func ShowMenu() {
for {
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.Println("客户端正在等待读取服务器发送的消息")
mes, err := tf.ReadPkg()
if err != nil {
fmt.Println("tf.ReadPkg err = ", err)
return
}
//如果读到消息,进入下一步处理逻辑
fmt.Printf("mes = %v\n", mes)
}
}
client/main/main.go
package main
import (
"fmt"
"os"
process2 "project/client/process"
)
// 用户id 和 密码
var userId int
var userPwd string
func main() {
// 接受用户的选择
var key int
for {
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 := &process2.UserProcess{}
up.Login(userId, userPwd)
case 2:
fmt.Println("注册用户")
case 3:
fmt.Println("退出系统")
os.Exit(0)
default:
fmt.Println("您的输入有误,请重新输入")
}
}
}
5.在Redis手动添加测试用户,实现用户信息校验
在model中添加
1)user.go
1.定义一个User结构体
2)userDao.go
1.dao: data access object
2.编写对User对象(实例)操作的各种方法。主要就是增删改查
error.go
1.自定义错误
使用连接池访问redis,提高效率
在redis中手动添加一个用户
hset users 100 "{\"userId\":100,\"userPwd\":\"123456\",\"userName\":\"alan\"}"
创建文件结构
- server
- model
- user.go
- userDao.go
- error.go
- main
- redis.go
- model
代码实现
user.go
package model
// 定义一个用户的结构体
type User struct {
//确定字段信息
//为了序列化和反序列化成功,需保证用户信息的json字符串的key 和结构体的字段对应的tag名字一致
UserId int `json:"userId"`
UserPwd string `json:"userPwd"`
UserName string `json:"userName"`
}
error.go
package model
import "errors"
// 根据业务逻辑需要,自定义一些错误
var (
ERROR_USER_NOTEXISTS = errors.New("用户不存在..")
ERROR_USER_EXISTS = errors.New("用户已经存在..")
ERROR_USER_PWD = errors.New("密码不正确")
)
userDao.go
package model
import (
"encoding/json"
"fmt"
"github.com/gomodule/redigo/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_NOTEXISTS
}
return
}
//把res反序列化成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)
fmt.Println(user)
if err != nil {
return
}
// 密码不正确返回错误
if user.UserPwd != userPwd {
err = ERROR_USER_PWD
return
}
return
}
redis.go
package main
import (
"github.com/gomodule/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) { //初始化连接的代码
return redis.Dial("tcp", address)
},
}
}
修改service/main/main.go
package main
import (
"fmt"
"net"
"project/server/model"
"time"
)
// 处理和客户端的通讯
func process(conn net.Conn) {
//这里需要延时关闭conn
defer conn.Close()
//调用主控
processor := &Processor{
Conn: conn,
}
err := processor.process()
if err != nil {
fmt.Println("客户端和服务端通讯协程错误 err = ", err)
return
}
}
// 完成对UserDao的初始化
func initUserDao() {
//这里的pool时一个全局变量
model.MyUserDao = model.NewUserDao(pool)
}
func main() {
//当服务器启动时,初始化redis的连接池
initPool("localhost: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
}
//延时关闭
defer listen.Close()
// 一旦监听成功,就等待客户端来链接服务器
for {
fmt.Println("等待客户端来链接服务器...")
conn, err := listen.Accept()
if err != nil {
fmt.Println("listen.Accept err = ", err)
}
// 一旦链接成功,则启动一个携程和客户端保持通讯
go process(conn)
}
}
修改server/process/userProcess.go
账号合法性验证代码
//完成用户登录信息验证
//1.使用model.MyUserDao到redis去验证
user, err := model.MyUserDao.Login(loginMes.UserId, loginMes.UserPwd)
if err != nil {
if err == model.ERROR_USER_NOTEXISTS {
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.UserName, "账户登录成功")
}
修改client/process/userProcess.go
错误信息返回
if loginResMes.Code == 200 {
//需要在客户端启动一个携程
//该携程保持喝服务器端的通讯,如果服务器有数据推送给客户端
//则接收并显示在客户端的终端
go serverProcessMes(conn)
//显示登录成功界面
ShowMenu()
} else {
fmt.Println(loginResMes.Error)
}
return