基于TCP协议的 海量用户即时通讯系统(聊天室) 实验详细过程

学习视频来源哔哩哔哩,写这篇博客纯粹是为了复习

一、实验目的

	1、练习熟悉C/S架构方法及规范
	2、了解熟悉基于golang的tcp协议网络编程的方法及步骤
	3、了解联系利用redis第三方插件,实现在go语言中使用redis
	4.、练习面向对象编程
	5、练习掌握golang的goroutine和channel等的使用,加强go
	语言编程的熟练度

二 、模块及功能

1.utils

	(1)定义信息传递对象Transfer
	(2)实现从conn读消息,写消息的方法

2.common

	(1)定义message结构体,服务器与客户端信息传递协议
	(2)定义各种类型消息的结构体,消息类型存放在message的Type字段,消息数据以json格式存放在message的Data字段
	(2)定义用户结构体User
	(4)定义消息类型常量

3.client端

	(1)实现注册,登录业务
	(2)实现在线用户列表,群发消息,获取所有历史消息
	(3)实现退出业务

4.server端

	(1)实现通过redis,创建redis连接池,插入用户信息,查询用户,修改用户信息
	(2)实现注册,登录的验证,信息存储
	(3)实现短信群发

三、模块实现

1.utils

1.定义用于c/s传输消息的对象,transfer

type Transfer struct{
	Conn net.Conn
	Buf [8096]byte
}

2.给transfer绑定从conn读方法,ReadPkg
为防止粘包,采用先读4个字节的长度,再根据长度,读真正的数据

func (this *Transfer)ReadPkg()(mes message.Message, err error){
 
	fmt.Println("读取客户端发送的数据")
	//(1)先读取的时长度,并判断长度对不对
		n, err := this.Conn.Read(this.Buf[:4])
		if n != 4||err != nil{
			if err == io.EOF{
				return
			}else{
				fmt.Println("conn.Read failed ,err", err)
				return
			}
		}
		fmt.Println("读到的buf :", this.Buf[:4])
		var pkglen uint32
		pkglen = binary.BigEndian.Uint32(this.Buf[:4])
		//(2)根据pkglen读取mes
		n, err = this.Conn.Read(this.Buf[:pkglen])
		if n != int(pkglen) || err != nil{
			fmt.Println("mes read failed, err:",err)
			return
		}
		//将buf反序列化,特别注意,mes要加&
		err = json.Unmarshal(this.Buf[:pkglen], &mes)
		if err != nil{
			fmt.Println("json.Unmarshal(buf[:pkglen], mes) failed, err:",err)
			return
		}
		return
}

3.绑定发消息的方法,也需要先发消息长度,再发消息体。

func (this *Transfer)WritePkg(data []byte)(err error){
	//先发送一个长度
	var pkglen uint32
	pkglen = uint32(len(data))
	//PutUint32(buf[0:4], pkglen), 将uint32转成byte切片
	binary.BigEndian.PutUint32(this.Buf[0:4], pkglen)
	//发送长度
	n, err := this.Conn.Write(this.Buf[:4])
	if n != 4||err!=nil{
		fmt.Println("conn.Write(buf[:4])failed err :",err)
		return
	}
	//发送data本身
	n, err = this.Conn.Write(data)
	if n != int(pkglen)||err!=nil{
		fmt.Println("conn.Write(data)failed err :",err)
		return
	}
	return
}

2.common

(1)定义message结构体,服务器与客户端信息传递协议

//这个是真正要发送给服务器的消息
type Message struct{
	Type string `json:"type"`//消息类型
	Data string `json:"data"`//
}

(2)定义各种类型消息的结构体,消息类型存放在message的Type字段,消息数据以json格式存放在message的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表示登陆成功
	UserIds []int		  //保存用户id的一个切片
	Error string `json:"error"`//返回错误信息
}

type RegisterMes struct{
	//注册
	User User`json:"user"`  //就是用户结构体
}

type RegisterResMes struct{
	Code int `json:"code"`//返回状态吗400表示该用户已经注册  200表示注册成功
	Error string `json:"error"`//返回错误信息
}


