Go学习笔记(18)-经典项目-海量用户即时通讯系统

Go学习笔记(18)

经典项目-海量用户即时通讯系统

项目开发流程

需求分析--> 设计阶段---> 编码实现 --> 测试阶段-->实施

需求分析

  1. 用户注册
  2. 用户登录
  3. 显示在线用户列表
  4. 群聊(广播)
  5. 点对点聊天
  6. 离线留言

界面设计

项目开发前技术准备

​ 项目要保存用户信息和消息数据,因此我们需要学习数据库(Redis 或者 Mysql) , 这里我们选择Redis , 所以先给同学们讲解如何在 Golang 中使用 Redis.

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

功能:能够正确的显示客户端的菜单。

界面:

思路分析:这个非常简单,直接写.

代码实现:

client/main.go

client/login.go

实现功能-完成用户登录

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

这里需要先说明一个 Message 的组成(示意图),并发送一个 Message 的流程

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

(1) 先确定消息 Message 的格式和结构

(2) 然后根据上图的分析完成代码

(3) 示意图

代码实现:

server/main.go

common/message/message.go

client/main.go

和前面的代码一样,没有修改

client/login.go

2.完成客户端可以发送消息本身,服务器端可以正常接收到消息,并根据客户 端发 送的消 息(LoginMes), 判断用户的合法性,并返回相应的 LoginResMes

思路分析:

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

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

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

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

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

代码实现:

client/login.go 做了修改

server/main.go 修改

将读取包的任务封装到了一个函数中.readPkg()

能够完成登录,并提示相应信息

server/main.go 修改

client/utils.go 新增

client/login.go 增加代码

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

  1. 先改进服务端, 先画出程序的框架图[思路],再写代码.
  1. 步骤

[1] . 先把分析出来的文件,创建好,然后放到相应的文件夹[包]

[2] 现在根据各个文件,完成的任务不同,将 main.go 的代码剥离到对应的文件中即可。

[3] 先修改了 utils/utils.go

[4] 修改了 process2/userProcess.go

[5] 修改了 main/processor.go

[6] 修改 main/main.go

修改客户端, 先画出程序的框架图[思路],再写代码

[1] 步骤 1-画出示意图

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

[3] 将 server/utils.go 拷贝到 client/utils/utils.go

[4] 创建了 server/process/userProcess.go

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

[5] 创建了 server/process/server.go

[6] server/main/main.go 修改

在 Redis 手动添加测试用户,并画图+说明注意. (后面通过程序注册用户)

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

如输入的用户名密码在 Redis 中存在则登录,否则退出系统,并给出相应的提示信息:

  1. 用户不存在,你也可以重新注册,再登录
  2. 你密码不正确。。

代码实现:

[1] 编写 model/user.go

[2] 编写 model/error.go

[3] 编写 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
}

//思考一下在 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", "users", id))
    if err != nil {
        //错误!
        if err == redis.ErrNil { //表示在 users 哈希中,没有找到对应 id
        	err = 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
}


//完成登录的校验 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] main/redis.go

[5] main/main.go

[6] 在 process/userProcess.go 使用到 redis 验证的功能

实现功能-完成注册用户

  1. 完成注册功能,将用户信息录入到 Redis 中
  2. 思路分析,并完成代码
  3. 思路分析的示意图

实现功能-完成注册用户

[1] common/message/user.go

[2] common/message/message.go

[3] client/process/userProcess.go

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. 创建一个 LoginMes 结构体
    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.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
    }
    //创建一个 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
}

[4] 在 client/main/main.go 增加了代码

[5] 在 server/model/userDao.go 增加方法

[6] 在 server/process/userProcess.go 增加了方法,处理注册

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
    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
    }
    
    data, err := json.Marshal(registerResMes)
    if err != nil {
        fmt.Println("json.Marshal fail", err)
        return
	}
    
    //4. 将 data 赋值给 resMes
	resMes.Data = string(data)
    
    //5. 对 resMes 进行序列化,准备发送
    data, err = json.Marshal(resMes)
    if err != nil {
        fmt.Println("json.Marshal fail", err)
        return
    }
    
    //6. 发送 data, 我们将其封装到 writePkg 函数
    //因为使用分层模式(mvc), 我们先创建一个 Transfer 实例,然后读取
    tf := &utils.Transfer{
    	Conn : this.Conn,
    }
    err = tf.WritePkg(data)
    return
}

[7] server/main/processor.go 调用了

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

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

思路分析:

代码实现:

[1] 编写了 server/process/userMgr.go

[2] server/process/userProcess.go

[3] common/message/message.go

[4] client/process/userProcess.go

当一个新的用户上线后,其它已经登录的用户也能获取最新在线用户列表,思路分析、示意图、代码实现

[1] server/process/userProcess.go

[2] sever/proces/userProcess.go [的 Login]

[3] common/mesage/message.go

[4] client/process/userMgr.go

[5] client/process/server.go

[6] client/process/server.go

实现功能-完成登录用可以群聊

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

思路分析:

代码实现:

[1] common/message/messag.go

[2] client/model/curUser.go

[3] client/process/smsProcess.go 增加了发送群聊消息

[4] 测试

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

思路分析:

代码实现:

[1] server/process/smsProcess.go

[2] server/main/processor.go

[3] client/process/smsMgr.go

[4] client/process/server.go

聊天的项目的扩展功能要求

  1. 实现私聊.[点对点聊天]
  2. 如果一个登录用户离线,就把这个人从在线列表去掉【】
  3. 实现离线留言,在群聊时,如果某个用户没有在线,当登录后,可以接受离线的消息
  4. 发送一个文件
end
  • 作者:AWhiteElephant(联系作者)
  • 发表时间:2022-06-09 20:50
  • 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
  • 转载声明:如果是转载栈主转载的文章,请附上原文链接
  • 评论