学习视频来源哔哩哔哩,写这篇博客纯粹是为了复习
一、实验目的
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), ®isterResMes)
if err != nil{
fmt.Println("json.Unmarshal([]byte(mes.Data), ®isterResMes)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),¬ifyUserStatusMes)
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
再注册登录一个用户:
发送消息