//为了配合服务器端推送用户状态变化消息
type NotifyUserStatusMes struct{
	UserId int `json:"userid"`
	Status int `json:"status"`//用户的状态
}

//增加一个SmsMes//发送的消息
type SmsMes struct{
	Content string`json:"content"`
	User//匿名结构体,继承
}

(3)定义用户结构体User

//定义一个用户的结构体
type User struct{
	//为了序列化和反序列化成功,必须保证用户信息的json字符串key和结构体的字段对应的tag字段一致
	UserId int`json:"userid"`
	UserPwd string`json:"userpwd"`
	UserName string`json:"username"`
	UserStatus int`json:"userstatus"`
}

(4)定义消息类型常量

const (
	LoginMesType	=	"LoginMes"
	LoginResMesType	=	"LoginResMes"
	RegisterMesType =   "RegisterMes"
	RegisterResMesType = "RegisterResMes"
	NotifyUserStatusMesType = "NotifyUserStatusMes"
	SmsMesType = "SmsMes"
)

3.client端

1.注册–register 步骤
(1)连接服务器,conn, err := net.Dial(“tcp”, “localhost:8889”)
并defer conn.close()
(2)输入信息

	fmt.Println("输入用户的id :")
	fmt.Scanf("%d\n",&userId)
	fmt.Println("输入用户的密码 :")
	fmt.Scanf("%s\n",&userPwd)
	fmt.Println("输入用户的昵称 :")
	fmt.Scanf("%s\n",&userName)

(3)实例化message,Type字段是RegisterMesType,Data是序列化后的RegisterMes

var mes message.Message
	mes.Type = message.RegisterMesType
	var registerMes message.RegisterMes
	registerMes.User.UserId = userId
	registerMes.User.UserPwd = userPwd
	registerMes.User.UserName = userName
	data, err := json.Marshal(registerMes)
	if err != nil{
		fmt.Println("Register.json.Marshal(registerMes)failed, err :", err)
		return
	}
	mes.Data = string(data)

(3)序列号mes,创建transfer实例tf,调用writerPkg方法发送给服务器

	data, err = json.Marshal(mes)
	if err != nil{
		fmt.Println("Register.jjson.Marshal(mes)failed, err :", err)
		return
	}
	tf := &utils.Transfer{
		Conn : conn,
	}
	err = tf.WritePkg(data)
	if err != nil {
		fmt.Println("Register.WritePkg(data) failed, err :", err)
		return
	}

(4)调用tf.ReadPkg方法,得到mes,mes的Data反序列后存入新创建的 registerResMes中,验证服务器返回的处理结果

mes, err = tf.ReadPkg() // 
	if err != nil{
		fmt.Println("Register.readPkg(conn) failed",err)
		return
	}
	var registerResMes message.RegisterResMes
	err = json.Unmarshal([]byte(mes.Data), &registerResMes)
	if err != nil{
		fmt.Println("json.Unmarshal([]byte(mes.Data), &registerResMes)err,", err)
		return
	}
	if registerResMes.Code == 200{
		fmt.Println("注册成功,可以重新登陆")
	}else{
		fmt.Println(registerResMes.Error)
	}

2.登录—land步骤同register相同,只是mes的类型和数据是LoginMesType,LoginMes类型,不做解释
登录成功后,表示已经进入聊天室了,这时要开一个协程,与服务器保持通信,

go serverProcessMes(conn)

serverProcessMes函数,创建一个transfer实例tf,循环接受服务器发来的msg,根据msg的Type,执行相应的业务,如,其他用户上线,用户下线,其他用户发消息。

