go 海量用户即时通讯系统

海量用户即时通讯系统

项目开发流程

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

需求分析

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

界面设计

在这里插入图片描述

项目开发前技术准备

项目要保存用户信息和消息数据,需要数据库(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
    思路:
  1. 让客户端发送消息本身
  2. 服务器端接收到消息,然后反序列化对应的消息结构体
  3. 服务器端根据反序列化成对应的消息,判断是否登录用户是合法,返回LoginResMes
  4. 客户端解析返回的LoginResMes,显示对应界面
  5. 做函数的封装

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("进行用户注册的逻辑...")
	}
}

程序结构改进

  1. 改进服务端

在这里插入图片描述

  1. 创建目录文件

在这里插入图片描述

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)
	}
}
  1. 修改客户端

在这里插入图片描述

创建对应的目录文件

在这里插入图片描述

将 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("输入有误,请重新输入")
		}
	}
}
  1. 在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)
	}
}


注册用户

完成注册【思路】

  1. 先把user.go 放入到common/message 文件夹.
  2. common/message/message.go 新增加两个消息类型
  3. 在 客户端接收用户的输入
  4. 在client/process/userProcess.go 增加一个 Register方法,完成请求注册
  5. 在sever/model/userDao.go 增加了一个方法 Register方法

显示在线用户列表

在这里插入图片描述

  • 用户登录后,可以得到当前在线用户列表
  1. 在服务器端维护一个onlineUsers map[int]*UserProcess
  2. 创建一个新的文件 userMgr .go ,完成功能
    对onlineUsers 这个map的增删改查
  3. 在 LoginResMess 增加一个字段 Users []int //将在线用户id返回
  4. 当用户登录后,可以显示当前在线用户列表

  • 当一个新的用户上线后,其它已经登录的用户也能获取最新在线用户列表

思路1:

  1. 当有一个用户上线后,服务器就马上把维护的onlineUsers map整体推送

思路2:
服务器有自己的策略,每隔一定的时间,把维护的onlineUsers map整体推送

思路3:

  1. 当一个用户A上线,服务器就把A用户的上线信息,推送给所有在线的用户
  2. 客户端也需要维护一个map, map中记录了他的好友(目前就是所有人)
    map[int]User
  3. 客户端和服务器的通讯通道,要依赖 serverProcessMes 协程

群发消息

完成客户端可以发送消息的思路

  1. 新增一个消息结构体 SmMes …
  2. 新增一个model CurUser
  3. 在smsProcess.g o 增加相应的方法 SendGroupMes, 发送一个群聊的消息
  4. 在服务器端接收到SmsMes 消息
  5. 在 server/process/smsProcess.go 文件增加群发消息的方法
  6. 在客户端 还要增加去处理 服务器端转发的群发消息SmsMes
发布了116 篇原创文章 · 获赞 27 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/wuxingge/article/details/104605483