一.项目介绍
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介入,用户登录,注册