go语言海量用户即时聊天系统

go语言海量用户即时聊天系统,第1张

项目整体框架图

需求分析得到的功能

1、用户注册

2、用户登陆

3、显示在线用户列表

4、群聊

5、点对点聊天

6、离线留言

这里选择redis数据库

代码实现

client/login.go

package main
import (
	"fmt"
	"net"
	"encoding/binary"
	"encoding/json"
	"chatroom/common/message"
)

// 写一个函数,完成登陆
func login(userId int, userPwd string) (err error) {

	// 下一步就要开始定协议。。。
	// fmt.Printf(" userID=%d userPwd=%s", userID, userPwd)
	// return nil

	//1、连接到feu武器
	conn, err := net.Dial("tcp", "localhost:8899")
	if err != nil {
		fmt.Println("net.Dial err", err)
		return
	}
	//延时关闭
	defer conn.Close()

	// 2、准备通过conn发送消息给服务器
	var mes message.Message
	mes.Type = message.LoginMesType

	// 3、创建一个LoginMes
	var loginMes message.LoginMes
	loginMes.UserId = userId
	loginMes.UserPwd = userPwd

	// 4、将loginMes序列化
	data, err := json.Marshal(loginMes)
	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
	}
	//7、此时data就是我们要发送的消息
	// 7.1 先把data的长度发生给服务器
	// 先获取到data的长度->转换成一个表示长度的byte切片
	 var pkgLen uint32
	 pkgLen = uint32(len(data))
	 var buf [4]byte
	 binary.BigEndian.PutUint32(buf[0:4], pkgLen)
	// 发送长度
	n, err := conn.Write(buf[0:4])
	if n != 4 || err != nil {
		fmt.Println("conn.Write fail", err)
		return
	}
	// fmt.Printf("客户端,发送消息长度成功=%d 内容是=%s", len(data), string(data))
	// 发送消息本身
	_, err = conn.Write(data)
	if  err != nil {
		fmt.Println("conn.Write(data) fail", err)
		return
	}
	//休眠20
	// time.Sleep(20 * time.Second)
	// fmt.Println("休眠了20秒")
	// 这里还需要处理服务器端的消息
	mes, err = readPkg(conn)  //mes就是
	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("用户登陆成功")
	} else if loginResMes.Code == 500 {
		fmt.Println(loginResMes.Error)
	}

	return
}

client/main.go

package main
import (
	"fmt"
	"os"
)

// 定义两个变量,一个表示用户的ID, 一个表示用户密码
var userId int
var userPwd string

func main() {
	// 接收用户选择
		var key int
	// 判断是否还继续显示菜单
		var loop = true
	for loop {
		fmt.Println("-----------欢迎登陆多人聊天系统----------")
		fmt.Println("\t\t\t 1 登陆聊天室")
		fmt.Println("\t\t\t 2 注册用户")
		fmt.Println("\t\t\t 3 退出系统")
		fmt.Println("\t\t\t 请选择(1-3))")

		fmt.Scanf("%d\n", &key)
		switch key {
			case 1 :
				fmt.Println("登陆聊天室")
				loop = false
			case 2 :	
				fmt.Println("注册用户")
				loop = false
			case 3 :	
				fmt.Println("退出系统")
				os.Exit(0)
			default :
				fmt.Println("你的输入有误,请重新输入")
		}
	}
	// 根据用户的输入,显示新的提示信息
	if key ==1 {
		// 说明用户要登陆
		fmt.Println("请输入用户ID:")
		fmt.Scanf("%d\n", &userId)
		fmt.Println("请输入用户密码:")
		fmt.Scanf("%s\n", &userPwd)
		// 先把登陆的函数,写到另一个文件
		err := login(userId, userPwd)
		if err != nil {
			fmt.Println("登陆失败")
		} else {
			fmt.Println("登陆成功")
		}
	} else if key ==2 {
		fmt.Println("进行用户注册的逻辑。。。。")
	}
}

common/message/message.go

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"`//用户Id
	 UserPwd string `json:"userPwd"`//用户密码
	 UserName string `json:"userName"`//用户名
 }

 type LoginResMes struct {
	 Code int `json:"code"`//返回状态码 500表示该用户未注册  200 用户登陆成功
	 Error string `json:"error"`//返回错误信息
 }

server/main.go

package main
import (
	"fmt"
	"net"
	"encoding/binary"
	"encoding/json"
	"chatroom/common/message"
	"io"
)
func readPkg(conn net.Conn) (mes message.Message, err error) {
	buf := make([]byte, 8096)
		fmt.Println("读取客户端发送的数据~~~")
		//conn.Read 在conn没有被关闭的情况下,才会阻塞
		// 如果客户端关闭了conn,则就不会阻塞了
		_, err = conn.Read(buf[:4])
		if err != nil {
			return
		}
		//根据读到的长度buf[:4] 转成一个uint32类型
		var pkgLen uint32
		pkgLen = binary.BigEndian.Uint32(buf[0:4])
		// 根据pkgLen读取内容
		n, err := conn.Read(buf[:pkgLen])
		if n != int(pkgLen) || err != nil {
			return
		}
		// 把pkgLen反序列化成 -> message.Message
		err = json.Unmarshal(buf[:pkgLen], &mes) 
		if err != nil {
			fmt.Println("json.Unmarshal fail err=", err)
			return
		}
	return
}

