[go学习笔记.第十六章.TCP编程] 4.项目-海量用户即时通讯系统-显示在线用户列表,群聊

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

用户登录后,可以得到当前在线用户列表

示意图如下: 

 步骤:

(1).编写了server/processBlock/userManager.go

package processBlock

import (
    "fmt"
)

//因为UserManager实例在服务器中有且只有一个,并且在很多地方都要使用,因此,将其定义为全局变量
var (
    userManager *UserManager
)

type UserManager struct {
    onlineUsers map[int]*UserProcess
}

//完成对UserManager的初始化
func init() {
    userManager = &UserManager {
        onlineUsers: make(map[int]*UserProcess, 1024),
    }
}

//完成对onlineUsers添加
func (this *UserManager) AddOnlineUser(up *UserProcess)  {
    this.onlineUsers[up.UserId] = up
}

//完成对onlineUsers删除
func (this *UserManager) DelOnlineUser(up *UserProcess)  {
    delete(this.onlineUsers, up.UserId)
}

//返回当前所有在线用户
func (this *UserManager) GetAllOnlineUser() map[int]*UserProcess {
    return this.onlineUsers
}

//根据id返回对应的值
func (this *UserManager) GetOnlineUserById(userId int) (up *UserProcess, err error)  {
    //如何从map中取出一个值,带检测方式
    up, ok := this.onlineUsers[userId]
    if !ok { // 说明要查找的这个用户,当前不在线
        err = fmt.Errorf("用户 %d 不存在",  userId)
        return
    }
    return
}

(2).完善了sercer/processBlock/userProcess.go中的Login()方法

        loginResMes.Code = 200
        //这里用户已经登录成功,把登录成功的用户放入userManager中
        //将登录成功的用户id赋给this
        this.UserId = loginMes.UserId
        userManager.AddOnlineUser(this)
        //将当前在线用户的id放入loginResMes.UsersId中
        //遍历userManager.onlineUsers
        for id, _ := range userManager.onlineUsers {
            loginResMes.UsersId = append(loginResMes.UsersId, id)
        }
        fmt.Println("登录成功", user)

(3).完善了client/processBlock/userProcess.go中的Login()方法

if loginResMes.Code == 200 {
        // fmt.Println("登录成功") 
        //显示当前在线用户列表:loginResMes.UsersId
        fmt.Println("当前在线用户列表:")
        for _, v := range loginResMes.UsersId {
            //如果要求不显示自己在线,使用continue
            if v == userId {
                continue
            }
            fmt.Printf("用户id:%v\n", v)
        }
        fmt.Println()

        //这里需要启动一个协程,保持和服务器通讯,如果服务器有数据,则推送给客户端,客户端接收后显示在终端
        go ServerProcessMes(conn)
        //1.显示登录成功的菜单[循环显示]
        for {
            ShowMenu()
        }
    }

(4).完善了common/message/message.go中的LoginResMes ()

type LoginResMes struct {
    Code int `json:"code"` //返回状态码: 200 登录成功, 500 用户未注册
    UsersId []int   //增加一个字段:保存用户id的切片,用户返回给客户端
    Error string `json:"error"` //返回错误信息
}

2.当一个新的用户上线后,其它已经登录的用户也能获取最新在线用户列表

思路1:

1.当有一个用户上线后,服务器就马上把维护的onlieUsers map整体推送

思路2:

1.服务器有自己的策略,每隔一定的时间,把维护的onlineUsers map整体推送

思路3:

1.当一个用户A上线,服务器就把A用户的上线信息,推送给所有在线的用户

2.客户端也需要维护一个map,map中记录了他的好友(目前就是所有人)map[int]User

3.客户端和服务器的通讯通道,要依赖serverProcessMes协程

(1).server/processBlock/userProcess.go增加了方法

