1.经典项目-海量用户即使通讯系统

1.项目开发流程

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

需求分析

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

项目开发前技术准备
项目要保存用户信息和消息数据,因此需要数据库(Mysql或Redis),这里使用Redis

2.Golang操作Redis

2.1 安装第三方开源Redis库

方法一
使用第三方开源的redis库:github.com/gomodule/redigo/redis
安装第三方Redis库,在GOPATH路径下执行安装命令:go get github.com/gomodule/redigo/redis
注意:先安装git

镜像:go env -w GOPROXY=https://mirrors.aliyun.com/goproxy/

2.2 Set/Get接口

通过Golang添加和获取key-value

func Redis_test() {
	c, err := redis.Dial("tcp", "localhost:6379")
	if err != nil {
		fmt.Println("conn redis faild,", err)
		return
	}

	defer c.Close()
	_, err = c.Do("set", "key1", 111)
	if err != nil {
		fmt.Println(err)
		return
	}
	r, err := redis.Int(c.Do("Get", "key1"))
	if err != nil {
		fmt.Println("get key1 faild,", err)
		return
	}
	fmt.Println(r)
}
2.3 Hash

通过Golang对Hash操作Hash数据类型

func Redis_test() {
	c, err := redis.Dial("tcp", "localhost:6379")
	if err != nil {
		fmt.Println("conn redis faild,", err)
		return
	}

	defer c.Close()
	_, err = c.Do("Hset", "user01", "name", "Tom")
	if err != nil {
		fmt.Println(err)
		return
	}
	_, err = c.Do("Hset", "user01", "age", 18)
	if err != nil {
		fmt.Println(err)
		return
	}
	r1, err := redis.String(c.Do("HGet", "user01", "name"))
	if err != nil {
		fmt.Println(err)
		return
	}
	r2, err := redis.Int(c.Do("HGet", "user01", "age"))
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Printf("user01 name:%v get:%v", r1, r2)
}

批量Get/Set

func Redis_test() {
	c, err := redis.Dial("tcp", "localhost:6379")
	if err != nil {
		fmt.Println("conn redis faild,", err)
		return
	}

	defer c.Close()
	_, err = c.Do("HMset", "user02", "name", "Jerry", "age", 15)
	if err != nil {
		fmt.Println(err)
		return
	}
	r, err := redis.Strings(c.Do("HMGet", "user02", "name", "age"))
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Printf("user01 name:%v get:%v", r[0], r[1])
}

给数据设置有效时间

//给name数据设置有效时间为10s
_, err = c.Do("expire", "name", 10)
2.4 操作List
_, err = c.Do("lpush", "personList", "Tom", 11, "Jerry", 15)
r, err = redis.Strings(c.Do("lrange", "personList", 0, -1))
2.5 Redis连接池

目的:节省临时获取Redis链接的时间,从而提高效率

var pool *redis.Pool

func init() {
	pool = &redis.Pool{
		MaxIdle:     8,
		MaxActive:   0,
		IdleTimeout: 100,
		Dial: func() (redis.Conn, error) {
			return redis.Dial("tcp", "localhost:6379")
		},
	}
}

func Redis_test() {
	c := pool.Get()
	_, err := c.Do("HMset", "user02", "name", "Jerry", "age", 15)
	if err != nil {
		fmt.Println(err)
		return
	}
	r, err := redis.Strings(c.Do("HMGet", "user02", "name", "age"))
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Printf("user01 name:%v get:%v\n", r[0], r[1])

	_, err = c.Do("lpush", "personList", "Tom", 11, "Jerry", 15)
	r, err = redis.Strings(c.Do("lrange", "personList", 0, -1))
	fmt.Println(r)
	c.Close()
}

3.实现功能-显示客户端登录菜单

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("退出系统")
			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("显示用户注册界面")
	}

}

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
}

4.实现功能-完成用户登录

要求:先完成指定用户的验证,用户id=100,密码pwd=123456可以登录,其他用户不能登录
这里需要先说明一个Message的组成,并发送与一个Message的流程

