需求分析-->设计阶段-->编码实现-->测试阶段-->实施
需求分析用户注册
用户登录
显示在线用户列表
群聊(广播)
点对点聊天
离线留言
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思路分析
让客户端发送消息本身
服务器端接收到消息,然后反序列化成相应的消息结构体。
服务器端根据反序列化成对应的消息,判断是否登录用户是合法,返回LoginResMes
客户端解析返回的LoginResMes,显示对应界面
这里需要做函数的封装
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)
}
}
优化效率用缓存,优化程序结构用分层模式解决
服务端结构改进前面的程序虽然完成了功能,但没有结构,系统的可读性、扩展性和维护性都不好,因此需要对程序的结构进行改进
步骤
先把分析出来的文件,创建好,然后放到相应的文件夹
根据各个文件,完成任务的不同,将main.go的代码剥离到对应的文件中即可
先修改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
}
修改了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
}
修改了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
}
}
}
修改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
}
参考文献