[go学习笔记.第十六章.TCP编程] 2.项目-海量用户即时通讯系统

news2024/10/7 11:22:58

一.项目介绍

1.项目开发流程

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

2.需求分析 

(1).用户注册

(2).用户登录

(3).显示在线用户列表

(4).群聊(广播)

(5).点对点聊天

(6).离线留言

3.示意图

 4.项目开发前技术准备 

项目要保存用户信息和消息数据,因此需要数据库(mysql或者redis),这里使用redis进行数据的保存

二.项目开发 

1.实现功能:显示客户端登录菜单

功能:

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

界面如下:

 代码如下:

  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
}

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("\t\t\t\t 1 登录聊天室")
        fmt.Println("\t\t\t\t 2 注册用户")
        fmt.Println("\t\t\t\t 3 退出系统")
        fmt.Println("\t\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("注册")
    }
}

2.实现功能:完成用户登录

要求: 

        先完成指定用户的验证,用户 id = 100 ,密码 pw =123456 可以登录,其它用户不能登录, 这里需要先说明一个 Message的组成(示意图),并发送一个 Message 的流程

1).完成客户端可以发送消息长度,服务器端可以正常收到该长度值

分析思路

(1).先确定消息 Message 的格式和结构

(2).根据上图的分析完成代码

(3).示意图如下

 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 err != nil || n != 4 {
            fmt.Printf(" conn read err = %v\n", err)
            return
        }
        fmt.Printf("读到的buf= %v\n", buf[:4])
    }
}