//编写通知所有在线用户的方法
//userId要通知其他在线用户:我上线了
func (this *UserProcess) NotifyOthersOnlineUser(userId int) {
    //遍历onlineUsers,然后一个一个地发送NotifyUserStatusMes
    for id, up := range userManager.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.UserStatus = message.UserOnline

    //将notifyUserStatusMes序列化
    data, err := json.Marshal(notifyUserStatusMes)
    if err != nil {
        fmt.Println("NotifyMeOnline json marshal fail, err=", err)
        return
    }
    //将序列化后的notifyUserStatusMes赋值给mes.Data 
    mes.Data = string(data)
    //对mes再次序列化
    data, err = json.Marshal(mes)
    if err != nil {
        fmt.Println("NotifyMeOnline json marshal fail, err=", err)
        return
    }
    //发送,创建一个Transfer实例
    tf := &utils.Transfer {
        Conn: this.Conn,
    }
    err = tf.WritePkg(data)
    if err != nil {
        fmt.Println("NotifyMeOnline WritePkg fail, err=", err)
        return
    }
    return 
}

(2).server/processBlock/userProcess.go Login方法修改了代码

        loginResMes.Code = 200
        //这里用户已经登录成功,把登录成功的用户放入userManager中
        //将登录成功的用户id赋给this
        this.UserId = loginMes.UserId
        userManager.AddOnlineUser(this)
        //通知其他在线用户,我上线了
        this.NotifyOthersOnlineUser(loginMes.UserId)
        //将当前在线用户的id放入loginResMes.UsersId中
        //遍历userManager.onlineUsers
        for id, _ := range userManager.onlineUsers {
            loginResMes.UsersId = append(loginResMes.UsersId, id)
        }
        fmt.Println("登录成功", user)

(3).common/message/message.go增加了方法以及常量配置

//定义消息类型
const (
    LoginMesType = "LoginMes"
    LoginResMesType = "LoginResMes"
    RegisterMesType = "RegisterMes"
    RegisterResMesType = "RegisterResMes"
    NotifyUserStatusMesType = "NotifyUserStatusMes"
)

//定义几个用户状态常量
const (
    UserOnline = iota
    UserOffline
    UserBusyStatus
)
//为了配合服务端推送用户状态变化的消息
type NotifyUserStatusMes struct {
    UserId int `json:"userId"` // 用户id
    UserStatus int `json:"userStatus"` // 用户状态
}

(4).新增文件client/processBlock/userManager.go

package processBlock

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", id)
    }
}
//编写一个方法,处理返回的NotifyUserStatusMes
func updateUserStatus(notifyUserStatusMes *message.NotifyUserStatusMes) {
    user, ok := onlineUsers[notifyUserStatusMes.UserId]
    if !ok { // 原来没有
        user = &message.User{
            UserId: notifyUserStatusMes.UserId,
        }       
    }
    user.UserStatus = notifyUserStatusMes.UserStatus
    onlineUsers[notifyUserStatusMes.UserId] = user

    outputOnlineUser()
}

(5).client/processBlock/server.go修改

//显示登录成功后的界面
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("显示在线用户列表")
            outputOnlineUser()
        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
        }
        //如果读取到消息,则进行下一步逻辑处理    
        switch mes.Type {
            case message.NotifyUserStatusMesType: //有人上线了
                //1.取出NotifyUserStatusMes
                var notifyUserStatusMes message.NotifyUserStatusMes
                json.Unmarshal([]byte(mes.Data), &notifyUserStatusMes)
                //2.把这个用户的消息,保存到客户维护的map[int]User中
                updateUserStatus(&notifyUserStatusMes)
                //处理
        }
        // fmt.Printf("mes=%v\n", mes) 
    }
}

3.实现功能-完成登录后用户可以群聊 

完成客户端可以发送消息的思路

1.新增一个消息结构体SmMes

2.新增一个model CurUser

3.在smsProcess.go增加相应的方法 SendGroupMes,发送一个群聊的消息

4.在服务器端接收到SmsMes消息

5.在server/process/smsProcess.go文件增加群发消息的方法

6.在客户端还要增加去处理服务器端转发的群发消息

示意图

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

(1).common/message/message.go增加方法

