Golang 海量用户通讯系统

尚硅谷 海量用户通讯系统

一. 海量用户即时通讯系统需求

在这里插入图片描述

  • 项目开发流程:

    需求分析->设计阶段->编码实现->测试阶段->实施

  • 需求分析
    1)用户注册
    2)用户登录
    3)显示在线用户列表
    4)群聊(广播)
    5)点对点聊天
    6)离线留言

  • 界面设计
    在这里插入图片描述

二. 实现功能

2.1 显示客户端登录菜单

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

在这里插入图片描述

  • 代码实现:

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("退出系统")
			//loop = false //不继续显示该菜单
			os.Exit(0)
		default:
			fmt.Println("你的输入有误,请重新输入")
		}
	}

	//根据用户的输入,显示新的提示信息
	if key == 1 {
		//说明用户要登录
		fmt.Println("请输入用户的id")
		fmt.Scanf("%d\n", &userId)
		fmt.Println("请输入用户的密码")
		fmt.Scanf("%s\n", &userPwd)
		//先把登录的函数(协议),写到另外一个文件,比如login.go
		//因为都在一个包(package main)下,所以可以直接调用
		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\n", userId, userPwd)

	return nil
}

先按以下步骤进行设置并编译,否则会有packageXXX not in GOROOT,GOPATH,以及后续导入github.redis包等错误。

将此行代码内容添加到go.mod文件中:

require github.com/garyburd/redigo v1.6.4 // indirect

在这里插入图片描述

关于GO111MODULE=on和off:goland报错:package XXX is not in GOROOT (X:\XXX\Go\src\XXX)

进入go.mod存放的那一级目录下,进行编译
在这里插入图片描述

在这里插入图片描述
表示编译./client目录下的所有.go文件,生成client.exe 文件,此处也可以以 go build xxx.go xxx.go的形式编译同一文件夹下的源文件,但是源文件多的时候就不方便了

  • 运行:

client.exe
在这里插入图片描述

2.2 收发消息

  • 示意图

在这里插入图片描述

2.2.1收发消息长度

客户端能发送消息长度,服务器能正常接收该长度值

  • 思路分析
    1)先确定消息Message的格式和结构
    2)根据上图的分析完成代码
    3)示意图
    在这里插入图片描述
     

  • 代码实现

client\login.go

package main

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

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

	开始定协议
	//fmt.Printf("userId=%d userPwd=%s\n", userId, userPwd)
	//
	//return nil

	//1.链接到服务器
	conn, err := net.Dial("tcp", "localhost:8889")
	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.Marshalk err=", err)
		return
	}

	//5.把data赋给mes.Data字段
	mes.Data = string(data) //因为上面Marshal处理得到的data为byte型切片,所以需要转化为string类型

	//6.将mes进行序列化,data即客户端需要发送的数据
	data, err = json.Marshal(mes)
	if err != nil {
		fmt.Println("json.Marshal err=", err)
		return
	}

	//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[:4])
	if n != 4 || err != nil { //四个字节
		fmt.Println("conn.Write(bytes) fail", err)
		return
	}

	fmt.Printf("客户端,发送消息的长度%d,内容=%s", len(data), string(data))
	return
}

common\message\message.go

package message

const (
	LoginMesType    = "LoginMes"
	LoginResMesType = "LoginResMes"
)

//因为发送的消息要用结构体,要用json.Marshal序列化传递输出,所以变量名称都首写大写 (笔记见:G:\GoLand Code\tutorial project 1\src\test_18\JSON处理(通过结构、map生成json).go)
//但是函数传递的时候参数为小写,所以此处都做了json处理,改为首写小写

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

//处理客户端的通讯
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("服务器在8889端口监听...")
	listen, err := net.Listen("tcp", "0.0.0.0:8889")
	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("listen.Accept err=", err)
			//这里不用return 因为可能一个客户端链接出错,其他没出错
		}

		//一旦链接成功,则启动一个协程和客户端保持通讯
		go process(conn)
	}
}

  • 运行

server.exe在这里插入图片描述
client.exe
在这里插入图片描述

2.2.2收发消息本身

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

  • 思路分析
    1)让客户端发送消息本身
    2)判断消息长度是否正确(是否在传输过程中丢包)
    3)服务器接收到消息(为字节切片),需要反序列化成对应的消息结构体
    4)服务器端根据反序列化成对应的消息,判断是否登录用户是合法的,返回LoginResMes(需要序列化)
    5)客户端解析(反序列化)返回的LoginResMes,显示对应界面
    6)这里需要做函数的封装

  • 代码实现

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("退出系统")
			//loop = false //不继续显示该菜单
			os.Exit(0)
		default:
			fmt.Println("你的输入有误,请重新输入")
		}
	}

	//根据用户的输入,显示新的提示信息
	if key == 1 {
		//说明用户要登录
		fmt.Println("请输入用户的id")
		fmt.Scanf("%d\n", &userId)
		fmt.Println("请输入用户的密码")
		fmt.Scanf("%s\n", &userPwd)
		//先把登录的函数(协议),写到另外一个文件,比如login.go
		//因为都在一个包(package main)下,所以可以直接调用
		login(userId, userPwd)
		//if err != nil {
		//	fmt.Println("登陆失败")
		//} else {
		//	fmt.Println("登陆成功")
		//}
	} else if key == 2 {
		fmt.Println("进行用户注册的逻辑...")
	}
}

client\login.go

package main

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

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

	开始定协议
	//fmt.Printf("userId=%d userPwd=%s\n", userId, userPwd)
	//
	//return nil

	//1.链接到服务器
	conn, err := net.Dial("tcp", "localhost:8889")
	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.Marshalk err=", err)
		return
	}

	//5.把data赋给mes.Data字段
	mes.Data = string(data) //因为上面Marshal处理得到的data为byte型切片,所以需要转化为string类型

	//6.将mes进行序列化,data即客户端需要发送的数据
	data, err = json.Marshal(mes)
	if err != nil {
		fmt.Println("json.Marshal err=", err)
		return
	}

	//7.1 先把data的长度发送给服务器
	//先获取到data的长度->转成一个表示长度的byte切片
	//因为Write()括号内写入类型的问题,这边要先做一个转换
	var pkgLen uint32
	pkgLen = uint32(len(data))
	var buf [4]byte
	binary.BigEndian.PutUint32(buf[0:4], pkgLen)
	//发送长度
	n, err := conn.Write(buf[:4])
	if n != 4 || err != nil { //四个字节
		fmt.Println("conn.Write(buf) 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
	}

	//休眠20s
	//time.Sleep(10 * time.Second)
	//fmt.Println("休眠了20...")

	//这里还需要处理服务器端返回的消息

	mes, err = readPkg(conn)

	if err != nil {
		fmt.Println("readPkg(conn) 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\utils.go

package main

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

//和server\main.go里一样
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 { //这边返回的err为io.EOF
		//err = errors.New("read pkg header error") //自定义错误
		return
	}

	//因为Read()括号内写入类型的原因,这边需要根据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 { //这边返回的err为io.EOF
		//err = errors.New("read pkg body error") //自定义错误
		return
	}

	//把pkgLen反序列化成->message.Message
	json.Unmarshal(buf[:pkgLen], &mes)
	if err != nil {

		fmt.Println("json Unmarshal err=", err)
		return
	}

	return
}

//和server\main.go里一样
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[:4])
	if n != 4 || err != nil {
		fmt.Println("conn.Write(bytes) fail=", err)
		return
	}

	//发送data本身
	n, err = conn.Write(data)
	if n != int(pkgLen) || err != nil {
		fmt.Println("conn.Write(bytes) fail=", err)
		return
	}
	return
}