func main()  {
    //提示信息
    fmt.Println("服务器正在监听8889端口...")
    listen, err := net.Listen("tcp", "127.0.0.1:8889")
    //这里需要延时关闭
    defer listen.Close()

    if err != nil {
        fmt.Printf("net listen err = %v\n", err)
        return
    }
    //一旦监听成功,就等待客户端来连接服务器
    for {
        fmt.Println("等待客户端来连接服务器...")
        conn, err := listen.Accept()
        if err != nil {
            fmt.Printf("listen accept err = %v\n", err)
            return
        }
        //一旦连接成功,则启动一个协程,保持和客户端通讯
        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"` //返回状态码: 200 登录成功, 500 用户未注册
    Error string `json:"error"` //返回错误信息
}

client/main.go 和前面代码一样,没有发生修改

clent/login.go

package main

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

//写一个函数, 完成登录
func login(userId int, userPwd string) (err error) {
    // //下一步 开始定协议
    // fmt.Printf("userId = %d, userPwd = %s \n", userId, userPwd)
    // return

    //1.连接到服务器端
    conn, err := net.Dial("tcp", "127.0.0.1:8889")
    if err != nil {
        fmt.Printf("net dial err = %v\n", 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.Printf("json marshal err = %v\n", err)
        return
    }
    //5.将序列化后的loginMes byte切片赋给mes.Data
    mes.Data = string(data)
    //6.将mes序列化
    data, err = json.Marshal(mes)
    if err != nil {
        fmt.Printf("json marshal err = %v\n", err)
        return
    }
    //7.这个时候data就是要发送的消息
    //7.1先把data的长度发送给服务器
    //先获取到data的长度,然后把长度转换成一个表示长度的byte切片
    var pkgLen uint32
    pkgLen = uint32(len(data))  //把data的长度转换成uint32,供后续转成byte切片长度使用
    var buf [4]byte
    binary.BigEndian.PutUint32(buf[0:4], pkgLen)
    //发送长度
    n, err := conn.Write(buf[:4])
    if err != nil || n != 4 {
        fmt.Printf(" conn write err = %v\n", err)
        return
    }

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

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

思路分析:

(1).让客户端发送消息本身

(2).服务器端接受到消息,然后反序列话化成对应的消息结构体

(3).服务器端根据反序列化成对应的消意,判断登录用户是合法,返回LoginResMes

(4).客户端解析返回的LoginResMes ,显示对应界面

(5).这里需要做函数的封装

client/login.go做了点修改 


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

    //休眠20s
    time.Sleep(20 * time.Second)
    fmt.Println("休眠20s...")
    //处理服务端返回的消息

    return
}

server/main.go做了改动

package main

import (
    "fmt"
    "net"
    "go_code/chatroom/common/message"
    "encoding/binary"
    "encoding/json"
    _"errors"
    "io"
)

//处理和客户端通讯
func process(conn net.Conn)  {
    //这里需要延时关闭
    defer conn.Close()
    
    //读取客户端发送的消息    
    for {
        //将读取数据包直接封装成一个函数readPkg(),返回messag,error
        mes, err := readPkg(conn)
        if err != nil {
            if err == io.EOF {
                fmt.Println("客户端退出了,服务器端也跟着退出")
            } else {
                fmt.Printf(" readPkg err = %v\n", err)
            }
            
            return
        }
        fmt.Printf("mes= %v\n", mes)
    }
}

//读取数据包直接封装成一个函数readPkg()
func readPkg(conn net.Conn) (mes message.Message, err error)  {
    //创建一个切片,用于后续conn.Read()
    buf := make([]byte, 8096)
    fmt.Println("读取客户端发送的数据...") 
    //conn.Read在conn没有被关闭的情况下,才会阻塞
    //如果客户端关闭了conn,则不会阻塞
    _, err = conn.Read(buf[:4])
    if err != nil {
        // err = errors.New("read pkg header error")
        return
    }
    //根据buf[:4]转成一个uint32类型
    var pkgLen uint32
    pkgLen = binary.BigEndian.Uint32(buf[0:4])
    //根据pkgLen读取消息内容
    n, err := conn.Read(buf[:pkgLen])
    if n != int(pkgLen) || err != nil {
        // err = errors.New("read pkg body error")
        return
    }
    //把pkgLen反序列化成message.Message
    err = json.Unmarshal(buf[:pkgLen], &mes)
    if err != nil {
        // err = errors.New("read pkg json.Unmarshal error")
        return
    }
    return
}

func main()  {
    //提示信息
    fmt.Println("服务器正在监听8889端口...")
    listen, err := net.Listen("tcp", "127.0.0.1:8889")
    //这里需要延时关闭
    defer listen.Close()

    if err != nil {
        fmt.Printf("net listen err = %v\n", err)
        return
    }
    //一旦监听成功,就等待客户端来连接服务器
    for {
        fmt.Println("等待客户端来连接服务器...")
        conn, err := listen.Accept()
        if err != nil {
            fmt.Printf("listen accept err = %v\n", err)
            return
        }
        //一旦连接成功,则启动一个协程,保持和客户端通讯
        go process(conn)
    }
}

能够完成登录,并提示相应信息 

server/server.go修改

package main

import (
    "fmt"
    "net"
    "go_code/chatroom/common/message"
    "encoding/binary"
    "encoding/json"
    _"errors"
    "io"
)

//处理和客户端通讯
func process(conn net.Conn)  {
    //这里需要延时关闭
    defer conn.Close()
    //读取客户端发送的消息    
    for {
        //将读取数据包直接封装成一个函数readPkg(),返回messag,error
        mes, err := readPkg(conn)
        if err != nil {
            if err == io.EOF {
                fmt.Println("客户端退出了,服务器端也跟着退出")
            } else {
                fmt.Printf(" readPkg err = %v\n", err)
            }
            return
        }
        err = serverProcessMes(conn, &mes)
        if err != nil {
            return
        } 
    }
}

//编写一个函数serverProcessLogin函数,专门处理登录请求
func serverProcessLogin(conn net.Conn, mes *message.Message) (err error)  {
    //先从mes中取出mes.Data,并直接反序列化成LoginMes
    var loginMes message.LoginMes
    err = json.Unmarshal([]byte(mes.Data), &loginMes)
    if err != nil {
        fmt.Printf("json.Unmarshal fail, err = %v\n", err)
        return
    }
    //1.声明一个resMes
    var resMes message.Message
    resMes.Type = message.LoginResMesType
    //2.再声明一个loginResMes,并完成赋值
    var loginResMes message.LoginResMes
    //如果用户id=100,密码=123456,则合法,不然,则不合法
    if loginMes.UserId == 100 && loginMes.UserPwd == "123456" {
        //合法
        loginResMes.Code = 200
    } else {
        //不合法
        loginResMes.Code = 500  //500 表示不存在
        loginResMes.Error = "该用户不存在,请注册后使用"
    }
    //3.将loginResMes序列化
    data, err := json.Marshal(loginResMes)
    if err != nil {
        fmt.Printf("json.Marshal fail, err = %v\n", err)
        return
    }
    //4.将data赋值给resMes
    resMes.Data = string(data)
    //5.对resMes序列化,准备发送
    data, err = json.Marshal(resMes)
    if err != nil {
        fmt.Printf("json.Marshal fail, err = %v\n", err)
        return
    }
    //6.发送data,将其封装到writePkg函数中
    err = writePkg(conn, data)
    return
}   

func writePkg(conn net.Conn, data []byte) (err error)  {
    //先发送一个长度给对方
    //先把data的长度发送给对方
    //先获取到data的长度,然后把长度转换成一个表示长度的byte切片
    var pkgLen uint32
    pkgLen = uint32(len(data))  //把data的长度转换成uint32,供后续转成byte切片长度使用
    var buf [4]byte
    binary.BigEndian.PutUint32(buf[0:4], pkgLen)
    //发送长度
    n, err := conn.Write(buf[:4])
    if err != nil || n != 4 {
        fmt.Printf(" conn write err = %v\n", err)
        return
    }
    //发送data本身
    n, err = conn.Write(data)
    if err != nil || n != int(pkgLen) {
        fmt.Printf(" conn write err = %v\n", err)
    }
    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
}

//读取数据包直接封装成一个函数readPkg()
func readPkg(conn net.Conn) (mes message.Message, err error)  {
    //创建一个切片,用于后续conn.Read()
    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 {
        // err = errors.New("read pkg body error")
        return
    }
    //把pkgLen反序列化成message.Message
    err = json.Unmarshal(buf[:pkgLen], &mes)
    if err != nil {
        return
    }
    return
}

func main()  {
    //提示信息
    fmt.Println("服务器正在监听8889端口...")
    listen, err := net.Listen("tcp", "127.0.0.1:8889")
    //这里需要延时关闭
    defer listen.Close()
    if err != nil {
        fmt.Printf("net listen err = %v\n", err)
        return
    }
    //一旦监听成功,就等待客户端来连接服务器
    for {
        fmt.Println("等待客户端来连接服务器...")
        conn, err := listen.Accept()
        if err != nil {
            fmt.Printf("listen accept err = %v\n", err)
            return
        }
        //一旦连接成功,则启动一个协程,保持和客户端通讯
        go process(conn)
    }
}

client/utils/utils.go

package main

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

//读取数据包直接封装成一个函数readPkg()
func readPkg(conn net.Conn) (mes message.Message, err error)  {
    //创建一个切片,用于后续conn.Read()
    buf := make([]byte, 8096)
    fmt.Println("读取客户端发送的数据...") 
    //conn.Read在conn没有被关闭的情况下,才会阻塞
    //如果客户端关闭了conn,则不会阻塞
    _, err = conn.Read(buf[:4])
    if err != nil {
        // err = errors.New("read pkg header error")
        return
    }
    //根据buf[:4]转成一个uint32类型
    var pkgLen uint32
    pkgLen = binary.BigEndian.Uint32(buf[0:4])
    //根据pkgLen读取消息内容
    n, err := conn.Read(buf[:pkgLen])
    if n != int(pkgLen) || err != nil {
        // err = errors.New("read pkg body error")
        return
    }
    //把pkgLen反序列化成message.Message
    err = json.Unmarshal(buf[:pkgLen], &mes)
    if err != nil {
        // err = errors.New("read pkg json.Unmarshal error")
        return
    }
    return
}

func writePkg(conn net.Conn, data []byte) (err error)  {
    //先发送一个长度给对方
    //先把data的长度发送给对方
    //先获取到data的长度,然后把长度转换成一个表示长度的byte切片
    var pkgLen uint32
    pkgLen = uint32(len(data))  //把data的长度转换成uint32,供后续转成byte切片长度使用
    var buf [4]byte
    binary.BigEndian.PutUint32(buf[0:4], pkgLen)
    //发送长度
    n, err := conn.Write(buf[:4])
    if err != nil || n != 4 {
        fmt.Printf(" conn write err = %v\n", err)
        return
    }
    //发送data本身
    n, err = conn.Write(data)
    if err != nil || n != int(pkgLen) {
        fmt.Printf(" conn write err = %v\n", err)
    }
    return
}

client/client.go

package main

import (
    "fmt"
    "net"
    _"time"
    "encoding/binary"
    "encoding/json"
    "go_code/chatroom/common/message"
)

//写一个函数, 完成登录
func login(userId int, userPwd string) (err error) {
    // //下一步 开始定协议
    // fmt.Printf("userId = %d, userPwd = %s \n", userId, userPwd)
    // return

    //1.连接到服务器端
    conn, err := net.Dial("tcp", "127.0.0.1:8889")
    if err != nil {
        fmt.Printf("net dial err = %v\n", 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.Printf("json marshal err = %v\n", err)
        return
    }
    //5.将序列化后的loginMes byte切片赋给mes.Data
    mes.Data = string(data)
    //6.将mes序列化
    data, err = json.Marshal(mes)
    if err != nil {
        fmt.Printf("json marshal err = %v\n", err)
        return
    }
    //7.这个时候data就是要发送的消息
    //7.1先把data的长度发送给服务器
    //先获取到data的长度,然后把长度转换成一个表示长度的byte切片
    var pkgLen uint32
    pkgLen = uint32(len(data))  //把data的长度转换成uint32,供后续转成byte切片长度使用
    var buf [4]byte
    binary.BigEndian.PutUint32(buf[0:4], pkgLen)
    //发送长度
    n, err := conn.Write(buf[:4])
    if err != nil || n != 4 {
        fmt.Printf(" conn write err = %v\n", err)
        return
    }
    // fmt.Printf("客户端发送消息长度=%d,内容=%s\n", len(data), string(data)) 
    //发送消息本身
    _, err = conn.Write(data)
    if err != nil {
        fmt.Printf(" conn write err = %v\n", err)
        return
    }

    //休眠20s
    // time.Sleep(20 * time.Second)
    // fmt.Println("休眠20s...")
    //处理服务端返回的消息
    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 err != nil {
        fmt.Println("json.Unmarshal err = ", err)
        return
    }
    if loginResMes.Code == 200 {
        fmt.Println("登录成功") 
    } else if loginResMes.Code == 500 {
        fmt.Println(loginResMes.Error)
    }

    return
}

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

1).先改进服务端.先画出程序的框架图,再写代码

2).步骤

(1).先把分析出来的文件,创建好,然后把对应的代码放入到相应的文件夹(包) 

(2).根据各个文件,完成的任务不同,将main.go的代码剥离到对应的文件中即可

server/main/main.go

package main

import (
    "fmt"
    "net"
)

//处理和客户端通讯
func process(conn net.Conn)  {
    //这里需要延时关闭
    defer conn.Close()
    //这里调用总控,创建一个总控实例
    processor := &Processor{
        Conn: conn,
    }
    err := processor.ProcessMain()
    if err != nil {
        fmt.Printf("客户端和服务器端的协程出问题了,err= %v\n", err)
        return
    }
}

func main()  {
    //提示信息
    fmt.Println("服务器[新结构]正在监听8889端口...")
    listen, err := net.Listen("tcp", "127.0.0.1:8889")
    //这里需要延时关闭
    defer listen.Close()
    if err != nil {
        fmt.Printf("net listen err = %v\n", err)
        return
    }
    //一旦监听成功,就等待客户端来连接服务器
    for {
        fmt.Println("等待客户端来连接服务器...")
        conn, err := listen.Accept()
        if err != nil {
            fmt.Printf("listen accept err = %v\n", err)
            return
        }
        //一旦连接成功,则启动一个协程,保持和客户端通讯
        go process(conn)
    }
}

 server/main/processor.go

package main

import (
    "fmt"
    "net"
    "io"
    "go_code/chatroom/common/message"
    "go_code/chatroom/server/utils"
    "go_code/chatroom/server/processBlock"
)

//创建一个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 := &processBlock.UserProcess{
                Conn: this.Conn,
            }
            err = up.ServerProcessLogin(mes)
        case message.RegisterMesType : 
            //处理注册
        default :
            fmt.Println("消息类型不存在, 无法处理...")
    }
    return
}