//定义消息类型
const (
    LoginMesType = "LoginMes"
    LoginResMesType = "LoginResMes"
    RegisterMesType = "RegisterMes"
    RegisterResMesType = "RegisterResMes"
    NotifyUserStatusMesType = "NotifyUserStatusMes"
    SmsMesType = "SmsMes"
)

//增加一个SmsMes,发送消息
type SmsMes struct {
    Content string `json:"content"`//内容
    User //匿名结构体,继承type User struct
}

(2).新建文件client/model/curUser.go

package model

import(
    "net"
    "go_code/chatroom/common/message"
)

//该结构体目的:维护当前连接
//在客户端很多地方会使用到CurUser,所以将其作为一个全局的,放在userManager.go中统一管理

type CurUser struct {
    Conn net.Conn
    message.User
}

(3).client/processBlock/smsProcess.go增加方法

package processBlock

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 smsMes fail, err = ", err)
        return
    }
    //4.给mes.Data赋值
    mes.Data = string(data)
    //5.再次序列化mes
    data, err = json.Marshal(mes)
    if err != nil {
        fmt.Println("SendGroupMes json Marshal mes fail, err = ", err)
        return
    }
    //6.将mes发送给服务器
    tf := &utils.Transfer{
        Conn: curUser.Conn,
    }
    err = tf.WritePkg(data)
    if err != nil {
        fmt.Println("SendGroupMes transfer writePkg fail, err = ", err)
        return
    }
    return 
}   

(4).client/processBlock/userProcess.go Login()方法初始化CurUser结构体

//初始化CurUser
        curUser.Conn = conn
        curUser.UserId = userId
        curUser.UserStatus = message.UserOnline

        // fmt.Println("登录成功") 
        //显示当前在线用户列表:loginResMes.UsersId
        fmt.Println("当前在线用户列表:")
        for _, v := range loginResMes.UsersId {
            //如果要求不显示自己在线,使用continue
            if v == userId {
                continue
            }
            fmt.Printf("用户id:%v\n", v)
            //完成客户端的onlineUsers完成初始化
            user := &message.User {
                UserId: v,
                UserStatus: message.UserOnline,
            }
            onlineUsers[v] = user
        }
        fmt.Println()

        //这里需要启动一个协程,保持和服务器通讯,如果服务器有数据,则推送给客户端,客户端接收后显示在终端
        go ServerProcessMes(conn)
        //1.显示登录成功的菜单[循环显示]
        for {
            ShowMenu()
        }

(5).client/processBlock/server.go调用群聊方法

//显示登录成功后的界面
func ShowMenu()  {
    fmt.Println("--------恭喜xxx登录成功--------")
    fmt.Println("--------1.显示在线用户列表--------")
    fmt.Println("--------2.发送消息--------")
    fmt.Println("--------3.信息列表--------")
    fmt.Println("--------4.退出系统--------")
    fmt.Println("-------请选择(1~4):----")
    var key int
    var content string

    //有时候总会使用SmsProcess实例,故把该实例定义在switch外面
    smsProcess := &SmsProcess{}
    fmt.Scanf("%d\n", &key)
    switch key {
        case 1:
            // fmt.Println("显示在线用户列表")
            outputOnlineUser()
        case 2:
            fmt.Println("请输入想对大家说的话:")
            fmt.Scanf("%s\n", &content)
            smsProcess.SendGroupMes(content)
        case 3:
            fmt.Println("信息列表")
        case 4:
            fmt.Println("退出了系统")
            os.Exit(0)
        default:
            fmt.Println("输入错误,请重新输入")
    }
}

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

完成客户端可以发送消息的思路

1.新增一个消息结构体SmMes 

2.新增一个model CurUser

3.在smsProcess.go增加相应的方法 SendGroupMes,发送一个群聊的消息

4.在服务器端接收到SmsMes消息

5.在server/process/smsProcess.go文件增加群发消息的方法

6.在客户端还要增加去处理服务器端转发的群发消息SmsMes

(1).server/processBlock/smsProcess.go新增方法

package processBlock