客户端:

  1. 接收输入的id和pwd
  2. 发送id和密码
  3. 判断是否成功,并显示对应页面
    关键问题是怎么组织发送的数据
  4. 设计消息协议

发送的流程

  1. 先创建一个Message的结构体
  2. mes.Type = 登录消息类型
  3. mes.Data = 登录消息的内容(序列)
  4. 对mes进行序列化
  5. 在网络传输中,最麻烦丢包
    (1) 先给服务器发送mes的长度[有多少个字节n]
    (2) 在发送消息本身

服务器

  1. 接收用户id,pwd【goroutine】
  2. 比较
  3. 返回结果

接收数据流程

  1. 接收到客户端发送的长度.len
  2. 根据接收到的长度len,在接收消息本身
  3. 接收时要判断实际接收到的消息内容是否等于len
  4. 如果不相等,就有纠错协议
  5. 取到反序列化>Message
  6. 取出message.Data(string)-反序列化>LoginMes
  7. 取出loginMes.userid和loginMes.userPwd
  8. 比较
  9. 根据比较结果,返回Mess
  10. 发送给客户端

1.完成客户端可以发送消息长度,服务器端可以正常接收该长度值
代码实现:
server/main.go

package main

import (
	"fmt"
	"net"
)

// 处理和客户端的通讯
func process(conn net.Conn) {
	//这里需要延时关闭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")
	if err != nil {
		fmt.Println("net.Listen err = ", err)
		return
	}
	//延时关闭
	defer listen.Close()

	// 一旦监听成功,就等待客户端来链接服务器
	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 (
	"encoding/binary"
	"encoding/json"
	"fmt"
	"net"
	"project/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 结构体
	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 bytes [4]byte
	binary.BigEndian.PutUint32(bytes[0:4], pkgLen)
	// 发送长度
	n, err := conn.Write(bytes[0:4])
	if n != 4 || err != nil {
		fmt.Println("conn.Write(bytes) fail ", err)
		return
	}

	fmt.Println("客户端发送长度ok")

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

2.完成客户端可以发送消息本身,服务端可以正常收到消息,并根据客户端发送的消息(LoginMes)判断用户的合法性,并返回相应的LoginResMes
思路分析:

  1. 让客户发送消息本身
  2. 服务器端接收到消息,然后反序列化成对应的消息结构体
  3. 服务器端根据反序列化成对应的消息,判断是否登录用户是合法,返回LoginResMes
  4. 客户端解析返回的LoginResMes显示对应界面

代码改动:
client/login.go
最后添加发送data代码

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

server/main.go


func readpkg(conn net.Conn) (mes message.Message, err error) {
	buf := make([]byte, 8096)
	fmt.Println("读到客户端发送的数据...")
	_, err = conn.Read(buf[:4])
	if err != nil {
		//fmt.Println("conn.Read err = ", err)
		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 {
		//fmt.Println("conn.Read fail err = ", err)
		return
	}

	//吧pkgLen反序列化为 message.Message
	err = json.Unmarshal(buf[:pkgLen], &mes)
	if err != nil {
		fmt.Println("json.Unmarshal err = ", err)
		return
	}
	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 {
				fmt.Println("客户端关闭,服务端正常关闭...")
				return
			}
			{
				fmt.Println("readpkg err = ", err)
				return
			}
		}
		fmt.Println("mes = ", mes)
	}
}

3.能够完成登录,并提示相应信息
新增common/utils/utils.go

package utils

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

func ReadPkg(conn net.Conn) (mes message.Message, err error) {
	buf := make([]byte, 8096)
	fmt.Println("读到对方发送的数据...")
	_, err = conn.Read(buf[:4])
	if err != nil {
		//fmt.Println("conn.Read err = ", err)
		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 {
		//fmt.Println("conn.Read fail err = ", err)
		return
	}

	//吧pkgLen反序列化为 message.Message
	err = 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) err = ", err)
		return
	}

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

server/main.go 修改

// 编写一个函数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.Marshal err = ", err)
		return
	}

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

	//声明一个loginResMes 用于赋值
	var loginResMes message.LoginResMes

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

	//将loginResMes 反序列化
	data, err := json.Marshal(loginResMes)
	if err != nil {
		fmt.Println("json.Marshal err = ", err)
		return
	}
	// 将data 赋值给 resMes
	resMes.Data = string(data)

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

	// 发送data,将其封装到writePkg函数
	err = utils.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)
	default:
		fmt.Println("消息类型不存在,无法处理...")
	}
	return
}

