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

需求分析
  1. 用户注册

  1. 用户登录

  1. 显示在线用户列表

  1. 群聊(广播)

  1. 点对点聊天

  1. 离线留言

功能实现-客户端登录菜单
package main


import "fmt"


var userId int
var userPwd string


func main() {
    // 接收用户的选择
    var key int
    // 判断是否还继续显示菜单
    var loop = true
    for loop {


        fmt.Println("------------------欢迎登录多人聊天系统------------------")
        fmt.Println("\t\t\t1.登陆聊天系统")
        fmt.Println("\t\t\t2.注册用户")
        fmt.Println("\t\t\t3.退出系统")
        fmt.Println("------------------------------------------------------")
        fmt.Println("\t\t\t请选择(1-3):")


        fmt.Scanf("%d\n", &key)
        switch key {
        case 1:
            fmt.Println("1.登陆聊天系统")
            loop = false
        case 2:
            fmt.Println("2.注册用户")
            loop = false
        case 3:
            fmt.Println("3.退出系统")
            loop = false
        default:
            fmt.Println("输入有误!请重新选择")
        }
    }


    // 根据用户的输入,显示新的提示信息


    if key == 1 {
        // 说明用户要登录
        fmt.Println("请输入你的id号")
        fmt.Scanf("%d\n", &userId)
        fmt.Println("请输入你的密码")
        fmt.Scanf("%s\n", &userPwd)
        // 先把登录的函数,写到另一个文件里
        err := login(userId, userPwd)
        if err != nil {
            fmt.Println("登录失败")
        } else {
            fmt.Println("登陆成功")
        }
    } else if key == 2 {
        fmt.Println("1")
    }
}
package main


import "fmt"


// 写一个函数,完成登录
func login(userId int, userPwd string) (err error) {
    // 下一步就要开始定协议。。。
    fmt.Printf("userId=%d\nuserPwd=%s\n", userId, userPwd)
    return err
}
功能实现-完成客户端发送消息本身,服务器可以正常接收到消息
package main


import (
    "fmt"
    "net"
)


func process(conn net.Conn) {
    // 延时关闭
    defer conn.Close()


    // 读客户端发送的信息
    for {
        buf := make([]byte, 8096)
        n, err := conn.Read(buf[:4])
        if n != 4 || err != nil {
            fmt.Println("conn.Read err=", err)
            return
        }
        fmt.Println("独到的buf=", buf)
    }


}
func main() {
    // 提示信息
    fmt.Println("服务器在8889端口监听...")
    listen, err := net.Listen("tcp", "0.0.0.0:8889")
    // defer listen.Close()
    if err != nil {
        fmt.Println("net.Listen errs=", err)
        return
    }
    for {
        fmt.Println("等待客户端连接服务器")
        conn, err := listen.Accept()
        if err != nil {
            fmt.Println("Listen.Accept err=", err)
        }
        // 一旦链接成功,则启动一个协程和客户端保持通讯
        go process(conn)
    }
}
package message


// 确定消息类型
const (
    LoginMesType    = "LoginMes"
    LoginResMesType = "LoginResMes"
)


type Message struct {
    Type string `json:"type"` // 消息的类型
    Data string `hson:"data"` // 消息的内容
}


// 定义两个消息


type LoginMes struct {
    UserId   int    `json:"userId"`
    UserPwd  string `json:"userPwd"`
    UserName string `json:"userName"`
}


type LoginResMes struct {
    Code  int    `json:"code"`  // 返回的状态码   500:未注册 200:登陆成功
    Error string `json:"error"` // 返回错误信息
}
package main


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


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


    // 1.链接到服务器
    conn, err := net.Dial("tcp", "localhost:8889")
    if err != nil {
        fmt.Println("net.Dail err=", err)
        return
    }
    var mes message.Message
    mes.Type = message.LoginMesType
    var loginMes message.LoginMes
    loginMes.UserId = userId
    loginMes.UserPwd = userPwd


    // 将loginMes 序列化
    data, err := json.Marshal(loginMes)
    if err != nil {
        fmt.Println("json.Marshal err", err)
        return
    }
    // 延时关闭
    defer conn.Close()


    // 把data赋给mes.Data字段
    mes.Data = string(data)


    // 将mes进行序列话
    data, err = json.Marshal(mes)
    if err != nil {
        fmt.Println("json.Marshal err", err)
        return
    }
    // data就是我们要发送的信息
    // 先把data的长度发送给服务器
    // 先获取到data长度->转成一个长度的切片
    // conn.Write(len(data))
    pkgLen := uint32(len(data))
    var buf [4]byte
    binary.BigEndian.PutUint32(buf[0:4], pkgLen)
    n, err := conn.Write(buf[:4])
    if n != 4 || err != nil {
        fmt.Println("conn.Write(bytes) fail", err)
        return
    }
    fmt.Printf("客户端,发送消息的长度=%d\n内容是=%s", len((data)), string(data))
    return
}