func writePkg(conn net.Conn, data []byte) (err error){
	// 先发送一个长度给对方
	var pkgLen uint32
	pkgLen = uint32(len(data))
	var buf [4]byte
	binary.BigEndian.PutUint32(buf[0:4], pkgLen)
	// 发送长度
	n, err := conn.Write(buf[0:4])
	if n != 4 || err != nil {
		fmt.Println("conn.Write fail", err)
		return
	}
	// 发送data本身
	n, err = conn.Write(data)
	if n != int(pkgLen) || err != nil {
		fmt.Println("conn.Write fail", err)
		return
	}
	return
}

// 编写一个函数serverProcessLogin函数,专门处理登陆请求
func serverProcessLogin(conn net.Conn, mes *message.Message) (err error){
	// 核心代码。。。
	// 1、先从mes中取出mes.Data,并直接反序列化成LoginMes
	var loginMes message.LoginMes
	err = json.Unmarshal([]byte(mes.Data), &loginMes)
	if err != nil {
		fmt.Println("json.Unmashal fail err=", err)
		return
	}
	// ① 先声明一个resMes
	var resMes message.Message 
	resMes.Type = message.LoginResMesType

	// ② 再声明一个 loginResMes,并完成赋值
	var loginResMes message.LoginResMes
	//如果用户的id为100,密码为123456,认为合法, 否则 不合法
	if loginMes.UserId == 100 && loginMes.UserPwd == "123456" {
		// 合法
		loginResMes.Code = 200
	} else {
		//不合法
		loginResMes.Code = 500 //500状态码,表示该用户不存在
		loginResMes.Error = "该用户不存在,请注册后再使用"
	}

	// ③ 将 loginResMes 序列化
	data, err := json.Marshal(loginResMes)
	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 fail", err)
		return
	}

	// ⑥ 发送data,将其封装到writePkg函数
	err = writePkg(conn, data)
	return
}

// 编写一个ServerProcessMes 函数
// 功能:根据客户端发送消息种类的不同,决定调用哪个函数来处理
func serverProcessMes(conn net.Conn, mes *message.Message) (err error){
	switch mes.Type {
		case message.LoginMesType : 
			// 处理登陆
			err = serverProcessLogin(conn, mes)
		case message.RegisterMesType :
			// 处理注册
		default :
			fmt.Println("消息类型不存在,无法处理。。。。")
	}
	return
}

//处理和客户端的通讯
func process(conn net.Conn) {
	//这里需要延迟关闭conn
	defer conn.Close()
	//循环读取读客户端发送的消息
	for {
	// 这里将读取数据包,直接封装成一个readPkg(),返回Message,err
		mes, err := readPkg(conn)
		if err != nil {
			if err == io.EOF {
				fmt.Println("客户端退出,服务器端也正常退出")
				return
			} else {
				fmt.Println("readPkg fail err=", err)
				return
			}			
		}
		err = serverProcessMes(conn, &mes)
			if err != nil {
				return
			}		
	}
}

func main() {
	//提示信息
	fmt.Println("服务器在8899监听。。。。")
	listen, err := net.Listen("tcp", "0.0.0.0:8899")
	defer listen.Close()
	if err != nil {
		fmt.Println("net listen err=", err)
		return
	}
	//一旦监听成功,就等待客户端链接服务器
	for {
		fmt.Println("等待客户端来接服务器。。。")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("liten Accept err=", err)
		}
	//一旦连接成功,则启动一个协程和客户端保持通讯。
	go process(conn)
	}
}

client/utils.go 新增文件整体代码

package main
import (
	"fmt"
	"net"
	"encoding/binary"
	"encoding/json"
	"chatroom/common/message"
)
func readPkg(conn net.Conn) (mes message.Message, err error) {
	buf := make([]byte, 8096)
		fmt.Println("读取客户端发送的数据~~~")
		//conn.Read 在conn没有被关闭的情况下,才会阻塞
		// 如果客户端关闭了conn,则就不会阻塞了
		_, err = conn.Read(buf[:4])
		if err != nil {
			return
		}
		//根据读到的长度buf[:4] 转成一个uint32类型
		var pkgLen uint32
		pkgLen = binary.BigEndian.Uint32(buf[0:4])
		// 根据pkgLen读取内容
		n, err := conn.Read(buf[:pkgLen])
		if n != int(pkgLen) || err != nil {
			return
		}
		// 把pkgLen反序列化成 -> message.Message
		err = json.Unmarshal(buf[:pkgLen], &mes) 
		if err != nil {
			fmt.Println("json.Unmarshal fail err=", err)
			return
		}
	return
}