// 处理和客户端的通讯
func process(conn net.Conn) {
	//这里需要延时关闭conn
	defer conn.Close()
	//训话客户端发送的消息
	for {
		//读取数据包,直接封装成一个函数readpkg(), 返回message, err
		mes, err := utils.ReadPkg(conn)
		if err != nil {
			if err == io.EOF {
				fmt.Println("客户端关闭,服务端正常关闭...")
				return
			}
			{
				fmt.Println("readpkg err = ", err)
				return
			}
		}
		serverProcessMes(conn, &mes)
		if err != nil {
			return
		}
	}
}

修改client/login.go

	//处理服务器端返回的消息
	mes, err = utils.ReadPkg(conn)
	fmt.Println("----------------")
	if err != nil {
		fmt.Println("utils.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

		// 先把登录的函数,写到另外一个文件,login.go
		login(userId, userPwd)
		//if err != nil {
		//	fmt.Println("登录失败")
		//} else {
		//	fmt.Println("登录成功")
		//}

4.程序结构的改进,前面完成了程序的功能,但是没有结构,系统的可读性、扩展性和维护性都不好,因此需要对程序结构进行改进
一. 改进服务端
1.结构划分
1)main.go

  • 监听
  • 等待客户端的链接
  • 初始化的工作

2)processor.go(总的处理器)

  • 根据客户端的请求,调用对应的处理器,完成相应的任务

3)smsProcess.go

  • 处理和短消息相关的请求
  • 群聊
  • 点对点聊天

4)userPorcess.go

  • 处理和用户相关的请求
  • 登录
  • 注册
  • 注销
  • 用户列表管理

5)utils.go

  • 一些常用的工具,函数,结构体
  • 提供常用的方法和函数

6)common/message

  • 有服务端和客户端公用的文件,比如说message.go

2.结构目录

  • common
    • message
      • message.go
    • utils
      • utils.go
  • server
    • main
      • main.go
      • processor.go
    • model
    • process
      • smsProcess.go
      • userProcess.go

将main.go的代码剥离到对应的文件中
common/utils/utils.go

package utils

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

// 将方法关联到结构体中
type Transfer struct {
	//分析应有哪些字段
	Conn net.Conn
	Buf  [8096]byte //传输时,使用缓冲
}

func (this *Transfer) ReadPkg() (mes message.Message, err error) {
	fmt.Println("读到对方发送的数据...")
	_, err = this.Conn.Read(this.Buf[:4])
	if err != nil {
		//fmt.Println("conn.Read err = ", err)
		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 {
		//fmt.Println("conn.Read fail err = ", err)
		return
	}

	//吧pkgLen反序列化为 message.Message
	err = 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)))
	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) err = ", err)
		return
	}

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

server/process/userProcess.go

package process

import (
	"encoding/json"
	"fmt"
	"net"
	"project/common/message"
	"project/common/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.Marshal err = ", err)
		return
	}

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

	//声明一个loginResMes 用于赋值
	var loginResMes message.LoginResMes

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

	//将loginResMes 反序列化
	data, err := json.Marshal(loginResMes)
	if err != nil {
		fmt.Println("json.Marshal err = ", err)
		return
	}
	// 将data 赋值给 resMes
	resMes.Data = string(data)

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

	// 发送data,将其封装到writePkg函数
	//因为使用分层模式(mvc)需要先创建一个Transfer实例,然后读取
	tf := &utils.Transfer{
		Conn: this.Conn,
	}
	err = tf.WritePkg(data)
	return
}