func (this *Processor) ProcessMain() (err error) {
    //读取客户端发送的消息    
    for {
        //将读取数据包直接封装成一个函数readPkg(),返回messag,error
        //创建一个Transfer,完成读包的任务
        tf := &utils.Transfer{
            Conn: this.Conn,
        }
        mes, err := tf.ReadPkg()
        if err != nil {
            if err == io.EOF {
                fmt.Println("客户端退出了,服务器端也跟着退出")
            } else {
                fmt.Printf(" readPkg err = %v\n", err)
            }
            return err
        }
        err = this.serverProcessMes(&mes)
        if err != nil {
            return err
        } 
    }
}

 server/utils/utils.go

package utils

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

//将方法关联到结构体中
type Transfer struct {
    Conn net.Conn
    Buf [8096]byte  //传输时,使用的缓冲
}

func (this *Transfer) WritePkg(data []byte) (err error)  {
    //先发送一个长度给对方
    //先把data的长度发送给对方
    //先获取到data的长度,然后把长度转换成一个表示长度的byte切片
    var pkgLen uint32
    pkgLen = uint32(len(data))  //把data的长度转换成uint32,供后续转成byte切片长度使用
    binary.BigEndian.PutUint32(this.Buf[0:4], pkgLen)
    //发送长度
    n, err := this.Conn.Write(this.Buf[:4])
    if err != nil || n != 4 {
        fmt.Printf(" conn write err = %v\n", err)
        return
    }
    //发送data本身
    n, err = this.Conn.Write(data)
    if err != nil || n != int(pkgLen) {
        fmt.Printf(" conn write err = %v\n", err)
    }
    return
}

