• go语言开发的简易聊天室


      最近在学习go,在B站看了尚硅谷的go基础课https://www.bilibili.com/video/BV1ME411Y71o,跟着老师把最后的聊天室项目做完了,写篇随笔记录一下。

      首先是项目实现的功能:1.注册,2.登录,3.用户状态显示,4.聊天,5.留言

      项目主要分为三部分:客户端,为用户提供操作界面,与服务器进行通讯;服务器,处理各种消息请求,与数据库交互;数据库,保存用户的基本信息,聊天信息等。

      

      该项目全程使用go语言,使用tcp进行通讯,比较基础。下面根据文件来介绍一下项目。

      client     

      ./main/main.go,客户端的启动文件,为用户提供一个基础界面,比较简陋,直接在控制台显示而已。

    package main
    
    import (
        "chatroom/client/process"
        "fmt"
    )
    
    var userId int
    var userPwd string
    var userName string
    
    func main() {
    
        var key int
    
        var loop = true
    
        for loop {
            fmt.Println("\n-------------------欢迎使用多人聊天室-------------------")
            fmt.Println("\t\t\t 1.登录系统")
            fmt.Println("\t\t\t 2.注册系统")
            fmt.Println("\t\t\t 3.退出系统")
            fmt.Printf("\t\t\t (请选择1-3):")
            fmt.Scanln(&key)
            switch key {
            case 1:
                fmt.Println("登录")
                fmt.Print("请输入用户Id:")
                fmt.Scanln(&userId)
                fmt.Print("请输入密码:")
                fmt.Scanln(&userPwd)
                ps := process.Process{}
                ps.Login(userId, userPwd)
                loop = false
            case 2:
                fmt.Println("注册")
                fmt.Print("请输入用户Id:")
                fmt.Scanln(&userId)
                fmt.Print("请输入密码:")
                fmt.Scanln(&userPwd)
                fmt.Print("请输入用户名:")
                fmt.Scanln(&userName)
                ps := process.Process{}
                ps.Register(userId, userPwd, userName)
            case 3:
                loop = false
            default:
                fmt.Println("输入有误,请重新输入")
            }
        }
    }

      ./model/curUser.go,存放一个结构体,管理当前登录的用户信息,与客户端的连接。在进行内容输入的时候,如果使用一般的fmt.Scan会认为空格也是结束符,所以要使用bufio.NewReader(os.Stdin)来接收控制台输入。

    package model
    
    import (
        model2 "chatroom/common/model"
        "net"
    )
    
    // CurUser 保存当前登录的用户,以及连接
    type CurUser struct {
        Conn net.Conn
        User model2.User
    }

      ./process/server.go,登录成功后显示的界面,保持与服务器的连接。

    package process
    
    import (
        "bufio"
        "chatroom/common/message"
        "chatroom/common/util"
        "encoding/json"
        "fmt"
        "net"
        "os"
        "strings"
    )
    
    // ShowMenu 登录成功后展示的页面
    func ShowMenu(userName string) (key int) {
        fmt.Printf("\n--------------恭喜%s登录成功-------------\n", userName)
        fmt.Println("--------------1.显示在线用户列表--------------")
        fmt.Println("--------------2.发送消息--------------------")
        fmt.Println("--------------3.信息列表--------------------")
        fmt.Println("--------------4.退出系统--------------------")
        fmt.Print("--------------请输入(1-4):")
        fmt.Scanln(&key)
        switch key {
        case 1:
            fmt.Println("显示用户列表")
            OutputOnlineUsers()
        case 2:
            var smsKey int
            fmt.Print("发送消息[群聊->1]/[私聊->2]:")
            fmt.Scanln(&smsKey)
            ShowSms(smsKey)
        case 3:
            fmt.Println("信息列表")
            ShowSmsList()
        case 4:
            fmt.Println("退出")
        default:
            fmt.Println("输入不正确")
        }
        return
    }
    
    // ShowSms 处理群聊和私聊两种消息的输入
    func ShowSms(smsKey int) {
        var content string
        smsProcess := &SmsProcess{}
        if smsKey == 1 {
            fmt.Print("请输入你想对大家说的话:")
            reader := bufio.NewReader(os.Stdin)  // 标准输入输出
            content, _ = reader.ReadString('\n') // 回车结束
            content = strings.TrimSpace(content)
            // 调用群发消息的功能
            smsProcess.SendGroupMes(content)
        } else {
            var ToUserId int
            fmt.Print("请输入发送的用户id:")
            fmt.Scanln(&ToUserId)
            fmt.Print("请输入你想对TA说的话:")
            reader := bufio.NewReader(os.Stdin)  // 标准输入输出
            content, _ = reader.ReadString('\n') // 回车结束
            content = strings.TrimSpace(content)
            // 调用私聊的功能
            smsProcess.SendUserMsg(ToUserId, content)
        }
    }
    
    // processServer 登录成功后保持与服务器的连接,处理服务器发送过来不同类型的消息种类
    func processServer(conn net.Conn) {
        tf := util.Transfer{
            Conn: conn,
        }
        for {
            // 等待服务器发送消息,如果服务器还没发送消息回来,这里会一直阻塞,直到服务器发送消息或者连接由某一方断开而产生错误
            mes, err := tf.ReadMes()
            if err != nil {
                if _, ok := err.(*net.OpError); ok {
                    fmt.Println("退出系统")
                    return
                }
                fmt.Println("readMes err=", err)
                return
            }
            switch mes.Type {
            // 用户上线的消息类型
            case message.NotifyOthersMesType:
                var notifyOthersMes = message.NotifyOthersMes{}
                err = json.Unmarshal([]byte(mes.Data), &notifyOthersMes)
                if err != nil {
                    fmt.Println("message.NotifyOthersMes json.Unmarshal err=", err)
                    break
                }
                // 当接收到用户登录或者下线的消息时,就更新AllUsers中该用户的状态
                UpdateUserStatus(&notifyOthersMes)
            // 接收聊天消息的消息类型
            case message.SmsResMesType:
                var smsResMes = message.SmsResMes{}
                err = json.Unmarshal([]byte(mes.Data), &smsResMes)
                if err != nil {
                    fmt.Println("message.SmsResMes json.Unmarshal err=", err)
                    break
                }
                // 将接收到的消息保存到smsList中
                SaveSmsList(&smsResMes)
                // 打印出该消息的内容
                OutPutSms(&smsResMes)
            // 接收离线消息的消息类型
            case message.SmsListResMesType:
                var smsListResMes = message.SmsListResMes{}
                err = json.Unmarshal([]byte(mes.Data), &smsListResMes)
                if err != nil {
                    fmt.Println("message.SmsListResMesType json.Unmarshal err=", err)
                    break
                }
                // 将离线的时收到的留言保存到smsList中
                SaveSmsList(&smsListResMes)
            }
        }
    }

      ./process/smsMgr.go,用于管理消息,逻辑比较简单,只是做了消息显示的功能。

    package process
    
    import (
        "chatroom/common/message"
        "encoding/json"
        "fmt"
    )
    
    // smsList 用于保存发送过来的消息
    var smsList []*message.SmsResMes
    
    // OutPutSms 根据消息的不同类型打印出消息内容
    func OutPutSms(smsResMes *message.SmsResMes) {
        if smsResMes.Type == message.SmsGroupType {
            fmt.Printf("\n%s对所有人说:%s\n", smsResMes.User.UserName, smsResMes.Content)
        } else {
            fmt.Printf("\n%s对你说:%s\n", smsResMes.User.UserName, smsResMes.Content)
        }
    }
    
    // SaveSmsList 将发送过来的消息保存到smsList中;有两种消息,在线时收到的消息和同步过来的离线消息
    func SaveSmsList(data interface{}) {
        switch t := data.(type) {
        case *message.SmsResMes:
            smsList = append(smsList, t)
        case *message.SmsListResMes:
            for _, v := range t.SmsList {
                // 因为SmsListResMes的SmsList中的值是字符串类型,所以需要反序列化为对应的结构体
                smsResMes := message.SmsResMes{}
                err := json.Unmarshal([]byte(v), &smsResMes)
                if err != nil {
                    fmt.Println("SaveSmsList json.Unmarshal err=", err)
                    continue
                }
                smsList = append(smsList, &smsResMes)
            }
        default:
            fmt.Println("Unexpect type")
        }
    }
    
    // ShowSmsList 显示smsList中的消息
    func ShowSmsList() {
        for _, v := range smsList {
            OutPutSms(v)
        }
    }

      ./process/smsProcess.go,实现聊天相关的功能,主要是群聊、私聊、获取离线消息。

    package process
    
    import (
        "chatroom/common/message"
        "chatroom/common/util"
        "encoding/json"
        "fmt"
    )
    
    // SmsProcess 实现与聊天相关的功能
    type SmsProcess struct {
    }
    
    // SendGroupMes 发送群聊的消息
    func (smsProcess *SmsProcess) SendGroupMes(content string) {
    
        // 构建群聊的消息结构体实例
        smsMes := &message.SmsMes{
            Content: content,
            User:    MyCurUser.User,
            Type:    message.SmsGroupType,
        }
    
        smsJsonMes, err := json.Marshal(smsMes)
        if err != nil {
            fmt.Println("SendGroupMes json.Marshal err=", err)
            return
        }
        // 构建消息结构体实例
        mes := &message.Mes{
            Type: message.SmsMesType,
            Data: string(smsJsonMes),
        }
        // 创建一个读写消息的实例,这里是写操作
        tf := &util.Transfer{
            Conn: MyCurUser.Conn,
        }
        err = tf.WriteMes(mes)
        if err != nil {
            fmt.Println("SendGroupMes writeMes err=", err)
            return
        }
    }
    
    // SendUserMsg 发送私聊的消息
    func (smsProcess *SmsProcess) SendUserMsg(userId int, content string) {
        // 构建私聊消息结构体实例
        smsMes := &message.SmsMes{
            Content:   content,
            User:      MyCurUser.User,
            RecUserId: userId,
            Type:      message.SmsPrivateType,
        }
        smsJsonMes, err := json.Marshal(smsMes)
        if err != nil {
            fmt.Println("SendGroupMes json.Marshal err=", err)
            return
        }
        // 构建消息结构体实例
        mes := &message.Mes{
            Type: message.SmsMesType,
            Data: string(smsJsonMes),
        }
        // 构建读写消息的实例,这里是写操作
        tf := &util.Transfer{
            Conn: MyCurUser.Conn,
        }
        err = tf.WriteMes(mes)
        if err != nil {
            fmt.Println("SendGroupMes writeMes err=", err)
            return
        }
    }
    
    // GetSmsList 登录时发送获取离线消息的请求
    func (smsProcess SmsProcess) GetSmsList(userId int) {
        // 构建获取离线消息结构体的实例
        smsListMes := &message.SmsListMes{
            UserId: userId,
        }
        smsListJsonMes, err := json.Marshal(smsListMes)
        if err != nil {
            fmt.Println("GetSmsList json.Marshal err=", err)
            return
        }
        // 构建消息结构体的实例
        mes := &message.Mes{
            Type: message.SmsListMesType,
            Data: string(smsListJsonMes),
        }
        // 构建读写消息的实例,这里是写操作
        tf := &util.Transfer{
            Conn: MyCurUser.Conn,
        }
        err = tf.WriteMes(mes)
        if err != nil {
            fmt.Println("GetSmsList writeMes err=", err)
            return
        }
    }

      ./process/userMgr.go,管理所有用户的信息,主要是在线状态的更新和显示。

    package process
    
    import (
        "chatroom/client/model"
        "chatroom/common/message"
        model2 "chatroom/common/model"
        "fmt"
    )
    
    // AllUsers 用户保存所有用户
    var AllUsers = make(map[int]*model2.User, 10)
    
    // MyCurUser 记录当前登录的用户,包括用户的信息和客户端与服务器的连接
    var MyCurUser model.CurUser
    
    // OutputOnlineUsers 显示当前在线的用户
    func OutputOnlineUsers() {
        flag := 0
        for _, user := range AllUsers {
            if flag == 0 {
                fmt.Println("当前在线用户")
            }
            switch user.UserStatus {
            case message.Online:
                fmt.Printf("%s[在线]\n", user.UserName)
            case message.Offline:
                fmt.Printf("%s[离线]\n", user.UserName)
            }
            flag++
        }
    }
    
    // UpdateUserStatus 更新用户的状态,在线或者是离线;如果是新注册的用户那么会添加一个新的用户到AllUsers中
    func UpdateUserStatus(notifyOthersMes *message.NotifyOthersMes) {
        user := &notifyOthersMes.User
        AllUsers[user.UserId] = user
        if user.UserStatus == message.Online {
            fmt.Printf("\n%s上线了\n", user.UserName)
        } else {
            fmt.Printf("\n%s下线了\n", user.UserName)
        }
    }

      ./process/userProcess.go,实现用户相关的功能,注册、登录、登出。在这里与服务器创建连接,在登录成功后,启动一个协程去对后续的聊天等功能进行管理,模块划分得不是很好。

    package process
    
    import (
        "chatroom/common/message"
        "chatroom/common/model"
        "chatroom/common/util"
        "encoding/json"
        "fmt"
        "net"
    )
    
    // Process 实现与用户相关的功能;注册、登录、下线
    type Process struct {
    }
    
    // Register 用于注册用户
    func (process Process) Register(userId int, userPwd, userName string) {
        // 创建与服务器的连接
        conn, err := net.Dial("tcp", "localhost:8888")
        defer conn.Close()
    
        if err != nil {
            fmt.Println("register net.Dial err=", err)
            return
        }
        // 创建用户结构体实例
        user := model.User{
            UserId:   userId,
            UserPwd:  userPwd,
            UserName: userName,
        }
        registerMes := message.RegisterMes{
            User: user,
        }
        registerJsonMes, _ := json.Marshal(registerMes)
        // 创建消息结构体实例
        mes := message.Mes{
            Type: message.RegisterMesType,
            Data: string(registerJsonMes),
        }
    
        tf := &util.Transfer{
            Conn: conn,
        }
        // 发送注册消息
        err = tf.WriteMes(&mes)
        if err != nil {
            fmt.Println("register tf.WriterMes err=", err)
            return
        }
    
        // 等待服务器返回的注册响应
        res, err := tf.ReadMes()
        resData := message.ResMes{}
        err = json.Unmarshal([]byte(res.Data), &resData)
        if err != nil {
            fmt.Println("register json.Unmarshal err=", err)
            return
        }
        if resData.Code == 200 {
            fmt.Println("注册成功,请登录")
        } else {
            fmt.Println(resData.Error)
        }
    }
    
    // Login 用户登录
    func (process Process) Login(userId int, userPwd string) {
        // 创建连接
        conn, err := net.Dial("tcp", "localhost:8888")
        defer conn.Close()
        if err != nil {
            fmt.Println("net.Dial err=", err)
            return
        }
        // 创建登录结构体实例
        loginMes := message.LoginMes{
            UserId:  userId,
            UserPwd: userPwd,
        }
        loginMesJsonData, _ := json.Marshal(loginMes)
        // 创建消息结构体实例
        mes := message.Mes{
            Type: message.LoginMesType,
            Data: string(loginMesJsonData),
        }
    
        tf := &util.Transfer{
            Conn: conn,
        }
        // 发送登录的消息
        err = tf.WriteMes(&mes)
    
        if err != nil {
            fmt.Println("tf.Writes err=", err)
            return
        }
        // 等待服务器返回的登录响应
        resMes, err := tf.ReadMes()
        loginResMes := message.LoginResMes{}
        err = json.Unmarshal([]byte(resMes.Data), &loginResMes)
        if err != nil {
            fmt.Println("json.Unmarshal, err=", err)
            return
        }
        // 如果状态码是200,那么登录成功
        if loginResMes.ResMes.Code == 200 {
    
            // 将当前的用户id、用户名、用户状态、与服务器的连接保存到MyCurUser中,以方便其他功能的使用
            MyCurUser.Conn = conn
            MyCurUser.User.UserId = userId
            MyCurUser.User.UserStatus = message.Online
            // 启动一个监听服务器请求的协程,处理各种服务器返回的消息
            go processServer(conn)
    
            // 登录成功后,服务器会返回所有用户的信息,将用户信息保存到AllUsers中,用于对所有用户的管理
            for _, v := range loginResMes.OnlineUsers {
                if v.UserId == userId {
                    MyCurUser.User.UserName = v.UserName
                    continue
                }
                switch v.UserStatus {
                case message.Online:
                    fmt.Printf("%s[在线]\n", v.UserName)
                case message.Offline:
                    fmt.Printf("%s[离线]\n", v.UserName)
                }
                user := &model.User{
                    UserId:     v.UserId,
                    UserName:   v.UserName,
                    UserStatus: v.UserStatus,
                }
                AllUsers[v.UserId] = user
            }
            // 登录成功后,向服务器发送同步离线消息的请求
            smsProcess := &SmsProcess{}
            smsProcess.GetSmsList(userId)
            for {
                // 展示登录成功后的功能页面
                key := ShowMenu(MyCurUser.User.UserName)
                if key == 4 {
                    // 向服务器发送下线的消息
                    process.SignOut(userId, MyCurUser.User.UserName, conn)
                    break
                }
            }
    
        } else {
            fmt.Println("err=", loginResMes.ResMes.Error)
        }
        return
    }
    
    // SignOut 退出系统,向服务器发送退出的消息
    func (process Process) SignOut(userId int, userName string, conn net.Conn) {
        // 创建离线消息的结构体
        signOutMes := message.SignOutMes{
            UserId:   userId,
            UserName: userName,
        }
        signOutJsonMes, err := json.Marshal(signOutMes)
        if err != nil {
            fmt.Println("SignOut json.Marshal err=", err)
            return
        }
        mes := message.Mes{
            Type: message.SignOutMesType,
            Data: string(signOutJsonMes),
        }
    
        tf := &util.Transfer{
            Conn: conn,
        }
        // 发送该消息
        err = tf.WriteMes(&mes)
        if err != nil {
            fmt.Println("SignOut tf.WriterMes err=", err)
            return
        }
    }

      common,这里存放client和server都会用到的方法。   

         ./message/message.go,这里定义了各种消息的结构体,用于客户端和服务器之间的通讯,根据不同给结构体区分不同的消息类型,所有消息都会封装成一个Mes实例进行发送。

    package message
    
    import "chatroom/common/model"
    
    // 不同消息的类型,用于客户端和服务器之间的消息通讯
    const (
        LoginMesType        = "LoginMes"
        RegisterMesType     = "RegisterMes"
        ResMesType          = "ResMes"
        NotifyOthersMesType = "NotifyOthersMes"
        SmsMesType          = "SmsMes"
        SmsResMesType       = "SmsResMes"
        SignOutMesType      = "SignOutMes"
        SmsListMesType      = "SmsListMes"
        SmsListResMesType   = "SmsListResMes"
    )
    
    // 用户当前的状态,Offline为0,Online为1
    const (
        Offline = iota
        Online
    )
    
    // 发送消息的类型,群聊或者私聊
    const (
        SmsGroupType   = "Group"
        SmsPrivateType = "Private"
    )
    
    // Mes 发送消息的结构体,下面各种类型的消息封装好后,保存到Data里面
    type Mes struct {
        Type string `json:"type"`
        Data string `json:"data"`
    }
    
    // LoginMes 客户端用于登录的消息结构体
    type LoginMes struct {
        UserId   int    `json:"userId"`
        UserPwd  string `json:"userPwd"`
        UserName string `json:"userName"`
    }
    
    // RegisterMes 客户端用于注册的消息结构体
    type RegisterMes struct {
        User model.User `json:"user"`
    }
    
    // ResMes 服务器响应客户端注册和登录消息的结构体
    type ResMes struct {
        Code  int    `json:"code"`
        Error string `json:"error"`
    }
    
    // LoginResMes 服务器响应登录消息的结构体,OnlineUsers记录服务器中所有用户的信息,将这个返回给客户端
    type LoginResMes struct {
        ResMes      ResMes        `json:"resMes"`
        OnlineUsers []*model.User `json:"onlineUsers"`
    }
    
    // SignOutMes 客户端用于发送下线消息的结构体
    type SignOutMes struct {
        UserId   int    `json:"userId"`
        UserName string `json:"userName"`
    }
    
    // NotifyOthersMes 服务器用于发送用户状态消息的结构体
    type NotifyOthersMes struct {
        User model.User `json:"user"`
    }
    
    // SmsMes 客户端用于发送消息的结构体
    type SmsMes struct {
        Content   string     `json:"content"`
        User      model.User `json:"user"`
        Type      string     `json:"type"`
        RecUserId int        `json:"recUserId"`
    }
    
    // SmsResMes 服务器用于将客户端发送的消息转发至其他用户的结构体
    type SmsResMes struct {
        Content string     `json:"content"`
        User    model.User `json:"user"`
        Type    string     `json:"type"`
    }
    
    // SmsListMes 客户端获取离线消息的结构体
    type SmsListMes struct {
        UserId int `json:"userId"`
    }
    
    // SmsListResMes 服务器返回离线消息的结构体
    type SmsListResMes struct {
        SmsList []string `json:"smsList"`
    }

      ./model/user.go,用户信息的结构体,用户id、用户密码、用户名、用户状态。

    package model
    
    // User 保存用户信息的结构体
    type User struct {
        UserId     int    `json:"userId"`     // 用户id,唯一
        UserPwd    string `json:"userPwd"`    // 用户密码
        UserName   string `json:"userName"`   // 用户名
        UserStatus int    `json:"userStatus"` // 用户状态,在线或离线
    }

      ./util/util.go,工具包,主要是写消息和发消息,这里的逻辑是自己定义的,本项目是先发送消息长度,在发送消息内容,验证长度后,如果正确,那么可以正常通讯,作用是防止在网络传输过程中丢包。

    package util
    
    import (
        "chatroom/common/message"
        "encoding/binary"
        "encoding/json"
        "errors"
        "fmt"
        "io"
        "net"
    )
    
    // Transfer 客户端和服务器进行通讯时的工具包
    type Transfer struct {
        Conn net.Conn       // 客户端与服务器的连接
        Buf  [1024 * 4]byte // 用于消息的缓存
    }
    
    func (transfer *Transfer) ReadMes() (mes message.Mes, err error) {
        // 读取消息体的长度大小
        _, err = transfer.Conn.Read(transfer.Buf[:4])
        if err != nil {
            if err == io.EOF {
                return
            }
            return
        }
    
        var pkgLen uint32
        // 转换消息体的长度大小
        pkgLen = binary.BigEndian.Uint32(transfer.Buf[:4])
        // 读取发送过来的消息体,返回的n就是消息体的长度
        // 因为这里会阻塞读取消息,所以对应的ReadMes方法与WriteMes方法需要有成对的Read和Write操作
        n, err := transfer.Conn.Read(transfer.Buf[:pkgLen])
    
        // 如果第一次接收的消息体长度大小和第二次接收的消息体的长度n一致,那么传输过程中没有丢包,如果不一致,则产生了丢包
        if n != int(pkgLen) || err != nil {
            err = errors.New("read body err")
            return
        }
        // 将消息反序列化为消息类型的结构体
        err = json.Unmarshal(transfer.Buf[:pkgLen], &mes)
        if err != nil {
            err = errors.New("unmarshal pkg err")
            return
        }
        return
    }
    
    func (transfer *Transfer) WriteMes(mes *message.Mes) (err error) {
        // 将消息类型的结构体序列化
        mesJsonData, _ := json.Marshal(mes)
    
        // 计算消息的长度大小,并发送
        var pkgLen uint32
        pkgLen = uint32(len(mesJsonData))
        binary.BigEndian.PutUint32(transfer.Buf[0:4], pkgLen)
    
        _, err = transfer.Conn.Write(transfer.Buf[0:4])
        if err != nil {
            fmt.Println("conn.Write header err=", err)
            return
        }
    
        // 发送消息体本身
        _, err = transfer.Conn.Write(mesJsonData)
    
        if err != nil {
            fmt.Println("conn.Write body err=", err)
            return
        }
        return nil
    }

      server    

         ./main/main.go,服务器的启动文件,创建端口监听,初始化全局变量,加载数据库中的用户信息,都在服务器启动的时候完成。

    package main
    
    import (
        "chatroom/server/model"
        "fmt"
        "github.com/garyburd/redigo/redis"
        "net"
        "time"
    )
    
    // firstProcess 监听连接的函数
    func firstProcess(conn net.Conn) {
        defer conn.Close()
        fmt.Println("等待输入")
        pms := TransProcess{
            Conn: conn,
        }
        pms.TransMes()
    }
    
    // redis连接池
    var (
        pool *redis.Pool
    )
    
    // InitPool 初始化redis连接池
    func InitPool(maxIdle int, maxActive int, idleTimeout time.Duration, address string) {
        pool = &redis.Pool{
            MaxIdle:     maxIdle,
            MaxActive:   maxActive,
            IdleTimeout: idleTimeout,
            Dial: func() (redis.Conn, error) {
                c, err := redis.Dial("tcp", address)
                if err != nil {
                    return nil, err
                }
                c.Do("SELECT", 3)
                return c, nil
            },
        }
    }
    
    // InitDao 初始化两个dao层,UserDao和SmsDao
    func InitDao(pool *redis.Pool) {
        model.MyUserDao = model.NewUserDao(pool)
        model.MySmsDao = model.NewSmsDao(pool)
    }
    
    func main() {
    
        InitPool(32, 0, time.Second*100, "localhost:6379")
        InitDao(pool)
        // 服务器启动的时候,加载redis中所有用户的信息
        model.MyUserDao.LoadAllUser()
    
        // 监听8888端口
        listen, err := net.Listen("tcp", "localhost:8888")
        defer listen.Close()
        if err != nil {
            fmt.Println("net.Listen err=", err)
            return
        }
        fmt.Println("正在监听8888端口")
        for {
            // 等待客户端连接
            conn, err := listen.Accept()
            if err != nil {
                fmt.Println("listen.Accept err=", err)
                continue
            }
            go firstProcess(conn)
        }
    }

      ./main/processMes.go,处理客户端发送过来的消息。

    package main
    
    import (
        "chatroom/common/message"
        "chatroom/common/util"
        "chatroom/server/process"
        "fmt"
        "io"
        "net"
    )
    
    // TransProcess 用户处理各种类型的客户端请求
    type TransProcess struct {
        Conn net.Conn
    }
    
    // ProcessMes 根据不同类型的客户端请求,调用不同的处理函数
    func (transProcess TransProcess) ProcessMes(mes *message.Mes) (err error) {
        data := mes.Data
        switch mes.Type {
        case message.LoginMesType:
            lp := &process.Process{
                Conn: transProcess.Conn,
            }
            // 登录
            err = lp.LoginProcess(data)
        case message.RegisterMesType:
            lp := &process.Process{
                Conn: transProcess.Conn,
            }
            // 注册
            err = lp.RegisterProcess(data)
        case message.SmsMesType:
            sp := &process.SmsProcess{}
            // 消息转发
            sp.TransmitMes(data)
        case message.SmsListMesType:
            sp := &process.SmsProcess{}
            // 获取离线消息
            sp.SendListSms(data)
        case message.SignOutMesType:
            lp := &process.Process{
                Conn: transProcess.Conn,
            }
            // 退出登录
            lp.SignOutProcess(data)
        default:
            fmt.Println("消息类型错误,没有这种消息类型")
        }
        return
    }
    
    // TransMes 等待客户端发送请求
    func (transProcess TransProcess) TransMes() {
        tf := &util.Transfer{
            Conn: transProcess.Conn,
        }
        for {
            mes, err := tf.ReadMes()
            if err != nil {
                if err == io.EOF {
                    fmt.Println("客户端退出")
                    return
                }
                if _, ok := err.(*net.OpError); ok {
                    fmt.Println("客户端退出")
                    return
                }
                fmt.Println("接受数据出错,err=", err)
                return
            }
            err = transProcess.ProcessMes(&mes)
            if err != nil {
                fmt.Println("处理消息出错,err=", err)
                return
            }
        }
    }

      ./model/error.go,自定义错误,目前只有用户相关的错误。

    package model
    
    import "errors"
    
    // 自定义错误
    var (
        USER_NOT_EXIT      = errors.New("用户不存在")
        PASSWORD_NOT_RIGHT = errors.New("账户或者密码错误")
        USER_EXIT          = errors.New("用户已经存在")
        USER_NOT_ONLINE    = errors.New("用户不在线")
    )

      ./model/smsDao.go,管理与聊天相关的数据库操作,处理的主要是离线的消息。

    package model
    
    import (
        "fmt"
        "github.com/garyburd/redigo/redis"
    )
    
    // MySmsDao 全局的smsDao,用于管理与redis连接,处理聊天类型的数据请求
    var MySmsDao *SmsDao
    
    // SmsDao 用于管理离线消息的结构体
    type SmsDao struct {
        Pool *redis.Pool
    }
    
    // NewSmsDao 初始化一个Sms连接池
    func NewSmsDao(pool *redis.Pool) (smsDao *SmsDao) {
        smsDao = &SmsDao{
            Pool: pool,
        }
        return
    }
    
    // SaveSms 当用户不在线的时候,保存消息
    func (smsDao SmsDao) SaveSms(userId int, data string) (err error) {
        conn := smsDao.Pool.Get()
        defer conn.Close()
    
        _, err = conn.Do("LPUSH", userId, data)
        if err != nil {
            fmt.Println("SaveSms conn.Do err=", err)
            return
        }
        return
    }
    
    // GetSms 用户上线后从redis中取出消息
    func (smsDao SmsDao) GetSms(userId int) []string {
        conn := smsDao.Pool.Get()
        defer conn.Close()
    
        // 将该用户的留言取出,redis中不再保存聊天信息
        var smsDatas []string
        for {
            res, err := redis.String(conn.Do("RPOP", userId))
            if err != nil {
                if err == redis.ErrNil {
                    break
                }
                fmt.Println("GetSms conn.Do err=", err)
                continue
            }
            smsDatas = append(smsDatas, res)
        }
        return smsDatas
    }

       ./model/userDao,管理用户相关的数据库操作,登录验证,注册验证,用户加载等。

    package model
    
    import (
        "chatroom/common/model"
        "encoding/json"
        "fmt"
        "github.com/garyburd/redigo/redis"
    )
    
    // MyUserDao 全局的UserDao,用于处理用户相关的数据请求
    var MyUserDao *UserDao
    
    type UserDao struct {
        Pool *redis.Pool
    }
    
    // NewUserDao 初始化
    func NewUserDao(pool *redis.Pool) (userDao *UserDao) {
        userDao = &UserDao{
            Pool: pool,
        }
        return
    }
    
    // LoadAllUser 加载所有用户的信息
    func (userDao UserDao) LoadAllUser() {
        conn := userDao.Pool.Get()
        defer conn.Close()
    
        res, err := redis.Values(conn.Do("HVals", "users"))
        if err != nil {
            fmt.Println("LoadAllUser conn.Do err=", err)
            return
        }
    
        for _, v := range res {
            var user model.User
            err = json.Unmarshal(v.([]byte), &user)
            if err != nil {
                fmt.Printf("LoadAllUser json.Unmarshal %s err=%s\n", string(v.([]byte)), err)
                continue
            }
            AllUser[user.UserId] = &user
        }
    }
    
    // GetUserById 根据用户的id获取用户信息
    func (userDao UserDao) GetUserById(conn redis.Conn, id int) (user model.User, err error) {
        res, err := redis.String(conn.Do("HGet", "users", id))
        if err != nil {
            // 如果没有找到该用户,则用户还没有注册,会抛出USER_NOT_EXIT异常
            if err == redis.ErrNil {
                err = USER_NOT_EXIT
                return
            }
            return
        }
        err = json.Unmarshal([]byte(res), &user)
        if err != nil {
            fmt.Println("userDao json.Unmarshal err=", err)
            return
        }
        return
    }
    
    // LoginVerify 验证用户的登录信息
    func (userDao UserDao) LoginVerify(userId int, userPwd string) (user model.User, err error) {
        conn := userDao.Pool.Get()
        defer conn.Close()
    
        // 通过id查找用户,出错则表示用户不存在
        user, err = userDao.GetUserById(conn, userId)
        if err != nil {
            return
        }
        // 若存在用户,则比对密码,密码一致,登录成功
        if userPwd != user.UserPwd {
            err = PASSWORD_NOT_RIGHT
            return
        }
        return
    }
    
    // RegisterVerify 注册用户
    func (userDao UserDao) RegisterVerify(user *model.User) (err error) {
        conn := userDao.Pool.Get()
        defer conn.Close()
    
        // 根据id获取用户,若用户存在,即没有发生错误,那么注册失败
        _, err = userDao.GetUserById(conn, user.UserId)
        if err == nil {
            err = USER_EXIT
            return
        }
    
        userJson, _ := json.Marshal(user)
        _, err = conn.Do("HSet", "users", user.UserId, userJson)
        if err != nil {
            fmt.Println("register conn.Do err=", err)
            return
        }
        return
    }

      ./model/userRecord.go,管理所有用户。

    package model
    
    import model2 "chatroom/common/model"
    
    // AllUser 保存所有的用户信息
    var (
        AllUser map[int]*model2.User
    )
    
    // 初始化AllUser
    func init() {
        AllUser = make(map[int]*model2.User, 1024)
    }
    
    // ChangeUserStatus 改变用户的状态,在线或离线
    func ChangeUserStatus(userId int, userStatus int) {
        user, _ := AllUser[userId]
        user.UserStatus = userStatus
    }

      ./process/smsProcess.go,处理聊天相关的消息,用户在线,直接发送,用户离线,那么将消息保持到数据库,用户登录之后,再把消息发送过去。

    package process
    
    import (
        "chatroom/common/message"
        "chatroom/common/util"
        "chatroom/server/model"
        "encoding/json"
        "fmt"
        "net"
    )
    
    // SmsProcess 处理消息相关的请求
    type SmsProcess struct {
    }
    
    // TransmitMes 转发消息
    func (smsProcess *SmsProcess) TransmitMes(data string) {
        smsMes := message.SmsMes{}
        err := json.Unmarshal([]byte(data), &smsMes)
        if err != nil {
            fmt.Println("TransmitMes json.Unmarshal err=", err)
            return
        }
        // 创建响应消息的结构体
        smsResMes := &message.SmsResMes{
            User:    smsMes.User,
            Content: smsMes.Content,
            Type:    smsMes.Type,
        }
        smsResJsonMes, err := json.Marshal(smsResMes)
        if err != nil {
            fmt.Println("TransmitMes json.Marshal err=", err)
            return
        }
        mes := message.Mes{
            Type: message.SmsResMesType,
            Data: string(smsResJsonMes),
        }
        // 根据不同的消息类型进行转发
        if smsMes.Type == message.SmsGroupType {
            // 群聊,转发给所有人
            smsProcess.SendToGroupMes(smsMes.User.UserId, &mes)
        } else {
            // 私聊,转发给指定的用户
            up, ok := UserMgrGlobal.UsersOnline[smsMes.RecUserId]
            if !ok {
                fmt.Println("该用户不在线")
                // 如果用户不在线,将消息保存到数据库中
                smsProcess.SaveOffLineSms(smsMes.RecUserId, mes.Data)
                return
            }
            smsProcess.SendToOtherMes(up.Conn, &mes)
        }
    }
    
    // SaveOffLineSms 如果用户不在线,那就把消息保存到redis中
    func (smsProcess *SmsProcess) SaveOffLineSms(userId int, data string) {
        err := model.MySmsDao.SaveSms(userId, data)
        if err != nil {
            fmt.Println("SaveOffLineSms SaveSms err=", err)
            return
        }
    }
    
    // SendToGroupMes 群发消息
    func (smsProcess *SmsProcess) SendToGroupMes(userId int, mes *message.Mes) {
        for _, v := range model.AllUser {
            // 过滤掉自己
            if v.UserId == userId {
                continue
            }
            // 根据用户状态,判断是否在线,若在线,直接发送,不在线,保存到数据库中
            if v.UserStatus == message.Online {
                up, _ := UserMgrGlobal.UsersOnline[v.UserId]
                smsProcess.SendToOtherMes(up.Conn, mes)
            } else {
                smsProcess.SaveOffLineSms(v.UserId, mes.Data)
            }
        }
    }
    
    // SendToOtherMes 发送消息的方法
    func (smsProcess *SmsProcess) SendToOtherMes(conn net.Conn, mes *message.Mes) {
        tf := &util.Transfer{
            Conn: conn,
        }
        err := tf.WriteMes(mes)
        if err != nil {
            fmt.Println("SendToOtherMes write err=", err)
            return
        }
    }
    
    // SendListSms 发送离线消息
    func (smsProcess SmsProcess) SendListSms(data string) {
        smsListMes := message.SmsListMes{}
        err := json.Unmarshal([]byte(data), &smsListMes)
        if err != nil {
            fmt.Println("SendListSms json.Unmarshal err=", err)
            return
        }
        smsList := model.MySmsDao.GetSms(smsListMes.UserId)
        smsListResMes := message.SmsListResMes{
            SmsList: smsList,
        }
        smsListResMesJson, err := json.Marshal(smsListResMes)
        if err != nil {
            fmt.Println("SendListSms json.Marshal err=", err)
            return
        }
        mes := message.Mes{
            Type: message.SmsListResMesType,
            Data: string(smsListResMesJson),
        }
        // 从在线用户列表中取出该客户端的连接,并将消息发送过去
        up, _ := UserMgrGlobal.UsersOnline[smsListMes.UserId]
        smsProcess.SendToOtherMes(up.Conn, &mes)
    }

      ./process/userMgr.go,管理在线用户,保持用户id(唯一标识)和客户端连接。

    package process
    
    import (
        "chatroom/server/model"
    )
    
    // UserMgrGlobal 创建管理用户的全局变量
    var (
        UserMgrGlobal *UserMgr
    )
    
    // UserMgr 用于管理在线用户,保存用户的id和客户端连接
    type UserMgr struct {
        UsersOnline map[int]*Process
    }
    
    //初始化
    func init() {
        UserMgrGlobal = &UserMgr{
            UsersOnline: make(map[int]*Process, 1024),
        }
    }
    
    // AddUser 增加在线用户
    func (userMgr *UserMgr) AddUser(up *Process) {
        userMgr.UsersOnline[up.UserId] = up
    }
    
    // DelUser 删除在线用户
    func (userMgr *UserMgr) DelUser(userId int) {
        delete(userMgr.UsersOnline, userId)
    }
    
    // GetAllOnlineUser 返回在线用户
    func (userMgr *UserMgr) GetAllOnlineUser() map[int]*Process {
        return userMgr.UsersOnline
    }
    
    // GetUserById 用过id获取在线用户
    func (userMgr *UserMgr) GetUserById(userId int) (up *Process, err error) {
        up, ok := userMgr.UsersOnline[userId]
        if !ok {
            err = model.USER_NOT_ONLINE
            return
        }
        return
    }

      ./process/userProcess.go,实现用户相关的消息处理,登录、注册、上线和离线的通知。

    package process
    
    import (
        "chatroom/common/message"
        model2 "chatroom/common/model"
        "chatroom/common/util"
        "chatroom/server/model"
        "encoding/json"
        "fmt"
        "net"
    )
    
    // Process 处理用户相关的请求
    type Process struct {
        Conn   net.Conn
        UserId int
    }
    
    // sendResMes 向客户端返回消息的方法
    func (process Process) sendResMes(resMes interface{}, mesType string) (err error) {
        resMesJson, err := json.Marshal(resMes)
        if err != nil {
            fmt.Println("json.Marshal err=", err)
            return
        }
    
        mes := message.Mes{
            Type: mesType,
            Data: string(resMesJson),
        }
    
        tf := util.Transfer{
            Conn: process.Conn,
        }
        err = tf.WriteMes(&mes)
        return
    }
    
    // NotifyProcess 发送用户上线及离线的通知
    func (process Process) NotifyProcess(userId int, userName string, userStatus int) {
        // 遍历在线用户列表,将新用户上线的消息通知给其他的在线用户
        for i, up := range UserMgrGlobal.UsersOnline {
            if i == userId {
                continue
            }
            var user = model2.User{
                UserId:     userId,
                UserName:   userName,
                UserStatus: userStatus,
            }
            var notifyOthersMes = message.NotifyOthersMes{
                User: user,
            }
            err := up.sendResMes(notifyOthersMes, message.NotifyOthersMesType)
            if err != nil {
                fmt.Println("NotifyProcess sendResMes err=", err)
                return
            }
        }
    }
    
    // RegisterProcess 处理注册的消息
    func (process Process) RegisterProcess(data string) (err error) {
        var registerMes message.RegisterMes
        err = json.Unmarshal([]byte(data), &registerMes)
        if err != nil {
            fmt.Println("registerProcess json.Unmarshal err=", err)
            return
        }
        user := model2.User{}
        user = registerMes.User
        // 数据库验证是否注册成功
        err = model.MyUserDao.RegisterVerify(&user)
    
        resMes := message.ResMes{}
        if err != nil {
            // 用户存在,返回401错误
            if err == model.USER_EXIT {
                resMes.Code = 401
                resMes.Error = err.Error()
            } else {
                resMes.Code = 500
                resMes.Error = "服务器未知错误"
            }
        } else {
            // 将注册的新用户添加到AllUser中
            model.AllUser[user.UserId] = &user
            resMes.Code = 200
        }
        // 返回注册的结果
        err = process.sendResMes(resMes, message.ResMesType)
        return
    }
    
    // LoginProcess 处理登录
    func (process Process) LoginProcess(data string) (err error) {
        var loginMes message.LoginMes
        err = json.Unmarshal([]byte(data), &loginMes)
        if err != nil {
            fmt.Println("json.Unmarshal err=", err)
            return
        }
        loginResMes := message.LoginResMes{}
        // 数据库验证是否登录成功
        user, err := model.MyUserDao.LoginVerify(loginMes.UserId, loginMes.UserPwd)
        if err != nil {
            if err == model.USER_NOT_EXIT { // 用户不存在
                loginResMes.ResMes.Code = 403
                loginResMes.ResMes.Error = err.Error()
            } else if err == model.PASSWORD_NOT_RIGHT { // 密码错误
                loginResMes.ResMes.Code = 400
                loginResMes.ResMes.Error = err.Error()
            } else {
                loginResMes.ResMes.Code = 500
                loginResMes.ResMes.Error = "服务器未知错误"
            }
        } else {
            fmt.Println(user, "登录了")
            process.UserId = loginMes.UserId
            loginMes.UserName = user.UserName
            // 登录成功后,更新AllUser中对应的用户状态
            model.ChangeUserStatus(loginMes.UserId, message.Online)
            // 新登录用户,添加到在线用户的map池里面
            UserMgrGlobal.AddUser(&process)
            // 将当前所有用户返回给客户端
            for _, v := range model.AllUser {
                onLineUser := &model2.User{
                    UserId:     v.UserId,
                    UserName:   v.UserName,
                    UserStatus: v.UserStatus,
                }
                loginResMes.OnlineUsers = append(loginResMes.OnlineUsers, onLineUser)
            }
            // 向其他用户广播新用户登录
            process.NotifyProcess(loginMes.UserId, loginMes.UserName, message.Online)
            loginResMes.ResMes.Code = 200
        }
        // 发送响应的消息
        err = process.sendResMes(loginResMes, message.ResMesType)
        return
    }
    
    // SignOutProcess 处理下线的消息
    func (process Process) SignOutProcess(data string) {
        var signOutMes message.SignOutMes
        err := json.Unmarshal([]byte(data), &signOutMes)
        if err != nil {
            fmt.Println("SignOutProcess json.Unmarshal err=", err)
            return
        }
        // 将该用户状态改为离线
        model.ChangeUserStatus(signOutMes.UserId, message.Offline)
        // 从在线用户中删除该用户
        UserMgrGlobal.DelUser(signOutMes.UserId)
        // 向其他用户发送该用户的下线消息
        process.NotifyProcess(signOutMes.UserId, signOutMes.UserName, message.Offline)
    }

      上面就是本项目的全部内容,比较基础,当时写代码的顺序是,注册->登录->数据库交互->用户状态->在线聊天->离线留言,本项目还有很多可以优化的地方,例如登录时只做了id和密码的验证,注册时密码的规范没有设置,重复登录的校验没有做,聊天的消息长度没有规定等等,模块的规划也可以再优化,这里只是实现比较基础的功能而已,比较适合新手。项目已经上传到github,https://github.com/liwilljinx/chatroom,感兴趣的同学可以自己玩一下。

  • 相关阅读:
    Jar包管理规范
    Base64编码原理与应用
    MySQL 5.7.14安装说明,解决服务无法启动
    idea注册
    Oracle 如何对中文字段进行排序
    SVN错误:Attempted to lock an already-locked dir
    排序算法
    设计模式
    分层
    阿里云
  • 原文地址:https://www.cnblogs.com/liwill/p/16086310.html
Copyright © 2020-2023  润新知