main.go代码没有修改

根据客户端发送的消息(LoginMes),判断用户合法性,并返回相应LoginResMes

思路分析

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

  1. 服务器端接收到消息,然后反序列化成相应的消息结构体。

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

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

  1. 这里需要做函数的封装

package main


import (
    "encoding/binary"
    "encoding/json"
    "fmt"
    "io"
    "net"
    "project_01/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 {
        // fmt.Println("conn.Read err=", err)
        // err = errors.New("read pkg header error")
        return
    }
    // fmt.Println("独到的buf=", buf)
    pkgLen := binary.BigEndian.Uint32(buf[0:4])


    //根据 pkgKLen 读取消息内容
    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.Unmarsha err=", err)
    }


    return
}
func process(conn net.Conn) {
    // 延时关闭
    defer conn.Close()
    // 读客户端发送的信息
    for {
        // 这里将读取数据包,直接封装成一个函数readPkg(),返回Message,Err
        mes, err := readPkg(conn)
        if err != nil {
            if err == io.EOF {
                fmt.Println("客户端退出,服务器端也退出..")
                return
            } else {
                fmt.Println("read Pakage err=", err)
                return
            }
        }
        fmt.Println("mes=", mes)
    }
}

func main() {
    // 提示信息
    fmt.Println("服务器在8889端口监听...")
    listen, err := net.Listen("tcp", "0.0.0.0:8889")
    // defer listen.Close()
    if err != nil {
        fmt.Println("net.Listen errs=", err)
        return
    }
    for {
        fmt.Println("等待客户端连接服务器")
        conn, err := listen.Accept()
        if err != nil {
            fmt.Println("Listen.Accept err=", err)
            return
        }
        // 一旦链接成功,则启动一个协程和客户端保持通讯
        go process(conn)
    }
}

优化效率用缓存,优化程序结构用分层模式解决

服务端结构改进

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

步骤

  1. 先把分析出来的文件,创建好,然后放到相应的文件夹

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

  1. 先修改util

package utils


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


// 将方法关联到结构体中
type Transfer struct {
    // 分析有哪些字段
    Conn net.Conn
    Buf  [8096]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 {
        // fmt.Println("conn.Read err=", err)
        // err = errors.New("read pkg header error")
        return
    }
    // fmt.Println("独到的buf=", buf)
    pkgLen := binary.BigEndian.Uint32(this.Buf[0:4])


    //根据 pkgKLen 读取消息内容
    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.Unmarsha err=", err)
    }


    return
}


func (this *Transfer) WritePkg(data []byte) (err error) {


    // 先发送一个长度给客户端
    pkgLen := uint32(len(data))
    // var buf [4]byte
    binary.BigEndian.PutUint32(this.Buf[0:4], pkgLen)
    n, err := this.Conn.Write(this.Buf[:4])
    if n != 4 || err != nil {
        fmt.Println("conn.Write(bytes) fail", err)
        return
    }


    // 发送 data 本身
    n, err = this.Conn.Write(data)
    if n != 4 || err != nil {
        fmt.Println("conn.Write(bytes) fail", err)
        return
    }
    return
}
  1. 修改了process/userProcess.go

package process


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


type UserProcess struct {
    Conn net.Conn
}