common\message\message.go

package message

const (
	LoginMesType    = "LoginMes"
	LoginResMesType = "LoginResMes"
	RegisterMesType = "registerMes"
)

//因为发送的消息要用结构体,要用json.Marshal序列化传递输出,所以变量名称都首写大写 (笔记见:G:\GoLand Code\tutorial project 1\src\test_18\JSON处理(通过结构、map生成json).go)
//但是函数传递的时候参数为小写,所以此处都做了json处理,改为首写小写

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

type RegisterMes struct {
	//...
}

server\main.go

package main

import (
	"chatroom/common/message"
	"encoding/binary"
	"encoding/json"
	"fmt"
	"io"
	"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 { //这边返回的err为io.EOF
		//err = errors.New("read pkg header error") //自定义错误
		return
	}

	//因为Read()括号内写入类型的原因,这边需要根据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 { //这边返回的err为io.EOF
		//err = errors.New("read pkg body error") //自定义错误
		return
	}

	//把pkgLen反序列化成->message.Message
	json.Unmarshal(buf[:pkgLen], &mes)
	if err != nil {

		fmt.Println("json Unmarshal 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[:4])
	if n != 4 || err != nil {
		fmt.Println("conn.Write(bytes) fail=", err)
		return
	}

	//发送data本身
	n, err = conn.Write(data)
	if n != int(pkgLen) || err != nil {
		fmt.Println("conn.Write(bytes) 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.Unmarshal fail err=", err)
		return
	}

	//1.先声明一个resMes
	var resMes message.Message
	resMes.Type = message.LoginResMesType

	//2.再声明一个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 = "该用户不存在,请注册再使用"
	}

	//3.将loginRes序列化
	data, err := json.Marshal(loginResMes)
	if err != nil {
		fmt.Println("json.Marshal fail=", err)
		return
	}

	//4.将data赋值给resMes
	resMes.Data = string(data)

	//5.对resMes进行序列化,准备发送
	data, err = json.Marshal(resMes)
	if err != nil {
		fmt.Println("json.Marshal fail=", err)
		return
	}

	//6.发送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 { //即readPkg中conn.Read的err
				fmt.Println("客户端退出,服务器也正常退出...")
				return
			} else {
				fmt.Println("readPkg err=", err) //其他错误信息
				return
			}
		}
		//fmt.Println("mes=", mes)

		err = serverProcessMes(conn, &mes)
		if err != nil {
			return
		}
	}
}

func main() {

	//提示信息
	fmt.Println("服务器在8889端口监听...")
	listen, err := net.Listen("tcp", "0.0.0.0:8889")
	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("listen.Accept err=", err)
			//这里不用return 因为可能一个客户端链接出错,其他没出错
		}

		//一旦链接成功,则启动一个协程和客户端保持通讯
		go process(conn)
	}
}

  • 运行

server.exe
在这里插入图片描述

client.exe
在这里插入图片描述

2.2.3 服务器结构改进

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

  • 服务器程序框架改进图
    在这里插入图片描述

  • 代码实现

server\main\main.go

package main

import (
	"fmt"
	"net"
)


//处理客户端的通讯
func process(conn net.Conn) {
	//这里需要延时关闭conn
	defer conn.Close()

	//这里调用总控,创建一个Processor实例
	processor := &Processor{
		Conn: conn,
	}
	err := processor.process2()
	if err != nil {
		fmt.Println("客户端和服务端通讯协程错误err=", err)
		return
	}
}


func main() {

	//提示信息
	fmt.Println("服务器[新的结构]在8889端口监听...")
	listen, err := net.Listen("tcp", "0.0.0.0:8889")
	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("listen.Accept err=", err)
			//这里不用return 因为可能一个客户端链接出错,其他没出错
		}

		//一旦链接成功,则启动一个协程和客户端保持通讯
		go process(conn)
	}
}

server\mian\processor.go

package main

import (
	"chatroom/common/message"
	process2 "chatroom/server/process"
	"chatroom/server/utils"
	"fmt"
	"io"
	"net"
)

//此文件总控服务器,完成调度

//先创建一个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 { //即readPkg中conn.Read的err
				fmt.Println("客户端退出,服务器也正常退出...")
				return err
			} else {
				fmt.Println("readPkg err=", err) //其他错误信息
				return err
			}
		}
		//fmt.Println("mes=", mes)

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

server\process\userProcess.go

package process2

//因为main包有一个函数名为process ,此处如果包为process和函数同名会有问题
import (
	"chatroom/common/message"
	"chatroom/server/utils"
	"encoding/json"
	"fmt"
	"net"
)

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.Unmarshal fail err=", err)
		return
	}

	//1.先声明一个resMes
	var resMes message.Message
	resMes.Type = message.LoginResMesType

	//2.再声明一个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 = "该用户不存在,请注册再使用"
	}

	//3.将loginRes序列化
	data, err := json.Marshal(loginResMes)
	if err != nil {
		fmt.Println("json.Marshal fail=", err)
		return
	}

	//4.将data赋值给resMes
	resMes.Data = string(data)

	//5.对resMes进行序列化,准备发送
	data, err = json.Marshal(resMes)
	if err != nil {
		fmt.Println("json.Marshal fail=", err)
		return
	}

	//6.发送data 将其封装到writePkg函数
	//因为使用分层模式,我们先创建一个Transfer实例,然后使用writePkg读取信息
	tf := &utils.Transfer{
		Conn: this.Conn, //此处不需要用到Buf,就不用写了
	}

	err = tf.WritePkg(data)
	return
}

server\utils\utils.go

package utils //储存工具性质的函数

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

//因为使用了分层加面向对象的思路,所以需要先将这些方法关联到结构体中
//用到的时候,直接创建实例,调用方法
//(其他文件中同理)
type Transfer struct {
	//分析它应该有哪些字段
	Conn net.Conn   //链接
	Buf  [8096]byte //传输时,使用的缓冲
}

//读消息
func (this *Transfer) ReadPkg() (mes message.Message, err error) {

	//buf := make([]byte, 8096)   //Transfer自带,这边就不用另外写了
	fmt.Println("读取客户端发送的数据...")
	//conn.Read 在conn没有被关闭的情况下,才会阻塞
	//如果客户端关闭了conn,则不会阻塞
	_, err = this.Conn.Read(this.Buf[:4])
	if err != nil { //这边返回的err为io.EOF
		//err = errors.New("read pkg header error") //自定义错误
		return
	}

	//因为Read()括号内写入类型的原因,这边需要根据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 { //这边返回的err为io.EOF
		//err = errors.New("read pkg body error") //自定义错误
		return
	}

	//把pkgLen反序列化成->message.Message
	json.Unmarshal(this.Buf[:pkgLen], &mes)
	if err != nil {

		fmt.Println("json Unmarshal 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[:4])
	if n != 4 || err != nil {
		fmt.Println("conn.Write(bytes) fail=", err)
		return
	}

	//发送data本身
	n, err = this.Conn.Write(data)
	if n != int(pkgLen) || err != nil {
		fmt.Println("conn.Write(bytes) fail=", err)
		return
	}
	return
}

  • 运行

server.exe
在这里插入图片描述
client.exe
在这里插入图片描述

2.2.4 客户端结构改进

  • 示意图
    在这里插入图片描述
  • 代码实现

client\main\main.go

package main

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

//定义两个变量,一个表示用户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)

		//loop = false //不继续显示该菜单  //已经把最初的两级菜单合并到这了,所以此行没意义,可注释掉
		//然后 登录成功的二级菜单到了process\server.go里
		case 2:
			fmt.Println("注册用户")
			//loop = false //不继续显示该菜单
		case 3:
			fmt.Println("退出系统")
			//loop = false //不继续显示该菜单
			os.Exit(0)
		default:
			fmt.Println("你的输入有误,请重新输入")
		}
	}
}