func serverProcessMes(conn net.Conn){
	//创建一个transfer实例,不停的读服务器发送的消息
	tf := &utils.Transfer{
		Conn : conn,
	}
	for {
		//客户端不停的读取
		fmt.Println("客户端正在等待读取服务器发送的消息")
		mes, err := tf.ReadPkg()
		if err != nil{
			fmt.Println("tf.ReadPkg()failed, err :", err)
			return
		}
		//如果读取到消息,又是下一步的处理逻辑
		//fmt.Println(mes)
		switch mes.Type{
		case message.NotifyUserStatusMesType:
			//处理
			var notifyUserStatusMes *message.NotifyUserStatusMes
			json.Unmarshal([]byte(mes.Data),&notifyUserStatusMes)
			updataUserStatus(notifyUserStatusMes)
		case message.SmsMesType :
			outputGroupMes(&mes)
		default :
			fmt.Println("服务器返回一个未知类型")
		}
	}
}

3.实现群发消息----SendGroupMsg 步骤:
(1)创建一个msg实例,Type是SmsMesType,Data是序列化后的SmsMes实例

	//1.创建一个message.Message
	var mes message.Message
	mes.Type = message.SmsMesType

	//2.创建一个SmsMes
	var smsMes message.SmsMes
	smsMes.Content = content
	smsMes.UserId = CurUser.UserId
	smsMes.UserStatus = CurUser.UserStatus

	//3.序列化
	data, err := json.Marshal(smsMes)
	if err != nil {
		fmt.Println("json.Marshal(smsMes) failed, err :", err)
		return
	}
	mes.Data = string(data)
	data, err = json.Marshal(mes)
	if err != nil {
		fmt.Println("json.Marshal(mes) failed, err :", err)
		return
	}

(2)创建tf,WritePkg

	tf := &utils.Transfer{
		Conn : CurUser.Conn,
	}
	err = tf.WritePkg(data)
	if err != nil{
		fmt.Println("tf.WritePkg(data) failed, err :", err)
		return
	}

4.server端

1.注册和登录都要连接redis,先初始化连接池,可以写一个init()函数,这里直接在主函数里写,redis的Pool有四个字段,具体在代码中

	func initPool(address string, maxIdle, maxActive int, idleTimeout time.Duration){

	pool = &redis.Pool{
		MaxIdle : maxIdle,//最大空闲连接数
		MaxActive : maxActive, //表实和数据库的最大连接数,0表示不限制
		IdleTimeout : idleTimeout,//最大空闲时间
		Dial: func()(redis.Conn, error){
		return redis.Dial("tcp", address)
		},
	}
}

2.我们用userdao把pool封装起来,用到redis就从userdao中取一个conn,并在主函数中创建一个实例

type UserDao struct{
	pool *redis.Pool
}

//使用工厂模式,创建一个userdao实例
//连接池必须在程序开始时就创建好了
func NewUserDao(pool *redis.Pool)(userDao *UserDao){
	userDao = &UserDao{
		pool : pool,
	}
	return
}
func initUserDao(){
	model.MyUserDao = model.NewUserDao(pool)
}

3.这里需要注意一个初始化的顺序问题,先initPool,在initUserDao做好前两个准备之后,写服务器监听函数

	listen, err := net.Listen("tcp", "127.0.0.1:8889")
	defer listen.Close()
	if err !=nil{
		fmt.Println("net.Listen failed, err :", err)
		return
	}

4.循环等待客户端连接,并启动协程与客户端保持通讯

//等待连接
	for {
		fmt.Println("等待用户连接服务器")
		conn, err := listen.Accept()
		if err != nil{
			fmt.Println("listen.Accept() failed, err :", err)
			return
		}
		//一旦连接成功, 启动一个协程与客户端保持通讯
		go process(conn)
	}

5.第四步的process,初始化一个Processor,其中封装了一个conn,有方法serverProcessMes,根据msg的Type字段,处理登录,注册,群发的方法。processor这是真正处理数据的接口,还有一个process2方法,从连接中得到msg,交给serverprocessmes处理,并通过err,检测客户端是否正常退出