//读取数据包直接封装成一个函数readPkg()
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 {
        // err = errors.New("read pkg body error")
        return
    }
    //把pkgLen反序列化成message.Message
    err = json.Unmarshal(this.Buf[:pkgLen], &mes)
    if err != nil {
        return
    }
    return
}

server/processBlock/userProcess.go

package processBlock

import (
    "fmt"
    "net"
    "go_code/chatroom/common/message"
    "go_code/chatroom/server/utils"
    "encoding/json"
)

type UserProcess struct {
    Conn net.Conn
}

//编写一个函数serverProcessLogin函数,专门处理登录请求
func (this *UserProcess) ServerProcessLogin(mes *message.Message) (err error)  {
    //先从mes中取出mes.Data,并直接反序列化成LoginMes
    var loginMes message.LoginMes
    err = json.Unmarshal([]byte(mes.Data), &loginMes)
    if err != nil {
        fmt.Printf("json.Unmarshal fail, err = %v\n", err)
        return
    }
    //1.声明一个resMes
    var resMes message.Message
    resMes.Type = message.LoginResMesType
    //2.再声明一个loginResMes,并完成赋值
    var loginResMes message.LoginResMes
    //如果用户id=100,密码=123456,则合法,不然,则不合法
    if loginMes.UserId == 100 && loginMes.UserPwd == "123456" {
        //合法
        loginResMes.Code = 200
    } else {
        //不合法
        loginResMes.Code = 500  //500 表示不存在
        loginResMes.Error = "该用户不存在,请注册后使用"
    }
    //3.将loginResMes序列化
    data, err := json.Marshal(loginResMes)
    if err != nil {
        fmt.Printf("json.Marshal fail, err = %v\n", err)
        return
    }
    //4.将data赋值给resMes
    resMes.Data = string(data)
    //5.对resMes序列化,准备发送
    data, err = json.Marshal(resMes)
    if err != nil {
        fmt.Printf("json.Marshal fail, err = %v\n", err)
        return
    }
    //6.发送data,将其封装到writePkg函数中
    //因为使用了分层模式(mvc),先创建Transfer实例,然后读取
    tf := &utils.Transfer{
        Conn: this.Conn,
    }
    err = tf.WritePkg(data)
    return
}   

