尚硅谷 海量用户通讯系统
目录
一. 海量用户即时通讯系统需求
-
项目开发流程:
需求分析->设计阶段->编码实现->测试阶段->实施
-
需求分析
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), ®isterResMes)
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), ®isterMes)
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(®isterMes.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), ®isterMes)
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(®isterMes.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), ®isterResMes)
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), ®isterMes)
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(®isterMes.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), ¬ifyUserStatusMes)
//2.把这个用户的信息,状态保存到客户map[int]User中
updateUserStatus(¬ifyUserStatusMes)
//处理
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), ¬ifyUserStatusMes)
//2.把这个用户的信息,状态保存到客户map[int]User中
updateUserStatus(¬ifyUserStatusMes)
//处理
case message.SmsMesType: //有人群发消息
outputGroupMes(&mes)
default:
fmt.Println("服务器端返回了未知的消息类型")
}
//fmt.Printf("mes=%v\n", mes)
}
}
- 运行
server.exe
client.exe