func writePkg(conn net.Conn, data []byte) (err error){
	// 先发送一个长度给对方
	var pkgLen uint32
	pkgLen = uint32(len(data))
	var buf [4]byte
	binary.BigEndian.PutUint32(buf[0:4], pkgLen)
	// 发送长度
	n, err := conn.Write(buf[0:4])
	if n != 4 || err != nil {
		fmt.Println("conn.Write fail", err)
		return
	}
	// 发送data本身
	n, err = conn.Write(data)
	if n != int(pkgLen) || err != nil {
		fmt.Println("conn.Write fail", err)
		return
	}
	return
}

服务端代码该进

package main
import (
	"fmt"
	"net"
)
// func readPkg(conn net.Conn) (mes message.Message, err error) {
// 	buf := make([]byte, 8096)
// 		fmt.Println("读取客户端发送的数据~~~")
// 		//conn.Read 在conn没有被关闭的情况下,才会阻塞
// 		// 如果客户端关闭了conn,则就不会阻塞了
// 		_, err = conn.Read(buf[:4])
// 		if err != nil {
// 			return
// 		}
// 		//根据读到的长度buf[:4] 转成一个uint32类型
// 		var pkgLen uint32
// 		pkgLen = binary.BigEndian.Uint32(buf[0:4])
// 		// 根据pkgLen读取内容
// 		n, err := conn.Read(buf[:pkgLen])
// 		if n != int(pkgLen) || err != nil {
// 			return
// 		}
// 		// 把pkgLen反序列化成 -> message.Message
// 		err = json.Unmarshal(buf[:pkgLen], &mes) 
// 		if err != nil {
// 			fmt.Println("json.Unmarshal fail err=", err)
// 			return
// 		}
// 	return
// }

// func writePkg(conn net.Conn, data []byte) (err error){
// 	// 先发送一个长度给对方
// 	var pkgLen uint32
// 	pkgLen = uint32(len(data))
// 	var buf [4]byte
// 	binary.BigEndian.PutUint32(buf[0:4], pkgLen)
// 	// 发送长度
// 	n, err := conn.Write(buf[0:4])
// 	if n != 4 || err != nil {
// 		fmt.Println("conn.Write fail", err)
// 		return
// 	}
// 	// 发送data本身
// 	n, err = conn.Write(data)
// 	if n != int(pkgLen) || err != nil {
// 		fmt.Println("conn.Write fail", err)
// 		return
// 	}
// 	return
// }

// 编写一个函数serverProcessLogin函数,专门处理登陆请求
// func serverProcessLogin(conn net.Conn, mes *message.Message) (err error){
// 	// 核心代码。。。
// 	// 1、先从mes中取出mes.Data,并直接反序列化成LoginMes
// 	var loginMes message.LoginMes
// 	err = json.Unmarshal([]byte(mes.Data), &loginMes)
// 	if err != nil {
// 		fmt.Println("json.Unmashal fail err=", err)
// 		return
// 	}
// 	// ① 先声明一个resMes
// 	var resMes message.Message 
// 	resMes.Type = message.LoginResMesType

// 	// ② 再声明一个 loginResMes,并完成赋值
// 	var loginResMes message.LoginResMes
// 	//如果用户的id为100,密码为123456,认为合法, 否则 不合法
// 	if loginMes.UserId == 100 && loginMes.UserPwd == "123456" {
// 		// 合法
// 		loginResMes.Code = 200
// 	} else {
// 		//不合法
// 		loginResMes.Code = 500 //500状态码,表示该用户不存在
// 		loginResMes.Error = "该用户不存在,请注册后再使用"
// 	}

// 	// ③ 将 loginResMes 序列化
// 	data, err := json.Marshal(loginResMes)
// 	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 fail", err)
// 		return
// 	}

// 	// ⑥ 发送data,将其封装到writePkg函数
// 	err = writePkg(conn, data)
// 	return
// }

// 编写一个ServerProcessMes 函数
// 功能:根据客户端发送消息种类的不同,决定调用哪个函数来处理
// func serverProcessMes(conn net.Conn, mes *message.Message) (err error){
// 	switch mes.Type {
// 		case message.LoginMesType : 
// 			// 处理登陆
// 			err = serverProcessLogin(conn, mes)
// 		case message.RegisterMesType :
// 			// 处理注册
// 		default :
// 			fmt.Println("消息类型不存在,无法处理。。。。")
// 	}
// 	return
// }

//处理和客户端的通讯
func process(conn net.Conn) {
	//这里需要延迟关闭conn
	defer conn.Close()
	// 这里调用总控创建一个总控
	processor := &Processor{
		Conn : conn,
	}
	err := processor.process2()
	if err != nil {
		fmt.Println("客户端和服务器端的通讯协程出现问题 err=", err)
		return
	}
}