client\process\server.go

package process

import (
	"chatroom/server/utils"
	"fmt"
	"net"
	"os"
)

//显示登录成功后的界面(菜单)
func ShowMenu() {
	fmt.Println("-----------恭喜xxx登陆成功----------")
	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("显示在线用户列表")
	case 2:
		fmt.Println("发送消息")
	case 3:
		fmt.Println("信息列表")
	case 4:
		fmt.Println("你选择退出系统...")
		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("tf.ReadPkg err=", err)
			return
		}
		//如果读取消息,又是下一步处理逻辑
		fmt.Printf("mes=%v\n", mes)
	}
}

client\process\userProcess.go

package process

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

type UserProcess struct {
	//暂时不需要任何字段
}

//原client\login.go 直接复制过来

//关联一个用户登录的方法
//写一个函数,完成登录
func (this *UserProcess) Login(userId int, userPwd string) (err error) {

	开始定协议
	//fmt.Printf("userId=%d userPwd=%s\n", userId, userPwd)
	//
	//return nil

	//1.链接到服务器
	conn, err := net.Dial("tcp", "localhost:8889")
	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.Marshalk err=", err)
		return
	}

	//5.把data赋给mes.Data字段
	mes.Data = string(data) //因为上面Marshal处理得到的data为byte型切片,所以需要转化为string类型

	//6.将mes进行序列化,data即客户端需要发送的数据
	data, err = json.Marshal(mes)
	if err != nil {
		fmt.Println("json.Marshal err=", err)
		return
	}

	//7.1 先把data的长度发送给服务器
	//先获取到data的长度->转成一个表示长度的byte切片
	//因为Write()括号内写入类型的问题,这边要先做一个转换
	var pkgLen uint32
	pkgLen = uint32(len(data))
	var buf [4]byte
	binary.BigEndian.PutUint32(buf[0:4], pkgLen)
	//发送长度
	n, err := conn.Write(buf[:4])
	if n != 4 || err != nil { //四个字节
		fmt.Println("conn.Write(buf) 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
	}

	//休眠20s
	//time.Sleep(10 * time.Second)
	//fmt.Println("休眠了20...")

	//这里还需要处理服务器端返回的消息

	//创建一个Transfer实例
	tf := &utils.Transfer{
		Conn: conn,
	}
	mes, err = tf.ReadPkg()

	if err != nil {
		fmt.Println("readPkg(conn) 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() //因为userProcess.go和server.go在同一个包里,所以可以直接调
		}

	} else if loginResMes.Code == 500 {
		fmt.Println(loginResMes.Error)
	}

	return
}

client\utils\utils.go

package utils //储存工具性质的函数
import (
	"fmt"
	"net"
)

//这里直接复制了server\utils\utils.go

import (
	"chatroom/common/message"
	"encoding/binary"
	"encoding/json"
)

//因为使用了分层加面向对象的思路,所以需要先将这些方法关联到结构体中
//用到的时候,直接创建实例,调用方法
//(其他文件中同理)
type Transfer struct {
	//分析它应该有哪些字段
	Conn net.Conn   //链接
	Buf  [8096]byte //传输时,使用的缓冲
}

//读消息
func (this *Transfer) ReadPkg() (mes message.Message, err error) {

	//buf := make([]byte, 8096)   //Transfer自带,这边就不用另外写了
	fmt.Println("读取客户端发送的数据...")
	//conn.Read 在conn没有被关闭的情况下,才会阻塞
	//如果客户端关闭了conn,则不会阻塞
	_, err = this.Conn.Read(this.Buf[:4])
	if err != nil { //这边返回的err为io.EOF
		//err = errors.New("read pkg header error") //自定义错误
		return
	}

	//因为Read()括号内写入类型的原因,这边需要根据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 { //这边返回的err为io.EOF
		//err = errors.New("read pkg body error") //自定义错误
		return
	}

	//把pkgLen反序列化成->message.Message
	json.Unmarshal(this.Buf[:pkgLen], &mes)
	if err != nil {

		fmt.Println("json Unmarshal 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[:4])
	if n != 4 || err != nil {
		fmt.Println("conn.Write(bytes) fail=", err)
		return
	}

	//发送data本身
	n, err = this.Conn.Write(data)
	if n != int(pkgLen) || err != nil {
		fmt.Println("conn.Write(bytes) fail=", err)
		return
	}
	return
}

  • 运行

server.exe
在这里插入图片描述
client.exe
在这里插入图片描述

2.3 Redis添加用户和用户登录

2.3.1 Redis用户登录分析

示意图
在这里插入图片描述

2.3.2 Redis手动添加测试用户

上一步骤中client\process\userProcess.go中的此行代码取消注释,在客户端输出查看用用户名:100,密码:123456登录时传递data的内容:

fmt.Printf("客户端,发送消息的长度%d,内容=%s", len(data), string(data))

在这里插入图片描述
在redis客户端手动添加此内容
在这里插入图片描述

2.3.3 用户登录

如输入的用户名密码正确,在Redis中存在则登录,否则退出系统,给出相应的提示信息:
1.用户不存在,你也可以重新注册再登录
2.你的密码不正确

  • 代码实现
    client\process\userProcess.go
package process

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

type UserProcess struct {
	//暂时不需要任何字段
}

//原client\login.go 直接复制过来

//关联一个用户登录的方法
//写一个函数,完成登录
func (this *UserProcess) Login(userId int, userPwd string) (err error) {

	开始定协议
	//fmt.Printf("userId=%d userPwd=%s\n", userId, userPwd)
	//
	//return nil

	//1.链接到服务器
	conn, err := net.Dial("tcp", "localhost:8889")
	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.Marshalk err=", err)
		return
	}

	//5.把data赋给mes.Data字段
	mes.Data = string(data) //因为上面Marshal处理得到的data为byte型切片,所以需要转化为string类型

	//6.将mes进行序列化,data即客户端需要发送的数据
	data, err = json.Marshal(mes)
	if err != nil {
		fmt.Println("json.Marshal err=", err)
		return
	}

	//7.1 先把data的长度发送给服务器
	//先获取到data的长度->转成一个表示长度的byte切片
	//因为Write()括号内写入类型的问题,这边要先做一个转换
	var pkgLen uint32
	pkgLen = uint32(len(data))
	var buf [4]byte
	binary.BigEndian.PutUint32(buf[0:4], pkgLen)
	//发送长度
	n, err := conn.Write(buf[:4])
	if n != 4 || err != nil { //四个字节
		fmt.Println("conn.Write(buf) 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
	}

	//休眠20s
	//time.Sleep(10 * time.Second)
	//fmt.Println("休眠了20...")

	//这里还需要处理服务器端返回的消息

	//创建一个Transfer实例
	tf := &utils.Transfer{
		Conn: conn,
	}
	mes, err = tf.ReadPkg()

	if err != nil {
		fmt.Println("readPkg(conn) 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() //因为userProcess.go和server.go在同一个包里,所以可以直接调
		}

	} else {
		fmt.Println(loginResMes.Error)
	}

	return
}

server\main\main.go

package main

import (
	"chatroom/server/model"
	"fmt"
	"net"
	"time"
)

//处理客户端的通讯
func process(conn net.Conn) {
	//这里需要延时关闭conn
	defer conn.Close()

	//这里调用总控,创建一个Processor实例
	processor := &Processor{
		Conn: conn,
	}
	err := processor.process2()
	if err != nil {
		fmt.Println("客户端和服务端通讯协程错误err=", err)
		return
	}
}

func Init() {
	//当服务器启动时,我们就去初始化我们的redis的连接池
	initPool("localhost:6379", 16, 0, 300*time.Second)
	initUserDao()
}

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

func main() {
	//当服务器启动时,我们就去初始化我们的redis的连接池
	//initPool("localhost:6379", 16, 0, 300*time.Second)
	//initUserDao()

	Init()
	
	//提示信息
	fmt.Println("服务器[新的结构]在8889端口监听...")
	listen, err := net.Listen("tcp", "0.0.0.0:8889")
	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("listen.Accept err=", err)
			//这里不用return 因为可能一个客户端链接出错,其他没出错
		}

		//一旦链接成功,则启动一个协程和客户端保持通讯
		go process(conn)
	}
}