修改客户端,先画出程序框架图,再写代码

client/main.go

package main

import (
    "fmt"
    "os"
    "go_code/chatroom/client/processBlock"
)

//定义两个全局变量,一个用户id,一个用户密码
var userId int
var userPwd string

func main()  {
    //接收用户的选择
    var key int

    for true {
        fmt.Println("-------------------欢迎登录多人聊天系统-------------------")
        fmt.Println("\t\t\t\t 1 登录聊天室")
        fmt.Println("\t\t\t\t 2 注册用户")
        fmt.Println("\t\t\t\t 3 退出系统")
        fmt.Println("\t\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)
                //先把登录的函数写到另一个文件,比如:login.go
                //因为使用了分层模式(mvc),故调用分层模式中processBlock.UserProcess.Login()处理
                up := &processBlock.UserProcess{}
                up.Login(userId, userPwd)
            case 2 : 
                fmt.Println("注册用户")
            case 3 :  
                fmt.Println("退出系统")
                os.Exit(0)
            default:
                fmt.Println("输入有误,请重新输入")
        }
    }
}

client/utils/utils.go

package utils

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

//将方法关联到结构体中
type Transfer struct {
    Conn net.Conn
    Buf [8096]byte  //传输时,使用的缓冲
}

func (this *Transfer) WritePkg(data []byte) (err error)  {
    //先发送一个长度给对方
    //先把data的长度发送给对方
    //先获取到data的长度,然后把长度转换成一个表示长度的byte切片
    var pkgLen uint32
    pkgLen = uint32(len(data))  //把data的长度转换成uint32,供后续转成byte切片长度使用
    binary.BigEndian.PutUint32(this.Buf[0:4], pkgLen)
    //发送长度
    n, err := this.Conn.Write(this.Buf[:4])
    if err != nil || n != 4 {
        fmt.Printf(" conn write err = %v\n", err)
        return
    }
    //发送data本身
    n, err = this.Conn.Write(data)
    if err != nil || n != int(pkgLen) {
        fmt.Printf(" conn write err = %v\n", err)
    }
    return
}