func main() {
	//提示信息
	fmt.Println("服务器[新的结构]在8899监听。。。。")
	listen, err := net.Listen("tcp", "0.0.0.0:8899")
	defer listen.Close()
	if err != nil {
		fmt.Println("net listen err=", err)
		return
	}
	//一旦监听成功,就等待客户端链接服务器
	for {
		fmt.Println("等待客户端来接服务器。。。")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("liten Accept err=", err)
		}
	//一旦连接成功,则启动一个协程和客户端保持通讯。
	go process(conn)
	}
}

server/main/process.go

package main
import (
	"fmt"
	"net"
	"chatroom/common/message"
	"chatroom/server/utils"
	"chatroom/server/process"
	"io"
)
// 先创建一个Processor 的结构体
type Processor struct {
	Conn  net.Conn
}

// 编写一个ServerProcessMes 函数
// 功能:根据客户端发送消息种类的不同,决定调用哪个函数来处理
func(this * Processor) serverProcessMes(mes *message.Message) (err error){
	switch mes.Type {
		case message.LoginMesType : 
			// 处理登陆
			// 创建一个  UserProcess实例
			up := &process2.UserProcess{
				Conn : this.Conn,
			}
			err = up.ServerProcessLogin(mes)
		case message.RegisterMesType :
			// 处理注册
		default :
			fmt.Println("消息类型不存在,无法处理。。。。")
	}
	return
}

func(this *Processor) process2() (err error){
	//循环读取读客户端发送的消息
	for {
		// 这里将读取数据包,直接封装成一个readPkg(),返回Message,err
		// 创建一个Transfer  实例完成读包任务
		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
				}			
			}
			err = this.serverProcessMes(&mes)
				if err != nil {
					return err
				}		
		}
}

server/process/userprocess.go

package process2
import (
	"fmt"
	"net"
	"encoding/json"
	"chatroom/common/message"
	"chatroom/server/utils"
)
type UserProcess struct {
	// 字段
	Conn net.Conn
}

// 编写一个函数serverProcessLogin函数,专门处理登陆请求
func(this *UserProcess) ServerProcessLogin(mes *message.Message) (err error){
	// 核心代码。。。
	// 1、先从mes中取出mes.Data,并直接反序列化成LoginMes
	var loginMes message.LoginMes
	err = json.Unmarshal([]byte(mes.Data), &loginMes)
	if err != nil {
		fmt.Println("json.Unmashal fail err=", err)
		return
	}
	// ① 先声明一个resMes
	var resMes message.Message 
	resMes.Type = message.LoginResMesType

	// ② 再声明一个 loginResMes,并完成赋值
	var loginResMes message.LoginResMes
	//如果用户的id为100,密码为123456,认为合法, 否则 不合法
	if loginMes.UserId == 100 && loginMes.UserPwd == "123456" {
		// 合法
		loginResMes.Code = 200
	} else {
		//不合法
		loginResMes.Code = 500 //500状态码,表示该用户不存在
		loginResMes.Error = "该用户不存在,请注册后再使用"
	}

	// ③ 将 loginResMes 序列化
	data, err := json.Marshal(loginResMes)
	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 fail", err)
		return
	}

	// ⑥ 发送data,将其封装到writePkg函数
	// 因为使用了分层的模式,先创建Transfer实例,然后读取
	tf := &utils.Transfer{
		Conn : this.Conn,
	}

	err = tf.WritePkg(data)
	return
}

server/utils/utils.go

package utils
import (
	"fmt"
	"net"
	"encoding/binary"
	"encoding/json"
	"chatroom/common/message"
)

// 这里将这些方法关联到结构体中
type Transfer struct {
	// 分析他应该有哪些字段
	Conn net.Conn
	Buf [8064]byte //这是传输时使用的缓冲
}

