项目展示
开始此项目之前请确保安装好redis,golang
源码下载:https://github.com/BeAlrightc/go-study.git
项目要保存用户信息和信息数据,因此我们需要学习数据(redis或者mysql),这里我们选择redis
代码编写
clien包下的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("输入有误,请输入1-3")
}
}
//根据用户的输入,显示新的提示信息
if key ==1 {
//说明用户要登录了
fmt.Println("请输入用户的id")
fmt.Scanf("%d\n",&userId)
fmt.Println("请输入用户的密码")
fmt.Scanf("%s\n",&userPwd)
//先把登录函数,写到另外一个文件,先写login.go
err := login(userId,userPwd)
if err != nil {
fmt.Println("登录失败")
}else {
fmt.Println("登录成功")
}
}else if key ==2 {
fmt.Println("进行用户注册的逻辑....")
}
}
clien包下的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
}
要求:完成指定用户的验证,用户id=100,密码pwd=123456可以登录,其他用户不能登录
理解从client到server中的程序执行流程,如图所示【Message组成的示意图。并发送一个message的流程介绍】
分析思路
1)先确定消息Message的格式
2)发送消息示意图
代码展示
sever
main.go
package main
import (
"fmt"
"net"
)
//处理和客户端的通讯
func process(conn net.Conn){
//这里需要延时关闭
defer conn.Close()
//循环地读客户端发送的信息
for {
buf := make([]byte,8096)
fmt.Println("读取客户端发送的数据...")
n, err :=conn.Read(buf[:4])
if n != 4 || 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)
}
//一旦连接成功,则则启动一个协程和客户端保持通讯。。
go process(conn)
}
}
common层的message
message.go
package message
const (
LoginMesType = "LoginMes"
LoginResMesType = "LoginResMes"
)
type Message struct {
Type string `json:"type"`//消息的类型
Data string `json:"data"`//消息的数据
}
//定义两个消息。。后面需要再添加
type LoginMes struct {
UserId int `json:"userId"`//用户Id
UserPwd string `json:"userPwd"`//用户密码
UserName string `json:"userName"`//用户名
}
type LoginResMes struct {
Code int `json:"code"`//返回状态码 500表示该用户未注册 200表示登录成功
Error string `json:"error"`//返回错误信息
}
client层
login.go
package main
import (
"fmt"
"net"
"encoding/json"
"encoding/binary"
"go_code/chatroom/common/message"
)
//写一个函数,完成登录操作
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.Mashal err=",err)
return
}
//5.将data赋给了mes.Data字段
mes.Data = string(data)
//6.将mes进行序列化
data, err =json.Marshal(mes)
if err != nil {
fmt.Println("json.Mashal err=",err)
return
}
//7.到这个时候,data就是我们要发送的消息
//7.1先把data的长度发送给服务器
//先获取data的长度->转成一个表示长度的byte切片
var pkgLen uint32
pkgLen = uint32(len(data))
var buf [4]byte
binary.BigEndian.PutUint32(buf[0:4],pkgLen) //将该、长度转成了byte类型是数据
//发送长度
n, err := conn.Write(buf[:4])
if n != 4 || err !=nil {
fmt.Println("connWrite(buf) fail ",err)
return
}
fmt.Printf("客户端发送数据的消息长度=%d 内容是=%s",len(data),string(data))
return
}
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("输入有误,请输入1-3")
}
}
//根据用户的输入,显示新的提示信息
if key ==1 {
//说明用户要登录了
fmt.Println("请输入用户的id")
fmt.Scanf("%d\n",&userId)
fmt.Println("请输入用户的密码")
fmt.Scanf("%s\n",&userPwd)
//先把登录函数,写到另外一个文件,先写login.go
err := login(userId,userPwd)
if err != nil {
fmt.Println("登录失败")
}else {
fmt.Println("登录成功")
}
}else if key ==2 {
fmt.Println("进行用户注册的逻辑....")
}
}
思路分析
1)让客户端发送消息本身
2)服务器端接收到消息,然后反序列化成对应的消息结构体
3)服务器端根据反序列化的消息,判断是否登录用户是合法,返回LoginReMes
4)客户端解析返回的LoginReMes,显示对应界面
5)这里我们需要做一些函数的封装
cient/login.go在结尾添加这些coding
//发送消息本身
_, err = conn.Write(data)
if err !=nil {
fmt.Println("connWrite(data) fail ",err)
return
}
//休眠20秒
time.Sleep(10 * time.Second)
fmt.Println("休眠了20秒..")
//这里还需要处理服务器端返回的消息
return
在server/main.go中我们做了以下改动
将读数据的过程封装了一个函数
package main
import (
"fmt"
"net"
"encoding/json"
"encoding/binary"
"go_code/chatroom/common/message"
//"errors"
"io"
)
func readPkg(conn net.Conn)(mes message.Message,err error){
buf := make([]byte,8096)
fmt.Println("读取客户端发送的数据...")
//conn.Read()只有在conn没有被关闭的情况下,才会阻塞
//如果客户端关闭conn则,就不会阻塞
_, err =conn.Read(buf[:4]) //read出buf中的数据
if err !=nil {
//fmt.Println("conn.Read err=",err)
//err = errors.New("read pkg header error")
return
}
//根据buf[:4]转成uint32类型
var pkgLen uint32
pkgLen=binary.BigEndian.Uint32(buf[0:4])
//根据pkgLen读取消息内容
n, err :=conn.Read(buf[:pkgLen])
if n != int(pkgLen) || err !=nil {
//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 process(conn net.Conn) {
//这里需要延时关闭
defer conn.Close()
//循环地读客户端发送的信息
for {
//这里我们将读取数据包,直接封装成一个函数readPkg(),返回Message,Err
mes, err :=readPkg(conn)
if err != nil {
if err == io.EOF {
fmt.Println("客户端退出,服务器端也退出...")
return
}else {
fmt.Println("readpkg err=",err)
}
return
}
fmt.Println("mes=",mes)
}
}
//main函数下的则没有改变
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)
}
//一旦连接成功,则则启动一个协程和客户端保持通讯。。
go process(conn)
}
}
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) //将该、长度转成了byte类型是数据
//发送长度
n, err := conn.Write(buf[:4])
if n != 4 || err !=nil {
fmt.Println("connWrite(buf) fail ",err)
return
}
//发送data本身
n, err = conn.Write(data)
if n != int(pkgLen) || err !=nil {
fmt.Println("connWrite(data) 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.将loginResMes 序列化
data, err := json.Marshal(loginResMes)
if err != nil {
fmt.Println("Marshal fail err=",err)
}
//4.将data赋值给resMes
resMes.Data = string(data)
//5.对resMes进行序列化,准备发送
data, err = json.Marshal(resMes)
if err != nil {
fmt.Println("Marshal fail err=",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
}
在process进行了改定
//处理和客户端的通讯
func process(conn net.Conn) {
//这里需要延时关闭
defer conn.Close()
//循环地读客户端发送的信息
for {
//这里我们将读取数据包,直接封装成一个函数readPkg(),返回Message,Err
mes, err :=readPkg(conn)
if err != nil {
if err == io.EOF {
fmt.Println("客户端退出,服务器端也退出...")
return
}else {
fmt.Println("readpkg err=",err)
}
return
}
//增加了这段代码进行调用这个函数
err = serverProcessMes(conn,&mes)
if err != nil {
return
}
}
}
client/utils(增加了一个utils.go用于read的write的操作)
package main
import (
"fmt"
"net"
"encoding/json"
"encoding/binary"
"go_code/chatroom/common/message"
)
func readPkg(conn net.Conn)(mes message.Message,err error){
buf := make([]byte,8096)
fmt.Println("读取客户端发送的数据...")
//conn.Read()只有在conn没有被关闭的情况下,才会阻塞
//如果客户端关闭conn则,就不会阻塞
_, err =conn.Read(buf[:4]) //先读取之前发送的数据长度
if err !=nil {
//fmt.Println("conn.Read err=",err)
//err = errors.New("read pkg header error")
return
}
//根据buf[:4]转成uint32类型
var pkgLen uint32
pkgLen=binary.BigEndian.Uint32(buf[0:4])
//根据pkgLen(data数据的长度)读取消息内容
n, err :=conn.Read(buf[:pkgLen])
if n != int(pkgLen) || err !=nil {
//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) //json的反序列化失败!
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) //将该、长度转成了byte类型是数据
//发送长度
n, err := conn.Write(buf[:4])
if n != 4 || err !=nil {
fmt.Println("connWrite(buf) fail ",err)
return
}
//发送data本身
n, err = conn.Write(data)
if n != int(pkgLen) || err !=nil {
fmt.Println("connWrite(data) fail ",err)
return
}
return
}
client/login.go
//在末尾加入了如下的代码
//这里还需要处理服务器端返回的消息
mes, err = readPkg(conn) //mes 就是
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
}
说明:前面的程序虽然完成了功能,但是没有结构,系统的可读性、拓展性和维护性都不好,因此需要对程序的结构进行改进
(1)先把分析出来的文件,创建好,然后放到相应的文件夹中
(2)现在根据各个文件完成的任务和作用不同,将main.go的代码剥离到对应的文件即可
(3)先修改了utils.go
package utils
import (
"fmt"
"net"
"encoding/json"
"encoding/binary"
"go_code/chatroom/common/message"
)
//将这些方法关联到结构体当中
type Transfer struct {
//分析应该有哪些字段
Conn net.Conn
Buf [8096]byte //这是传输时使用缓冲
}
func (this *Transfer) ReadPkg()(mes message.Message,err error){
fmt.Println("读取客户端发送的数据...")
//conn.Read()只有在conn没有被关闭的情况下,才会阻塞
//如果客户端关闭conn则,就不会阻塞
_, err =this.Conn.Read(this.Buf[:4]) //先读取之前发送的数据长度
if err !=nil {
//fmt.Println("conn.Read err=",err)
//err = errors.New("read pkg header error")
return
}
//根据buf[:4]转成uint32类型
var pkgLen uint32
pkgLen=binary.BigEndian.Uint32(this.Buf[0:4])
//根据pkgLen(data数据的长度)读取消息内容
n, err :=this.Conn.Read(this.Buf[:pkgLen])
if n != int(pkgLen) || err !=nil {
//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) //json的反序列化失败!
return
}
return
}
func (this *Transfer) WritePkg(data []byte)(err error) {
//先发送一个长度给对方
var pkgLen uint32
pkgLen = uint32(len(data))
binary.BigEndian.PutUint32(this.Buf[0:4],pkgLen) //将该、长度转成了byte类型是数据
//发送长度
n, err := this.Conn.Write(this.Buf[:4])
if n != 4 || err !=nil {
fmt.Println("connWrite(this.Buf) fail ",err)
return
}
//发送data本身
n, err = this.Conn.Write(data)
if n != int(pkgLen) || err !=nil {
fmt.Println("connWrite(data) fail ",err)
return
}
return
}
(4)修改了process2/userProcess.go
package process2
import (
"fmt"
"net"
"encoding/json"
"go_code/chatroom/common/message"
"go_code/chatroom/server/utils"
)
type UserProcess struct {
//字段
Conn net.Conn
}
//编写一个函数serverProcessLogin函数,专门处理登录请求
func (this *UserProcess) ServerProcessLogin(mes *message.Message)(err error){
//核心代码
//1.先从mes中取出mes.Data,并直接反序列化成LoginMes
var loginMes message.LoginMes
err =json.Unmarshal([]byte(mes.Data),&loginMes)
if err != nil {
fmt.Println("json.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.将loginResMes 序列化
data, err := json.Marshal(loginResMes)
if err != nil {
fmt.Println("Marshal fail err=",err)
}
//4.将data赋值给resMes
resMes.Data = string(data)
//5.对resMes进行序列化,准备发送
data, err = json.Marshal(resMes)
if err != nil {
fmt.Println("Marshal fail err=",err)
return
}
//6.发送data 我们将其封装为writePkg
//因为使用了分层模式(mvc),我们先创建一个Transfer实例,然后读取
tf := &utils.Transfer{
Conn : this.Conn,
}
err = tf.WritePkg(data)
return
}
(5)修改了main/processor.go
package main
import (
"fmt"
"net"
"go_code/chatroom/common/message"
"go_code/chatroom/server/utils"
"go_code/chatroom/server/process"
"io"
)
//先创建一个Processor的结构体
type Processor struct {
Conn net.Conn
}
//编写一个ServerProcessMes函数
//功能 :根据客户端发送的消息种类不同,决定调用哪个函数处理
func (this *Processor) serverProcessMes(mes *message.Message)(err error) {
switch mes.Type {
case message.LoginMesType :
//处理登录的逻辑
//创建一个UserProcess实例
up := &process2.UserProcess{
Conn : this.Conn,
}
err = up.ServerProcessLogin(mes)
case message.RegisterMesType :
//处理注册
default :
fmt.Println("消息类型不存在,无法处理...")
}
return
}
func (this *Processor) process2()(err error){
//循环地读客户端发送的信息
for {
//这里我们将读取数据包,直接封装成一个函数readPkg(),返回Message,Err
//创建一个Transfer实例完成读包任务
tf := &utils.Transfer{
Conn : this.Conn,
}
mes, err :=tf.ReadPkg()
if err != nil {
if err == io.EOF {
fmt.Println("客户端退出,服务器端也退出...")
return err
}else {
fmt.Println("readpkg err=",err)
}
return err
}
err = this.serverProcessMes(&mes)
if err != nil {
return err
}
}
}
修改了main/main.go
package main
import (
"fmt"
"net"
)
//处理和客户端的通讯
func process(conn net.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)
}
//一旦连接成功,则则启动一个协程和客户端保持通讯。。
go process(conn)
}
}
修改客户端。先画出程序的框架图,再写代码
(2)先把各个文件放到对应的文件夹[包]
(3)将server/utils.go拷贝到client/utils/utils.go
(4)创建了client/process/userProcess.go
package process
import (
"fmt"
"net"
"encoding/json"
"encoding/binary"
"go_code/chatroom/common/message"
"go_code/chatroom/client/utils"
)
type UserProcess struct {
//暂时不需要字段
}
//给关联一个用户登录的方法
//写一个函数,完成登录操作
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.Mashal err=",err)
return
}
//5.将data赋给了mes.Data字段
mes.Data = string(data)
//6.将mes进行序列化
data, err =json.Marshal(mes)
if err != nil {
fmt.Println("json.Mashal err=",err)
return
}
//7.到这个时候,data就是我们要发送的消息
//7.1先把data的长度发送给服务器
//先获取data的长度->转成一个表示长度的byte切片
var pkgLen uint32
pkgLen = uint32(len(data))
var buf [4]byte
binary.BigEndian.PutUint32(buf[0:4],pkgLen) //将该、长度转成了byte类型是数据
//发送长度
n, err := conn.Write(buf[:4])
if n != 4 || err !=nil {
fmt.Println("connWrite(buf) fail ",err)
return
}
//fmt.Printf("客户端发送数据的消息长度=%d 内容是=%s",len(data),string(data))
//发送消息本身
_, err = conn.Write(data)
if err !=nil {
fmt.Println("connWrite(data) fail ",err)
return
}
//休眠20秒
// time.Sleep(10 * time.Second)
// fmt.Println("休眠了20秒..")
//这里还需要处理服务器端返回的消息
//创建一个Transfer实例
tf := &utils.Transfer{
Conn : conn,
}
mes, err = tf.ReadPkg() //mes 就是
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()
}
}else if loginResMes.Code == 500 {
fmt.Println(loginResMes.Error)
}
return
}
说明:该文件就是在原来login.go做了一个改进,封装到userProcess结构体
(5)创建了server/process/server.go
package process
import (
"fmt"
"os"
"go_code/chatroom/client/utils"
"net"
)
//显示登录后的界面..
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.Printf("客户端正在等待读取服务器发送的消息")
mes, err:=tf.ReadPkg()
if err != nil {
fmt.Println("tf.ReadPkg err=",err)
return
}
//如果读取到消息,又是下一步处理逻辑
fmt.Printf("mes=%v",mes)
}
}
(6)client/main/main.go
package main
import (
"fmt"
"os"
"go_code/chatroom/client/process"
)
//定义两个变量,一个表示用户的id,一个表示用户的密码
var userId int
var userPwd string
func main() {
//接收用户的选择
var key int
//判断是否还继续显示菜单
// 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
case 2 :
fmt.Println("注册用户")
//loop=false
case 3 :
fmt.Println("退出系统")
//loop=false
os.Exit(0)
default:
fmt.Println("输入有误,请输入1-3")
}
}
}
手动直接在redis增加一个用户信息
package model
//定义一个用户的结构体
type User struct {
//确定字段信息
//为了序列化和反序列化成功
//用户信息的json字符串与结构体字段对应的Tag名字一致
UserId int `json:"userId"`
UserPwd string `json:"userPwd"`
UserName string `json:"userName"`
}
package model
import (
"errors"
)
//根据业务逻辑的需要,自定义一些错误
var (
ERROR_USER_NOTEXIST = errors.New("用户不存在。。")
ERROR_USER_EXIST = errors.New("用户已存在。。")
ERROR_USER_PWD = errors.New("密码错误")
)
package model
import (
"fmt"
"github.com/garyburd/redigo/redis"
"encoding/json"
)
//我们在服务器启动后,就初始化一个UserDao实例
//把它做成全局的变量,在需要和redis操作时,就直接使用即可
var (
MyUserDao *UserDao
)
//定义一个UserDao结构体
//完成对User 结构体的各种操作
type UserDao struct {
pool *redis.Pool
}
//使用工厂模式创建一个UserDao实例
func NewUserDao(pool *redis.Pool) (userDao *UserDao){
userDao = &UserDao{
pool:pool,
}
return
}
//写方法,应该提供哪个方法呢
//1,根据用户id返回一个User实例+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))
if err != nil {
//错误
if err == redis.ErrNil {//表示在users中没有找到对应的id
err= ERROR_USER_NOTEXIST
}
return
}
user = &User{}
//这里我们需要反序列化成一个User实例
err = json.Unmarshal([]byte(res),user)
if err != nil {
fmt.Println("json.Unmarshal Err=",err)
return
}
return
}
//完成登录的校验 Login
//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)
if err != nil {
return
}
//这时证明用户是获取到了
if user.UserPwd != userPwd {
err = ERROR_USER_PWD
return
}
return
}
package main
import (
"github.com/garyburd/redigo/redis"
"time"
)
//定义一个全局的pool
var pool *redis.Pool
func initPool(address string,maxIdle,maxActive int,idleTimeout time.Duration) {
pool = &redis.Pool{
MaxIdle: maxIdle, //最大空闲连接数
MaxActive: maxActive,//表示和数据库的最大连接数,0表示没有限制
IdleTimeout: idleTimeout,//最大空闲时间
Dial:func()(redis.Conn,error){//初始化连接的代码。连接哪个ip
return redis.Dial("tcp",address)
},
}
}
//我们需要到redis数据库去完成验证
//1.使用model.MyUserDao到redis去验证
user, err := model.MyUserDao.Login(loginMes.UserId,loginMes.UserPwd)
if err != nil {
if err ==model.ERROR_USER_NOTEXIST {
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 = "服务器内部错误..."
}
//这里我们先测试成功,然后再返回具体的错误信息
}else{
loginResMes.Code = 200
fmt.Println(user,"登录成功")
}
func init(){
//当服务器启动时,我们就去初始化我们的redis的连接池
initPool("localhost:6379",16,0,300 * time.Second)
initUserDao()
}
//这里我们编写一个函数完成对UserDao的初始化任务
func initUserDao() {
//这里的pool本身就是一个全局的变量
//这里需要注意一个初始化的顺序问题
//initPool,在initUserDao
model.MyUserDao =model.NewUserDao(pool)
}
完成注册功能,将用户信息录入到Redis中
思路分析,并完成代码
思路分析的示意图
package message
// User 定义一个用户的结构体
type User struct {
//确定字段信息
//为了序列化和反序列化成功
//用户信息的json字符串与结构体字段对应的Tag名字一致
UserId int `json:"userId"`
UserPwd string `json:"userPwd"`
UserName string `json:"userName"`
}
type RegisterMes struct {
User User `json:"user"` //类型就是User结构体
}
type RegisterResMes struct {
Code int `json:"code"` //返回状态码400表示该用户已经占用 200表示登录注册成功
Error string `json` //返回错误信息
}
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.再声明一个RegisterMes
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.将loginResMes 序列化
data, err := json.Marshal(registerResMes)
if err != nil {
fmt.Println("Marshal fail err=", err)
}
//4.将data赋值给resMes
resMes.Data = string(data)
//5.对resMes进行序列化,准备发送
data, err = json.Marshal(resMes)
if err != nil {
fmt.Println("Marshal fail err=", err)
return
}
//6.发送data 我们将其封装为writePkg
//因为使用了分层模式(mvc),我们先创建一个Transfer实例,然后读取
tf := &utils.Transfer{
Conn: this.Conn,
}
err = tf.WritePkg(data)
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 = ERROR_USER_EXISTS
return
}
//这时说明id在redis还没有,则可以完成注册
data, err :=json.Marshal(user) //序列化
if err != nil {
return
}
//入库
_,err = conn.Do("HSet","users",user.UserId,string(data))
if err != nil {
fmt.Println("保存注册用户错误 err=",err)
return
}
return
}
case 2 :
fmt.Println("注册用户")
fmt.Println("请输入用户id")
fmt.Scanf("%d\n",&userId)
fmt.Println("请输入用户的密码")
fmt.Scanf("%s\n",&userPwd)
fmt.Println("请输入用户的名字(昵称)")
fmt.Scanf("%s\n",&userName)
//2.调用UserProcess,完成注册的请求
up :=&process.UserProcess{}
up.Register(userId,userPwd,userName)
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.创建一个RegisterMes 结构体
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.Mashal err=",err)
return
}
//5.将data赋给了mes.Data字段
mes.Data = string(data)
//6.将mes进行序列化
data, err =json.Marshal(mes)
if err != nil {
fmt.Println("json.Mashal err=",err)
return
}
//7.到这个时候,data就是我们要发送的消息
//7.1先把data的长度发送给服务器
//先获取data的长度->转成一个表示长度的byte切片
var pkgLen uint32
pkgLen = uint32(len(data))
var buf [4]byte
binary.BigEndian.PutUint32(buf[0:4],pkgLen) //将该、长度转成了byte类型是数据
//发送长度
n, err := conn.Write(buf[:4])
if n != 4 || err !=nil {
fmt.Println("connWrite(buf) fail ",err)
return
}
fmt.Printf("客户端发送数据的消息长度=%d 内容是=%s",len(data),string(data))
//发送消息本身
_, err = conn.Write(data)
if err !=nil {
fmt.Println("connWrite(data) fail ",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
}
1)用户登陆后,可以得到当前在线用户列表 思路分析、示意图代码实现
用户登陆后,可以得到当前在线用户列表
(1)在服务器端维护一个onlineUsers map[int] *UserProcess
(2)创建一个新的文件userMgr.go,完成功能,对onlineUsers这个map进行增删改查
(3)在loginResMes增加一个字段 User []int 将在线的用户ID返回
(4)当用户登陆后,可以显示当前在线用户列表
2)示意图
package process
import (
"fmt"
)
//因为UserMge实例在服务其中有且只有一个
//因为在很多的地方,都会使用,因此,我们
//将其定义为全局变量
var (
userMgr *UserMgr
)
type UserMgr struct {
onlineUsers map[int]*UserProcess
}
//完成对userMge的初始化工作
func init() {
userMgr = &UserMgr{
onlineUsers : make(map[int]*UserProcess,1024),
}
}
//完成对onlineUsers的添加
func (this *UserMgr) AddOnlinesUser(up *UserProcess) {
this.onlineUsers[up.UserId] = up
}
//删除
func (this *UserMgr) DeleteOnlinesUser(userId int ) {
delete(this.onlineUsers,userId)
}
//返回当前所有在线的用户
func (this *UserMgr)GetAllUsers() 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("用户id不存在",userId)
return
}
return
}
} else {
loginResMes.Code = 200
//这里,因为用户登录成功,我们就把登录成功的用户放入到userMgr中
//将登录成功的用户的userId赋给this
this.UserId = loginMes.UserId
userMgr.AddOnlinesUser(this)
//将当前在线用户的id放入到loginResMes.UsersId
//遍历userMgr.onlineUsers
for id, _ := range userMgr.onlineUsers{
loginResMes.UsersId = append(loginResMes.UsersId,id)
}
fmt.Println(user, "登录成功")
}
(3)client
/process/userProcess.go(在login成功的地方加入代码)
//现在可以显示当前在线的列表 遍历loginResMes.UsersId
fmt.Println("当前在线用户列表如下")
for _, v := range loginResMes.UsersId {
//如果我们要求不显示自己在线,下面我们增加一个代码
if v == userId {
continue
}
fmt.Println("用户id:\t",v)
}
fmt.Println("\n\n")
思路1:
当有一个用户上线后,服务其就马上把维护的onlineUser map整体推送
思路2:
服务其有自己的策略,每隔一段时间,把维护的onlineUsers map整体推送
思路3:
(1)当一个用户上线后,服务器就把A用户的上线信息推送给所有在线用户即可
(2)客户端也要维护一个map,map中记录了他的好友(目前就是所有人)map[int]User
(3)客户端和服务器的通讯通道要依赖于serverProcess协程
代码实现
(1)在server/process/userMgr.go
package process
import (
"fmt"
"go_code/chatroom/common/message"
)
//客户端要维护的Map
var onlineUsers map[int]*message.User = make(map[int]*message.User,10)
//在客户端显示当前在线的用户
func outputOnlineUser() {
//遍历一把onlineUsers
fmt.Println("当前在线用户列表:")
for id,_ := range onlineUsers{
//如果不显示自己
fmt.Println("用户id:\t\t",id)
}
}
//编写一个方法,处理返回的NotifyUserStatusMes
func updateUserStatus(notifyUserStatusMes *message.NotifyUserStatusMes) {
//适当的优化
user,ok :=onlineUsers[notifyUserStatusMes.UserId]
if !ok { //原来没有
user = &message.User{
UserId : notifyUserStatusMes.UserId,
}
}
user.UserStatus = string(notifyUserStatusMes.Status)
onlineUsers[notifyUserStatusMes.UserId] = user
outputOnlineUser()
}
(2)server/process/userProcess.go
//这里我们编写通知所有在线用户的方法
//这个id要通知其他的在线用户,我上线
func (this *UserProcess) NotifyOthersOnlineUser(userId int) {
//遍历 onlineUsers ,然后一个一个的发送 NotifyUserStatusMes
for id, up := range userMgr.onlineUsers {
//过滤掉自己
if id == userId {
continue
}
//开始通知【单独的写一个方法】
up.NotifyMeOnline(userId)
}
}
func (this *UserProcess) NotifyMeOnline(userId int){
//组装我们的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)
return
}
//将序列化后的notifyUserStatusMes赋值给mes.Data
mes.Data = string(data)
//对message再次序列化
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("json.Marshal err",err)
return
}
//发送,创建一个transfer实例发送
tf := &utils.Transfer{
Conn : this.Conn,
}
err = tf.WritePkg(data)
if err != nil {
fmt.Println("NotifyMeOline err=",err)
return
}
}
下面调用
//通知其他的用户我上线了
this.NotifyOthersOnlineUser(loginMes.UserId)
(3)common/message/message.go
//为了配合服务器端推送用户状态变化类型
type NotifyUserStatusMes struct {
UserId int `json:"userId"` //用户id
Status int `json:"status"` //用户的状态
}
(4)客户端client/process/userMgr.go
package process
import (
"fmt"
"go_code/chatroom/common/message"
)
//客户端要维护的Map
var onlineUsers map[int]*message.User = make(map[int]*message.User,10)
//在客户端显示当前在线的用户
func outputOnlineUser() {
//遍历一把onlineUsers
fmt.Println("当前在线用户列表:")
for id,_ := range onlineUsers{
//如果不显示自己
fmt.Println("用户id:\t\t",id)
}
}
//编写一个方法,处理返回的NotifyUserStatusMes
func updateUserStatus(notifyUserStatusMes *message.NotifyUserStatusMes) {
//适当的优化
user,ok :=onlineUsers[notifyUserStatusMes.UserId]
if !ok { //原来没有
user = &message.User{
UserId : notifyUserStatusMes.UserId,
}
}
user.UserStatus = string(notifyUserStatusMes.Status)
onlineUsers[notifyUserStatusMes.UserId] = user
outputOnlineUser()
}
(5)client/main/server.go
case 1:
//fmt.Println("显示用户在线列表")
outputOnlineUser()
case 2:
//如果读取到消息,又是下一步处理逻辑
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("服务其端返回了未知的消息类型")
}
当一个用户上线后,可以将群聊消息发给服务器。服务器可以接收到
(1)新增一个消息结构体
(2)新增一个model CurUser
(3)在smsProcess增加相应的方法 SendGroupMes,
(1)common/message/message.go
//增加一个SmsMes //发送的
type SmsMes struct {
Content string `json:"content"` //内容
User //匿名结构体,继承
}
(2)client/model/curUser.go
package model
import (
"net"
"go_code/chatroom/common/message"
)
//因为在客户端,我们很多地方会使用到curUser,我们将其作为一个全局的
type CurUser struct {
Conn net.Conn
message.User
}
(3)client/process/smsProcess.go
package process
import (
"fmt"
"encoding/json"
"go_code/chatroom/common/message"
"go_code/chatroom/client/utils"
)
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.序列化smsMes
data, err := json.Marshal(smsMes)
if err != nil {
fmt.Println("SendGroupMes json.Marshal err=",err.Error())
return
}
mes.Data = string(data)
//4.对mes再次序列化
data, err = json.Marshal(mes)
if err != nil {
fmt.Println(" json.Marshal err=",err.Error())
return
}
//5.将mes发送给服务器
tf := &utils.Transfer{
Conn : CurUser.Conn,
}
//6.发送
err = tf.WritePkg(data)
if err != nil {
fmt.Println("SendGroupsMes err=",err.Error())
return
}
return
}
(4)测试
服务器可以将接收到的消息,群发给所有在线用户(发送者除外)
(1)在服务器端接收到SmsMes消息
(2)在server/process/SmsProcess.go文件增加群发消息的方法
(3)在客户端还要增加去处理服务器端转发的群发消息
(1)server/main/processor.go[在server中调用转发消息的方法]
//处理注册
up := &process2.UserProcess{
Conn : this.Conn,
}
err = up.ServerProcessRegister(mes)
case message.SmsMesType :
//创建一个SmsProcess实例完成转发群聊消息。
smsProcess := &process2.SmsProcess{}
smsProcess.SendGroupMes(mes)
(2)client/process/smsMes.go
package process
import (
"fmt"
"encoding/json"
"go_code/chatroom/common/message"
"go_code/chatroom/client/utils"
)
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.序列化smsMes
data, err := json.Marshal(smsMes)
if err != nil {
fmt.Println("SendGroupMes json.Marshal err=",err.Error())
return
}
mes.Data = string(data)
//4.对mes再次序列化
data, err = json.Marshal(mes)
if err != nil {
fmt.Println(" json.Marshal err=",err.Error())
return
}
//5.将mes发送给服务器
tf := &utils.Transfer{
Conn : CurUser.Conn,
}
//6.发送
err = tf.WritePkg(data)
if err != nil {
fmt.Println("SendGroupsMes err=",err.Error())
return
}
return
}
(3)client/process/smsMgr.go
package process
import (
"fmt"
"encoding/json"
"go_code/chatroom/common/message"
)
func outputGroupMes(mes *message.Message) {//这个地方一定是SmsMes
//显示即可
//1.反序列化mes.Data
var smsMes message.SmsMes
err := json.Unmarshal([]byte(mes.Data),&smsMes)
if err != nil {
fmt.Println("json.Unmarshal err=",err.Error())
return
}
//显示信息
info := fmt.Sprintf("用户id:\t%d 对大家说:\t%s",smsMes.UserId,smsMes.Content)
fmt.Println(info)
fmt.Println()
}
(4)client/process/server.go
case message.SmsMesType : //有人群发消息了
outputGroupMes(&mes)
1.可以实现私聊(点对点聊天)
2.如果一个登录用户离线,就把这个人从在线列表中去掉
3.实现离线留言,在群聊时,如果某个用户没有在线,当登录后,可以接受到离线的消息