server\main\redis.go

package main

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

//定义一个全局的pool
var pool *redis.Pool

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

	pool = &redis.Pool{
		MaxIdle:     maxIdel,     //最大空闲链接数
		MaxActive:   maxActive,   //表示和数据库的最大连接数,0表示没有限制
		IdleTimeout: idleTimeout, //最大空闲时间
		Dial: func() (redis.Conn, error) { //初始化链接的代码,链接哪个ip的redis
			return redis.Dial("tcp", address)
		},
	}
}

server\model\error.go

package model

import "errors"

//根据业务逻辑的需要,自定义一些错误

var (
	ERROR_USER_NOTEXISTS = errors.New("用户不存在..")
	ERROR_USER_EXISTS    = errors.New("用户已经存在..")
	ERROR_USER_PWD       = errors.New("密码不正确")
)

server\model\user.go

package model

//定义一个用户的结构体

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

server\model\userDao.go

package model

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

//我们在服务器启动后,就初始化一个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, //传参pool赋值给UserDao实例userDao的pool
	}
	return
}

//1.根据用户id返回一个User实例+err
func (this *UserDao) getUserById(conn redis.Conn, id int) (user *User, err error) {

	//通过给定id去redis查询这个用户
	res, err := redis.String(conn.Do("HGet", "users", id)) //找到对应id,返回
	if err != nil {
		//错误!
		if err == redis.ErrNil { //表示在users哈希表中,没有找到对应id
			err = ERROR_USER_NOTEXISTS //用户不存在
		}
		return
	}

	user = &User{}

	//这里我们需要把res反序列化成User实例user
	err = json.Unmarshal([]byte(res), user)
	if err != nil {
		fmt.Println("json.Unmarshal err=", err) //未知错误
		return
	}
	return
}

//完成登录的校验
//1.Login完成对用户的验证
//2.如果用户的id和pwd都正确,则返回一个User实例
//3.如果用户的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) //用户的id和pwd都正确,返回一个User实例user
	if err != nil {
		return
	}
	if user.UserPwd != userPwd {
		err = ERROR_USER_PWD //密码错误
		return
	}
	return
}

server\process\userProcess.go

package process2

//因为main包有一个函数名为process ,此处如果包为process和函数同名会有问题
import (
	"chatroom/common/message"
	"chatroom/server/model"
	"chatroom/server/utils"
	"encoding/json"
	"fmt"
	"net"
)

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.Unmarshal fail err=", err)
		return
	}

	//1.先声明一个resMes
	var resMes message.Message
	resMes.Type = message.LoginResMesType

	//2.再声明一个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_NOTEXISTS { //用户不存在
			loginResMes.Code = 500
			loginResMes.Error = err.Error()
		} else if err == model.ERROR_USER_PWD { //密码不正确
			loginResMes.Code = 403
			loginResMes.Error = err.Error()
		} else {
			loginResMes.Code = 505
			loginResMes.Error = "服务器内部错误..."
		}
		//loginResMes.Code = 500
		//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 = "该用户不存在,请注册再使用"
	//}

	//3.将loginRes序列化
	data, err := json.Marshal(loginResMes)
	if err != nil {
		fmt.Println("json.Marshal fail=", err)
		return
	}

	//4.将data赋值给resMes
	resMes.Data = string(data)

	//5.对resMes进行序列化,准备发送
	data, err = json.Marshal(resMes)
	if err != nil {
		fmt.Println("json.Marshal fail=", err)
		return
	}

	//6.发送data 将其封装到writePkg函数
	//因为使用分层模式,我们先创建一个Transfer实例,然后使用writePkg读取信息
	tf := &utils.Transfer{
		Conn: this.Conn, //此处不需要用到Buf,就不用写了
	}

	err = tf.WritePkg(data)
	return
}

  • 运行

server.exe
在这里插入图片描述

client.exe
在这里插入图片描述

2.4 用户注册

完成注册功能,将用户信息录入到Redis中
在这里插入图片描述
common\message\user.go

package message

//定义一个用户的结构体

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

common\message\message.go

package message

const (
	LoginMesType       = "LoginMes"
	LoginResMesType    = "LoginResMes"
	RegisterMesType    = "RegisterMes"
	RegisterResMesType = "RegisterResMes"
)

//因为发送的消息要用结构体,要用json.Marshal序列化传递输出,所以变量名称都首写大写 (笔记见:G:\GoLand Code\tutorial project 1\src\test_18\JSON处理(通过结构、map生成json).go)
//但是函数传递的时候参数为小写,所以此处都做了json处理,改为首写小写

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

type RegisterMes struct {
	User User `json:"user"` //类型是User结构体
}

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

client\process\userProcess.go

package process

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

type UserProcess struct {
	//暂时不需要任何字段
}

func (this *UserProcess) Register(userId int, userPwd string, userName string) (err error) {

	//1.链接到服务器
	conn, err := net.Dial("tcp", "localhost:8889")
	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
	}

	//6.把data赋给mes.Data字段
	mes.Data = string(data)

	//6.将mes进行序列化,data即客户端需要发送的数据
	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(conn) err=", err)
		return
	}

	//将mes的Data部分反序列化成RegisterResMes
	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\login.go 直接复制过来

//关联一个用户登录的方法
//写一个函数,完成登录
func (this *UserProcess) Login(userId int, userPwd string) (err error) {

	开始定协议
	//fmt.Printf("userId=%d userPwd=%s\n", userId, userPwd)
	//
	//return nil

	//1.链接到服务器
	conn, err := net.Dial("tcp", "localhost:8889")
	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) //因为上面Marshal处理得到的data为byte型切片,所以需要转化为string类型

	//6.将mes进行序列化,data即客户端需要发送的数据
	data, err = json.Marshal(mes)
	if err != nil {
		fmt.Println("json.Marshal err=", err)
		return
	}

	//7.1 先把data的长度发送给服务器
	//先获取到data的长度->转成一个表示长度的byte切片
	//因为Write()括号内写入类型的问题,这边要先做一个转换
	var pkgLen uint32
	pkgLen = uint32(len(data))
	var buf [4]byte
	binary.BigEndian.PutUint32(buf[0:4], pkgLen)
	//发送长度
	n, err := conn.Write(buf[:4])
	if n != 4 || err != nil { //四个字节
		fmt.Println("conn.Write(buf) 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
	}

	//休眠20s
	//time.Sleep(10 * time.Second)
	//fmt.Println("休眠了20...")

	//这里还需要处理服务器端返回的消息

	//创建一个Transfer实例
	tf := &utils.Transfer{
		Conn: conn,
	}
	mes, err = tf.ReadPkg()

	if err != nil {
		fmt.Println("readPkg(conn) 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() //因为userProcess.go和server.go在同一个包里,所以可以直接调
		}

	} else {
		fmt.Println(loginResMes.Error)
	}

	return
}