//读取数据包直接封装成一个函数readPkg()
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 {
        // err = errors.New("read pkg body error")
        return
    }
    //把pkgLen反序列化成message.Message
    err = json.Unmarshal(this.Buf[:pkgLen], &mes)
    if err != nil {
        return
    }
    return
}

client/processBlock/server.go

package processBlock

import (
    "fmt"
    "os"
    "net"
    "go_code/chatroom/client/utils"
)

//显示登录成功后的界面
func ShowMenu()  {
    fmt.Println("--------恭喜xxx登录成功--------")
    fmt.Println("--------1.显示在线用户列表--------")
    fmt.Println("--------2.发送消息--------")
    fmt.Println("--------3.信息列表--------")
    fmt.Println("--------4.退出系统--------")
    fmt.Println("-------请选择(1~4):----")
    var key int
    fmt.Scanf("%d\n", &key)
    switch key {
        case 1:
            fmt.Println("显示在线用户列表")
        case 2:
            fmt.Println("发送消息")
        case 3:
            fmt.Println("信息列表")
        case 4:
            fmt.Println("退出了系统")
            os.Exit(0)
        default:
            fmt.Println("输入错误,请重新输入")
    }
}

//和服务端端保持通讯
func ServerProcessMes(conn net.Conn)  {
    //创建一个Transfer实例,让它不停地读取服务器发送的消息
    tf := &utils.Transfer {
        Conn: conn,
    }
    for {
        fmt.Println("客户端正在等待读取服务器发送的消息")
        mes, err := tf.ReadPkg()
        if err != nil {
            fmt.Println("tf.readpkg err =", err)
            return
        }
        //如果读取到消息,则进行下一步逻辑处理
        fmt.Printf("mes=%v\n", mes)
    }
}

client/processBlock/userProcess.go

package processBlock

import (
    "fmt"
    "net"
    "encoding/binary"
    "encoding/json"
    "go_code/chatroom/common/message"
    "go_code/chatroom/client/utils"
)

type UserProcess  struct {

}

