海量用户即时通讯系统
项目开发流程
需求分析–> 设计阶段—> 编码实现 —> 测试阶段 —> 实施
需求分析
- 用户注册
- 用户登录
- 显示在线用户列表
- 群聊(广播)
- 点对点聊天
- 离线留言
界面设计
项目开发前技术准备
项目要保存用户信息和消息数据,需要数据库(redis或mysql)
显示客户端登录菜单
功能:能够正确的显示客户端的菜单
client/main.go
package main
import (
"fmt"
"os"
)
var userId int
var userPwd string
func main() {
//接收用户选择
var key int
//判断是否继续显示菜单
var loop = true
for loop {
fmt.Println("-----------欢迎登陆多人聊天系统------------")
fmt.Println("\t\t\t 1 登陆聊天室")
fmt.Println("\t\t\t 2 注册用户")
fmt.Println("\t\t\t 3 退出系统")
fmt.Println("\t\t\t 请选择(1-3): ")
fmt.Scanf("%d\n", &key)
switch key {
case 1:
fmt.Println("登陆聊天室")
loop = false
case 2:
fmt.Println("注册用户")
loop = false
case 3:
fmt.Println("退出系统")
os.Exit(0)
default:
fmt.Println("输入有误,请重新输入")
}
}
if key == 1 {
//说明用户要登陆
fmt.Println("请输入用户id")
fmt.Scanf("%d\n", &userId)
fmt.Println("请输入密码")
fmt.Scanf("%s\n", &userPwd)
//登陆函数写到另一个文件,login.go
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
}
用户登录
- 指定用户验证,用户 id=100, 密码pwd=123456 可以登录,其它用户不能登录
- Message组成
- 1 客户端发送消息长度,服务端可以正常收到该长度值
思路:
确定消息Message的格式和结构
server/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.Acept 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/main.go
没有修改
package main
import (
"fmt"
"os"
)
var userId int
var userPwd string
func main() {
//接收用户选择
var key int
//判断是否继续显示菜单
var loop = true
for loop {
fmt.Println("-----------欢迎登陆多人聊天系统------------")
fmt.Println("\t\t\t 1 登陆聊天室")
fmt.Println("\t\t\t 2 注册用户")
fmt.Println("\t\t\t 3 退出系统")
fmt.Println("\t\t\t 请选择(1-3): ")
fmt.Scanf("%d\n", &key)
switch key {
case 1:
fmt.Println("登陆聊天室")
loop = false
case 2:
fmt.Println("注册用户")
loop = false
case 3:
fmt.Println("退出系统")
os.Exit(0)
default:
fmt.Println("输入有误,请重新输入")
}
}
if key == 1 {
//说明用户要登陆
fmt.Println("请输入用户id")
fmt.Scanf("%d\n", &userId)
fmt.Println("请输入密码")
fmt.Scanf("%s\n", &userPwd)
//登陆函数写到另一个文件,login.go
err := login(userId, userPwd)
if err != nil {
fmt.Println("登陆失败")
} else {
fmt.Println("登陆成功")
}
} else if key == 2 {
fmt.Println("进行用户注册的逻辑...")
}
}
client/login.go
package main
import (
"encoding/binary"
"encoding/json"
"fmt"
"go_code/chatroot/common/message"
"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", "127.0.0.1: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)
//6. 将mes进行序列化
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("json.Marshal err=", err)
return
}
//7. 这时候data就是要发送的消息
//7.1 先把data的长度发送给服务器
//先获取到data的长度-> 转成一个表示长度的byte切片
var pkgLen uint32
pkgLen = uint32(len(data))
var buf [4]byte
binary.BigEndian.PutUint32(buf[0:4], pkgLen)
//发送长度
n, err := conn.Write(buf[:4])
if n != 4 || err != nil {
fmt.Println("conn.Write(bytes) fail", err)
return
}
fmt.Printf("客户端发送消息长度=%d 内容=%s", len(data), string(data))
return
}
- 2 完成客户端可以发送消息本身,服务器端可以正常接收到消息,并根据客户端发送的消息(LoginMes), 判断用户的合法性,并返回相应的LoginResMes
思路:
- 让客户端发送消息本身
- 服务器端接收到消息,然后反序列化对应的消息结构体
- 服务器端根据反序列化成对应的消息,判断是否登录用户是合法,返回LoginResMes
- 客户端解析返回的LoginResMes,显示对应界面
- 做函数的封装
client/login.go
package main
import (
"encoding/binary"
"encoding/json"
"fmt"
"go_code/chatroot/common/message"
"net"
"time"
)
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", "127.0.0.1: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)
//6. 将mes进行序列化
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("json.Marshal err=", err)
return
}
//7. 这时候data就是要发送的消息
//7.1 先把data的长度发送给服务器
//先获取到data的长度-> 转成一个表示长度的byte切片
var pkgLen uint32
pkgLen = uint32(len(data))
var buf [4]byte
binary.BigEndian.PutUint32(buf[0:4], pkgLen)
//发送长度
_, err = conn.Write(data)
if err != nil {
fmt.Println("conn.Write(data) fail", err)
return
}
//fmt.Printf("客户端发送消息长度=%d 内容=%s", len(data), string(data))
//休眠2秒
time.Sleep(2 * time.Second)
fmt.Println("休眠了2..")
return
}
server/main.go
修改
package main
import (
"encoding/binary"
"encoding/json"
"fmt"
"go_code/chatroot/common/message"
"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 {
return
}
//根据buf[:4]转成一个uint32类型
var pkgLen uint32
pkgLen = binary.BigEndian.Uint32(buf[0:4])
//根据pkgLen读取消息内容
n, err := conn.Read(buf[:pkgLen])
if n != int(pkgLen) || err != nil {
return
}
//把pkgLen 反序列化成 -> message.Message
err = json.Unmarshal(buf[:pkgLen], &mes)
if err != nil {
fmt.Println("json.Unmarsha 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)
}
}
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.Acept err=", err)
}
//一旦连接成功,则启动一个协程和客户端保持通讯
go process(conn)
}
}
- 完成登录
server/main.go
修改
package main
import (
"encoding/binary"
"encoding/json"
"fmt"
"go_code/chatroot/common/message"
"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 {
return
}
//根据buf[:4]转成一个uint32类型
var pkgLen uint32
pkgLen = binary.BigEndian.Uint32(buf[0:4])
//根据pkgLen读取消息内容
n, err := conn.Read(buf[:pkgLen])
if n != int(pkgLen) || err != nil {
return
}
//把pkgLen 反序列化成 -> message.Message
err = json.Unmarshal(buf[:pkgLen], &mes)
if err != nil {
fmt.Println("json.Unmarsha err=", err)
return
}
return
}
//专门处理登陆请求
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
}
//先声明一个resMes
var resMes message.Message
resMes.Type = message.LoginResMesType
//在声明一个 LoginResMes, 并完成赋值
var loginResMes message.LoginResMes
//如果用户id=100,密码=123456,认为合法,否则不合法
if loginMes.UserId == 100 && loginMes.UserPwd == "123456" {
loginResMes.Code = 200
} else {
loginResMes.Code = 500 //500状态吗,表示用户不存在
loginResMes.Error = "该用户不存在,请注册再使用..."
}
//将 loginResMes序列化
_, err = json.Marshal(loginResMes)
if err != nil {
fmt.Println("json.Marshal fail", err)
return
}
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) {
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
}
}
}
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.Acept err=", err)
}
//一旦连接成功,则启动一个协程和客户端保持通讯
go process(conn)
}
}
client/utils.go
package main
import (
"encoding/binary"
"encoding/json"
"fmt"
"go_code/chatroot/common/message"
"net"
)
func readPkg(conn net.Conn) (mes message.Message, err error) {
buf := make([]byte, 8096)
fmt.Println("读取客户端发送的数据...")
//conn.Read在conn没有被关闭的情况下,才会阻塞
//如果客户端关闭了conn则不会阻塞
_, err = conn.Read(buf[:4])
if err != nil {
return
}
//根据buf[:4]转成一个uint32类型
var pkgLen uint32
pkgLen = binary.BigEndian.Uint32(buf[0:4])
//根据pkgLen读取消息内容
n, err := conn.Read(buf[:pkgLen])
if n != int(pkgLen) || err != nil {
return
}
//把pkgLen反序列化 -> message.Message
err = json.Unmarshal(buf[:pkgLen], &mes)
if err != nil {
fmt.Println("json.Unmarsha 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
}
client/login.go
package main
import (
"encoding/binary"
"encoding/json"
"fmt"
"go_code/chatroot/common/message"
"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", "127.0.0.1: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)
//6. 将mes进行序列化
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("json.Marshal err=", err)
return
}
//7. 这时候data就是要发送的消息
//7.1 先把data的长度发送给服务器
//先获取到data的长度-> 转成一个表示长度的byte切片
var pkgLen uint32
pkgLen = uint32(len(data))
var buf [4]byte
binary.BigEndian.PutUint32(buf[0:4], pkgLen)
//发送长度
_, err = conn.Write(data)
if err != nil {
fmt.Println("conn.Write(data) fail", err)
return
}
//fmt.Printf("客户端发送消息长度=%d 内容=%s", len(data), string(data))
//休眠2秒
//time.Sleep(2 * time.Second)
//fmt.Println("休眠了2..")
//处理服务器端返回的消息
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/main.go
package main
import (
"fmt"
"os"
)
var userId int
var userPwd string
func main() {
//接收用户选择
var key int
//判断是否继续显示菜单
var loop = true
for loop {
fmt.Println("-----------欢迎登陆多人聊天系统------------")
fmt.Println("\t\t\t 1 登陆聊天室")
fmt.Println("\t\t\t 2 注册用户")
fmt.Println("\t\t\t 3 退出系统")
fmt.Println("\t\t\t 请选择(1-3): ")
fmt.Scanf("%d\n", &key)
switch key {
case 1:
fmt.Println("登陆聊天室")
loop = false
case 2:
fmt.Println("注册用户")
loop = false
case 3:
fmt.Println("退出系统")
os.Exit(0)
default:
fmt.Println("输入有误,请重新输入")
}
}
if key == 1 {
//说明用户要登陆
fmt.Println("请输入用户id")
fmt.Scanf("%d\n", &userId)
fmt.Println("请输入密码")
fmt.Scanf("%s\n", &userPwd)
//登陆函数写到另一个文件,login.go
err := login(userId, userPwd)
if err != nil {
fmt.Println("登陆失败")
} else {
fmt.Println("登陆成功")
}
} else if key == 2 {
fmt.Println("进行用户注册的逻辑...")
}
}
程序结构改进
- 改进服务端
- 创建目录文件
utils/utils.go
修改
package utils
import (
"encoding/binary"
"encoding/json"
"fmt"
"go_code/chatroot/common/message"
"net"
)
//方法关联到结构体
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 {
return
}
//根据buf[:4]转成一二uint32类型
var pkgLen uint32
pkgLen = binary.BigEndian.Uint32(this.Buf[0:4])
//根据pkgLen 读取消息内容
n, err := this.Conn.Read(this.Buf[:pkgLen])
if n != int(pkgLen) || err != nil {
return
}
//把pkgLen反序列化成 -> message.Message
err = json.Unmarshal(this.Buf[:pkgLen], &mes)
if err != nil {
fmt.Println("json.Unmarsha err=", err)
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)
//发送长度
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
}
process2/userProcess.go
package process2
import (
"encoding/json"
"fmt"
"go_code/chatroot/common/message"
"go_code/chatroot/server/utils"
"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
}
//先声明一个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("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函数
//因为使用分层模式(mvc),先创建一个Transfer实例,然后读取
tf := &utils.Transfer{
Conn: this.Conn,
}
err = tf.WritePkg(data)
return
}
main/processor.go
修改
package main
import (
"fmt"
"go_code/chatroot/common/message"
"go_code/chatroot/server/process2"
"go_code/chatroot/server/utils"
"io"
"net"
)
type Processor struct {
Conn net.Conn
}
//ServerProcessMes函数
//功能:genuine客户端发送消息种类不同,决定调用哪个函数来处理
func (this *Processor) serverProcessMes(mes *message.Message) (err error) {
switch mes.Type {
case message.LoginMesType:
//处理登陆
//创建一个UserProcess实例
up := &process2.UserProcess{
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
}
}
}
编译
d:\goproject\src
go build -o server.exe go_code\chatroot\server\main\
main/main.go
修改
package main
import (
"encoding/binary"
"encoding/json"
"fmt"
"go_code/chatroot/common/message"
"net"
)
func readPkg(conn net.Conn) (mes message.Message, err error) {
buf := make([]byte, 8096)
fmt.Println("读取客户端发送的数据..")
//conn.Read 在conn没有被关闭的情况下,才会阻塞
//如果客户端关闭了 conn 则就不会阻塞
_, err = conn.Read(buf[:4])
if err != nil {
return
}
//根据buf[:4]转成一个uint32类型
var pkgLen uint32
pkgLen = binary.BigEndian.Uint32(buf[0:4])
//根据pkgLen读取消息内容
n, err := conn.Read(buf[:pkgLen])
if n != int(pkgLen) || err != nil {
return
}
//把pkgLen 反序列化成 -> message.Message
err = json.Unmarshal(buf[:pkgLen], &mes)
if err != nil {
fmt.Println("json.Unmarsha err=", err)
return
}
return
}
//专门处理登陆请求
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
}
//先声明一个resMes
var resMes message.Message
resMes.Type = message.LoginResMesType
//在声明一个 LoginResMes, 并完成赋值
var loginResMes message.LoginResMes
//如果用户id=100,密码=123456,认为合法,否则不合法
if loginMes.UserId == 100 && loginMes.UserPwd == "123456" {
loginResMes.Code = 200
} else {
loginResMes.Code = 500 //500状态吗,表示用户不存在
loginResMes.Error = "该用户不存在,请注册再使用..."
}
//将 loginResMes序列化
_, err = json.Marshal(loginResMes)
if err != nil {
fmt.Println("json.Marshal fail", err)
return
}
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) {
defer conn.Close()
//这里调用总控,创建一个
processor := &Processor{
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.Acept err=", err)
}
//一旦连接成功,则启动一个协程和客户端保持通讯
go process(conn)
}
}
- 修改客户端
创建对应的目录文件
将 server/utils.go 拷贝到 client/utils/utils.go
client/process/userProcess.go
package process
type UserProcess struct {
}
//给关联一个用户登陆的方法
//写一个函数,完成登录
func (this *UserProcess) Login(userId int, userPwd string) (err error) {
//
}
client/process/server.go
package process
import (
"fmt"
"go_code/chatroot/client/utils"
"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: nil,
}
for {
fmt.Println("客户端正在等待读取服务器发送的消息")
mes, err := tf.ReadPkg()
if err != nil {
fmt.Println("tf.ReadPkg err=", err)
return
}
//如果读取到消息,又是下一步处理逻辑
fmt.Println("mes=%v\n", mes)
}
}
client/main/main.go
修改
package main
import (
"fmt"
"go_code/chatroot/client/process"
"os"
)
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("登陆聊天室")
fmt.Println("请输入用户id")
fmt.Scanf("%d\n", &userId)
fmt.Println("请输入用户密码")
fmt.Scanf("%s\n", &userPwd)
//完成登录
//1. 创建一个UserProcess的实例
up := &process.UserProcess{}
up.Login(userId, userPwd)
case 2:
fmt.Println("注册用户")
//loop = false
case 3:
fmt.Println("退出系统")
os.Exit(0)
default:
fmt.Println("输入有误,请重新输入")
}
}
}
- 在redis手动添加测试用户
手动在redis增加一个用户信息
127.0.0.1:6379> hset users 100 "{\"userId\":100,\"userPwd\":\"123456\",\"userName\":\"scott\"}"
(integer) 1
127.0.0.1:6379> hget users 100
"{\"userId\":100,\"userPwd\":\"123456\",\"userName\":\"scott\"}"
输入的用户名密码在redis中存在则登录,否则退出系统,并给出相应的提示信息:
1.用户不存在,可以注册,再登录
2. 密码不正确
model/user.go
package model
//定义一个用户的结构体
type User struct {
//为了序列化和反序列化成功,必须保证用户信息的json字符串的
//key和结构体的字段对应的tag名字一致
UserId int `json:"userId"`
UserPwd string `json:"userPwd"`
UserName string `json:"userName"`
}
model/error.go
package model
import "errors"
var (
ERROR_USER_NOTEXISTS =errors.New("用户不存在...")
ERROR_USER_EXISTS = errors.New("用户已经存在...")
ERROR_USER_PWD = errors.New("密码不正确")
)
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,
}
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_NOTEXISTS
}
return
}
user = &User{}
//把res反序列化成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
}
main/redis.go
package main
import (
"github.com/garyburd/redigo/redis"
"time"
)
//定义一个全局的pool
var pool *redis.Pool
func initPool(address string, maxIdle, maxActive int, idleTimeout time.Duration) {
pool = &redis.Pool{
Dial: func() (conn redis.Conn, e error) {
return redis.Dial("tcp", address)
},
MaxIdle: maxIdle,
MaxActive: maxActive,
IdleTimeout: idleTimeout,
}
}
main/main.go
package main
import (
"encoding/binary"
"encoding/json"
"fmt"
"go_code/chatroot/common/message"
"go_code/chatroot/server/model"
"net"
"time"
)
func readPkg(conn net.Conn) (mes message.Message, err error) {
buf := make([]byte, 8096)
fmt.Println("读取客户端发送的数据..")
//conn.Read 在conn没有被关闭的情况下,才会阻塞
//如果客户端关闭了 conn 则就不会阻塞
_, err = conn.Read(buf[:4])
if err != nil {
return
}
//根据buf[:4]转成一个uint32类型
var pkgLen uint32
pkgLen = binary.BigEndian.Uint32(buf[0:4])
//根据pkgLen读取消息内容
n, err := conn.Read(buf[:pkgLen])
if n != int(pkgLen) || err != nil {
return
}
//把pkgLen 反序列化成 -> message.Message
err = json.Unmarshal(buf[:pkgLen], &mes)
if err != nil {
fmt.Println("json.Unmarsha err=", err)
return
}
return
}
//专门处理登陆请求
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
}
//先声明一个resMes
var resMes message.Message
resMes.Type = message.LoginResMesType
//在声明一个 LoginResMes, 并完成赋值
var loginResMes message.LoginResMes
//如果用户id=100,密码=123456,认为合法,否则不合法
if loginMes.UserId == 100 && loginMes.UserPwd == "123456" {
loginResMes.Code = 200
} else {
loginResMes.Code = 500 //500状态吗,表示用户不存在
loginResMes.Error = "该用户不存在,请注册再使用..."
}
//将 loginResMes序列化
_, err = json.Marshal(loginResMes)
if err != nil {
fmt.Println("json.Marshal fail", err)
return
}
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) {
defer conn.Close()
//这里调用总控,创建一个
processor := &Processor{
conn,
}
err := processor.process2()
if err != nil {
fmt.Println("客户端和服务器通讯协程错误=err", err)
return
}
}
//编写一个函数,完成对UserDao的初始化任务
func initUserDao() {
//这里的pool本身就是一个全局变量
//这里需要注意一个初始化顺序问题
//initPool,在initUserDao
model.MyUserDao = model.NewUserDao(pool)
}
func main() {
//当服务器启动时,初始化redis的连接池
initPool("127.0.0.1:6379", 16, 0, 300 * time.Second)
initUserDao()
//提示信息
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.Acept err=", err)
}
//一旦连接成功,则启动一个协程和客户端保持通讯
go process(conn)
}
}
注册用户
完成注册【思路】
- 先把user.go 放入到common/message 文件夹.
- common/message/message.go 新增加两个消息类型
- 在 客户端接收用户的输入
- 在client/process/userProcess.go 增加一个 Register方法,完成请求注册
- 在sever/model/userDao.go 增加了一个方法 Register方法
显示在线用户列表
- 用户登录后,可以得到当前在线用户列表
- 在服务器端维护一个onlineUsers map[int]*UserProcess
- 创建一个新的文件 userMgr .go ,完成功能
对onlineUsers 这个map的增删改查 - 在 LoginResMess 增加一个字段 Users []int //将在线用户id返回
- 当用户登录后,可以显示当前在线用户列表
- 当一个新的用户上线后,其它已经登录的用户也能获取最新在线用户列表
思路1:
- 当有一个用户上线后,服务器就马上把维护的onlineUsers map整体推送
思路2:
服务器有自己的策略,每隔一定的时间,把维护的onlineUsers map整体推送
思路3:
- 当一个用户A上线,服务器就把A用户的上线信息,推送给所有在线的用户
- 客户端也需要维护一个map, map中记录了他的好友(目前就是所有人)
map[int]User - 客户端和服务器的通讯通道,要依赖 serverProcessMes 协程
群发消息
完成客户端可以发送消息的思路
- 新增一个消息结构体 SmMes …
- 新增一个model CurUser
- 在smsProcess.g o 增加相应的方法 SendGroupMes, 发送一个群聊的消息
- 在服务器端接收到SmsMes 消息
- 在 server/process/smsProcess.go 文件增加群发消息的方法
- 在客户端 还要增加去处理 服务器端转发的群发消息SmsMes