client\main\main.go

package main

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

//定义两个变量,一个表示用户id, 一个表示用户密码
var userId int
var userPwd string
var userName 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)

		//loop = false //不继续显示该菜单  //已经把最初的两级菜单合并到这了,所以此行没意义,可注释掉
		//然后 登录成功的二级菜单到了process\server.go里
		case 2:
			fmt.Println("注册用户")
			//loop = false //不继续显示该菜单
			fmt.Println("请输入用户id:")
			fmt.Scanf("%d\n", &userId)
			fmt.Println("请输入用户密码:")
			fmt.Scanf("%s\n", &userPwd)
			fmt.Println("请输入用户名字(nickname):")
			fmt.Scanf("%s\n", &userName)
			//2.调用UserProcess,完成注册的请求,注册由服务器完成
			up := &process.UserProcess{}
			up.Register(userId, userPwd, userName)

		case 3:
			fmt.Println("退出系统")
			//loop = false //不继续显示该菜单
			os.Exit(0)
		default:
			fmt.Println("你的输入有误,请重新输入")
		}
	}
}

server\model\userDao

package model

import (
	"chatroom/common/message"
	"encoding/json"
	"fmt"
	"github.com/garyburd/redigo/redis"
)

//我们在服务器启动后,就初始化一个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, //传参pool赋值给UserDao实例userDao的pool
	}
	return
}

//1.根据用户id返回一个User实例+err
func (this *UserDao) getUserById(conn redis.Conn, id int) (user *User, err error) {

	//通过给定id去redis查询这个用户
	res, err := redis.String(conn.Do("HGet", "users", id)) //找到对应id,返回
	if err != nil {
		//错误!
		if err == redis.ErrNil { //表示在users哈希表中,没有找到对应id
			err = ERROR_USER_NOTEXISTS //用户不存在
		}
		return
	}

	user = &User{}

	//这里我们需要把res反序列化成User实例user
	err = json.Unmarshal([]byte(res), user)
	if err != nil {
		fmt.Println("json.Unmarshal err=", err) //未知错误
		return
	}
	return
}

//完成登录的校验
//1.Login完成对用户的验证
//2.如果用户的id和pwd都正确,则返回一个User实例
//3.如果用户的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) //用户的id和pwd都正确,返回一个User实例user
	if err != nil {
		return
	}
	if user.UserPwd != userPwd {
		err = ERROR_USER_PWD //密码错误
		return
	}
	return
}

//判断用户是否存在,如果不存在则完成注册
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为空,则说明该用户存在
		err = ERROR_USER_EXISTS
		return
	}

	//如果id不为空
	//这时,说明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

package process2

//因为main包有一个函数名为process ,此处如果包为process和函数同名会有问题
import (
	"chatroom/common/message"
	"chatroom/server/model"
	"chatroom/server/utils"
	"encoding/json"
	"fmt"
	"net"
)

type UserProcess struct {
	//字段
	Conn net.Conn
}

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.Unmarshal fail err=", err)
		return
	}

	//1.先声明一个resMes
	var resMes message.Message
	resMes.Type = message.RegisterResMesType

	//2.再声明一个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
	}

	//3.将loginRes序列化
	data, err := json.Marshal(registerResMes)
	if err != nil {
		fmt.Println("json.Marshal fail=", err)
		return
	}

	//4.将data赋值给resMes
	resMes.Data = string(data)

	//5.对resMes进行序列化,准备发送
	data, err = json.Marshal(resMes)
	if err != nil {
		fmt.Println("json.Marshal fail=", err)
		return
	}

	//6.发送data 将其封装到writePkg函数
	//因为使用分层模式,我们先创建一个Transfer实例,然后使用writePkg读取信息
	tf := &utils.Transfer{
		Conn: this.Conn, //此处不需要用到Buf,就不用写了
	}

	err = tf.WritePkg(data)
	return

}

//处理用户登录
//编写一个函数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.Unmarshal fail err=", err)
		return
	}

	//1.先声明一个resMes
	var resMes message.Message
	resMes.Type = message.LoginResMesType

	//2.再声明一个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_NOTEXISTS { //用户不存在
			loginResMes.Code = 500
			loginResMes.Error = err.Error()
		} else if err == model.ERROR_USER_PWD { //密码不正确
			loginResMes.Code = 403
			loginResMes.Error = err.Error()
		} else {
			loginResMes.Code = 505
			loginResMes.Error = "服务器内部错误..."
		}
		//loginResMes.Code = 500
		//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 = "该用户不存在,请注册再使用"
	//}

	//3.将loginRes序列化
	data, err := json.Marshal(loginResMes)
	if err != nil {
		fmt.Println("json.Marshal fail=", err)
		return
	}

	//4.将data赋值给resMes
	resMes.Data = string(data)

	//5.对resMes进行序列化,准备发送
	data, err = json.Marshal(resMes)
	if err != nil {
		fmt.Println("json.Marshal fail=", err)
		return
	}

	//6.发送data 将其封装到writePkg函数
	//因为使用分层模式,我们先创建一个Transfer实例,然后使用writePkg读取信息
	tf := &utils.Transfer{
		Conn: this.Conn, //此处不需要用到Buf,就不用写了
	}

	err = tf.WritePkg(data)
	return
}

server\main\processor.go

package main

import (
	"chatroom/common/message"
	process2 "chatroom/server/process" //如果文件夹名和包名不一样 导入时路径前面会自动加一个包名
	"chatroom/server/utils"
	"fmt"
	"io"
	"net"
)

//此文件总控服务器,完成调度

//先创建一个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{ //这边因为调用process2包和定义process2函数都在这个源文件里,所以不冲突?
			Conn: this.Conn,
		}
		err = up.ServerProcessLogin(mes)
	case message.RegisterMesType:
		//处理注册
		up := &process2.UserProcess{
			Conn: this.Conn,
		}
		err = up.ServerProcessRegister(mes)
	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 { //即readPkg中conn.Read的err
				fmt.Println("客户端退出,服务器也正常退出...")
				return err
			} else {
				fmt.Println("readPkg err=", err) //其他错误信息
				return err
			}
		}
		//fmt.Println("mes=", mes)

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

  • 运行
    server.exe
    在这里插入图片描述
    client.exe
    在这里插入图片描述
    redis-cli.exe
    在这里插入图片描述
    【若需要重新注册,可在redis-cli.exe输入flushdb可删除当前所有users,重新注册】

2.5显示在线用户列表

2.5.1 功能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) AddOnlineUser(up *UserProcess) {
	this.onlineUsers[up.UserId] = up
}

//删除
func (this *UserMgr) DelOnlineUser(userId int) {
	delete(this.onlineUsers, userId)
}

//返回当前所有在线的用户(显示当前在线用户列表)
func (this *UserMgr) GetAllOnlineUser() map[int]*UserProcess {
	return this.onlineUsers
}