func(this *Transfer) ReadPkg() (mes message.Message, err error) {
	// buf := make([]byte, 8096)
		fmt.Println("读取客户端发送的数据~~~")
		//conn.Read 在conn没有被关闭的情况下,才会阻塞
		// 如果客户端关闭了conn,则就不会阻塞了
		_, err = this.Conn.Read(this.Buf[:4])
		if err != nil {
			return
		}
		//根据读到的长度buf[:4] 转成一个uint32类型
		var pkgLen uint32
		pkgLen = binary.BigEndian.Uint32(this.Buf[0:4])
		// 根据pkgLen读取内容
		n, err := this.Conn.Read(this.Buf[:pkgLen])
		if n != int(pkgLen) || err != nil {
			return
		}
		// 把pkgLen反序列化成 -> message.Message
		err = json.Unmarshal(this.Buf[:pkgLen], &mes) 
		if err != nil {
			fmt.Println("json.Unmarshal fail err=", 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[0:4])
	if n != 4 || err != nil {
		fmt.Println("conn.Write fail", err)
		return
	}
	// 发送data本身
	n, err = this.Conn.Write(data)
	if n != int(pkgLen) || err != nil {
		fmt.Println("conn.Write fail", err)
		return
	}
	return
}
客户端

框架图

client/main/main.go

package main
import (
	"fmt"
	"os"
	"chatroom/client/process"
)

// 定义两个变量,一个表示用户的ID, 一个表示用户密码
var userId int
var userPwd string

func main() {
	// 接收用户选择
		var key int
	// 判断是否还继续显示菜单
		// var loop = true
	for true {
		fmt.Println("-----------欢迎登陆多人聊天系统----------")
		fmt.Println("\t\t\t 1 登陆聊天室")
		fmt.Println("\t\t\t 2 注册用户")
		fmt.Println("\t\t\t 3 退出系统")
		fmt.Println("\t\t\t 请选择(1-3))")

		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)
				// 完成登陆
				// 1、创建一个UserProcess实例
				up := &process.UserProcess{}
				up.Login(userId, userPwd)
			case 2 :	
				fmt.Println("注册用户")
				// loop = false
			case 3 :	
				fmt.Println("退出系统")
				os.Exit(0)
			default :
				fmt.Println("你的输入有误,请重新输入")
		}
	}
	// 根据用户的输入,显示新的提示信息
	// if key ==1 {
	// 	// 说明用户要登陆

	// // 因为使用了分层的结构,
	// 	//先把登陆的函数,写到另一个文件
	// 	// 这里我们会需要重新调用
	// 	// login(userId, userPwd)
	// 	// if err != nil {
	// 	// 	fmt.Println("登陆失败")
	// 	// } else {
	// 	// 	fmt.Println("登陆成功")
	// 	// }
	// } else if key ==2 {
	// 	fmt.Println("进行用户注册的逻辑。。。。")
	// }
}

client/process/server.go

package process
import (
	"fmt"
	"os"
	"net"
	"chatroom/client/utils"
)
// 显示登陆成功后的界面...
func ShowMenu() {

	fmt.Println("----------恭喜登陆成功---------")
	fmt.Println("-------1、显示在线用户列表------")
	fmt.Println("-------2、发送消息------")
	fmt.Println("-------3、信息列表------")
	fmt.Println("-------4、退出系统------")
	fmt.Println("请选择1-4")
	var key int 
	fmt.Scanf("%d\n", &key)
	switch key {
		case 1 : 
			fmt.Println("1、显示在线用户列表")
		case 2 : 
			fmt.Println("2、发送消息")
		case 3 : 
			fmt.Println("3、信息列表")
		case 4 : 
			fmt.Println("4、退出系统")
			os.Exit(0)
		default :
			fmt.Println("你输入的选项不正确")
	}
}

// 和服务器端保持通讯的协程
func serverProcessMes(Conn net.Conn){
	// 创建transfer实例,不停的读取服务器发送的消息
	tf := &utils.Transfer{
		Conn : Conn,
	}
	for {
		// 客户端不停的读取
		fmt.Println("客户端正在等待读取服务器发送的消息")
		mes, err := tf.ReadPkg()
		if err != nil {
			fmt.Println("RedPkg err=", err)
			return
		}
		// 如果读取到了消息,下一步处理
		fmt.Printf("mes=%v", mes)

	}
}

client/process/userprocess.go

package process
import (
	"fmt"
	"net"
	"encoding/binary"
	"encoding/json"
	"chatroom/common/message"
	"chatroom/client/utils"
)
type UserProcess struct {
	//暂时不需要字段
}
// 给关联一个用户登陆方法
// 写一个函数,完成登陆
func (this *UserProcess) Login(userId int, userPwd string) (err error) {

	// 下一步就要开始定协议。。。
	// fmt.Printf(" userID=%d userPwd=%s", userID, userPwd)
	// return nil

	//1、连接到feu武器
	conn, err := net.Dial("tcp", "localhost:8899")
	if err != nil {
		fmt.Println("net.Dial err", err)
		return
	}
	//延时关闭
	defer conn.Close()

	// 2、准备通过conn发送消息给服务器
	var mes message.Message
	mes.Type = message.LoginMesType

	// 3、创建一个LoginMes
	var loginMes message.LoginMes
	loginMes.UserId = userId
	loginMes.UserPwd = userPwd

	// 4、将loginMes序列化
	data, err := json.Marshal(loginMes)
	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
	}
	//7、此时data就是我们要发送的消息
	// 7.1 先把data的长度发生给服务器
	// 先获取到data的长度->转换成一个表示长度的byte切片
	 var pkgLen uint32
	 pkgLen = uint32(len(data))
	 var buf [4]byte
	 binary.BigEndian.PutUint32(buf[0:4], pkgLen)
	// 发送长度
	n, err := conn.Write(buf[0:4])
	if n != 4 || err != nil {
		fmt.Println("conn.Write fail", err)
		return
	}
	// fmt.Printf("客户端,发送消息长度成功=%d 内容是=%s", len(data), string(data))
	// 发送消息本身
	_, err = conn.Write(data)
	if  err != nil {
		fmt.Println("conn.Write(data) fail", err)
		return
	}
	//休眠20
	// time.Sleep(20 * time.Second)
	// fmt.Println("休眠了20秒")
	// 这里还需要处理服务器端的消息
	// 创建Transfer实例
	tf := &utils.Transfer{
		Conn : conn,
	}
	mes, err = tf.ReadPkg()  //mes就是
	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("登陆成功")
		// 这里需要在客户端启动一个协程
		// 该协程保持和服务器端的通讯,如果服务器有数据推送客户端
		// 则接收并显示在客户端的终端
		go serverProcessMes(conn)
		
		// 1、显示我们登陆成功的菜单[循环]
		for {
			ShowMenu()
		}
	} else if loginResMes.Code == 500 {
		fmt.Println(loginResMes.Error)
	}

	return
}