//写一个函数, 完成登录
func (this *UserProcess) Login(userId int, userPwd string) (err error) {
    // //下一步 开始定协议
    // fmt.Printf("userId = %d, userPwd = %s \n", userId, userPwd)
    // return

    //1.连接到服务器端
    conn, err := net.Dial("tcp", "127.0.0.1:8889")
    if err != nil {
        fmt.Printf("net dial err = %v\n", 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.Printf("json marshal err = %v\n", err)
        return
    }
    //5.将序列化后的loginMes byte切片赋给mes.Data
    mes.Data = string(data)
    //6.将mes序列化
    data, err = json.Marshal(mes)
    if err != nil {
        fmt.Printf("json marshal err = %v\n", err)
        return
    }
    //7.这个时候data就是要发送的消息
    //7.1先把data的长度发送给服务器
    //先获取到data的长度,然后把长度转换成一个表示长度的byte切片
    var pkgLen uint32
    pkgLen = uint32(len(data))  //把data的长度转换成uint32,供后续转成byte切片长度使用
    var buf [4]byte
    binary.BigEndian.PutUint32(buf[0:4], pkgLen)
    //发送长度
    n, err := conn.Write(buf[:4])
    if err != nil || n != 4 {
        fmt.Printf(" conn write err = %v\n", err)
        return
    }
    // fmt.Printf("客户端发送消息长度=%d,内容=%s\n", len(data), string(data)) 
    //发送消息本身
    _, err = conn.Write(data)
    if err != nil {
        fmt.Printf(" conn write err = %v\n", err)
        return
    }

    //休眠20s
    // time.Sleep(20 * time.Second)
    // fmt.Println("休眠20s...")
    //处理服务端返回的消息
    //创建一个Transfer实例
    tf := &utils.Transfer{
        Conn: conn,
    }
    mes, err = tf.ReadPkg()
    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 err != nil {
        fmt.Println("json.Unmarshal err = ", err)
        return
    }
    if loginResMes.Code == 200 {
        // fmt.Println("登录成功") 
        //这里需要启动一个协程,保持和服务器通讯,如果服务器有数据,则推送给客户端,客户端接收后显示在终端
        go ServerProcessMes(conn)
        //1.显示登录成功的菜单[循环显示]
        for {
            ShowMenu()
        }
    } else if loginResMes.Code == 500 {
        fmt.Println(loginResMes.Error)
    }

    return
}

[上一节][go学习笔记.第十六章.TCP编程] 1.基本介绍以及入门案例

[下一节][go学习笔记.第十六章.TCP编程] 3.项目-海量用户即时通讯系统-redis介入,用户登录,注册 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/25257.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Vue】vue项目用qrcodejs2生成带log的二维码图片,vue生成二维码图片中间带log,自定义log

系列文章目录 提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加 提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录系列文章目录前言一、安装qrcodejs2二、在页面中使用1.引入…

英国Paper写作思路和精髓如何了解?

第一学期即将结束,为了能帮助更多英国留学生顺利完成Paper,增加对英国Paper写作的理解,取得高分。本文小编为大家分享英国Paper写作的思路和精髓,帮助自己修改提升Paper质量。 The first semester is coming to an end.In order t…

flutter AnimatedSwitcher 动画切换过渡组件 跑马灯动画封装

flutter AnimatedSwitcher 动画切换过渡组件前言一、AnimatedSwitcher 简介二、AnimatedSwitcher 的简单使用三、AnimatedSwitcher 自定义跑马灯动画四、SlideTransitionX 的封装总结前言 本篇文章将记录 AnimatedSwitcher 过渡组件,这个组件动画是一个新的小部件来…

制作一个简单HTML宠物猫网页(HTML+CSS)

🎉精彩专栏推荐 💭文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 💂 作者主页: 【主页——🚀获取更多优质源码】 🎓 web前端期末大作业: 【📚毕设项目精品实战案例 (10…

在 Spring Boot中配置日志

Spring Boot 在引擎盖下使用Apache Commons Logging。但是,它允许您选择所需的日志记录库。让我们来看看使用 Spring Boot 时的一些配置和最佳实践。 目录 概述简单日志记录示例配置日志记录 更改日志级别将日志写入文件在 Spring 引导中更改日志记录模式对日志条…

基于小波域的隐马尔可夫树模型的图像去噪方法的matlab实现代码

目录 1.算法概述 2.仿真效果预览 3.MATLAB部分代码预览 4.完整MATLAB程序 1.算法概述 为适应图像的空域非平稳变化, 提出了一种基于小波域分类隐马尔可夫树(CHMT)模型的图像去噪方法.该模型中,图像在每一尺度每一子带的小波系数均被分成C组以突出其空域非平稳变化 的特征,这…

MySQL有哪些锁

这次,来说说 MySQL 的锁,主要是 Q&A 的形式,看起来会比较轻松。 在 MySQL 里,根据加锁的范围,可以分为全局锁、表级锁和行锁三类。 全局锁 全局锁是怎么用的? 要使用全局锁,则要执行这条命…

彻底搞明白概率论:事件间的关系与运算;频率与概率

文章目录事件间的关系事件间的运算事件间的运算法则概率描述性定义统计性定义频率频率的性质频率是否能够作为概率呢?公理化定义概率的重要性质事件间的关系 注意互斥关系和对立关系: 互斥关系是:只要 A,BA,BA,B 不同时发生(不存在…

山东菏泽家乡网页代码 html静态网页设计制作 dw静态网页成品模板素材网页 web前端网页设计与制作 div静态网页设计

家乡旅游景点网页作业制作 网页代码运用了DIV盒子的使用方法,如盒子的嵌套、浮动、margin、border、background等属性的使用,外部大盒子设定居中,内部左中右布局,下方横向浮动排列,大学学习的前端知识点和布局方式都有…

MindFusion JavaScript R2 套件 Crack

MindFusion JavaScript R2将您的 JAVASCRIPT 应用程序更快地推向市场 MindFusion JavaScript 库旨在显着缩短开发任何类型的交互式 JS 应用程序所需的时间。它们还使开发更加容易。 用于 JavaScript 的 MindFusion 包 JavaScript 数据视图 数据网格JavaScript 图 绘图JavaScr…

DJYGUI系列文章八:GDD绘图系统

目录 1 GDD绘图系统概述 1.1绘图上下文 1.2 DrawColor,FillColor,TextColor的作用与区别 2 API说明 2.1 SetRopCode: 设置当前光栅码 2.2 GetRopCode: 获得当前光栅码 2.3 MoveTo: 设置当前坐标位置 2.4 SetDr…

Web基础习题

1.语义化标签 1.现需要设置一个按钮&#xff0c;请填写语义化标签补全代码片段&#xff08;仅填写一个标签名即可&#xff09; <_____>点我!</_____> 2.在HTML中一般用哪个语义化标签表示斜体文本效果 3.在HTML中一般用哪个语义化标签表示头部导航 4.在HTML中一…

web前端-html-css-字体(字体的样式,字体的分类,字体其他样式,字体简写样式,文本样式)

字体的样式 <!DOCTYPE html> <html><head><meta charset"utf-8" /><title></title><style type"text/css">.p1 {font-size: 30px;font-family: "curlz mt";}</style></head><body>…

vite+ts前期准备(尽量详细在更新)

创建和准备vitets项目 打开命令行或进入vscode打开终端 输入命令:npm init vite 选择vuets cd 进入项目 cnpm/yarn/npm install 初次运行项目 终端输入命令:cnpm run dev 可以设置运行之后自动打开浏览器 目录打开package.json dev:vite --open 添加–open 查看环境变量 目录打…

Latex IEEE模板导入中文问题

IEEE模板下载 下载IEEE的conference的latex模板文件&#xff1a; conference&#xff1a;https://www.ieee.org/conferences/publishing/templates.html 模板包括以下文件 编译 IEEE模板需要用pdflatex编译&#xff0c;否则英文的粗体等无法正常显示。 使用pdflatex编译 …

select......for update会锁表还是锁行?

select查询语句是不会加锁的&#xff0c;但是select .......for update除了有查询的作用外&#xff0c;还会加锁呢&#xff0c;而且它是悲观锁。 那么它加的是行锁还是表锁&#xff0c;这就要看是不是用了索引/主键。 没用索引/主键的话就是表锁&#xff0c;否则就是是行锁。…

Flutter组件--TabBar使用详情(分段控制器)

TabBar介绍 一个显示水平行选项卡的Widget。 通常创建为 AppBar 的 AppBar.bottom 部分并与 TabBarView 结合使用 在什么情况下使用TabBar 当你的app内容类别比较多的时候&#xff0c;我们常常会用到TabBar&#xff0c;例如网易新闻、京东、B站等&#xff0c;所以TabBar是一…

CentOS7 设置 MySQL 主备同步

文章目录环境准备修改配置文件创建同步数据账户设置主库信息测试参考资料本文主要介绍在 MySQL 的主备同步设置方法。环境准备 Linux&#xff1a;Centos 7 MySQL&#xff1a;5.7 主节点&#xff1a;192.168.210.18 备节点&#xff1a;192.168.210.19 主备节点 MySQL 均支持…

2022 Android 高级进阶学习资料与高频精选面试题精讲(圆梦大厂)

序言 可能每个技术人都有个大厂梦&#xff0c;我也不例外。最近准备跳槽&#xff0c;前一阵子在准备各种面试&#xff0c;也面了几个大厂&#xff0c;其中包括字节、阿里 就以字节面试为例&#xff0c;面试总共花费了 20 天左右&#xff0c;包含了 4 轮电话面试、1 轮笔试、1 轮…

锐捷OSPF基础实验配置

目录 配置基础的邻居建立 配置Area4的虚链路 配置OSPF特殊区域 配置路由重分发 配置OSPF汇总 下发缺省路由 配置OSPF邻居认证 配置OSPF的网络类型 配置基础的邻居建立 以R1和R2建立邻居为例子&#xff08;其余设备邻居建立配置相同&#xff09; R1配置 int g0/0 no swi…