//根据id返回对应的值,用于后续点对点聊天等等
func (this *UserMgr) GetOnlineUserById(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

package process2

//因为main包有一个函数名为process ,此处如果包为process和函数同名会有问题
import (
	"chatroom/common/message"
	"chatroom/server/model"
	"chatroom/server/utils"
	"encoding/json"
	"fmt"
	"net"
)

type UserProcess struct {
	//字段
	Conn net.Conn
	//增加一个字段,表示该Conn是哪个用户的
	UserId int
}

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.Unmarshal fail err=", err)
		return
	}

	//1.先声明一个resMes
	var resMes message.Message
	resMes.Type = message.RegisterResMesType

	//2.再声明一个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
	}

	//3.将loginRes序列化
	data, err := json.Marshal(registerResMes)
	if err != nil {
		fmt.Println("json.Marshal fail=", err)
		return
	}

	//4.将data赋值给resMes
	resMes.Data = string(data)

	//5.对resMes进行序列化,准备发送
	data, err = json.Marshal(resMes)
	if err != nil {
		fmt.Println("json.Marshal fail=", err)
		return
	}

	//6.发送data 将其封装到writePkg函数
	//因为使用分层模式,我们先创建一个Transfer实例,然后使用writePkg读取信息
	tf := &utils.Transfer{
		Conn: this.Conn, //此处不需要用到Buf,就不用写了
	}

	err = tf.WritePkg(data)
	return

}

//处理用户登录
//编写一个函数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.Unmarshal fail err=", err)
		return
	}

	//1.先声明一个resMes
	var resMes message.Message
	resMes.Type = message.LoginResMesType

	//2.再声明一个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_NOTEXISTS { //用户不存在
			loginResMes.Code = 500
			loginResMes.Error = err.Error()
		} else if err == model.ERROR_USER_PWD { //密码不正确
			loginResMes.Code = 403
			loginResMes.Error = err.Error()
		} else {
			loginResMes.Code = 505
			loginResMes.Error = "服务器内部错误..."
		}
		//loginResMes.Code = 500
		//loginResMes.Error = "该用户不存在,请注册再使用"
		这里我们先测试成功,然后再可以返回具体错误信息
	} else {
		loginResMes.Code = 200
		//这里,因为用户登录成功,我们就把该登录成功的用户放入到userMgr中
		//将登录成功的用户的userId赋给this
		this.UserId = loginMes.UserId
		userMgr.AddOnlineUser(this)
		//将当前在线用户的id 放入到loginResMes.UsersId
		//遍历userMgr.onlineUsers
		for id, _ := range userMgr.onlineUsers {
			loginResMes.UsersId = append(loginResMes.UsersId, id)
		}

		fmt.Println(user, "登录成功")
	}

	如果用户id=100,密码=123456,认为合法,否则不合法
	//if loginMes.UserId == 100 && loginMes.UserPwd == "123456" {
	//	//合法
	//	loginResMes.Code = 200
	//} else {
	//	//不合法
	//	loginResMes.Code = 500 //500状态码,表示该用户不存在
	//	loginResMes.Error = "该用户不存在,请注册再使用"
	//}

	//3.将loginRes序列化
	data, err := json.Marshal(loginResMes)
	if err != nil {
		fmt.Println("json.Marshal fail=", err)
		return
	}

	//4.将data赋值给resMes
	resMes.Data = string(data)

	//5.对resMes进行序列化,准备发送
	data, err = json.Marshal(resMes)
	if err != nil {
		fmt.Println("json.Marshal fail=", err)
		return
	}

	//6.发送data 将其封装到writePkg函数
	//因为使用分层模式,我们先创建一个Transfer实例,然后使用writePkg读取信息
	tf := &utils.Transfer{
		Conn: this.Conn, //此处不需要用到Buf,就不用写了
	}

	err = tf.WritePkg(data)
	return
}

common\message\message.go

package message

const (
	LoginMesType       = "LoginMes"
	LoginResMesType    = "LoginResMes"
	RegisterMesType    = "RegisterMes"
	RegisterResMesType = "RegisterResMes"
)

//因为发送的消息要用结构体,要用json.Marshal序列化传递输出,所以变量名称都首写大写 (笔记见:G:\GoLand Code\tutorial project 1\src\test_18\JSON处理(通过结构、map生成json).go)
//但是函数传递的时候参数为小写,所以此处都做了json处理,改为首写小写

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表示登陆成功
	UsersId []int  //增加字段,保存用户id的切片
	Error   string `json:"error"` //返回错误信息
}

type RegisterMes struct {
	User User `json:"user"` //类型是User结构体
}

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

client\process\userProcess.go

package process

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

type UserProcess struct {
	//暂时不需要任何字段
}

func (this *UserProcess) Register(userId int, userPwd string, userName string) (err error) {

	//1.链接到服务器
	conn, err := net.Dial("tcp", "localhost:8889")
	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
	}

	//6.把data赋给mes.Data字段
	mes.Data = string(data)

	//6.将mes进行序列化,data即客户端需要发送的数据
	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(conn) err=", err)
		return
	}

	//将mes的Data部分反序列化成RegisterResMes
	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\login.go 直接复制过来

//关联一个用户登录的方法
//写一个函数,完成登录
func (this *UserProcess) Login(userId int, userPwd string) (err error) {

	开始定协议
	//fmt.Printf("userId=%d userPwd=%s\n", userId, userPwd)
	//
	//return nil

	//1.链接到服务器
	conn, err := net.Dial("tcp", "localhost:8889")
	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) //因为上面Marshal处理得到的data为byte型切片,所以需要转化为string类型

	//6.将mes进行序列化,data即客户端需要发送的数据
	data, err = json.Marshal(mes)
	if err != nil {
		fmt.Println("json.Marshal err=", err)
		return
	}

	//7.1 先把data的长度发送给服务器
	//先获取到data的长度->转成一个表示长度的byte切片
	//因为Write()括号内写入类型的问题,这边要先做一个转换
	var pkgLen uint32
	pkgLen = uint32(len(data))
	var buf [4]byte
	binary.BigEndian.PutUint32(buf[0:4], pkgLen)
	//发送长度
	n, err := conn.Write(buf[:4])
	if n != 4 || err != nil { //四个字节
		fmt.Println("conn.Write(buf) 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
	}

	//休眠20s
	//time.Sleep(10 * time.Second)
	//fmt.Println("休眠了20...")

	//这里还需要处理服务器端返回的消息

	//创建一个Transfer实例
	tf := &utils.Transfer{
		Conn: conn,
	}
	mes, err = tf.ReadPkg()

	if err != nil {
		fmt.Println("readPkg(conn) err=", err)
		return
	}

	//将mes的Data部分反序列化成LoginResMes
	var loginResMes message.LoginResMes
	err = json.Unmarshal([]byte(mes.Data), &loginResMes)
	if loginResMes.Code == 200 {
		//fmt.Println("登录成功")

		//可以显示当前在线用户列标配,遍历loginResMes.UserId
		fmt.Println("当前在线用户列表如下:")
		for _, v := range loginResMes.UsersId {

			//如果我们要求不显示自己在线,可以增加以下代码
			if v == userId {
				continue
			}

			fmt.Println("用户id\t", v)
		}
		fmt.Print("\n\n")

		//这里我们还需要在客户端启动一个协程
		//该协程保持和服务器端的通讯,如果服务器有数据推送给客户端
		//则接收并显示在客户端的终端
		go serverProcessMes(conn)

		//1.显示我们登录成功的菜单(循环显示)...
		for {
			ShowMenu() //因为userProcess.go和server.go在同一个包里,所以可以直接调
		}

	} else {
		fmt.Println(loginResMes.Error)
	}

	return
}

2.5.2 功能2:当一个新的用户上线后,其他已经登录的用户也能获得最新的在线用户列表

  • 示意图
    在这里插入图片描述
  • 代码实现

server\process\userProcess.go

package process2

//因为main包有一个函数名为process ,此处如果包为process和函数同名会有问题
import (
	"chatroom/common/message"
	"chatroom/server/model"
	"chatroom/server/utils"
	"encoding/json"
	"fmt"
	"net"
)