client/utils/utils.go

package utils
import (
	"fmt"
	"net"
	"encoding/binary"
	"encoding/json"
	"chatroom/common/message"
)

// 这里将这些方法关联到结构体中
type Transfer struct {
	// 分析他应该有哪些字段
	Conn net.Conn
	Buf [8064]byte //这是传输时使用的缓冲
}

func(this *Transfer) ReadPkg() (mes message.Message, err error) {
	// buf := make([]byte, 8096)
		fmt.Println("读取客户端发送的数据~~~")
		//conn.Read 在conn没有被关闭的情况下,才会阻塞
		// 如果客户端关闭了conn,则就不会阻塞了
		_, err = this.Conn.Read(this.Buf[:4])
		if err != nil {
			return
		}
		//根据读到的长度buf[:4] 转成一个uint32类型
		var pkgLen uint32
		pkgLen = binary.BigEndian.Uint32(this.Buf[0:4])
		// 根据pkgLen读取内容
		n, err := this.Conn.Read(this.Buf[:pkgLen])
		if n != int(pkgLen) || err != nil {
			return
		}
		// 把pkgLen反序列化成 -> message.Message
		err = json.Unmarshal(this.Buf[:pkgLen], &mes) 
		if err != nil {
			fmt.Println("json.Unmarshal fail err=", 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[0:4])
	if n != 4 || err != nil {
		fmt.Println("conn.Write fail", err)
		return
	}
	// 发送data本身
	n, err = this.Conn.Write(data)
	if n != int(pkgLen) || err != nil {
		fmt.Println("conn.Write fail", err)
		return
	}
	return
}

如果输入的用户名和密码正确在Redis中存在则登陆,否则退出系统,并给出相应的提示信息:

1、用户不存在,你也可以重新注册,再登录

2、你的密码错误

代码实现:

编写了 server/model/user.go

package model

// 定义一个用户结构体
type User struct {
	// 确定字段信息
	// 为了序列化和反序列化成功,我们必须保证
	// 用户信息的json字符串  和  结构体的字段对应的  tag名字  必须一致
	UserId int `json:"userId"`
	UserPwd string `json:"userPwd"`
	UserName string `json:"userName"`
}

编写了 server/model/error.go

package model
import (
	"errors"
)
// 根据业务逻辑的需要,自定义一些错误

var (
	ERROR_USER_NOEXISTS = errors.New("该用户不存在")
	ERROR_USER_EXISTS = errors.New("用户已经存在")
	ERROR_USER_PWD = errors.New("用户密码错误")
)

编写了 server/model/userDao.go

package model
import (
	"fmt"
	"github.com/garyburd/redigo/redis"
	"encoding/json"
)

// 在服务器启动后,就初始化一个userDao实例
// 做成全局的变量,在需要和redis *** 作时,直接使用即可
var (
	MyUserDao *UserDao
)

// 这里定义一个UserDao 结构体
// 完成对User 结构体的各种 *** 作

type UserDao struct {
	pool  *redis.Pool
}
// 使用工厂模式,创建一个UserDao实例
func NewUserDao(pool *redis.Pool) (userDao *UserDao) {
	userDao = &UserDao{
		pool : pool,
	}
	return
}

// 1、根据一个用户id返回一个User实例
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{ //表示在users  哈希中,没有找到对应id
			err = ERROR_USER_NOEXISTS
		}
		return
	}
	user = &User{}
	// 需要把 res反序列化成user实例
	err = json.Unmarshal([]byte(res), &user)
	if err != nil {
		fmt.Println("json.Unmarshal err=", err)
		return
	}
	return
}

