【声明】
非完全原创,部分内容来自于学习其他人的理论和B站视频。如果有侵权,请联系我,可以立即删除掉。
一、聊天系统需求和分析
1、需求
实现一个海量用户的通讯系统,要求主界面有用户登录、注销用户、退出系统三个功能。其中,登录用户需要输入用户ID、密码,校验通过后才能进入聊天室,聊天室中用户可互相发送消息
2、分析
采用C-S架构,服务端死循环进行端口监听,一旦检测到有客户端连接时则开启线程处理;客户端需要在连接时,需要先登录上才能发消息
2.1、消息的流程
由于涉及到服务端与客户端的网络通讯,因此传输的数据使用序列化后的json串。通讯时,传输的数据消息需要划分为多种:用户登录的消息,用户发送聊天数据的消息;服务器响应用户操作的消息
分析登录消息传输流程
以客户端发送用户登录消息,服务端返回登录验证结果消息为例,分析整个流程:
(1)客户端接收用户输入的ID、用户名,并将其传给服务端
(2)服务端接收客户端的登录数据,用服务器存储的密码进行校验,返回登录验证的结果
(3)客户端接收服务端的登录验证结果,判断登录成功还是失败,并打开相应的界面
(4)考虑如何组织服务端和客户端间通讯的数据格式
设计消息协议
为了方便上述流程的顺利进行,可以将用户输入的ID、用户名封装为一个结构体LoginMsg,将服务端相应用户操作的数据封装为结构体LoginReturn。
为了确保接收侧能够正确解析当前数据,需要将服务端和客户侧的消息统一格式,定义一个消息结构体Message,类型表示指定待解析的结构体,数据表示服务端或者客户端序列化后的数据
为了确保数据的完整性,需要先对数据的长度进行校验。
客户端发送数据、服务端接收数据流程
按照上述的方法,客户端发送的流程为:
(1)创建一个LoginMsg结构体对象,用于接收用户的ID、密码,并将其序列化为数据
(2)创建一个消息结构体Message,类型为登录消息,数据为LoginMsg结构体对象序列化后的数据
(3)将Message结构体序列化,为了防止丢包,有两种办法:在Message结构体中添加数据长度,一次性发送结构体序列化后的数据,接受侧解析数据后根据长度字段来确定数据是否完整;先发送Message结构体序列化后数据的长度,再发送数据的内容,接收侧校验长度。本案例中选用方法2,先发长度,再发数据
服务端接收数据的流程:
(1)接收客户侧发送的消息数据长度,和消息数据本身
(2)校验消息数据的长度
(3)如果长度不等,则需要纠错协议
(4)长度相等,则对消息反序列化,根据Message消息类型,将数据反序列化出其对应的结构体
(5)如果是登录消息LoginMsg,则根据密码来校验
(6)根据校验结果,构建LoginReturn结构体,将其序列化为Message结构体的数据,再将Message结构体序列化的数据发送给客户端
3、实现登录、注册
3.1、登录消息长度校验
3.1.1、go文件
utils/msgdef.go
package utils
const (
ClientLoginMsg = "LoginMsg"
ServerReturnMsg = "LoginReturn"
)
type Message struct {
MsgType string
MsgData string
}
type LoginMsg struct {
UsrId int
UsrPwd string
}
type LoginReturn struct {
ErrCode int
ErrInfo string
}
server/server.go
func msgProcess(con net.Conn) {
buf := make([]byte, 4)
defer con.Close()
lens, err := con.Read(buf)
fmt.Printf("Server receive message from client[%s] ", con.RemoteAddr())
if lens != 4 || err != nil {
fmt.Println("failed, len = ", lens, "err = ", err)
return
}
fmt.Printf("sucessful, len = %d, content = %v\n", lens, buf)
}
func main() {
listen, err := net.Listen("tcp", "0.0.0.0:8088")
if err != nil {
fmt.Println("Server create listener failed, err = ", err)
return
}
defer listen.Close()
fmt.Println("Server[0.0.0.0:8088] continuously listening for connections")
for {
con, err := listen.Accept()
if err != nil {
fmt.Println("Server accept connection failed, err = ", err)
return
}
go msgProcess(con)
}
}
client/client.go
package main
import (
"fmt"
)
func main() {
//定义全局变量接收用户的序号选择、用户ID、密码
var key, id int
var pwd string
for {
fmt.Println("-------------欢迎来到简易及时通讯系统-------------")
fmt.Println("\t\t\t 1. 用户登录")
fmt.Println("\t\t\t 2. 注销用户")
fmt.Println("\t\t\t 3. 退出系统")
fmt.Printf("\t\t\t请选择(1~3): ")
fmt.Scanln(&key)
switch key {
case 1:
fmt.Printf("请输入用户ID: ")
fmt.Scanln(&id)
fmt.Printf("请输入用户密码: ")
fmt.Scanln(&pwd)
if err := Login(id, pwd); err != nil {
fmt.Println("client login failed")
} else {
fmt.Println("client login successful")
}
case 2:
case 3:
default:
fmt.Println("序号输入有误,请重新输入!")
}
if key == 1 || key == 2 || key == 3 {
break
}
}
}
client/login.go
package main
import (
"Test0/IMS/utils"
"encoding/binary"
"encoding/json"
"fmt"
"net"
)
func Login(usrId int, usrPwd string) (err error) {
//1. 连接到服务器
con, err := net.Dial("tcp", "localhost:8088")
if err != nil {
fmt.Println("Client connect Server[localhost:8088] failed, err = ", err)
return
}
defer con.Close()
//2. 创建LoginMsg结构体对象并序列化
loginbuf, err := json.Marshal(&utils.LoginMsg{
UsrId: usrId, UsrPwd: usrPwd})
if err != nil {
fmt.Println("Client login data marshal failed, err = ", err)
return
}
//3. 创建Message消息结构体,并序列化
data, err := json.Marshal(&utils.Message{
MsgType: utils.ClientLoginMsg, MsgData: string(loginbuf)})
if err != nil {
fmt.Println("Client message data marshal failed, err = ", err)
return
}
//4. 发送序列化后的数据
//4.1 使用大端的方式先发送消息数据的长度
buf := make([]byte, 4) //4字节记录消息序列化后的长度
binary.BigEndian.PutUint32(buf, uint32(len(data)))
lens, err := con.Write(buf)
fmt.Printf("Client send message length to Server[%s] ", con.RemoteAddr())
if lens != 4 || err != nil {
fmt.Println("failed, len = ", lens, "err = ", err)
return
}
fmt.Printf("sucessful, len = %d, content = %+v\n", lens, buf)
return nil
}
3.1.2、验证结果
PS Test0\IMS\server> go run .\server.go
Server[0.0.0.0:8088] continuously listening for connections
Server receive message from client[127.0.0.1:62402] sucessful, len = 4, content = [0 0 0 71]
Test0\IMS\client>go run client.go
-------------欢迎来到简易及时通讯系统-------------
1. 用户登录
2. 注销用户
3. 退出系统
请选择(1~3): 1
请输入用户ID: 1234
请输入用户密码: root
Client send message to Server[127.0.0.1:8088] sucessful, len = 4, content = [0 0 0 71]
client login successful
3.2、登录消息的校验
3.2.1、go文件修改点
client/login.go
//func Login(usrId int, usrPwd string) (err error)
//4. 发送序列化后的数据
//4.1 使用大端的方式先发送消息数据的长度
buf := make([]byte, 4) //4字节记录消息序列化后的长度
binary.BigEndian.PutUint32(buf, uint32(len(data)))
lens, err := con.Write(buf)
fmt.Printf("Client send message length to Server[%s] ", con.RemoteAddr())
if lens != 4 || err != nil {
fmt.Println("failed, len = ", lens, "err = ", err)
return
}
fmt.Printf("sucessful, len = %d, content = %+v\n\n", lens, buf)
//4.2 发送消息数据
lens, err = con.Write(data)
fmt.Printf("Client send message data to Server[%s] ", con.RemoteAddr())
if err != nil {
fmt.Println("failed, len = ", lens, "err = ", err)
return
}
fmt.Printf("sucessful, len = %d, content = %+v\n", lens, data)
time.Sleep(time.Second * 2)
return nil
server/dealMsg.go
package main
import (
"Test0/IMS/utils"
"encoding/binary"
"encoding/json"
"fmt"
"net"
)
func readMsg(con net.Conn) (msg utils.Message, err error) {
//1. 读取前4个字节,即数据长度
buf := make([]byte, 8096)
_, err = con.Read(buf[:4])
if err != nil {
fmt.Printf("Server receive message length from client[%s] failed, err = %v\n", con.RemoteAddr(), err)
return
}
pkgLens := binary.BigEndian.Uint32(buf[:4])
//2. 再读pkgLens个字节到buf中
lens, err := con.Read(buf[:pkgLens])
if lens != int(pkgLens) || err != nil {
fmt.Printf("Server receive message data from client[%s] failed, err = %v", con.RemoteAddr(), err)
return
}
//3. 读到的数据反序列化为Message结构体
err = json.Unmarshal(buf[:pkgLens], &msg)
if err != nil {
fmt.Println("Server received message unmarshal failed, err = ", err)
return
}
return
}
server/server.go
func msgProcess(con net.Conn) {
/* //check message length is ok
buf := make([]byte, 8096)
defer con.Close()
lens, err := con.Read(buf)
fmt.Printf("Server receive message from client[%s] ", con.RemoteAddr())
if lens != 4 || err != nil {
fmt.Println("failed, len = ", lens, "err = ", err)
return
}
fmt.Printf("sucessful, len = %d, content = %+v\n", lens, buf)*/
//1. 获取客户端反序列化的消息结构体
defer con.Close()
for {
msg, err := readMsg(con)
if err != nil {
if err == io.EOF {
fmt.Printf("client[%s] closed, err = %v\n", con.RemoteAddr(), err)
} else {
fmt.Printf("Server receive message from client[%s] check length failed, err = %v\n", con.RemoteAddr()