import(
    "fmt"
    "net"
    "encoding/json"
    "go_code/chatroom/common/message"
    "go_code/chatroom/server/utils"
)

type SmsProcess struct {

}

//转发消息
func (this *SmsProcess) SendGroupMes(mes *message.Message)  {
    //遍历服务器端的onlineUsers map[int]*UserProcess
    //将消息转发出去
    //取出mes中的内容
    var smsMes message.SmsMes
    err := json.Unmarshal([]byte(mes.Data), &smsMes)
    if err != nil {
        fmt.Println("SendGroupMes json Ummarshal fail, err = ", err)
        return
    }

    data, err := json.Marshal(mes)
    if err != nil {
        fmt.Println("SendGroupMes json Marshal fail, err = ", err)
        return
    }
    for id, up := range userManager.onlineUsers {
        //过滤自己:不需要给自己发送消息
        if id == smsMes.UserId {
            continue
        }       
        this.SendMesToEachOnlineUser(data, up.Conn)
    }
}

func (this *SmsProcess) SendMesToEachOnlineUser(data []byte, conn net.Conn) {
    //创建一个Transfer实例,发送data
    tf := &utils.Transfer {
        Conn : conn,
    }
    err := tf.WritePkg(data)
    if err != nil {
        fmt.Println("转发消息失败, err = ", err)
    }
    return 
}

 (2).server/main/processor.go中调用smsProcess. SendGroupMes()方法

//编写一个ServerProcessMes函数
//功能:根据客户端发送消息类型不同,决定调用哪个函数来处理
func (this *Processor) serverProcessMes(mes *message.Message) (err error)  {
    switch mes.Type {
        case message.LoginMesType : 
            //处理登录消息
            //创建一个UserProcess
            up := &processBlock.UserProcess{
                Conn: this.Conn,
            }
            err = up.ServerProcessLogin(mes)
        case message.RegisterMesType : 
            //处理注册
            up := &processBlock.UserProcess{
                Conn: this.Conn,
            }
            err = up.ServerProcessRegister(mes)
        case message.SmsMesType : 
            //穿甲一个SmsProcess实例,完成消息的转发
            sms := &processBlock.SmsProcess{}
            sms.SendGroupMes(mes)
        default :
            fmt.Println("消息类型不存在, 无法处理...")
    }
    return
}

(3).client/processBlock/smsManager.go 新增方法

package processBlock

import(
    "fmt"
    "encoding/json"
    "go_code/chatroom/common/message"
)

//该文件目前是为了处理输出消息相关逻辑

func outputGroupMes(mes *message.Message)  { //这个地方mes一定是smsMes
    //显示
    //1.反序列化mes
    var smsMes message.SmsMes
    err := json.Unmarshal([]byte(mes.Data), &smsMes)
    if err != nil {
        fmt.Println("outputGroupMes json.Unmarshal fail, err =", err)
        return
    }
    //显示消息
    info := fmt.Sprintf("用户:%d\t,对大家说:%s", smsMes.UserId, smsMes.Content)
    fmt.Println(info)
}

(4).client/processBlock/server.go中调用方法 smsManger.outputGroupMes()

//和服务端端保持通讯
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
        }
        //如果读取到消息,则进行下一步逻辑处理    
        switch mes.Type {
            case message.NotifyUserStatusMesType: //有人上线了
                //1.取出NotifyUserStatusMes
                var notifyUserStatusMes message.NotifyUserStatusMes
                json.Unmarshal([]byte(mes.Data), &notifyUserStatusMes)
                //2.把这个用户的消息,保存到客户维护的map[int]User中
                updateUserStatus(&notifyUserStatusMes)
                //处理
            case message.SmsMesType:  //有人群发消息了
                outputGroupMes(&mes)
        default:
                fmt.Println("")
        }
        // fmt.Printf("mes=%v\n", mes) 
    }

[上一节][go学习笔记.第十六章.TCP编程] 3.项目-海量用户即时通讯系统-redis介入,用户登录,注册

[下一节][go学习笔记.第十七章.redis的使用] 1.redis的使用

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值