1.需求分析
(1)用户登录
(2)用户注册
(3)用户列表
(4)群聊
(5)点对点聊天
(6)离线留言
2.功能实现
2.1用户登录和注册
2.1.1客户端
系统主界面,包含登录、注册、退出功能,登陆注册需要用户的ID和密码。调用处理层方法实现功能。
package main
import (
"fmt"
"main/QQ/client/process"
)
var userId int
var userPwd string
var userName string
func main() {
var key int
var loop bool = true
for loop {
fmt.Println("-----欢迎登陆多人聊天系统-----")
fmt.Println("\t\t1.登录")
fmt.Println("\t\t2.注册")
fmt.Println("\t\t3.退出")
fmt.Print("请输入选项:")
fmt.Scanln(&key)
switch key {
case 1:
fmt.Println("登录系统")
fmt.Print("请输入用户Id:")
fmt.Scanln(&userId)
fmt.Print("请输入密码:")
fmt.Scanln(&userPwd)
up := &process.UserProcess{}
up.Login(userId, userPwd)
case 2:
fmt.Println("注册账号")
fmt.Print("请输入用户Id:")
fmt.Scanln(&userId)
fmt.Print("请输入密码:")
fmt.Scanln(&userPwd)
fmt.Print("请输入用户名:")
fmt.Scanln(&userName)
up := &process.UserProcess{}
up.Register(userId, userPwd, userName)
case 3:
fmt.Println("退出系统")
loop = false
default:
fmt.Println("请重新输入")
}
}
}
process包中包含各种业务处理,创建userProcess包用于处理用户注册、管理;创建结构体作为接收者。用户向服务器发送连接信息需要序列化。
创建message包,里面包含各种需要序列化的结构体,Message用于在用户端和服务端传送,包含了待发送的消息类型和消息数据,其中消息数据要先转为字符串。
package message
const (
LoginMesType = "LoginMes"
LoginResMesType = "LoginResMes"
)
type Message struct {
Type string `json:"type"`
Data string `json:"data"`
}
type LoginMes struct {
UserId int `json:"userId"`
UserPwd string `json:"userPwd"`
UserName string `json:"userName"`
}
在发送时为了增加通信时的健壮性,这里采用先发送长度再发送主体数据。因为发送所需要的操作都是一样的,这里将其封装到utils包中,同理接收也先接受长度再接收数据。
其他包通过对象调取方法,在发送或接收时需要知道建立的连接以及发送的数据
type Transfer struct {
Conn net.Conn
Buf [8096]byte
}
func (this *Transfer) WritePkg(data []byte) (err error) {
//发送data 为了防止发送错误,先发送长度
var pkgLen uint32
pkgLen = uint32(len(data))
binary.BigEndian.PutUint32(this.Buf[0:4], pkgLen)
n, err := this.Conn.Write(this.Buf[0:4])
if n != 4 || err != nil {
fmt.Println("conn,write err", err)
return
}
fmt.Printf("发送长度=%d,发送内容=%v", len(data), string(data))
//传送数据
_, err = this.Conn.Write(data)
if err != nil {
fmt.Println("conn write err=", err)
return
}
return
}
func (this *Transfer) ReadPkg() (mes message.Message, err error) {
fmt.Println("读取客户端发送消息")
_, err = this.Conn.Read(this.Buf[:4])
if err != nil {
fmt.Println("conn read err1=", err)
return
}
var pkgLen uint32
pkgLen = binary.BigEndian.Uint32(this.Buf[0:4])
n, err := this.Conn.Read(this.Buf[:pkgLen])
if n != int(pkgLen) || err != nil {
//fmt.Println("conn read err3=", err)
return
}
//反序列化
err = json.Unmarshal(this.Buf[:pkgLen], &mes)
if err != nil {
fmt.Println("Server unmarshal err=", err)
}
return
}
用户登陆时,向服务器发送的消息类型为登陆类型。将id和pwd封装成message.LoginMes,序列化成json字符数组,转换为mes的data后序列化mes发送。
发送登录消息至服务器后若返回为登陆成功,则服务端应该将建立的连接维护起来 ,即创建一个结构体,其中包含了用户的属性信息以及本次连接信息
type CurUser struct {
message.User
Conn net.Conn
}
// Login 用户登录
func (this *UserProcess) Login(userId int, userPwd string) (err error) {
//连接服务器
conn, err := net.Dial("tcp", "localhost:8889")
if err != nil {
fmt.Println(err)
return
}
defer conn.Close()
//创建消息mes
var mes message.Message
mes.Type = message.LoginMesType
//创建login消息并序列化
var loginMes message.LoginMes
loginMes.UserId = userId
loginMes.UserPwd = userPwd
data, err := json.Marshal(loginMes)
if err != nil {
fmt.Println("login marshal err=", err)
return
}
//data赋值给mes
mes.Data = string(data)
//mes序列化
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("mes err=", err)
return
}
tf := &utils.Transfer{
Conn: conn,
}
tf.WritePkg(data)
// 客户端接受返回消息
resMes, err := tf.ReadPkg()
if err != nil {
fmt.Println("read err=", err)
return
}
var loginRes message.LoginResMes
err = json.Unmarshal([]byte(resMes.Data), &loginRes)
if err != nil {
fmt.Println("unmarshal err=", err)
}
//返回登陆成功
if loginRes.Code == 200 {
CurUser.Conn = conn
CurUser.UserId = userId
for _, v := range loginRes.UserIds {
if v == userId {
continue
}
fmt.Println("用户id:", v)
}
go ServerProcessMes(conn)
for {
//登陆界面
ShowMenu()
//与服务器建立连接
}
} else {
fmt.Println(loginRes.Error)
}
return
}
2.1.2 服务端
在服务端需要不断监听客户端发送信息,同时还要根据客户端发送的信息对数据库实行对应的操作。本项目采用redis数据库,在开启服务器时初始化线程池,线程池被封装到UserDao中,对数据库操作需要先通过userDao创建redis连接,服务端只需要一个userDao就可以为所有客户分配数据库连接。
var (
MyUserDao *UserDao
)
type UserDao struct {
pool *redis.Pool
}
// NewUserDao 使用工厂创建userDao实例
func NewUserDao(pool *redis.Pool) (userDao *UserDao) {
return &UserDao{
pool: pool,
}
}
var (
pool *redis.Pool
)
func InitPool(address string, maxIdle, maxActive int, idleTimeOut time.Duration) {
pool = &redis.Pool{
MaxIdle: maxIdle,
MaxActive: maxActive,
IdleTimeout: idleTimeOut,
Dial: func() (redis.Conn, error) {
return redis.Dial("tcp", address)
},
}
}
客户端在与服务端连接上后就要开启一个协程,以便主线程还可以处理其他客户请求信息,相较于客户端,服务端的连接会有多条,所以服务端的服务层在创建结构体时必须要写入连接。
func main() {
InitPool("localhost:6379", 16, 0, 300*time.Second)
initUserDao()
fmt.Println("服务器在8889端口接听")
listen, err := net.Listen("tcp", "0.0.0.0:8889")
defer listen.Close()
if err != nil {
fmt.Println("dial err=", err)
return
}
for {
time.Sleep(5 * time.Second)
fmt.Println("等待客户端连接")
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept err=", err)
}
go process(conn)
}
}
func initUserDao() {
model.MyUserDao = model.NewUserDao(pool)
}
func process(conn net.Conn) {
defer conn.Close()
processor := &Processor{
Conn: conn,
}
err := processor.process1()
if err != nil {
fmt.Println("开启协程错误")
return
}
}
type Processor struct {
Conn net.Conn
}
服务器接收消息
func (this *Processor) process1() (err error) {
for {
tf := &utils.Transfer{
Conn: this.Conn,
}
//读取客户端发送数据包
mes, err := tf.ReadPkg()
if err != nil {
if err == io.EOF {
fmt.Println("客户端退出,服务端退出")
return err
} else {
fmt.Println("readPkg err=", err)
return err
}
}
err = this.ServerProcessMes(&mes)
if err != nil {
return err
}
}
}
客户端在接收到mes信息时,需要判断信息的种类进而进行不同的操作。
func (this *Processor) ServerProcessMes(mes *message.Message) (err error) {
switch mes.Type {
case message.LoginMesType:
//用户登录
up := &process1.UserProcess{
Conn: this.Conn,
}
err = up.ServerProcessLogin(mes)
case message.RegisterMesType:
// 用户注册
up := &process1.UserProcess{
Conn: this.Conn,
}
err = up.ServiceProcessRegister(mes)
case message.SmsMesType:
sp := &process1.SmsProcess{}
sp.SendGropeMes(mes)
default:
fmt.Println("未知类型,无法处理")
}
return
}
当为登录和注册是分别在process包中调用对应的函数
func (this *UserProcess) ServerProcessLogin(mes *message.Message) (err error) {
// string类型登录信息
var loginMes message.LoginMes
err = json.Unmarshal([]byte(mes.Data), &loginMes)
if err != nil {
fmt.Println("unmarshal fail err=", err)
}
// 返回消息
var resMes message.Message
resMes.Type = message.LoginResMesType
// 返回消息数据
var loginResMes message.LoginResMes
user, err := model.MyUserDao.Login(loginMes.UserId, loginMes.UserPwd)
if err != nil {
if err == model.ERROR_USER_NOTEXISTS {
loginResMes.Code = 500
loginResMes.Error = err.Error()
} else if err == model.ERROR_USER_PWD {
loginResMes.Code = 403
loginResMes.Error = err.Error()
} else {
loginResMes.Code = 505
loginResMes.Error = "未知错误"
}
} else {
loginResMes.Code = 200
this.UserId = loginMes.UserId
userMgr.AddOnlineUser(this)
this.NotifyOthersOnlineUser(this.UserId)
for id, _ := range userMgr.onlineUsers {
loginResMes.UserIds = append(loginResMes.UserIds, id)
}
fmt.Println(user, "登录成功")
}
data, err := json.Marshal(loginResMes)
if err != nil {
fmt.Println("marshal err= ", err)
return
}
resMes.Data = string(data)
// 将resMes序列化后发送
data, err = json.Marshal(resMes)
if err != nil {
fmt.Println("marshal err=", err)
}
tf := &utils.Transfer{
Conn: this.Conn,
}
err = tf.WritePkg(data)
return
}
// ServiceProcessRegister 注册
func (up *UserProcess) ServiceProcessRegister(mes *message.Message) (err error) {
var registerMes message.RegisterMes
err = json.Unmarshal([]byte(mes.Data), ®isterMes)
if err != nil {
fmt.Println("registerMes unmarshal err=", err)
return
}
var resMes message.Message
resMes.Type = message.RegisterResMesType
var registerResMes message.RegisterResMes
err = model.MyUserDao.Register(®isterMes.User)
if err != nil {
if err == model.ERROR_USER_EXISTS {
registerResMes.Code = 505
registerResMes.Error = model.ERROR_USER_EXISTS.Error()
} else {
registerResMes.Code = 506
registerResMes.Error = "注册时未知错误"
}
} else {
registerResMes.Code = 200
}
data, err := json.Marshal(registerResMes)
if err != nil {
fmt.Println("registerResMes Marshal err=", err)
return
}
resMes.Data = string(data)
data, err = json.Marshal(resMes)
if err != nil {
fmt.Println("register resMes Marshal err=", err)
return
}
tf := &utils.Transfer{Conn: up.Conn}
err = tf.WritePkg(data)
return
}
登录和注册都会对redis数据库进行操作,前面已经初始过线程池并封装到userDao中,通过userDao获取数据库连接并对其进行增删改查操作。
func (userDao *UserDao) Login(userId int, userPwd string) (user *message.User, err error) {
conn := userDao.pool.Get()
defer conn.Close()
user, err = userDao.getUserById(conn, userId)
if err != nil {
return
}
if user.UserPwd != userPwd {
err = ERROR_USER_PWD
return
}
return
}
func (userDao *UserDao) Register(user *message.User) (err error) {
conn := userDao.pool.Get()
defer conn.Close()
_, err = userDao.getUserById(conn, user.UserId)
if err == nil {
err = ERROR_USER_EXISTS
return
}
// 操作redis完成注册
data, err := json.Marshal(user)
_, err = conn.Do("HSet", "users", user.UserId, string(data))
if err != nil {
fmt.Println("保存时注册失败")
return
}
return
}
func (userDao *UserDao) getUserById(conn redis.Conn, id int) (user *message.User, err error) {
res, err := redis.String(conn.Do("HGet", "users", id))
if err != nil {
if err == redis.ErrNil {
err = ERROR_USER_NOTEXISTS
}
return
}
user = &message.User{}
err = json.Unmarshal([]byte(res), user)
if err != nil {
fmt.Println("Unmarshal err=", err)
return
}
return
}
2.2用户列表
2.2.1服务端
在用户登陆成功后,需要能够显示已经登陆过的成员列表。在服务器端维护一个在线成员列表,结构为map[id]*UserProcess,当成员登陆时,将成员信息写入map,并将map的id写入数组当作登陆返回信息返回给用户。
var (
userMgr *UserMgr
)
type UserMgr struct {
onlineUsers map[int]*UserProcess
}
func init() {
userMgr = &UserMgr{
onlineUsers: make(map[int]*UserProcess, 1024),
}
}
func (mgr *UserMgr) AddOnlineUser(up *UserProcess) {
mgr.onlineUsers[up.UserId] = up
}
func (mgr *UserMgr) DeleteOnlineUser(userId int) {
delete(mgr.onlineUsers, userId)
}
func (mgr *UserMgr) getAllOnlineUser() map[int]*UserProcess {
return mgr.onlineUsers
}
func (mgr *UserMgr) getOnlineUserById(userId int) (up *UserProcess, err error) {
up, ok := mgr.onlineUsers[userId]
if !ok { //用户不在线
err = fmt.Errorf("用户%d不在线", userId)
return
}
return
}
type UserProcess struct {
Conn net.Conn
UserId int
}
userMgr.AddOnlineUser(this)
this.NotifyOthersOnlineUser(this.UserId)
for id, _ := range userMgr.onlineUsers {
loginResMes.UserIds = append(loginResMes.UserIds, id)
}
2.2.2客户端
客户端在本地会维护一个map记录用户的登陆状态,这样就不用每次查找时都访问服务器了,每次用户的登录信息改变时,服务器会向所有客户端发送消息更改本地信息。
// 客户端维护的map
var onlineUsers map[int]*message.User = make(map[int]*message.User, 10)
func outputOnlineUser() {
fmt.Println("显示用户列表")
for id, user := range onlineUsers {
fmt.Printf("id=%d,userName=%s\n", id, user.UserName)
}
}
// 更新用户登录信息(更新map)
func updateUserStatus(notifyUserStatusMes *message.NotifyUserStatusMes) {
user, ok := onlineUsers[notifyUserStatusMes.UserId]
if !ok {
user = &message.User{
UserId: notifyUserStatusMes.UserId,
UserStatus: notifyUserStatusMes.Status,
}
}
user.UserStatus = notifyUserStatusMes.Status
onlineUsers[user.UserId] = user
}
for _, v := range loginRes.UserIds {
if v == userId {
continue
}
user := &message.User{
UserId: v,
UserStatus: message.UserOnline,
}
onlineUsers[v] = user
}
2.3 群聊
2.3.1 客户端
菜单调用
case 2:
fmt.Println("send message")
fmt.Println("context:")
fmt.Scanln(&context)
sp.SendGropeMes(context)
通过保存的连接向服务器发送消息
func (this *SmsProcess) SendGropeMes(context string) (err error) {
var mes message.Message
mes.Type = message.SmsMesType
var smsMes message.SmsMes
smsMes.UserId = CurUser.UserId
smsMes.Content = context
data, err := json.Marshal(smsMes)
if err != nil {
fmt.Println("smsMes Marshal err=", err)
return
}
mes.Data = string(data)
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("data marshal err=", err)
return
}
tf := &utils.Transfer{Conn: CurUser.Conn}
err = tf.WritePkg(data)
if err != nil {
fmt.Println("sendGropeMes err=", err)
return
}
return
}
其他客户端接收消息
case message.SmsMesType:
outputGropeMes(&mes)
func outputGropeMes(mes *message.Message) {
var smsMes message.SmsMes
err := json.Unmarshal([]byte(mes.Data), &smsMes)
if err != nil {
fmt.Println("outputGropeMes err=", err)
return
}
info := fmt.Sprintf("%d对你说:%s", smsMes.UserId, smsMes.Content)
fmt.Println(info)
}
2.3.2 服务端
服务器端接收来自客户端的数据报
case message.SmsMesType:
sp := &process1.SmsProcess{}
sp.SendGropeMes(mes)
向其他客户端发送
type SmsProcess struct {
}
func (sp *SmsProcess) SendGropeMes(mes *message.Message) {
var smsMes message.SmsMes
err := json.Unmarshal([]byte(mes.Data), &smsMes)
if err != nil {
fmt.Println("SendGropeMes err", err)
return
}
data, err := json.Marshal(mes)
if err != nil {
fmt.Println("sendGropeMes Marshal err=", err)
return
}
for id, up := range userMgr.onlineUsers {
if id == smsMes.UserId {
continue
}
sp.SendMesToOthers(data, up.Conn)
}
}
func (sp *SmsProcess) SendMesToOthers(data []byte, conn net.Conn) {
tf := utils.Transfer{
Conn: conn,
}
err := tf.WritePkg(data)
if err != nil {
fmt.Println("sendMesToOthers err=", err)
return
}
}