// 2、完成登陆的校验 Login
// ①、Login  完成对用户的验证
// ②、如果用户的id和pwd都正确,则返回一个user实例
// ③、如果用户的id和密码pwd都正确,则返回对应的错误信息

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	
	}
	// 这时证明这个用户是获取到,
	// 但也有可能是用户密码不正确,下面做判断
	if user.UserPwd != userPwd {
		err = ERROR_USER_PWD
		return
	}
	return
}

编写server/main/redis.go

package main
import (
	"github.com/garyburd/redigo/redis"
	"time"
)

// 定义一个全局pool
var pool *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","localhost:6379")
    	}
	}
}

server/main/main.go 新增代码

package main
import (
	"fmt"
	"net"
	"time"
	"chatroom/server/model"
)
//处理和客户端的通讯
func process(conn net.Conn) {
	//这里需要延迟关闭conn
	defer conn.Close()
	// 这里调用总控创建一个总控
	processor := &Processor{
		Conn : conn,
	}
	err := processor.process2()
	if err != nil {
		fmt.Println("客户端和服务器端的通讯协程出现问题 err=", err)
		return
	}
}

// 这里编写一个函数,完成对UserDao初始化任务
func initUserDao() {
	// 这里的pool本身就是一个全局的变量
	// 需要注意一个初始化顺序问题
	// 先initPool 再 initUserDao
	model.MyUserDao = model.NewUserDao(pool)
}

func main() {
	// 当服务器启动时,我们就去初始化Redis连接池
	initPool("localhost:6379", 16, 0, 300 * time.Second)
	initUserDao()
	//提示信息
	fmt.Println("服务器[新的结构]在8899监听。。。。")
	listen, err := net.Listen("tcp", "0.0.0.0:8899")
	defer listen.Close()
	if err != nil {
		fmt.Println("net listen err=", err)
		return
	}
	//一旦监听成功,就等待客户端链接服务器
	for {
		fmt.Println("等待客户端来接服务器。。。")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("liten Accept err=", err)
		}
	//一旦连接成功,则启动一个协程和客户端保持通讯。
	go process(conn)
	}
}

在server/process/userProcess.go 使用到redis的验证功能,新增代码

package process2
import (
	"fmt"
	"net"
	"encoding/json"
	"chatroom/common/message"
	"chatroom/server/utils"
	"chatroom/server/model"
)
type UserProcess struct {
	// 字段
	Conn net.Conn
}

// 编写一个函数serverProcessLogin函数,专门处理登陆请求
func(this *UserProcess) ServerProcessLogin(mes *message.Message) (err error){
	// 核心代码。。。
	// 1、先从mes中取出mes.Data,并直接反序列化成LoginMes
	var loginMes message.LoginMes
	err = json.Unmarshal([]byte(mes.Data), &loginMes)
	if err != nil {
		fmt.Println("json.Unmashal fail err=", err)
		return
	}
	// ① 先声明一个resMes
	var resMes message.Message 
	resMes.Type = message.LoginResMesType

	// ② 再声明一个 loginResMes,并完成赋值
	var loginResMes message.LoginResMes
	// 需要到redis数据库完成验证
	// 1、使用model.MyUserDao到Redis验证
	user, err := model.MyUserDao.Login(loginMes.UserId, loginMes.UserPwd)	
	if err != nil {
		if err == model.ERROR_USER_NOEXISTS {
			loginResMes.Code = 500
			loginResMes.Error = err.Error()
		} else if err == model.ERROR_USER_PWD {
			loginResMes.Code = 300
			loginResMes.Error = err.Error()
		} else {
			loginResMes.Code = 403
			loginResMes.Error = "未知错误"
		}	
	} else {
		loginResMes.Code = 200
		fmt.Println(user, "登陆成功")
	}
	
	
	// //如果用户的id为100,密码为123456,认为合法, 否则 不合法
	// if loginMes.UserId == 100 && loginMes.UserPwd == "123456" {
	// 	// 合法
	// 	loginResMes.Code = 200
	// } else {
	// 	//不合法
	// 	loginResMes.Code = 500 //500状态码,表示该用户不存在
	// 	loginResMes.Error = "该用户不存在,请注册后再使用"
	// }

	// ③ 将 loginResMes 序列化
	data, err := json.Marshal(loginResMes)
	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 fail", err)
		return
	}

	// ⑥ 发送data,将其封装到writePkg函数
	// 因为使用了分层的模式,先创建Transfer实例,然后读取
	tf := &utils.Transfer{
		Conn : this.Conn,
	}

	err = tf.WritePkg(data)
	return
}

client/process/userprocess.go

func (this *UserProcess) Register(userId int, userPwd string, userName string) (err error) {
	
	//1、连接到服务器
	conn, err := net.Dial("tcp", "localhost:8899")
	if err != nil {
		fmt.Println("net.Dial err", err)
		return
	}
	//延时关闭
	defer conn.Close()

	// 2、准备通过conn发送消息给服务器
	var mes message.Message
	mes.Type = message.RegisterMesType

	// 3、创建一个LoginMes
	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 err=", err)
		return
	}
