聊天通讯系统
需求分析
- 用户注册
- 用户登陆
- 显示在线用户列表
- 群聊
界面设计
rooms/client/main.go实现,这同样也是main.go文件的全部内容
根据接收用户的选择,是登录、注册,还是退出,登录和注册都需要按要求给出相应的用户信息
var userID int
var userPwd string
var userName string
func main() {
//接收选择
var key int
for true {
fmt.Println("---------欢迎登陆---------")
fmt.Println("\t 1 登陆")
fmt.Println("\t 2 注册")
fmt.Println("\t 3 退出")
fmt.Println("\t 请选择")
fmt.Scanf("%d\n", &key)
switch key {
case 1:
fmt.Println("登陆聊天室")
fmt.Println("请输入用户id")
fmt.Scanf("%d\n", &userID)
fmt.Println("请输入用户密码")
fmt.Scanf("%s\n", &userPwd)
//完成登陆
//创建实例
up := &process.UserProcess{}
up.Login(userID,userPwd)
case 2:
fmt.Println("注册用户")
fmt.Println("请输入用户id")
fmt.Scanf("%d\n",&userID)
fmt.Println("请输入用户密码")
fmt.Scanf("%s\n",&userPwd)
fmt.Println("请输入用户名")
fmt.Scanf("%s\n",&userName)
up:=&process.UserProcess{}
up.Register(userID,userPwd,userName)
case 3:
fmt.Println("退出系统")
default:
fmt.Println("输入有误,请重新输入")
}
}
userProcess.go完成登录和注册的定义
在userProcess.go,可以处理用户的登录,首先与服务器连接,将传来的用户名和密码进行赋值,将数据进行序列化操作,因为数据是要发送的消息,防止丢包,还要限制序列的长度,当登录的状态码=200时,登录则成功。在登陆中,还会对于在线用户进行显示,也就是遍历目前的用户列表,除去自己,都输入。然后登陆成功之后又显示一个登陆后的界面
func(this *UserProcess) Login(userId int,userPwd string) (err error) {
/*fmt.Printf("userID=%d, userPwd=%s",userID,userPwd)
return nil*/
conn,err:=net.Dial("tcp","localhost:8889")
if err!=nil{
fmt.Println("Dial err",err)
return
}
//延时关闭
defer conn.Close()
//通过conn发送消息给服务
var mes message.Message
mes.Type=message.LoginResMesType
// 创建结构体
var loginMes message.LoginMes
loginMes.UserID=userId
loginMes.UserPwd=userPwd
//loginMes 序列化
data,err:=json.Marshal(loginMes)
if err!=nil{
fmt.Println("json err",err)
return
}
//把data给mes.data字段
mes.Data=string(data)
//将Mse序列化
data,err=json.Marshal(mes)
if err!=nil{
fmt.Println("json err",err)
return
}
//data 就是要发送的消息
//先发送data的长度 防止丢包
var pkgLen uint32
pkgLen = uint32(len(data))
var buf [4]byte
binary.BigEndian.PutUint32(buf[0:4],pkgLen)
n,err :=conn.Write(buf[:4])
if n!=4 || err!=nil{
fmt.Println("读取(buf)失败 err",err)
return
}
//fmt.Printf("客户端发送消息长度=%d,内容是%s",len(data),data)
_,err =conn.Write(data)
if err!=nil{
fmt.Println("读取(data)失败 err",err)
return
}
//返回内容
//创建Transfer实例
tf:=&utils.Transfer{
Conn:conn,
}
mes,err= tf.ReadPkg()
if err!=nil{
fmt.Println("readPkg err",err)
return
}
//将mes data部分反序列化成loginResMes
var loginResMes message.LoginResMes
err = json.Unmarshal([]byte(mes.Data),&loginResMes)
if loginResMes.Code==200{
//fmt.Println("登陆成功")
//显示登陆成功菜单
//再起协程保持和服务器端的通讯
//接收并显示在客户端的终端
//初始化
CurUser.Conn = conn
CurUser.UserId=userId
CurUser.UserStatus=message.UserOnline//显示在线
//显示当前用户列表
fmt.Println("当前在线用户列表如下:")
for _,v:=range loginResMes.UsersId {
if v==userId{
continue
}
fmt.Printf("用户id:%d\t",v)
//完成初始化
user:=&message.User{
UserId:v,
UserStatus: message.UserOnline,
}
onlineUsers[v]=user
}
go serverProcessMes(conn)
for {
ShowMenu()
}
}else {
fmt.Println(loginResMes.Error)
}
return
}
在同文件下还有同样一个处理注册的,几乎直接是把处理登陆相关的给复制过来,其中只是重新创建了一个注册的结构体,再以相同的方式进行序列化。
func (this *UserProcess) Register(userId int,
userPwd string, userName string) (err error) {
//1. 链接到服务器
conn, err := net.Dial("tcp", "localhost:8889")
if err != nil {
fmt.Println("net.Dial err=", err)
return
}
//延时关闭
defer conn.Close()
//2. 准备通过conn发送消息给服务
var mes message.Message
mes.Type = message.RegisterMesType
//3. 创建一个注册 结构体
var registerMes message.RegisterMes
registerMes.User.UserId = userId
registerMes.User.UserPwd = userPwd
registerMes.User.UserName = userName
//4.将registerMes 序列化
data, err := json.Marshal(registerMes)
if err != nil {
fmt.Println("json.Marshal err=", err)
return
}
// 5. 把data赋给 mes.Data字段
mes.Data = string(data)
// 6. 将 mes进行序列化化
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("json.Marshal err=", err)
return
}
//创建一个Transfer 实例
tf := &utils.Transfer{
Conn : conn,
}
//发送data给服务器端
err = tf.WritePkg(data)
if err != nil {
fmt.Println("注册发送信息错误 err=", err)
}
mes, err = tf.ReadPkg() // mes 就是 RegisterResMes
if err != nil {
fmt.Println("readPkg(conn) err=", err)
return
}
//将mes的Data部分反序列化成 RegisterResMes
var registerResMes message.RegisterResMes
err = json.Unmarshal([]byte(mes.Data), ®isterResMes)
if registerResMes.Code == 200 {
fmt.Println("注册成功, 请重新登录")
os.Exit(0)
} else {
fmt.Println(registerResMes.Error)
os.Exit(0)
}
return
}
userMgr.go文件用于输出当前在线用户的列表和更新状态。
var onlineUsers map[int]*message.User = make(map[int]*message.User,10)
var CurUser model.CurUser
func outputOnlineUser(){
fmt.Println("当前在线用户列表")
for id,_:=range onlineUsers{
fmt.Println("用户Id\t",id)
}
}
//处理返回NotifyUserStatusMes
func updatauserStatus(notifyUserStatusMes *message.NotifyUserStatusMes){
user,ok:=onlineUsers[notifyUserStatusMes.UserId]
if !ok{
//没有 才去创建
user=&message.User{
UserId:notifyUserStatusMes.UserId,
//UserStatus: notifyUserStatusMes.Status,
}
}
user.UserStatus=notifyUserStatusMes.Status
onlineUsers[notifyUserStatusMes.UserId]=user
outputOnlineUser()
}
smsProcess.go这里定义函数用于发送群聊消息,将内容传入函数中,再定义相应的类型,并对发出消息的用户ID和用户状态记录下来,在对要发送的数据进行序列化。
func (this *SmsProcess) SendGroupMes(content string) (err error){
//创建Mes
var mes message.Message
mes.Type=message.SmsMesType
var smsMes message.SmsMes
smsMes.Content=content
smsMes.UserId=CurUser.UserId
smsMes.UserStatus=CurUser.UserStatus
//序列化
data,err:=json.Marshal(smsMes)
if err!=nil{
fmt.Println("群聊消息序列化失败",err.Error())
return
}
mes.Data=string(data)
data,err = json.Marshal(mes)
if err!=nil {
fmt.Println("群聊消息序列化失败", err.Error())
return
}
tf:=&utils.Transfer{
Conn: CurUser.Conn,
}
err = tf.WritePkg(data)
if err!=nil{
fmt.Println("群发失败",err.Error())
return
}
return
群聊消息发送了则需要一个函数去输出群聊的消息,这里定义的函数就是用于将接收到的数据序列化之后进行输出,并输出相应用户的id
func outputGroupMes(mes *message.Message){
//smsMes类型
var smsMes message.SmsMes
err:=json.Unmarshal([]byte(mes.Data),&smsMes)
if err!=nil{
fmt.Println("反序列化失败 ",err)
return
}
info:=fmt.Sprintf("用户id %d 发送消息 %s",smsMes.UserId,smsMes.Content)
fmt.Println(info)
fmt.Println()
}
上述代码为实现客户端部分
下面的代码是实现对于读取和写入数据进行函数封装,并为了防止丢包,限制了大小,再对于数据进行序列化和反序列化的操作。
type Transfer struct {
Conn net.Conn
Buf[8096]byte //传输时 使用缓冲
}
func(this *Transfer) ReadPkg() (mes message.Message,err error) {
//buf := make([]byte, 8096)
fmt.Println("读取客户端发送的数据")
_, err = this.Conn.Read(this.Buf[:4])
if err != nil {
//fmt.Println("读取错误 err", err)
//err = errors.New("read pkg header error")
return
}
var pkgLen uint32
pkgLen=binary.BigEndian.Uint32(this.Buf[0:4])
//从 conn读 扔到buf
n, err := this.Conn.Read(this.Buf[:pkgLen])
if n!=int(pkgLen)||err != nil {
fmt.Println("read pkg body error", err)
return
}
err = json.Unmarshal(this.Buf[:pkgLen], &mes)
if err != nil {
fmt.Println("反序列化出错",err)
return
}
return
}
func (this *Transfer) WritePkg(data[]byte)(err error){
//先发送长度
var pkgLen uint32
pkgLen = uint32(len(data))
//var buf [4]byte
binary.BigEndian.PutUint32(this.Buf[0:4],pkgLen)
n,err :=this.Conn.Write(this.Buf[:4])
if n!=4 || err!=nil{
fmt.Println("读取(buf)失败 err",err)
return
}
//发送数据本身
n,err =this.Conn.Write(data)
if n!=int(pkgLen) || err!=nil{
fmt.Println("读取(buf)失败 err",err)
return
}
return
}
初次之外,还定义了服务器端和客户机端都共用的,当然上述客户端也用到了其中的一些函数和方法。
定义的是message包下,用于区别不用的信息,比如说登录信息、注册信息,用户状态、发送的消息等,它们都是一个结构体,比如登录信息就要包括登录的ID、密码和登录名,而对应这个登录信息的也又另外一个结构体去实现它的状态码,并对于一些不正确状态码去返回错误信息。
const (
LoginMseType= "LoginMes"
LoginResMesType="LoginResMes"
RegisterMesType = "RegisterMes"
RegisterResMesType="RegisterResMes"
NotifyUserStatusMesType = "NotifyUserStatusMes"
SmsMesType = "SmsMes"
)
const(
UserOnline = iota
UserOfline
UserBusyStatus
)
type User struct {
UserId int `json:"userId"`
UserPwd string `json:"userPwd"`
UserName string `json:"userName"`
UserStatus int `json:"userStatus"`
}
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"`
}
type LoginResMes struct {
Code int`json:"code"`//状态码 500 未注册 200登陆成功
Error string`json:"error"` //返回错误
UsersId []int //保存用户ID
}
type RegisterMes struct {
User User`json:"user"`
}
type RegisterResMes struct {
Code int `json:"code"`
Error string `json:"error"`
}
//用户状态
type NotifyUserStatusMes struct {
UserId int `json:"userId"`
Status int `json:"status"`
}
//发送的消息
type SmsMes struct {
Content string `json:"content"` //内容
User
}
下面开始服务器端的代码部分,服务器端main文件主要是处理与客户端通讯,实现与客户端等待客户端连接,连接成功就保持通讯状态,和对redis相连接,再定义redis的连接池,到时候需要时直接调用函数去取出相应的数据即可,并开启协程用于通讯。
func process(conn net.Conn){
defer conn.Close()
//调用总控函数
processor:= &Processor{
Conn:conn,
}
err:=processor.process2()
if err!=nil{
fmt.Println("客户端和服务器端通信协程错误",err)
return
}
func init() {
initPool("localhost:6379",16,0,300*time.Second)
initUserDao()
}
//编写函数完成 UserDao的初始化
func initUserDao(){
model.MyUserDao=model.NewUserDao(pool)
}
func main() {
//服务器启动时, 初始化连接池
fmt.Println("服务器在端口监听")
listen,err:=net.Listen("tcp","0.0.0.0:8889")
defer listen.Close()
if err!=nil{
fmt.Println("监听出错 err",err)
return
}
//监听成功则等待连接服务器
for {
fmt.Println("等待客户端连接")
conn,err:=listen.Accept()
if err!=nil{
fmt.Println("连接出错 err",err)
}
//启动协程 和客户端保持通讯
go process(conn)
}
}
下面是定义连接池的初始化,对它一些属性进行定义。
var pool *redis.Pool
func initPool(addredss 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",addredss)
},
}
}
在服务器端,会根据传来消息类型来进行相应不同的处理,比如是登录消息类型,就实例化进行登录相关的处理,如果是注册那就执行注册。
//创建processor的结构体
type Processor struct {
Conn net.Conn
}
//根据客户端发送消息钟类不同,决定调用那个函数来处理
func (this *Processor)severProcessMes(mes *message.Message) (err error){
switch mes.Type {
case message.LoginResMesType:
//处理登陆
//创建实例
up:=&process2.UserProcess{
Conn:this.Conn,
}
err =up.SeverProcessLogin(mes)
case message.RegisterMesType:
//处理注册
up:=&process2.UserProcess{
Conn:this.Conn,
}
err =up.SeverProcessRegister(mes)
case message.SmsMesType:
//完成转发消息
smsProcess:=&process2.SmsProcess{}
smsProcess.SendGroupMes(mes)
default:
fmt.Println("消息类型不存在,无法处理")
}
return
}
func (this *Processor) process2() (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
}
}
//fmt.Println("mes=",mes)
err =this.severProcessMes(&mes)
if err!=nil{
return err
}
}
}
对于已经定义好的连接池,需要对登录注册和连接池同步,先创建实例,在去通过id去在连接池中查找这个人,如果没有提示请注册的错误信息,如果又的化则验证密码是否正确,注册的话也要从连接池中拿出看之前有没有注册过。
var (
MyUserDao *UserDao
)
type UserDao struct {
pool *redis.Pool
}
//创建UserDao实例
func NewUserDao(pool *redis.Pool)(userDao *UserDao){
userDao=&UserDao{
pool: pool,
}
return
}
func (this *UserDao) getUserById(conn redis.Conn,id int) (user *User,err error){
//通过id去redis去查询用户
res,err:=redis.String(conn.Do("HGet","users",id))
if err!=nil{
if err==redis.ErrNil{
//表示在user 哈希中没有找到对应的ID
err = ERROR_USER_NOTEXISTS
}
return
}
user = &User{}
//需要把 res发序列化成User实例
err = json.Unmarshal([]byte(res),user)
if err!=nil{
fmt.Println("反序列化错误 err",err)
return
}
return
}
//完成登陆校验
func (this *UserDao)Login(userId int,usePwd string)(user *User,err error) {
//先从UserDao 连接池中取出连接
conn:=this.pool.Get()
defer conn.Close()
user,err= this.getUserById(conn,userId)
if err!=nil{
return
}
//获取到用户
//密码不同则错误
if user.UserPwd!=usePwd{
err=ERROR_USER_PWD
return
}
return
}
//注册
func (this *UserDao)Register(user *message.User)(err error) {
//先从UserDao 连接池中取出连接
conn:=this.pool.Get()
defer conn.Close()
_,err= this.getUserById(conn,user.UserId)
if err!=nil{
err = ERROR_USER_EXISTS
return
}
//ID在redis还没有注册 则可以完成注册
//序列化
data,err:=json.Marshal(user)
if err!=nil{
return
}
//入库
_,err=conn.Do("Hset","users",user.UserId,string(data))
if err!=nil {
fmt.Println("保存注册用户错误 err", err)
return
}
return
}
对于错误信息都定义在这里。
var (
ERROR_USER_NOTEXISTS = errors.New("用户不存在")
ERROR_USER_EXISTS = errors.New("用户已存在")
ERROR_USER_PWD = errors.New("密码不正确")
)
之后就是服务器端处理客户端发送来的登录和注册的命令。登录根据传来的处理信息类型,将数据如用户id和密码做处理,并对不同的错误类型赋予不同的状态码值,正确的才是200,对于成功登录的用于信息,作于相应的记录,也就是广播通知其他在线该用户已上线。
func (this *UserProcess) SeverProcessLogin(mes *message.Message) (err error){
//从mes中取出data,并直接反序列化成LoginMes
var loginMes message.LoginMes
err = json.Unmarshal([]byte(mes.Data),&loginMes)
if err!=nil{
fmt.Println("反序列化失败 err",err)
return
}
//先声明resMes
var resMes message.Message
resMes.Type = message.LoginResMesType
//再声明LoginResMes
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(loginMes.UserID)
//遍历在线用户
for id,_:=range userMgr.onlineUsers{
loginResMes.UsersId=append(loginResMes.UsersId,id)
}
fmt.Println(user,"登录成功")
//将loginResMes序列化
data,err:=json.Marshal(loginResMes)
if err!=nil{
fmt.Println("loginResMes 序列化失败",err)
return
}
//将data赋值给resMes
resMes.Data=string(data)
//再对它序列化 因为是结构体
data,err=json.Marshal(resMes)
if err!=nil{
fmt.Println("loginResMes 序列化失败",err)
return
}
//fmt.Printf("客户端发送的消息 长度为%d 内容为%s",len(data),string(data))
//发送data
//先创建transfer实例,然后读取
tf:=&utils.Transfer{
Conn:this.Conn,
}
err=tf.WritePkg(data)
return
}
}
处理注册也与登录差不多,甚至还要简单一点,它只用序列化反序列化等,并提示用户出错的消息和出错类型。
func (this *UserProcess)SeverProcessRegister(mes *message.Message)(err error){
var registerMes message.RegisterMes
err=json.Unmarshal([]byte(mes.Data),®isterMes)
if err!=nil{
fmt.Println("序列化出错 err",err)
return
}
var resMes message.Message
resMes.Type=message.RegisterMesType
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("json.Marshal fail", err)
return
}
//将data 赋值给 resMes
resMes.Data = string(data)
//对resMes 进行序列化,准备发送
data, err = json.Marshal(resMes)
if err != nil {
fmt.Println("json.Marshal fa il", err)
return
}
//6. 发送data, 我们将其封装到writePkg函数
tf := &utils.Transfer{
Conn : this.Conn,
}
err = tf.WritePkg(data)
return
}
再有两个函数,一个是用于验证在线的用户,根据传来的信息类型,还是将数据做序列化反序列化等操作,第二是通知在线的用户,遍历当前在线用户就可以了
//通知在线用户
func (this *UserProcess)NotifyOthersOnlineUser(userId int){
//遍历
for id,up:=range userMgr.onlineUsers{
if id==userId{
continue//除去自己不通知
}
up.NotifyMeonline(userId)
}
}
func (this *UserProcess)NotifyMeonline(userId int) {
var mes message.Message
mes.Type=message.NotifyUserStatusMesType
var notifyUserStatusMes message.NotifyUserStatusMes
notifyUserStatusMes.UserId=userId
notifyUserStatusMes.Status=message.UserOnline
data,err:=json.Marshal(notifyUserStatusMes)
if err!=nil{
fmt.Println("序列化错误 err",err)
return
}
mes.Data=string(data)
data,err=json.Marshal(mes)
if err!=nil{
fmt.Println("序列化错误 err",err)
return
}
tf:=&utils.Transfer{
Conn:this.Conn,
}
err = tf.WritePkg(data)
if err!=nil{
fmt.Println("NotifyMeOnline err",err)
return
}
}
转发消息也定义了两个函数去实现转发的效果。首先还是通过将消息序列化,再遍历,除去自己都进行发送操作。
func (this *SmsProcess) SendGroupMes(mes *message.Message) {
//遍历服务器端map将消息转发
var smsMes message.SmsMes
err:=json.Unmarshal([]byte(mes.Data),&smsMes)
if err!=nil{
fmt.Println("反序列化失败 ",err)
}
data,err :=json.Marshal(mes)
if err!=nil{
fmt.Println("序列化出错",err)
return
}
for id,up:=range userMgr.onlineUsers{
//过滤自己 不给自己发送消息
if id ==smsMes.UserId{
continue
}
this.SendEachOnlineUser(data,up.Conn)
}
}
func (this *SmsProcess)SendEachOnlineUser(data []byte,conn net.Conn) {
//创建TransFer
tf:=&utils.Transfer{
Conn: conn,
}
err:=tf.WritePkg(data)
if err!=nil{
fmt.Println("转发消息失败 ",err)
}
}