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,感兴趣的同学可以自己玩一下。

posted @ 2022-04-01 12:22  一个小哥哥  阅读(379)  评论(0编辑  收藏  举报