type UserProcess struct {
	//字段
	Conn net.Conn
	//增加一个字段,表示该Conn是哪个用户的
	UserId int
}

//这利编写通知所有在线用户的方法
//userId要通知其他的在线用户,我上线了
func (this *UserProcess) NotifyOthersOnlineUser(userId int) {

	//遍历onlineUsers,然后一个一个的发送NotifyUserStatusMes
	for id, up := range userMgr.onlineUsers { //onlineUsers为全局变量,所以需要用包引,不能用this
		//过滤自己
		if id == userId {
			continue
		}
		//开始通知【单独写一个方法】
		up.NotifyMeOnline(userId)
	}
}

func (this *UserProcess) NotifyMeOnline(userId int) { //这里的this代表上面的up

	//组装我们的NotifyUserStatusMes
	var mes message.Message
	mes.Type = message.NotifyUserStatusMesType

	var notifyUserStatusMes message.NotifyUserStatusMes
	notifyUserStatusMes.UserId = userId
	notifyUserStatusMes.Status = message.UserOnline

	//将notifyUserStatusMes序列化
	data, err := json.Marshal(notifyUserStatusMes)
	if err != nil {
		fmt.Println("json.Marshal err=", err)
	}

	//将序列化后的notifyUserStatusMes赋值给mes.Data
	mes.Data = string(data)

	//对mes再次序列化,准备发送
	data, err = json.Marshal(mes)
	if err != nil {
		fmt.Println("json.Marshal err=", err)
	}

	//发送,创建我们的Transfer实例,发送
	tf := &utils.Transfer{
		Conn: this.Conn,
	}

	err = tf.WritePkg(data)
	if err != nil {
		fmt.Println("NotifyMeOnline err=", err)
		return
	}

}

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.Unmarshal fail err=", err)
		return
	}

	//1.先声明一个resMes
	var resMes message.Message
	resMes.Type = message.RegisterResMesType

	//2.再声明一个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
	}

	//3.将loginRes序列化
	data, err := json.Marshal(registerResMes)
	if err != nil {
		fmt.Println("json.Marshal fail=", err)
		return
	}

	//4.将data赋值给resMes
	resMes.Data = string(data)

	//5.对resMes进行序列化,准备发送
	data, err = json.Marshal(resMes)
	if err != nil {
		fmt.Println("json.Marshal fail=", err)
		return
	}

	//6.发送data 将其封装到writePkg函数
	//因为使用分层模式,我们先创建一个Transfer实例,然后使用writePkg读取信息
	tf := &utils.Transfer{
		Conn: this.Conn, //此处不需要用到Buf,就不用写了
	}

	err = tf.WritePkg(data)
	return

}

//处理用户登录
//编写一个函数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.Unmarshal fail err=", err)
		return
	}

	//1.先声明一个resMes
	var resMes message.Message
	resMes.Type = message.LoginResMesType

	//2.再声明一个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_NOTEXISTS { //用户不存在
			loginResMes.Code = 500
			loginResMes.Error = err.Error()
		} else if err == model.ERROR_USER_PWD { //密码不正确
			loginResMes.Code = 403
			loginResMes.Error = err.Error()
		} else {
			loginResMes.Code = 505
			loginResMes.Error = "服务器内部错误..."
		}
		//loginResMes.Code = 500
		//loginResMes.Error = "该用户不存在,请注册再使用"
		这里我们先测试成功,然后再可以返回具体错误信息
	} else {
		loginResMes.Code = 200
		//这里,因为用户登录成功,我们就把该登录成功的用户放入到userMgr中
		//将登录成功的用户的userId赋给this
		this.UserId = loginMes.UserId
		userMgr.AddOnlineUser(this)

		//通知其它在线用户,我上线了
		this.NotifyOthersOnlineUser(loginMes.UserId)

		//将当前在线用户的id 放入到loginResMes.UsersId
		//遍历userMgr.onlineUsers
		for id, _ := range userMgr.onlineUsers {
			loginResMes.UsersId = append(loginResMes.UsersId, id)
		}

		fmt.Println(user, "登录成功")
	}

	如果用户id=100,密码=123456,认为合法,否则不合法
	//if loginMes.UserId == 100 && loginMes.UserPwd == "123456" {
	//	//合法
	//	loginResMes.Code = 200
	//} else {
	//	//不合法
	//	loginResMes.Code = 500 //500状态码,表示该用户不存在
	//	loginResMes.Error = "该用户不存在,请注册再使用"
	//}

	//3.将loginRes序列化
	data, err := json.Marshal(loginResMes)
	if err != nil {
		fmt.Println("json.Marshal fail=", err)
		return
	}

	//4.将data赋值给resMes
	resMes.Data = string(data)

	//5.对resMes进行序列化,准备发送
	data, err = json.Marshal(resMes)
	if err != nil {
		fmt.Println("json.Marshal fail=", err)
		return
	}

	//6.发送data 将其封装到writePkg函数
	//因为使用分层模式,我们先创建一个Transfer实例,然后使用writePkg读取信息
	tf := &utils.Transfer{
		Conn: this.Conn, //此处不需要用到Buf,就不用写了
	}

	err = tf.WritePkg(data)
	return
}

common\message\message.go

package message

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

//因为发送的消息要用结构体,要用json.Marshal序列化传递输出,所以变量名称都首写大写 (笔记见:G:\GoLand Code\tutorial project 1\src\test_18\JSON处理(通过结构、map生成json).go)
//但是函数传递的时候参数为小写,所以此处都做了json处理,改为首写小写

//这里我们定义几个用户状态的常量
const (
	UserOnline = iota
	UserOffline
	UserBusyStatus
)

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表示登陆成功
	UsersId []int  //增加字段,保存用户id的切片
	Error   string `json:"error"` //返回错误信息
}

type RegisterMes struct {
	User User `json:"user"` //类型是User结构体
}

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

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

client\process\userMgr.go

package process

import (
	"chatroom/common/message"
	"fmt"
)

//客户端要维护的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

}

client\process\server.go

package process

import (
	"chatroom/common/message"
	"chatroom/server/utils"
	"encoding/json"
	"fmt"
	"net"
	"os"
)

//显示登录成功后的界面(菜单)
func ShowMenu() {
	fmt.Println("-----------恭喜xxx登陆成功----------")
	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("显示在线用户列表")
		outputOnlineUser()
	case 2:
		fmt.Println("发送消息")
	case 3:
		fmt.Println("信息列表")
	case 4:
		fmt.Println("你选择退出系统...")
		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("tf.ReadPkg err=", err)
			return
		}
		//如果读取消息,又是下一步处理逻辑
		switch mes.Type {
		case message.NotifyUserStatusMesType: //有人上线了

			//1.取出NotifyUserStatusMes
			var notifyUserStatusMes message.NotifyUserStatusMes
			json.Unmarshal([]byte(mes.Data), &notifyUserStatusMes)
			//2.把这个用户的信息,状态保存到客户map[int]User中
			updateUserStatus(&notifyUserStatusMes)
			//处理
		default:
			fmt.Println("服务器端返回了未知的消息类型")

		}

		//fmt.Printf("mes=%v\n", mes)

	}
}
  • 运行

server.exe
在这里插入图片描述
client.exe
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.6 客户端发消息

  • 示意图
    在这里插入图片描述
  • 代码实现

common\message\message.go

package message

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

//因为发送的消息要用结构体,要用json.Marshal序列化传递输出,所以变量名称都首写大写 (笔记见:G:\GoLand Code\tutorial project 1\src\test_18\JSON处理(通过结构、map生成json).go)
//但是函数传递的时候参数为小写,所以此处都做了json处理,改为首写小写