// 将mes的Data部分反序列化成 ResterResMes
	var registerResMes message.RegisterResMes
	err = json.Unmarshal([]byte(mes.Data), &registerResMes)
	if registerResMes.Code == 200 {
		fmt.Println("注册成功,你重新登陆一把")
		os.Exit(0)
	} else {
		fmt.Println(registerResMes.Error)
		os.Exit(0)
	}
	return
}

server/model/userDao.go 增加了方法

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
}

在server/process/userProcess.go 增加了方法,处理注册

func (this *UserProcess) ServerProcessRegister(mes *message.Message) (err error){
	// 1、先从mes中取出mes.Data,并直接反序列化成RegisterMes
	var registerMes message.RegisterMes
	err = json.Unmarshal([]byte(mes.Data), &registerMes)
	if err != nil {
		fmt.Println("json.Unmashal fail err=", err)
		return
	}

	// ① 先声明一个resMes
	var resMes message.Message 
	resMes.Type = message.RegisterResMesType
	// ② 再声明一个 loginResMes,并完成赋值
	var registerResMes message.RegisterResMes

	// 需要到redis数据库完成注册
	// 1、使用model.MyUserDao到Redis验证
	err = model.MyUserDao.Register(&registerMes.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 fail", err)
		return
	}

	// ⑥ 发送data,将其封装到writePkg函数
	// 因为使用了分层的模式,先创建Transfer实例,然后读取
	tf := &utils.Transfer{
		Conn : this.Conn,
	}

	err = tf.WritePkg(data)
	return
}

server/main/processor.go 调用了方法

func(this * Processor) serverProcessMes(mes *message.Message) (err error){
	switch mes.Type {
		case message.LoginMesType : 
			// 处理登陆
			// 创建一个  UserProcess实例
			up := &process2.UserProcess{
				Conn : this.Conn,
			}
			err = up.ServerProcessLogin(mes)
		case message.RegisterMesType :
			// 处理注册
			up := &process2.UserProcess{
				Conn : this.Conn,
			}
			err = up.ServerProcessRegister(mes)  //type :  data
		default :
			fmt.Println("消息类型不存在,无法处理。。。。")
	}
	return
}

实现功能- 完成登陆时能返回当前在线用户

用户登陆后,可以得到当前在线用户列表

编写了 server/process/userMgr.go

package process2
import(
	"fmt"
)

// 因为UserMgr实例在服务器有且只有一个
// 因为在很多地方,都会使用,因此,我们
// 将其定义为全局变量

var (
	 userMgr *UserMgr
)

type UserMgr struct {
	onlineUsers map[int]*UserProcess
}

// 完成对userMgr初始化工作
func init() {
	userMgr = &UserMgr {
		onlineUsers : make(map[int]*UserProcess, 1024),
	}
}

// 完成对onlineUsers添加
func (this *UserMgr) AddonlineUserup(up *UserProcess){
	this.onlineUsers[up.UserId] = up
}
// 删除
func (this *UserMgr) DelonlineUserup(userId int){
	delete(this.onlineUsers, userId)
}
//返回当前所有在线用户
func (this *UserMgr) DelonlineUserupmap() map[int]*UserProcess{
	return this.onlineUsers
}

// 根据ID返回对应的值
func (this *UserMgr) GetOnlienUserById(userId int) (up *UserProcess, err error) {
	// 如何从map取出一个值,带检测的方式
	up, ok := this.onlineUsers[userId]
	if !ok  { //说明你要查找的用户,当前不在线
		err = fmt.Errorf("用户%d 不存在", userId)
		return
	}
	return
}

client/process/userMg.go

package process
import (
	"fmt"
	"chatroom/common/message"
)
// 客户端要维护的map
var onlineUsers map[int]*message.User = make(map[int]*message.User, 10)
// 在客户端显示在线用户
func outputOnlineUser(){
	// 遍历 一把 onlineUsers
	fmt.Println("当前在线用户列表:")
	for id, _ := range onlineUsers {
		//如果不显示自己
		fmt.Println("用户id:\t", id)
	}
}
// 编写一个方法,处理返回的NotifyUserStatusMes
func updateUserStatus(notifyUserStatusMes *message.NotifyUserStatusMes){	
		// 适当优化
		user, ok := onlineUsers[notifyUserStatusMes.UserId]
		if !ok { //原来没有
			user = &message.User {
				UserId : notifyUserStatusMes.UserId,
			}
		}	
	user.UserStatus = notifyUserStatusMes.Status
	onlineUsers[notifyUserStatusMes.UserId] = user
	outputOnlineUser()
}

感觉实现的不是很完美,还有一下些功能需要该进。留着给有需要的人改进了偷个懒。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/996324.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存