Go_海量用户即时通讯系统

18.4.1 需求分析

1、用户注册

2、用户登陆

3、显示在线用户列表

4、群聊

5、点对点聊天

6、离线留言

18.4.2 界面设计

部分:

NALV8P.png

18.4.3 项目开发前技术准备

​ 项目要保存用户信息和消息数据,因此需要学习数据库(redis或者mysql),这里先选择redis

NAOHYR.png

18.4.4 实现功能-显示客户端登陆菜单

功能:能够正确显示客户端的菜单

NALV8P.png

MMIIII.PNG

代码实现:

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("进行用户注册的逻辑。。。。")
	}
}

client/login.go

package main
import (
	"fmt"
)

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

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

18.4.5 实现功能-完成用户登陆

1、先完成指定用户的验证,用户id=100,密码pwd=123456 可以登陆,其他用户不能登陆。

Message 的组成,并发送一个Message的流程图:

NmjEXn.png

1)客户端和服务器端发送消息长度测试

​ 完成客户端可以放消息长度,服务器端可以正常收到该长度

分析思路:

① 先确定Message的格式和结构

② 然后根据上图的分析完成代码

③ 示意图

Nn0tqf.png

代码规划

Nn0IzR.png

代码实现

server/main.go

package main
import (
	"fmt"
	"net"
)

//处理和客户端的通讯
func process(conn net.Conn) {
	//这里需要延迟关闭conn
	defer conn.Close()
    buf := make([]byte, 8096)
	//循环读取读客户端发送的消息
	for {		
		fmt.Println("读取客户端发送的数据")
		_, err := conn.Read(buf[:4])
		if err != nil {
			fmt.Println("conn.Read err=", err)
			return
		}
		fmt.Println("读到的buf=", buf[:4])
	}
}

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/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))
	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"`//返回错误信息
 }

测试结果:

NnrYs1.png

2) 客户端和服务器端互通,接收客户端发送的消息

​ 完成客户端可以发送消息本身,服务器端可以正常接收到消息,并根据客户端发送的消息(LoginMes),判断用户的合法性,并返回相应的LoginResMes

思路分析:

① 让客户端发送消息本身

② 服务器端接收到消息,反序列化成对应的消息结构体

③ 服务器端根据反序列化成对应的消息,判断是否登陆用户是合法用户,返回LoginResMes

④ 客户端解析返回的LoginResMes显示对应的界面

⑤ 这里需要做函数的封装

⑥ 代码实现

client/login.go

改动代码如下图

MMIIII.PNG

完整的代码

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

// 写一个函数,完成登陆
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秒")
	// 这里还需要处理服务器端的消息
	return
}

server/main.go

代码改动如下图:

MMIIII.PNG

将读取包的任务封装到了一个函数中 readPkg()

MMIIII.PNG

完整代码:

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 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
			}			
		}
		fmt.Println("mes=", mes)
	}
}

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)
	}
}

运行结果

MMIIII.PNG

3)指定用户登陆

​ 能够完成登陆,并提示相应信息(指定用户100 密码123456)

server/main.go

新增代码

MMIIII.PNG

MMIIII.PNG

MMIIII.PNG

MMIIII.PNG

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
}

client/login.go

改动代码

MMIIII.PNG

整体代码

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
}

common/messag/message.go 改动代码

MMIIII.PNG

运行结果

MMIIII.PNG

4)程序改进

​ 程序结构的改进,前面的程序虽然完成了功能,但是没有结构,系统的可读性,扩展性和维护性都不好,因此需要对程序进行改进。

改进1

① 先改进服务器端,先画出程序的框架图

MMIIII.PNG

② 步骤

  • 先把分析出来的文件,创建好,然后放到相应的文件夹[包],下图为代码规划图:

    MMIIII.PNG

  • 现在根据各个文件,完成的任务不同,将server/main/main.go的代码剥离到对应的文件中即可。

服务器端

server/main/main.go

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/smsprocess.go

package process2

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
}

代码运行结果

MMIIII.PNG

改进2

客户端

① 修改客户端,先画出程序的框架图

NKrqTP.png

② 先把各个文件放到对应的文件夹/包

代码规划图:

MMIIII.PNG

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/smsprocess.go

package process

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
}

运行结果

MMIIII.PNG

改进3

在Redis手动添加测试用户,并画图+说明注意

MMIIII.PNG

① client/process/userprocess.go打开接收的消息本身

MMIIII.PNG

② 启动server和client端

MMIIII.PNG

上图圈出的改为:

"{\"userId\":100,\"userPwd\":\"123456\",\"userName\":\"scott\"}"

③ 启动Redis的客户端和服务器端,手动上传数据

MMIIII.PNG

改进4

如果输入的用户名和密码正确在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 新增代码

Nlr7VA.png

整体代码:

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的验证功能,新增代码

Nls6sg.png

整体代码

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 看箭头和之前的代码对比,去除一部分代码

NlrEBd.png

18.4.6 实现功能- 完成注册用户

1、完成注册功能,将用户信息录入到redis中

N1Iw4O.png

① 在ccommon/message/user.go 新增文件和代码

N1fdp9.png

② 在common/message/ message.go 新增文件和代码

N1frm6.png

③ client/process/userprocess.go

部分截图,整体新增代码在下面

N1f2fH.png

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
}

④ 在client/main/main.go 新增了代码,处理注册请求

N1fOpj.png

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

N1hSBV.png

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 增加了方法,处理注册

部分新增代码截图,整体代码往下看

N1hQ4e.png

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 调用了方法

N1haE8.png

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
}

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

N1bzuQ.png

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

① 编写了 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
}

② server/process/userprocess.go 新增了

N1L7fP.png

③ server/process/userprocess.go

N1v9fI.png

④ common/message/message.go

N1vuhn.png

⑤ client/process/userprocess.go

N1v1XT.png

2、当一个新的用户上线后,其它已经登陆的用户也能获取最新在线用户列表

猜你喜欢

转载自www.cnblogs.com/jiaxiaozia/p/13171861.html