server/mian/processor.go

package main

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

type Processor struct {
	Conn net.Conn
}

// 编写一个serverProcessMes 函数
// 功能:根据客户端发送消息种类不同,决定调用哪个函数来处理
func (this *Processor) ServerProcessMes(mes *message.Message) (err error) {
	switch mes.Type {
	case message.LoginMesType:
		//处理登录
		//创建UserPorcess实例
		up := &process2.UserProcess{
			Conn: this.Conn,
		}
		err = up.ServerProcessLogin(mes)
	default:
		fmt.Println("消息类型不存在,无法处理...")
	}
	return
}

func (this *Processor) process() (err error) {
	for {
		//读取数据包,直接封装成一个函数readpkg(), 返回message, err
		tf := &utils.Transfer{
			Conn: this.Conn,
		}
		mes, err := tf.ReadPkg()
		if err != nil {
			if err == io.EOF {
				fmt.Println("客户端关闭,服务端正常关闭...")
				return err
			}
			{
				fmt.Println("readpkg err = ", err)
				return err
			}
		}
		err = this.ServerProcessMes(&mes)
		if err != nil {
			return err
		}
	}
}

server/main/main.go

package main

import (
	"fmt"
	"net"
)

// 处理和客户端的通讯
func process(conn net.Conn) {
	//这里需要延时关闭conn
	defer conn.Close()
	//调用主控
	processor := &Processor{
		Conn: conn,
	}
	err := processor.process()
	if err != nil {
		fmt.Println("客户端和服务端通讯协程错误 err = ", err)
		return
	}
}

func main() {
	// 信息提示
	fmt.Println("服务器在8889端口监听...")
	listen, err := net.Listen("tcp", "0.0.0.0:8889")
	if err != nil {
		fmt.Println("net.Listen err = ", err)
		return
	}
	//延时关闭
	defer listen.Close()

	// 一旦监听成功,就等待客户端来链接服务器
	for {
		fmt.Println("等待客户端来链接服务器...")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("listen.Accept err = ", err)
		}

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

二.修改客户端
1.结构划分
1)main.go

  • 显示第一级菜单
  • 根据用户的输入,去调用对应的处理器

2)smsProcess.go

  • 处理和短消息相关逻辑
  • 私聊
  • 群发

3)userProcess.go

  • 处理和用户相关的业务
  • 登录
  • 注册等

4)server.go

  • 显示登录成功界面
  • 保持和服务器通讯【启动协程】
  • 当读取服务器发送的消息后,就会显示在界面

2.文件结构

  • client
    • main
      • main.go
    • model
    • process
      • server.go
      • smsProcess.go
      • userProcess.go

3.代码修改
client/process/userProcess.go

package process

import (
	"encoding/binary"
	"encoding/json"
	"fmt"
	"net"
	"project/common/message"
	"project/common/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 结构体
	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 bytes [4]byte
	binary.BigEndian.PutUint32(bytes[0:4], pkgLen)
	// 发送长度
	n, err := conn.Write(bytes[0:4])
	if n != 4 || err != nil {
		fmt.Println("conn.Write(bytes) fail ", err)
		return
	}

	//fmt.Printf("客户端,发送消息的长度 = %d 内容 = %s", len(data), string(data))
	//发送消息本身
	_, err = conn.Write(data)
	if err != nil {
		fmt.Println("conn.Write(data) err = ", err)
		return
	}

	//处理服务器端返回的消息
	tf := &utils.Transfer{
		Conn: conn,
	}
	mes, err = tf.ReadPkg()
	fmt.Println("----------------")
	if err != nil {
		fmt.Println("utils.ReadPkg(conn) err = ", err)
		return
	}

	//将mes的Data部分反序列化为LoginResMes
	var loginResMes message.LoginResMes
	err = json.Unmarshal([]byte(mes.Data), &loginResMes)
	if loginResMes.Code == 200 {
		//需要在客户端启动一个携程
		//该携程保持喝服务器端的通讯,如果服务器有数据推送给客户端
		//则接收并显示在客户端的终端
		go serverProcessMes(conn)
		//显示登录成功界面
		ShowMenu()
	} else if loginResMes.Code == 500 {
		fmt.Println(loginResMes.Error)
	}
	return
}

