海量用户及时通讯系统

18.4 海量用户即时通讯系统

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

猜你喜欢

转载自www.cnblogs.com/jiaxiaozia/p/13164540.html
今日推荐