//这里我们定义几个用户状态的常量
const (
	UserOnline = iota
	UserOffline
	UserBusyStatus
)

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表示登陆成功
	UsersId []int  //增加字段,保存用户id的切片
	Error   string `json:"error"` //返回错误信息
}

type RegisterMes struct {
	User User `json:"user"` //类型是User结构体
}

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

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

//增加一个SmsMes  //发送的消息(客户端)
type SmsMes struct {
	Content string `json:"content"` //内容
	User           //因为在同一个包里,这边直接用匿名的结构体,继承
}

//SmsReMes //直接用服务器转发消息实现

client\model\curUser.go

package model

import (
	"chatroom/common/message"
	"net"
)

type CurUser struct { //当前发送消息的用户
	Conn         net.Conn
	message.User //匿名字段  //这边在终端对应某些部分会没有显示内容
}

client\process\smsProcess.go

package process

import (
	"chatroom/common/message"
	"chatroom/server/utils"
	"encoding/json"
	"fmt"
)

type SmsProcess struct {
}

//发送群发消息(广播)
func (this *SmsProcess) SendGroupMes(content string) (err error) {

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

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

	//3.序列化
	data, err := json.Marshal(smsMes)
	if err != nil {
		fmt.Println("sendGroupMes json.Marshal fail=", err.Error())
		return
	}

	mes.Data = string(data)

	//4.对mes再次序列化
	data, err = json.Marshal(mes)
	if err != nil {
		fmt.Println("sendGroupMes json.Marshal fail=", err.Error())
		return
	}

	//5.将mes发送给服务器
	tf := &utils.Transfer{
		Conn: CurUser.Conn,
	}

	//6.发送
	err = tf.WritePkg(data)
	if err != nil {
		fmt.Println("SendGroupMes err=", err.Error())
		return
	}
	return

}

server\main\processor.go增添如下代码:
在这里插入图片描述

  • 运行

server.exe
在这里插入图片描述
client.exe
在这里插入图片描述

2.7 服务器转发消息

服务器可以将接收到的消息,群发给所有在线用户(发送者除外)

  • 示意图
    在这里插入图片描述
  • 代码实现

server\process\smsProcess.go

package process2

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

type SmsProcess struct {
	//暂时不需要字段
}

//写方法转发消息
func (this *SmsProcess) SendGroupMes(mes *message.Message) {

	//遍历服务器端的onlineUsers map[int]*UserProcess,
	//将消息转发出去

	//取出mes的内容 SmsMes
	var smsMes message.SmsMes
	err := json.Unmarshal([]byte(mes.Data), &smsMes)
	if err != nil {
		fmt.Println("json.Unmarshal err=", err)
		return
	}

	data, err := json.Marshal(mes)
	if err != nil {
		fmt.Println("json.Marshal err=", err)
		return
	}

	for id, up := range userMgr.onlineUsers {
		//这里,还需要过滤掉自己
		if id == smsMes.UserId {
			continue
		}
		this.SendMesToEachOnlineUser(data, up.Conn)
	}
}

func (this *SmsProcess) SendMesToEachOnlineUser(data []byte, conn net.Conn) {

	//创建一个Transfer实例,发送data
	tf := &utils.Transfer{
		Conn: conn,
	}
	err := tf.WritePkg(data)
	if err != nil {
		fmt.Println("转发消息失败 err=", err)
	}

}

server\main\processor.go

package main

import (
	"chatroom/common/message"
	process2 "chatroom/server/process" //如果文件夹名和包名不一样 导入时路径前面会自动加一个包名
	"chatroom/server/utils"
	"fmt"
	"io"
	"net"
)

//此文件总控服务器,完成调度

//先创建一个Processor的结构体
type Processor struct {
	Conn net.Conn
}

//处理消息
//编写一个ServerProcessMes函数
//功能:根据客户端发送消息种类不同,决定调用哪个函数来处理
func (this *Processor) serverProcessMes(mes *message.Message) (err error) {

	//看看是否能接收到客户端发送的群发消息
	fmt.Println("mes=", mes)

	switch mes.Type {
	case message.LoginMesType:
		//处理登录
		//创建一个UserProcess实例
		up := &process2.UserProcess{ //这边因为调用process2包和定义process2函数都在这个源文件里,所以不冲突?
			Conn: this.Conn,
		}
		err = up.ServerProcessLogin(mes)
	case message.RegisterMesType:
		//处理注册
		up := &process2.UserProcess{
			Conn: this.Conn,
		}
		err = up.ServerProcessRegister(mes)
	case message.SmsMesType:
		//创建一个SmsProcess实例完成转发群聊消息
		smsProcess := &process2.SmsProcess{}
		smsProcess.SendGroupMes(mes)
	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 { //即readPkg中conn.Read的err
				fmt.Println("客户端退出,服务器也正常退出...")
				return err
			} else {
				fmt.Println("readPkg err=", err) //其他错误信息
				return err
			}
		}
		//fmt.Println("mes=", mes)

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

client\process\smsMgr.go

package process

import (
	"chatroom/common/message"
	"encoding/json"
	"fmt"
)

func outputGroupMes(mes *message.Message) { //这个地方mes一定为SmsMes
	//显示即可
	//1.反序列化
	var smsMes message.SmsMes
	err := json.Unmarshal([]byte(mes.Data), &smsMes)
	if err != nil {
		fmt.Println("json.Unmarshal err=", err)
		return
	}

	//显示信息
	info := fmt.Sprintf("用户id:\t%d 对大家说:\t%s", smsMes.UserId, smsMes.Content)
	fmt.Println(info)
	fmt.Println()

}

client\process\server.go

package process

import (
	"chatroom/common/message"
	"chatroom/server/utils"
	"encoding/json"
	"fmt"
	"net"
	"os"
)

//显示登录成功后的界面(菜单)
func ShowMenu() {
	fmt.Println("-----------恭喜xxx登陆成功----------")
	fmt.Println("-----------1.显示在线用户列表----------")
	fmt.Println("-----------2.发送消息----------")
	fmt.Println("-----------3.信息列表----------")
	fmt.Println("-----------4.退出系统----------")
	fmt.Println("请选择(1-4):")
	var key int
	var content string

	//因为总会使用到SmsProcess实例,因此将其定义在switch外部
	SmsProcess := &SmsProcess{}

	fmt.Scanf("%d\n", &key)
	switch key {
	case 1:
		fmt.Println("显示在线用户列表")
		outputOnlineUser()
	case 2:
		fmt.Println("你想对大家说点什么:")
		fmt.Scanf("%s\n", &content)
		SmsProcess.SendGroupMes(content)
	case 3:
		fmt.Println("信息列表")
	case 4:
		fmt.Println("你选择退出系统...")
		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("tf.ReadPkg err=", err)
			return
		}
		//如果读取消息,又是下一步处理逻辑
		switch mes.Type {
		case message.NotifyUserStatusMesType: //有人上线了

			//1.取出NotifyUserStatusMes
			var notifyUserStatusMes message.NotifyUserStatusMes
			json.Unmarshal([]byte(mes.Data), &notifyUserStatusMes)
			//2.把这个用户的信息,状态保存到客户map[int]User中
			updateUserStatus(&notifyUserStatusMes)
		//处理
		case message.SmsMesType: //有人群发消息
			outputGroupMes(&mes)
		default:
			fmt.Println("服务器端返回了未知的消息类型")

		}

		//fmt.Printf("mes=%v\n", mes)

	}
}

  • 运行

server.exe
在这里插入图片描述
client.exe
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


在这里插入图片描述

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值