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), ¬ifyOthersMes) if err != nil { fmt.Println("message.NotifyOthersMes json.Unmarshal err=", err) break } // 当接收到用户登录或者下线的消息时,就更新AllUsers中该用户的状态 UpdateUserStatus(¬ifyOthersMes) // 接收聊天消息的消息类型 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 := ¬ifyOthersMes.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), ®isterMes) 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,感兴趣的同学可以自己玩一下。