zoukankan      html  css  js  c++  java
  • Go_海量用户即时通讯系统

    18.4 海量用户即时通讯系统

    18.4.1 需求分析

    1、用户注册

    2、用户登陆

    3、显示在线用户列表

    4、群聊

    5、点对点聊天

    6、离线留言

    18.4.2 界面设计

    部分:

    NALV8P.png

    18.4.3 项目开发前技术准备

    ​ 项目要保存用户信息和消息数据,因此需要学习数据库(redis或者mysql),这里先选择redis

    NAOHYR.png

    18.4.4 实现功能-显示客户端登陆菜单

    功能:能够正确显示客户端的菜单

    NALV8P.png

    MMIIII.PNG

    代码实现:

    client/main.go

    package main
    import (
    	"fmt"
    	"os"
    )
    
    // 定义两个变量,一个表示用户的ID, 一个表示用户密码
    var userID int
    var userPwd string
    
    func main() {
    	// 接收用户选择
    		var key int
    	// 判断是否还继续显示菜单
    		var loop = true
    	for loop {
    		fmt.Println("-----------欢迎登陆多人聊天系统----------")
    		fmt.Println("			 1 登陆聊天室")
    		fmt.Println("			 2 注册用户")
    		fmt.Println("			 3 退出系统")
    		fmt.Println("			 请选择(1-3))")
    
    		fmt.Scanf("%d
    ", &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
    ", &userID)
    		fmt.Println("请输入用户密码:")
    		fmt.Scanf("%s
    ", &userPwd)
    		// 先把登陆的函数,写到另一个文件
    		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", userID, userPwd)
    	return nil
    }
    

    18.4.5 实现功能-完成用户登陆

    1、先完成指定用户的验证,用户id=100,密码pwd=123456 可以登陆,其他用户不能登陆。

    Message 的组成,并发送一个Message的流程图:

    NmjEXn.png

    1)客户端和服务器端发送消息长度测试

    ​ 完成客户端可以放消息长度,服务器端可以正常收到该长度

    分析思路:

    ① 先确定Message的格式和结构

    ② 然后根据上图的分析完成代码

    ③ 示意图

    Nn0tqf.png

    代码规划

    Nn0IzR.png

    代码实现

    server/main.go

    package main
    import (
    	"fmt"
    	"net"
    )
    
    //处理和客户端的通讯
    func process(conn net.Conn) {
    	//这里需要延迟关闭conn
    	defer conn.Close()
        buf := make([]byte, 8096)
    	//循环读取读客户端发送的消息
    	for {		
    		fmt.Println("读取客户端发送的数据")
    		_, err := conn.Read(buf[:4])
    		if err != nil {
    			fmt.Println("conn.Read err=", err)
    			return
    		}
    		fmt.Println("读到的buf=", buf[:4])
    	}
    }
    
    func main() {
    	//提示信息
    	fmt.Println("服务器在8899监听。。。。")
    	listen, err := net.Listen("tcp", "0.0.0.0:8899")
    	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("liten Accept err=", err)
    		}
    	//一旦连接成功,则启动一个协程和客户端保持通讯。
    	go process(conn)
    	}
    }
    

    client/login.go

    package main
    import (
    	"fmt"
    	"net"
    	"encoding/binary"
    	"encoding/json"
    	"chatroom/common/message"
    )
    
    // 写一个函数,完成登陆
    func login(userId int, userPwd string) (err error) {
    
    	// 下一步就要开始定协议。。。
    	// fmt.Printf(" userID=%d userPwd=%s", userID, userPwd)
    	// return nil
    
    	//1、连接到feu武器
    	conn, err := net.Dial("tcp", "localhost:8899")
    	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[0:4])
    	if n != 4 || err != nil {
    		fmt.Println("conn.Write fail", err)
    		return
    	}
    	fmt.Printf("客户端,发送消息长度成功=%d 内容是=%s", len(data), string(data))
    	return
    }
    

    client/main.go

    package main
    import (
    	"fmt"
    	"os"
    )
    
    // 定义两个变量,一个表示用户的ID, 一个表示用户密码
    var userId int
    var userPwd string
    
    func main() {
    	// 接收用户选择
    		var key int
    	// 判断是否还继续显示菜单
    		var loop = true
    	for loop {
    		fmt.Println("-----------欢迎登陆多人聊天系统----------")
    		fmt.Println("			 1 登陆聊天室")
    		fmt.Println("			 2 注册用户")
    		fmt.Println("			 3 退出系统")
    		fmt.Println("			 请选择(1-3))")
    
    		fmt.Scanf("%d
    ", &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
    ", &userId)
    		fmt.Println("请输入用户密码:")
    		fmt.Scanf("%s
    ", &userPwd)
    		// 先把登陆的函数,写到另一个文件
    		err := login(userId, userPwd)
    		if err != nil {
    			fmt.Println("登陆失败")
    		} else {
    			fmt.Println("登陆成功")
    		}
    	} else if key ==2 {
    		fmt.Println("进行用户注册的逻辑。。。。")
    	}
    }
    

    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"`//返回错误信息
     }
    

    测试结果:

    NnrYs1.png

    2) 客户端和服务器端互通,接收客户端发送的消息

    ​ 完成客户端可以发送消息本身,服务器端可以正常接收到消息,并根据客户端发送的消息(LoginMes),判断用户的合法性,并返回相应的LoginResMes

    思路分析:

    ① 让客户端发送消息本身

    ② 服务器端接收到消息,反序列化成对应的消息结构体

    ③ 服务器端根据反序列化成对应的消息,判断是否登陆用户是合法用户,返回LoginResMes

    ④ 客户端解析返回的LoginResMes显示对应的界面

    ⑤ 这里需要做函数的封装

    ⑥ 代码实现

    client/login.go

    改动代码如下图

    MMIIII.PNG

    完整的代码

    package main
    import (
    	"fmt"
    	"net"
    	"encoding/binary"
    	"encoding/json"
    	"chatroom/common/message"
    	"time"
    )
    
    // 写一个函数,完成登陆
    func login(userId int, userPwd string) (err error) {
    
    	// 下一步就要开始定协议。。。
    	// fmt.Printf(" userID=%d userPwd=%s", userID, userPwd)
    	// return nil
    
    	//1、连接到feu武器
    	conn, err := net.Dial("tcp", "localhost:8899")
    	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[0:4])
    	if n != 4 || err != nil {
    		fmt.Println("conn.Write fail", err)
    		return
    	}
    	// fmt.Printf("客户端,发送消息长度成功=%d 内容是=%s", len(data), string(data))
    	// 发送消息本身
    	_, err = conn.Write(data)
    	if  err != nil {
    		fmt.Println("conn.Write(data) fail", err)
    		return
    	}
    	//休眠20
    	time.Sleep(20 * time.Second)
    	fmt.Println("休眠了20秒")
    	// 这里还需要处理服务器端的消息
    	return
    }
    

    server/main.go

    代码改动如下图:

    MMIIII.PNG

    将读取包的任务封装到了一个函数中 readPkg()

    MMIIII.PNG

    完整代码:

    package main
    import (
    	"fmt"
    	"net"
    	"encoding/binary"
    	"encoding/json"
    	 "chatroom/common/message"
    	 "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])
    		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.Unmarshal fail 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
    			} else {
    				fmt.Println("readPkg fail err=", err)
    				return
    			}			
    		}
    		fmt.Println("mes=", mes)
    	}
    }
    
    func main() {
    	//提示信息
    	fmt.Println("服务器在8899监听。。。。")
    	listen, err := net.Listen("tcp", "0.0.0.0:8899")
    	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("liten Accept err=", err)
    		}
    	//一旦连接成功,则启动一个协程和客户端保持通讯。
    	go process(conn)
    	}
    }
    

    运行结果

    MMIIII.PNG

    3)指定用户登陆

    ​ 能够完成登陆,并提示相应信息(指定用户100 密码123456)

    server/main.go

    新增代码

    MMIIII.PNG

    MMIIII.PNG

    MMIIII.PNG

    MMIIII.PNG

    server/main.go整体代码

    package main
    import (
    	"fmt"
    	"net"
    	"encoding/binary"
    	"encoding/json"
    	"chatroom/common/message"
    	"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])
    		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.Unmarshal fail 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[0:4])
    	if n != 4 || err != nil {
    		fmt.Println("conn.Write fail", err)
    		return
    	}
    	// 发送data本身
    	n, err = conn.Write(data)
    	if n != int(pkgLen) || err != nil {
    		fmt.Println("conn.Write 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.Unmashal 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 序列化
    	data, err := json.Marshal(loginResMes)
    	if err != nil {
    		fmt.Println("json.Marshal fail", err)
    		return
    	}
    
    	// ④ 将data 赋值给resMes
    	resMes.Data = string(data)
    
    	// ⑤ 对resMes  进行序列化,准备发送
    	data, err = json.Marshal(resMes)
    	if err != nil {
    		fmt.Println("json.Marshal fail", err)
    		return
    	}
    
    	// ⑥ 发送data,将其封装到writePkg函数
    	err = writePkg(conn, data)
    	return
    }
    
    // 编写一个ServerProcessMes 函数
    // 功能:根据客户端发送消息种类的不同,决定调用哪个函数来处理
    func serverProcessMes(conn net.Conn, mes *message.Message) (err error){
    	switch mes.Type {
    		case message.LoginMesType : 
    			// 处理登陆
    			err = serverProcessLogin(conn, mes)
    		case message.RegisterMesType :
    			// 处理注册
    		default :
    			fmt.Println("消息类型不存在,无法处理。。。。")
    	}
    	return
    }
    
    //处理和客户端的通讯
    func process(conn net.Conn) {
    	//这里需要延迟关闭conn
    	defer conn.Close()
    	//循环读取读客户端发送的消息
    	for {
    	// 这里将读取数据包,直接封装成一个readPkg(),返回Message,err
    		mes, err := readPkg(conn)
    		if err != nil {
    			if err == io.EOF {
    				fmt.Println("客户端退出,服务器端也正常退出")
    				return
    			} else {
    				fmt.Println("readPkg fail err=", err)
    				return
    			}			
    		}
    		err = serverProcessMes(conn, &mes)
    			if err != nil {
    				return
    			}		
    	}
    }
    
    func main() {
    	//提示信息
    	fmt.Println("服务器在8899监听。。。。")
    	listen, err := net.Listen("tcp", "0.0.0.0:8899")
    	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("liten Accept err=", err)
    		}
    	//一旦连接成功,则启动一个协程和客户端保持通讯。
    	go process(conn)
    	}
    }
    

    client/utils.go 新增文件整体代码

    package main
    import (
    	"fmt"
    	"net"
    	"encoding/binary"
    	"encoding/json"
    	"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 {
    			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.Unmarshal fail 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[0:4])
    	if n != 4 || err != nil {
    		fmt.Println("conn.Write fail", err)
    		return
    	}
    	// 发送data本身
    	n, err = conn.Write(data)
    	if n != int(pkgLen) || err != nil {
    		fmt.Println("conn.Write fail", err)
    		return
    	}
    	return
    }
    

    client/login.go

    改动代码

    MMIIII.PNG

    整体代码

    package main
    import (
    	"fmt"
    	"net"
    	"encoding/binary"
    	"encoding/json"
    	"chatroom/common/message"
    )
    
    // 写一个函数,完成登陆
    func login(userId int, userPwd string) (err error) {
    
    	// 下一步就要开始定协议。。。
    	// fmt.Printf(" userID=%d userPwd=%s", userID, userPwd)
    	// return nil
    
    	//1、连接到feu武器
    	conn, err := net.Dial("tcp", "localhost:8899")
    	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[0:4])
    	if n != 4 || err != nil {
    		fmt.Println("conn.Write fail", err)
    		return
    	}
    	// fmt.Printf("客户端,发送消息长度成功=%d 内容是=%s", len(data), string(data))
    	// 发送消息本身
    	_, err = conn.Write(data)
    	if  err != nil {
    		fmt.Println("conn.Write(data) fail", err)
    		return
    	}
    	//休眠20
    	// time.Sleep(20 * time.Second)
    	// fmt.Println("休眠了20秒")
    	// 这里还需要处理服务器端的消息
    	mes, err = readPkg(conn)  //mes就是
    	if err != nil {
    		fmt.Println("readPkg 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
    }
    

    common/messag/message.go 改动代码

    MMIIII.PNG

    运行结果

    MMIIII.PNG

    4)程序改进

    ​ 程序结构的改进,前面的程序虽然完成了功能,但是没有结构,系统的可读性,扩展性和维护性都不好,因此需要对程序进行改进。

    改进1

    ① 先改进服务器端,先画出程序的框架图

    MMIIII.PNG

    ② 步骤

    • 先把分析出来的文件,创建好,然后放到相应的文件夹[包],下图为代码规划图:

      MMIIII.PNG

    • 现在根据各个文件,完成的任务不同,将server/main/main.go的代码剥离到对应的文件中即可。

    服务器端

    server/main/main.go

    package main
    import (
    	"fmt"
    	"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.Unmarshal fail 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[0:4])
    // 	if n != 4 || err != nil {
    // 		fmt.Println("conn.Write fail", err)
    // 		return
    // 	}
    // 	// 发送data本身
    // 	n, err = conn.Write(data)
    // 	if n != int(pkgLen) || err != nil {
    // 		fmt.Println("conn.Write 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.Unmashal 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 序列化
    // 	data, err := json.Marshal(loginResMes)
    // 	if err != nil {
    // 		fmt.Println("json.Marshal fail", err)
    // 		return
    // 	}
    
    // 	// ④ 将data 赋值给resMes
    // 	resMes.Data = string(data)
    
    // 	// ⑤ 对resMes  进行序列化,准备发送
    // 	data, err = json.Marshal(resMes)
    // 	if err != nil {
    // 		fmt.Println("json.Marshal fail", err)
    // 		return
    // 	}
    
    // 	// ⑥ 发送data,将其封装到writePkg函数
    // 	err = writePkg(conn, data)
    // 	return
    // }
    
    // 编写一个ServerProcessMes 函数
    // 功能:根据客户端发送消息种类的不同,决定调用哪个函数来处理
    // func serverProcessMes(conn net.Conn, mes *message.Message) (err error){
    // 	switch mes.Type {
    // 		case message.LoginMesType : 
    // 			// 处理登陆
    // 			err = serverProcessLogin(conn, mes)
    // 		case message.RegisterMesType :
    // 			// 处理注册
    // 		default :
    // 			fmt.Println("消息类型不存在,无法处理。。。。")
    // 	}
    // 	return
    // }
    
    //处理和客户端的通讯
    func process(conn net.Conn) {
    	//这里需要延迟关闭conn
    	defer conn.Close()
    	// 这里调用总控创建一个总控
    	processor := &Processor{
    		Conn : conn,
    	}
    	err := processor.process2()
    	if err != nil {
    		fmt.Println("客户端和服务器端的通讯协程出现问题 err=", err)
    		return
    	}
    }
    
    func main() {
    	//提示信息
    	fmt.Println("服务器[新的结构]在8899监听。。。。")
    	listen, err := net.Listen("tcp", "0.0.0.0:8899")
    	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("liten Accept err=", err)
    		}
    	//一旦连接成功,则启动一个协程和客户端保持通讯。
    	go process(conn)
    	}
    }
    

    server/main/process.go

    package main
    import (
    	"fmt"
    	"net"
    	"chatroom/common/message"
    	"chatroom/server/utils"
    	"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 fail err=", err)
    					return err
    				}			
    			}
    			err = this.serverProcessMes(&mes)
    				if err != nil {
    					return err
    				}		
    		}
    }
    

    server/process/smsprocess.go

    package process2
    

    server/process/userprocess.go

    package process2
    import (
    	"fmt"
    	"net"
    	"encoding/json"
    	"chatroom/common/message"
    	"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.Unmashal 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 序列化
    	data, err := json.Marshal(loginResMes)
    	if err != nil {
    		fmt.Println("json.Marshal fail", err)
    		return
    	}
    
    	// ④ 将data 赋值给resMes
    	resMes.Data = string(data)
    
    	// ⑤ 对resMes  进行序列化,准备发送
    	data, err = json.Marshal(resMes)
    	if err != nil {
    		fmt.Println("json.Marshal fail", err)
    		return
    	}
    
    	// ⑥ 发送data,将其封装到writePkg函数
    	// 因为使用了分层的模式,先创建Transfer实例,然后读取
    	tf := &utils.Transfer{
    		Conn : this.Conn,
    	}
    
    	err = tf.WritePkg(data)
    	return
    }
    

    server/utils/utils.go

    package utils
    import (
    	"fmt"
    	"net"
    	"encoding/binary"
    	"encoding/json"
    	"chatroom/common/message"
    )
    
    // 这里将这些方法关联到结构体中
    type Transfer struct {
    	// 分析他应该有哪些字段
    	Conn net.Conn
    	Buf [8064]byte //这是传输时使用的缓冲
    }
    
    func(this *Transfer) ReadPkg() (mes message.Message, err error) {
    	// buf := make([]byte, 8096)
    		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.Unmarshal fail err=", err)
    			return
    		}
    	return
    }
    
    func(this *Transfer) WritePkg(data []byte) (err error){
    	// 先发送一个长度给对方
    	var pkgLen uint32
    	pkgLen = uint32(len(data))
    	// var buf [4]byte
    	binary.BigEndian.PutUint32(this.Buf[0:4], pkgLen)
    	// 发送长度
    	n, err := this.Conn.Write(this.Buf[0:4])
    	if n != 4 || err != nil {
    		fmt.Println("conn.Write fail", err)
    		return
    	}
    	// 发送data本身
    	n, err = this.Conn.Write(data)
    	if n != int(pkgLen) || err != nil {
    		fmt.Println("conn.Write fail", err)
    		return
    	}
    	return
    }
    

    代码运行结果

    MMIIII.PNG

    改进2

    客户端

    ① 修改客户端,先画出程序的框架图

    NKrqTP.png

    ② 先把各个文件放到对应的文件夹/包

    代码规划图:

    MMIIII.PNG

    client/main/main.go

    package main
    import (
    	"fmt"
    	"os"
    	"chatroom/client/process"
    )
    
    // 定义两个变量,一个表示用户的ID, 一个表示用户密码
    var userId int
    var userPwd string
    
    func main() {
    	// 接收用户选择
    		var key int
    	// 判断是否还继续显示菜单
    		// var loop = true
    	for true {
    		fmt.Println("-----------欢迎登陆多人聊天系统----------")
    		fmt.Println("			 1 登陆聊天室")
    		fmt.Println("			 2 注册用户")
    		fmt.Println("			 3 退出系统")
    		fmt.Println("			 请选择(1-3))")
    
    		fmt.Scanf("%d
    ", &key)
    		switch key {
    			case 1 :
    				fmt.Println("登陆聊天室")
    				fmt.Println("请输入用户ID:")
    				fmt.Scanf("%d
    ", &userId)
    				fmt.Println("请输入用户密码:")
    				fmt.Scanf("%s
    ", &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("你的输入有误,请重新输入")
    		}
    	}
    	// 根据用户的输入,显示新的提示信息
    	// if key ==1 {
    	// 	// 说明用户要登陆
    
    	// // 因为使用了分层的结构,
    	// 	//先把登陆的函数,写到另一个文件
    	// 	// 这里我们会需要重新调用
    	// 	// login(userId, userPwd)
    	// 	// if err != nil {
    	// 	// 	fmt.Println("登陆失败")
    	// 	// } else {
    	// 	// 	fmt.Println("登陆成功")
    	// 	// }
    	// } else if key ==2 {
    	// 	fmt.Println("进行用户注册的逻辑。。。。")
    	// }
    }
    

    client/process/server.go

    package process
    import (
    	"fmt"
    	"os"
    	"net"
    	"chatroom/client/utils"
    )
    // 显示登陆成功后的界面...
    func ShowMenu() {
    
    	fmt.Println("----------恭喜登陆成功---------")
    	fmt.Println("-------1、显示在线用户列表------")
    	fmt.Println("-------2、发送消息------")
    	fmt.Println("-------3、信息列表------")
    	fmt.Println("-------4、退出系统------")
    	fmt.Println("请选择1-4")
    	var key int 
    	fmt.Scanf("%d
    ", &key)
    	switch key {
    		case 1 : 
    			fmt.Println("1、显示在线用户列表")
    		case 2 : 
    			fmt.Println("2、发送消息")
    		case 3 : 
    			fmt.Println("3、信息列表")
    		case 4 : 
    			fmt.Println("4、退出系统")
    			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("RedPkg err=", err)
    			return
    		}
    		// 如果读取到了消息,下一步处理
    		fmt.Printf("mes=%v", mes)
    
    	}
    }
    

    client/process/smsprocess.go

    package process
    

    client/process/userprocess.go

    package process
    import (
    	"fmt"
    	"net"
    	"encoding/binary"
    	"encoding/json"
    	"chatroom/common/message"
    	"chatroom/client/utils"
    )
    type UserProcess struct {
    	//暂时不需要字段
    }
    // 给关联一个用户登陆方法
    // 写一个函数,完成登陆
    func (this *UserProcess) Login(userId int, userPwd string) (err error) {
    
    	// 下一步就要开始定协议。。。
    	// fmt.Printf(" userID=%d userPwd=%s", userID, userPwd)
    	// return nil
    
    	//1、连接到feu武器
    	conn, err := net.Dial("tcp", "localhost:8899")
    	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[0:4])
    	if n != 4 || err != nil {
    		fmt.Println("conn.Write fail", err)
    		return
    	}
    	// fmt.Printf("客户端,发送消息长度成功=%d 内容是=%s", len(data), string(data))
    	// 发送消息本身
    	_, err = conn.Write(data)
    	if  err != nil {
    		fmt.Println("conn.Write(data) fail", err)
    		return
    	}
    	//休眠20
    	// time.Sleep(20 * time.Second)
    	// fmt.Println("休眠了20秒")
    	// 这里还需要处理服务器端的消息
    	// 创建Transfer实例
    	tf := &utils.Transfer{
    		Conn : conn,
    	}
    	mes, err = tf.ReadPkg()  //mes就是
    	if err != nil {
    		fmt.Println("readPkg 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
    }
    

    client/utils/utils.go

    package utils
    import (
    	"fmt"
    	"net"
    	"encoding/binary"
    	"encoding/json"
    	"chatroom/common/message"
    )
    
    // 这里将这些方法关联到结构体中
    type Transfer struct {
    	// 分析他应该有哪些字段
    	Conn net.Conn
    	Buf [8064]byte //这是传输时使用的缓冲
    }
    
    func(this *Transfer) ReadPkg() (mes message.Message, err error) {
    	// buf := make([]byte, 8096)
    		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.Unmarshal fail err=", err)
    			return
    		}
    	return
    }
    
    func(this *Transfer) WritePkg(data []byte) (err error){
    	// 先发送一个长度给对方
    	var pkgLen uint32
    	pkgLen = uint32(len(data))
    	// var buf [4]byte
    	binary.BigEndian.PutUint32(this.Buf[0:4], pkgLen)
    	// 发送长度
    	n, err := this.Conn.Write(this.Buf[0:4])
    	if n != 4 || err != nil {
    		fmt.Println("conn.Write fail", err)
    		return
    	}
    	// 发送data本身
    	n, err = this.Conn.Write(data)
    	if n != int(pkgLen) || err != nil {
    		fmt.Println("conn.Write fail", err)
    		return
    	}
    	return
    }
    

    运行结果

    MMIIII.PNG

    改进3

    在Redis手动添加测试用户,并画图+说明注意

    MMIIII.PNG

    ① client/process/userprocess.go打开接收的消息本身

    MMIIII.PNG

    ② 启动server和client端

    MMIIII.PNG

    上图圈出的改为:

    "{"userId":100,"userPwd":"123456","userName":"scott"}"
    

    ③ 启动Redis的客户端和服务器端,手动上传数据

    MMIIII.PNG

    改进4

    如果输入的用户名和密码正确在Redis中存在则登陆,否则退出系统,并给出相应的提示信息:

    1、用户不存在,你也可以重新注册,再登录

    2、你的密码错误

    代码实现:

    ① 编写了 server/model/user.go

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

    ② 编写了 server/model/error.go

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

    ③ 编写了 server/model/userDao.go

    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实例
    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_NOEXISTS
    		}
    		return
    	}
    	user = &User{}
    	// 需要把 res反序列化成user实例
    	err = json.Unmarshal([]byte(res), &user)
    	if err != nil {
    		fmt.Println("json.Unmarshal err=", err)
    		return
    	}
    	return
    }
    
    // 2、完成登陆的校验 Login
    // ①、Login  完成对用户的验证
    // ②、如果用户的id和pwd都正确,则返回一个user实例
    // ③、如果用户的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
    }
    

    ④ 编写server/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{
        	MaxIdle:maxIdle,  //最大空闲连接数
        	MaxActive:maxActive, //表示和数据库最大连接数,0表示没有限制
        	IdleTimeout:idleTimeout, //最大空闲时间
        	Dial:func()(redis.Conn,error){  //初始化连接的代码
            	return redis.Dial("tcp","localhost:6379")
        	},
    	}
    }
    

    ⑤ server/main/main.go 新增代码

    Nlr7VA.png

    整体代码:

    package main
    import (
    	"fmt"
    	"net"
    	"time"
    	"chatroom/server/model"
    )
    //处理和客户端的通讯
    func process(conn net.Conn) {
    	//这里需要延迟关闭conn
    	defer conn.Close()
    	// 这里调用总控创建一个总控
    	processor := &Processor{
    		Conn : 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("localhost:6379", 16, 0, 300 * time.Second)
    	initUserDao()
    	//提示信息
    	fmt.Println("服务器[新的结构]在8899监听。。。。")
    	listen, err := net.Listen("tcp", "0.0.0.0:8899")
    	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("liten Accept err=", err)
    		}
    	//一旦连接成功,则启动一个协程和客户端保持通讯。
    	go process(conn)
    	}
    }
    

    ⑥ 在server/process/userProcess.go 使用到redis的验证功能,新增代码

    Nls6sg.png

    整体代码

    package process2
    import (
    	"fmt"
    	"net"
    	"encoding/json"
    	"chatroom/common/message"
    	"chatroom/server/utils"
    	"chatroom/server/model"
    )
    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.Unmashal fail err=", err)
    		return
    	}
    	// ① 先声明一个resMes
    	var resMes message.Message 
    	resMes.Type = message.LoginResMesType
    
    	// ② 再声明一个 loginResMes,并完成赋值
    	var loginResMes message.LoginResMes
    	// 需要到redis数据库完成验证
    	// 1、使用model.MyUserDao到Redis验证
    	user, err := model.MyUserDao.Login(loginMes.UserId, loginMes.UserPwd)	
    	if err != nil {
    		if err == model.ERROR_USER_NOEXISTS {
    			loginResMes.Code = 500
    			loginResMes.Error = err.Error()
    		} else if err == model.ERROR_USER_PWD {
    			loginResMes.Code = 300
    			loginResMes.Error = err.Error()
    		} else {
    			loginResMes.Code = 403
    			loginResMes.Error = "未知错误"
    		}	
    	} else {
    		loginResMes.Code = 200
    		fmt.Println(user, "登陆成功")
    	}
    	
    	
    	// //如果用户的id为100,密码为123456,认为合法, 否则 不合法
    	// if loginMes.UserId == 100 && loginMes.UserPwd == "123456" {
    	// 	// 合法
    	// 	loginResMes.Code = 200
    	// } else {
    	// 	//不合法
    	// 	loginResMes.Code = 500 //500状态码,表示该用户不存在
    	// 	loginResMes.Error = "该用户不存在,请注册后再使用"
    	// }
    
    	// ③ 将 loginResMes 序列化
    	data, err := json.Marshal(loginResMes)
    	if err != nil {
    		fmt.Println("json.Marshal fail", err)
    		return
    	}
    
    	// ④ 将data 赋值给resMes
    	resMes.Data = string(data)
    
    	// ⑤ 对resMes  进行序列化,准备发送
    	data, err = json.Marshal(resMes)
    	if err != nil {
    		fmt.Println("json.Marshal fail", err)
    		return
    	}
    
    	// ⑥ 发送data,将其封装到writePkg函数
    	// 因为使用了分层的模式,先创建Transfer实例,然后读取
    	tf := &utils.Transfer{
    		Conn : this.Conn,
    	}
    
    	err = tf.WritePkg(data)
    	return
    }
    

    ⑦ client/process/userprocess.go 看箭头和之前的代码对比,去除一部分代码

    NlrEBd.png

    18.4.6 实现功能- 完成注册用户

    1、完成注册功能,将用户信息录入到redis中

    N1Iw4O.png

    ① 在ccommon/message/user.go 新增文件和代码

    N1fdp9.png

    ② 在common/message/ message.go 新增文件和代码

    N1frm6.png

    ③ client/process/userprocess.go

    部分截图,整体新增代码在下面

    N1f2fH.png

    func (this *UserProcess) Register(userId int, userPwd string, userName string) (err error) {
    	
    	//1、连接到服务器
    	conn, err := net.Dial("tcp", "localhost:8899")
    	if err != nil {
    		fmt.Println("net.Dial err", err)
    		return
    	}
    	//延时关闭
    	defer conn.Close()
    
    	// 2、准备通过conn发送消息给服务器
    	var mes message.Message
    	mes.Type = message.RegisterMesType
    
    	// 3、创建一个LoginMes
    	var registerMes message.RegisterMes
    	registerMes.User.UserId = userId
    	registerMes.User.UserPwd = userPwd
    	registerMes.User.UserName = userName
    
    	// 4、将RegisterMes序列化
    	data, err := json.Marshal(registerMes)
    	if err != nil {
    		fmt.Println("json.Marshal err=", err)
    		return
    	}
    	//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
    	}
    	// 创建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 err=", err)
    		return
    	}
    // 将mes的Data部分反序列化成 ResterResMes
    	var registerResMes message.RegisterResMes
    	err = json.Unmarshal([]byte(mes.Data), &registerResMes)
    	if registerResMes.Code == 200 {
    		fmt.Println("注册成功,你重新登陆一把")
    		os.Exit(0)
    	} else {
    		fmt.Println(registerResMes.Error)
    		os.Exit(0)
    	}
    	return
    }
    

    ④ 在client/main/main.go 新增了代码,处理注册请求

    N1fOpj.png

    ⑤ server/model/userDao.go 增加了方法

    N1hSBV.png

    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
    }
    

    ⑥ 在server/process/userProcess.go 增加了方法,处理注册

    部分新增代码截图,整体代码往下看

    N1hQ4e.png

    func (this *UserProcess) ServerProcessRegister(mes *message.Message) (err error){
    	// 1、先从mes中取出mes.Data,并直接反序列化成RegisterMes
    	var registerMes message.RegisterMes
    	err = json.Unmarshal([]byte(mes.Data), &registerMes)
    	if err != nil {
    		fmt.Println("json.Unmashal fail err=", err)
    		return
    	}
    
    	// ① 先声明一个resMes
    	var resMes message.Message 
    	resMes.Type = message.RegisterResMesType
    	// ② 再声明一个 loginResMes,并完成赋值
    	var registerResMes message.RegisterResMes
    
    	// 需要到redis数据库完成注册
    	// 1、使用model.MyUserDao到Redis验证
    	err = model.MyUserDao.Register(&registerMes.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
    	}
    
    	data, err := json.Marshal(registerResMes)
    	if err != nil {
    		fmt.Println("json.Marshal fail", err)
    		return
    	}
    	
    	// ④ 将data 赋值给resMes
    	resMes.Data = string(data)
    
    	// ⑤ 对resMes  进行序列化,准备发送
    	data, err = json.Marshal(resMes)
    	if err != nil {
    		fmt.Println("json.Marshal fail", err)
    		return
    	}
    
    	// ⑥ 发送data,将其封装到writePkg函数
    	// 因为使用了分层的模式,先创建Transfer实例,然后读取
    	tf := &utils.Transfer{
    		Conn : this.Conn,
    	}
    
    	err = tf.WritePkg(data)
    	return
    }
    

    ⑦ server/main/processor.go 调用了方法

    N1haE8.png

    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 :
    			// 处理注册
    			up := &process2.UserProcess{
    				Conn : this.Conn,
    			}
    			err = up.ServerProcessRegister(mes)  //type :  data
    		default :
    			fmt.Println("消息类型不存在,无法处理。。。。")
    	}
    	return
    }
    

    18.4.7 实现功能- 完成登陆时能返回当前在线用户

    N1bzuQ.png

    1、用户登陆后,可以得到当前在线用户列表

    ① 编写了 server/process/userMgr.go

    package process2
    import(
    	"fmt"
    )
    
    // 因为UserMgr实例在服务器有且只有一个
    // 因为在很多地方,都会使用,因此,我们
    // 将其定义为全局变量
    
    var (
    	 userMgr *UserMgr
    )
    
    type UserMgr struct {
    	onlineUsers map[int]*UserProcess
    }
    
    // 完成对userMgr初始化工作
    func init() {
    	userMgr = &UserMgr {
    		onlineUsers : make(map[int]*UserProcess, 1024),
    	}
    }
    
    // 完成对onlineUsers添加
    func (this *UserMgr) AddonlineUserup(up *UserProcess){
    	this.onlineUsers[up.UserId] = up
    }
    // 删除
    func (this *UserMgr) DelonlineUserup(userId int){
    	delete(this.onlineUsers, userId)
    }
    //返回当前所有在线用户
    func (this *UserMgr) DelonlineUserupmap() map[int]*UserProcess{
    	return this.onlineUsers
    }
    
    // 根据ID返回对应的值
    func (this *UserMgr) GetOnlienUserById(userId int) (up *UserProcess, err error) {
    	// 如何从map取出一个值,带检测的方式
    	up, ok := this.onlineUsers[userId]
    	if !ok  { //说明你要查找的用户,当前不在线
    		err = fmt.Errorf("用户%d 不存在", userId)
    		return
    	}
    	return
    }
    

    ② server/process/userprocess.go 新增了

    N1L7fP.png

    ③ server/process/userprocess.go

    N1v9fI.png

    ④ common/message/message.go

    N1vuhn.png

    ⑤ client/process/userprocess.go

    N1v1XT.png

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

    1592721254649

    ①server/process/userProcess.go

    N3cbwD.png

    N3grhd.png

    N32ZUe.png

    ② client/common/messsage/user.go

    MMIIII.PNG

    ③ common/message/message.go

    MMIIII.PNG

    MMIIII.PNG

    ④ client/process/userMg.go

    N3RuoF.png

    N3RfYQ.png

    整体代码

    package process
    import (
    	"fmt"
    	"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:	", id)
    	}
    }
    // 编写一个方法,处理返回的NotifyUserStatusMes
    func updateUserStatus(notifyUserStatusMes *message.NotifyUserStatusMes){	
    		// 适当优化
    		user, ok := onlineUsers[notifyUserStatusMes.UserId]
    		if !ok { //原来没有
    			user = &message.User {
    				UserId : notifyUserStatusMes.UserId,
    			}
    		}	
    	user.UserStatus = notifyUserStatusMes.Status
    	onlineUsers[notifyUserStatusMes.UserId] = user
    	outputOnlineUser()
    }
    

    ⑤ client/process/server.go

    N3fAbV.png

    N3WT9H.png

    运行结果

    N3h1zj.png

    18.4.8 实现功能- 群聊

    N3LCng.png

    1、当一个用户上线后,可以将群聊消息发送给服务器,服务器可以接收到

    ① common/message/message.go

    N3bPxS.png

    ② client/model/curUser.go

    N3buGV.png

    ③ client/process/smsprocess.go 增加了发送群聊消息

    N3bRRf.png

    N3b7on.png

    代码运行结果

    N3qtmQ.png

    2、服务器可以将收到的消息,群发给所有在线用户(发送者除外)

    N3Le3V.png

    ①在server/process/smsProcess.go

    N3xOAg.png

    N3zScq.png

    ②server/main/processor.go

    N891ET.png

    ③client/process/smsMgr.go

    N8SZdS.png

    ④client/process/server.go

    N8pElR.png

    代码运行结果

    N8FByd.png

    项目整体规划图

    N34gjs.png

    项目全部代码链接

    百度网盘:https://pan.baidu.com/s/183VFYxJUaU2_g3zLVtvviQ    提取码:nxd9
    如若教程失效,请联系QQ:2259887619,开源不易,常被举报,望耐心等候
    感谢分享
    
  • 相关阅读:
    java加密算法-MD5
    java加密算法-DES
    java加密算法-AES
    java写入内容到本地文件 -读取文件内容
    java 图片base64互转
    java上传文件
    判断请求是否是同一个域名
    java计算两个经纬度之间的距离
    java请求url可以带参数
    Java编程基础篇第五章
  • 原文地址:https://www.cnblogs.com/jiaxiaozia/p/13171861.html
Copyright © 2011-2022 走看看