client/process/server.go

package process

import (
	"fmt"
	"net"
	"os"
	"project/common/utils"
)

// 显示登录成功后的界面..
func ShowMenu() {
	for {
		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/main/main.go

package main

import (
	"fmt"
	"os"
	process2 "project/client/process"
)

// 用户id 和 密码
var userId int
var userPwd string

func main() {
	// 接受用户的选择
	var key int
	for {
		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 := &process2.UserProcess{}
			up.Login(userId, userPwd)
		case 2:
			fmt.Println("注册用户")
		case 3:
			fmt.Println("退出系统")
			os.Exit(0)
		default:
			fmt.Println("您的输入有误,请重新输入")
		}
	}
}

5.在Redis手动添加测试用户,实现用户信息校验
在model中添加
1)user.go
1.定义一个User结构体

2)userDao.go
1.dao: data access object
2.编写对User对象(实例)操作的各种方法。主要就是增删改查

error.go
1.自定义错误

使用连接池访问redis,提高效率

在redis中手动添加一个用户

hset users 100 "{\"userId\":100,\"userPwd\":\"123456\",\"userName\":\"alan\"}"

创建文件结构

  • server
    • model
      • user.go
      • userDao.go
      • error.go
    • main
      • redis.go

代码实现
user.go

package model

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

error.go

package model

import "errors"

// 根据业务逻辑需要,自定义一些错误
var (
	ERROR_USER_NOTEXISTS = errors.New("用户不存在..")
	ERROR_USER_EXISTS    = errors.New("用户已经存在..")
	ERROR_USER_PWD       = errors.New("密码不正确")
)

userDao.go

package model

import (
	"encoding/json"
	"fmt"
	"github.com/gomodule/redigo/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_NOTEXISTS
		}
		return
	}
	//把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)
	fmt.Println(user)
	if err != nil {
		return
	}
	// 密码不正确返回错误
	if user.UserPwd != userPwd {
		err = ERROR_USER_PWD
		return
	}
	return
}

redis.go

package main

import (
	"github.com/gomodule/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) { //初始化连接的代码
			return redis.Dial("tcp", address)
		},
	}
}

修改service/main/main.go

package main

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

// 处理和客户端的通讯
func process(conn net.Conn) {
	//这里需要延时关闭conn
	defer conn.Close()
	//调用主控
	processor := &Processor{
		Conn: conn,
	}
	err := processor.process()
	if err != nil {
		fmt.Println("客户端和服务端通讯协程错误 err = ", err)
		return
	}
}

// 完成对UserDao的初始化
func initUserDao() {
	//这里的pool时一个全局变量
	model.MyUserDao = model.NewUserDao(pool)
}

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

	// 信息提示
	fmt.Println("服务器在8889端口监听...")
	listen, err := net.Listen("tcp", "0.0.0.0:8889")
	if err != nil {
		fmt.Println("net.Listen err = ", err)
		return
	}
	//延时关闭
	defer listen.Close()

	// 一旦监听成功,就等待客户端来链接服务器
	for {
		fmt.Println("等待客户端来链接服务器...")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("listen.Accept err = ", err)
		}

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

修改server/process/userProcess.go
账号合法性验证代码

	//完成用户登录信息验证
	//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 = "服务器内部错误..."
		}
	} else {
		loginResMes.Code = 200
		fmt.Println(user.UserName, "账户登录成功")
	}

修改client/process/userProcess.go
错误信息返回

	if loginResMes.Code == 200 {
		//需要在客户端启动一个携程
		//该携程保持喝服务器端的通讯,如果服务器有数据推送给客户端
		//则接收并显示在客户端的终端
		go serverProcessMes(conn)
		//显示登录成功界面
		ShowMenu()
	} else {
		fmt.Println(loginResMes.Error)
	}
	return
  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值