// 编写一个函数serverProcessLogin函数,专登处理登录请求
func (this *UserProcess) ServerProcessLogin(mes *message.Message) (err error) {
    // 核心代码。。。
    // 先从mes 中去除mes.Data,并直接反序列化成LoginMes
    var loginMes message.LoginMes
    json.Unmarshal([]byte(mes.Data), &loginMes)
    if err != nil {
        fmt.Println("json.Unmarshal fail err=", err)
        return
    }
    //先声明一个resMes
    var resMes message.Message
    resMes.Type = message.LoginResMesType


    // 在声明一个LoginResMes
    var loginResMes message.LoginResMes


    // 如果用户id = 123,密码= 123123,认为合法,否则不合法
    if loginMes.UserId == 123 && loginMes.UserPwd == "123123" {
        loginResMes.Code = 200


    } else {
        loginResMes.Code = 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函数中


    // 因为使用了分层模式(mvc),我们先创建一个Transfer实例,然后读取
    tf := &utils.Transfer{
        Conn: this.Conn,
    }
    err = tf.WritePkg(data)
    return
}
  1. 修改了main/processor.go

package main


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


// 创建一个Processor结构体
type Processor struct {
    Conn net.Conn
}


// 编写一个ServerProcessMes 函数
// 根据客户端发送消息的种类不同,决定调用那个函数来处理
func (pc *Processor) serverProcessMes(mes *message.Message) (err error) {


    switch mes.Type {
    case message.LoginMesType:
        // 处理登录逻辑
        // 创建一个UserProcess实例
        up := &process.UserProcess{
            Conn: pc.Conn,
        }
        err = up.ServerProcessLogin(mes)
    case message.RegisterMessType:
        // 处理注册
    default:
        fmt.Println("消息类型不存在,无法处理...")
    }
    return
}


func (pc *Processor) process3() (err error) {
    // 延时关闭
    // defer conn.Close()
    // 读客户端发送的信息
    for {
        // 这里将读取数据包,直接封装成一个函数readPkg(),返回Message,Err
        // 创建一个Transfer,完成读包
        tf := &utils.Transfer{
            Conn: pc.Conn,
        }
        mes, err := tf.ReadPkg()
        if err != nil {
            if err == io.EOF {
                fmt.Println("客户端退出,服务器端也退出..")
                return err
            } else {
                fmt.Println("read Pakage err=", err)
                return err
            }
        }
        // fmt.Println("mes=", mes)


        err = pc.serverProcessMes(&mes)
        if err != nil {
            return err
        }
    }
}
  1. 修改main/main.go

package main


import (
    "fmt"
    "net"
)


func process2(conn net.Conn) {
    // // 延时关闭
    defer conn.Close()


    processor := &Processor{
        Conn: conn,
    }
    err := processor.process3()
    if err != nil {
        fmt.Println("客户端和服务器的通讯协程错误=err", err)
        return
    }
}


func main() {
    // 提示信息
    fmt.Println("服务器[新的结构]在8889端口监听...")
    listen, err := net.Listen("tcp", "0.0.0.0:8889")
    // defer listen.Close()
    if err != nil {
        fmt.Println("net.Listen errs=", err)
        return
    }
    for {
        fmt.Println("等待客户端连接服务器")
        conn, err := listen.Accept()
        if err != nil {
            fmt.Println("Listen.Accept err=", err)
            return
        }
        // 一旦链接成功,则启动一个协程和客户端保持通讯
        go process2(conn)
    }
}
客户端结构改进
package main


import (
    "fmt"
    "project_01/client/process"
)


var userId int
var userPwd string


func main() {
    // 接收用户的选择
    var key int
    // 判断是否还继续显示菜单
    var loop = true
    for loop {


        fmt.Println("------------------欢迎登录多人聊天系统------------------")
        fmt.Println("\t\t\t1.登陆聊天系统")
        fmt.Println("\t\t\t2.注册用户")
        fmt.Println("\t\t\t3.退出系统")
        fmt.Println("------------------------------------------------------")
        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)
            // loop = false
            // 创建一个UserProcess的实例
            up := &process.UserProcess{}
            up.Login(userId, userPwd)


        case 2:
            fmt.Println("注册用户")
            // loop = false
        case 3:
            fmt.Println("退出系统")
            // loop = false
        default:
            fmt.Println("输入有误!请重新选择")
        }
    }


}
package process


import (
    "fmt"
    "net"
    "os"
    "project_01/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\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 ProcessMesServer(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.Println("mes=", mes)
    }
}
package process


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


type UserProcess struct {
    //字段..


}