func process(conn net.Conn){
	//延时关闭
	defer conn.Close()
	//循环读客户端发送的信息
	//调用总控
	processor := &Processor{
		Conn : conn,
	}
	err := processor.process2()
	if err != nil{
		fmt.Println("客户端和服务器端通信协程错误, err :", err)
		return
	}
}
//编写一个serverProcessMes 函数
//功能: 根据客户端发送消息种类的不同,决定调用那个函数来处理
func (this *Processor)serverProcessMes(mes *message.Message)(err error){
	switch mes.Type{
	case message.LoginMesType:
		//处理登陆
		//创建一个UserProcess实例
		up := &processes.UserProcess{
			Conn : this.Conn,
		}
		err = up.ServerProcessLogin(mes)
	case message.RegisterMesType:
		//处理注册
		up := &processes.UserProcess{
			Conn : this.Conn,
		}
		err = up.ServerProcessRegister(mes)
	case message.SmsMesType:
		smsProcess := &processes.SmsProcess{}
		smsProcess.SendGroupMes(mes)
	default :
		fmt.Println("消息类型不存在,无法处理。。。")
	}
	return
}
func (this *Processor)process2()(err error){
	for {
		//这里封装了readpack函数,用于接收数据包mes
		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 fail, err", err)
				return err
			}
		}
		fmt.Println("mes", mes)

		err = this.serverProcessMes(&mes)
		if err != nil{
			fmt.Println(err)
			return err
		}
	}
}

6.处理登录业务
给 userdao绑定方法Login,得到redis的连接,根据输入信息 调用getUserByID,入redis查找用户,验证用户密码

func(this *UserDao)Login(userId int, userPwd string)(user *User, err error){
	//从userdao的连接池取出一个连接
	conn := this.pool.Get()
	defer conn.Close()
	user, err = this.getUserByID(conn, userId)
	if err != nil{
		return
	}
	//Id存在了,则密码是否存在
	if user.UserPwd != userPwd{
		err = ERROR_USER_PWD
		return
	}
	return
}
func (this *UserDao)getUserByID(conn redis.Conn, id int)(user *User, err error){

	//通过给定id,去redis查询这个用户
	res, err := redis.String(conn.Do("hget", "user",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("json.Unmarshal failed, err:", err)
		return
	}
	return
}

7.处理注册业务,与登录的业务类似,先获取redis连接,调用getuserbyid查看是否注册过,没注册过就写入redis

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_EXISTED
		return
	}
	//Id还没有注册过
	data, err := json.Marshal(user)
	if err != nil{
		return
	}
	//入库
	_, err = conn.Do("hset", "user", user.UserId, string(data))
	if err != nil{
		fmt.Println("入库错误 err :", err)
		return
	}
	return
}

8.处理群发消息的业务
创建结构体SmsProcess,实现方法,将msg序列化,然后遍历所有在线用户,调用SendMesToEachOnlineUser,给每个用户发送消息。SendMesToEachOnlineUser就是简单的根据连接发送json

func (this *SmsProcess)SendGroupMes(mes *message.Message){

	var smsMes message.SmsMes
	err := json.Unmarshal([]byte(mes.Data), &smsMes)
	if err != nil{
		fmt.Println("json.Unmarshal([]byte(mes.Data), &smsMes)failed", err)
		return
	}
	//将mes重新序列化,发送
	data, err := json.Marshal(mes)
	if err != nil {
		fmt.Println("json.Marshal(mes) failed, err :", err)
		return
	}
	//遍历服务器端的onlineUSers的map
	//将消息转发出去
	for id, up := range userMgr.onlineUsers{
		if id == smsMes.UserId{
			continue
		}
		this.SendMesToEachOnlineUser(data, up.Conn)
	}
}
func (this *SmsProcess)SendMesToEachOnlineUser(data []byte, conn net.Conn){
	//创建一个transfer,发送data
	tf := &utils.Transfer{
		Conn : conn,
	}
	err := tf.WritePkg(data)
	if err != nil {
		fmt.Println("转发消息失败",err)
		return
	}
}

四、测试

1.启动客户端,和服务器
启动客户的短
在这里插入图片描述
2.测试注册登录
client1

在这里插入图片描述

server
在这里插入图片描述
client1
在这里插入图片描述
server在这里插入图片描述
再注册登录一个用户:
在这里插入图片描述
发送消息
在这里插入图片描述
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_44477844/article/details/107869462