func (pc *UserProcess) Login(userId int, userPwd string) (err error) {
    // 下一步就要开始定协议。。。
    // fmt.Printf("userId=%d\nuserPwd=%s\n", userId, userPwd)
    // return err


    // 1.链接到服务器
    conn, err := net.Dial("tcp", "localhost:8889")
    if err != nil {
        fmt.Println("net.Dail err=", err)
        return
    }
    var mes message.Message
    mes.Type = message.LoginMesType
    var loginMes message.LoginMes
    loginMes.UserId = userId
    loginMes.UserPwd = userPwd


    // 将loginMes 序列化
    data, err := json.Marshal(loginMes)
    if err != nil {
        fmt.Println("json.Marshal err", err)
        return
    }
    // 延时关闭
    defer conn.Close()


    // 把data赋给mes.Data字段
    mes.Data = string(data)


    // 将mes进行序列话
    data, err = json.Marshal(mes)
    if err != nil {
        fmt.Println("json.Marshal err", err)
        return
    }
    // data就是我们要发送的信息
    // 先把data的长度发送给服务器
    // 先获取到data长度->转成一个长度的切片
    // conn.Write(len(data))
    pkgLen := uint32(len(data))
    var buf [4]byte
    binary.BigEndian.PutUint32(buf[0:4], pkgLen)
    n, err := conn.Write(buf[:4])
    if n != 4 || err != nil {
        fmt.Println("conn.Write(bytes) fail", err)
        return
    }
    // fmt.Printf("客户端,发送消息的长度=%d\n内容是=%s", len((data)), string(data))


    //  发送消息本身
    _, err = conn.Write(data)
    if err != nil {
        fmt.Println("conn.Write(bytes) fail", err)
        return
    }
    // 还需要处理服务器端返回的消息


    // 创建一个Transfer实例
    tf := &utils.Transfer{
        Conn: conn,
    }
    mes, err = tf.ReadPkg() // mes 就是
    if err != nil {
        fmt.Println("readPkg(conn) err=", err)
        return
    }
    // 将mes的Data部分反序列化成 LoginResMes
    var loginResMes message.LoginResMes
    err = json.Unmarshal([]byte(mes.Data), &loginResMes)
    if loginResMes.Code == 200 {
        // fmt.Println("登陆成功")


        // 需要在客户端启动一个协程
        // 该协程保持和服务器端的通讯,如果服务器有数据推送给客户端
        go ProcessMesServer(conn)


        // 显示我们登录成功的菜单
        for {
            ShowMenu()
        }


    } else if loginResMes.Code == 500 {
        fmt.Println(loginResMes.Error)


    }
    return
}
package utils


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


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


func (pc *Transfer) ReadPkg() (mes message.Message, err error) {
    // buf := make([]byte, 8096)
    fmt.Println("读取客户端发送的数据...")
    // conn.Read 在conn没有关闭的情况下,才会发生阻塞
    // 如果客户端关闭了 conn 则,就不会阻塞
    _, err = pc.Conn.Read(pc.Buf[:4])
    if err != nil {
        // fmt.Println("conn.Read err=", err)
        // err = errors.New("read pkg header error")
        return
    }
    // fmt.Println("独到的buf=", buf)
    pkgLen := binary.BigEndian.Uint32(pc.Buf[0:4])


    //根据 pkgKLen 读取消息内容
    n, err := pc.Conn.Read(pc.Buf[:pkgLen])
    if n != int(pkgLen) || err != nil {
        fmt.Println("conn.Read fail err=", err)
        return
    }


    // 把pkgLen 反序列化成 -> message.Message
    err = json.Unmarshal(pc.Buf[:pkgLen], &mes)
    if err != nil {
        fmt.Println("json.Unmarsha err=", err)
    }


    return
}


func (pc *Transfer) WritePkg(data []byte) (err error) {


    // 先发送一个长度给客户端
    pkgLen := uint32(len(data))
    // var buf [4]byte
    binary.BigEndian.PutUint32(pc.Buf[0:4], pkgLen)
    n, err := pc.Conn.Write(pc.Buf[:4])
    if n != 4 || err != nil {
        fmt.Println("conn.Write(bytes) fail", err)
        return
    }


    // 发送 data 本身
    n, err = pc.Conn.Write(data)
    if n != 4 || err != nil {
        fmt.Println("conn.Write(bytes) fail", err)
        return
    }
    return
}
参考文献