非完全原创,部分内容来自于学习其他人的理论和B站视频。如果有侵权,请联系我,可以立即删除掉。
实现一个海量用户的通讯系统,要求主界面有用户登录、注销用户、退出系统三个功能。其中,登录用户需要输入用户ID、密码,校验通过后才能进入聊天室,聊天室中用户可互相发送消息
采用C-S架构,服务端死循环进行端口监听,一旦检测到有客户端连接时则开启线程处理;客户端需要在连接时,需要先登录上才能发消息
由于涉及到服务端与客户端的网络通讯,因此传输的数据使用序列化后的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结构体序列化的数据发送给客户端
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
}
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
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(), err)
}
return
}
fmt.Printf("Server receive message data from client[%s] sucessful, msg = %+v\n",
con.RemoteAddr(), msg)
}
}
PS Test0\IMS> go build -o server.exe .\server\
PS Test0\IMS> .\server.exe
Server[0.0.0.0:8088] continuously listening for connections
Server receive message data from client[127.0.0.1:63154] sucessful, msg = {MsgType:LoginMsg MsgData:{"UsrId":12356,"UsrPwd":"root"}}
Server receive message length from client[127.0.0.1:63761] failed, err = EOF
client[127.0.0.1:63761] closed, err = EOF
Test0\IMS>go build -o client.exe ./client/
Test0\IMS>client.exe
-------------欢迎来到简易及时通讯系统-------------
1. 用户登录
2. 注销用户
3. 退出系统
请选择(1~3): 1
请输入用户ID: 12356
请输入用户密码: root
Client send message length to Server[127.0.0.1:8088] sucessful, len = 4, content = [0 0 0 72]
Client send message data to Server[127.0.0.1:8088] sucessful, len = 72, content = [123 34 77 115 103 84 121 112 101 34 58 34 76 111 103 105 110 77 115 103 34 44 34 77 115 103 68 97 116 97 34 58 34 123 92 34 85 115 114 73 100 92 34 58 49 50 51 53 54 44 92 34 85 115 114 80 119 100 92 34 58 92 34 114 111 111 116 92 34 125 34 125]
client login successful
utils/msgutils.go 将接收消息(校验长度)、发送消息封装到工具包
package utils
import (
"encoding/binary"
"encoding/json"
"fmt"
"net"
)
func ReadMsg(con net.Conn) (msg Message, err error) {
//1. 读取前4个字节,即数据长度
buf := make([]byte, 8096)
_, err = con.Read(buf[:4])
if err != nil {
fmt.Printf("[%s] receive message length from [%s] failed, err = %v\n", con.LocalAddr(), 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("[%s] receive message data from [%s] failed, err = %v\n", con.LocalAddr(), con.RemoteAddr(), err)
return
}
//3. 读到的数据反序列化为Message结构体
err = json.Unmarshal(buf[:pkgLens], &msg)
if err != nil {
fmt.Printf("[%s] received message unmarshal failed, err = %v\n", con.LocalAddr(), err)
return
}
return
}
func SendMsg(con net.Conn, buf []byte, msgType string) (err error) {
//1. 根据服务端返回消息/客户端登录消息(如LoginReturn、LoginMsg)序列化的切片来创建消息Message
var msg Message
msg.MsgData = string(buf)
msg.MsgType = msgType
data, err := json.Marshal(&msg)
if err != nil {
fmt.Printf("[%s] message data unmarshal failed, err = %v\n", con.LocalAddr(), err)
return
}
//2. 将Message序列化后的数据长度、内容发送给客户端
//2.1 发送数据长度
bytebuf := make([]byte, 4)
binary.BigEndian.PutUint32(bytebuf, uint32(len(data)))
lens, err := con.Write(bytebuf)
//fmt.Printf("[%s] return message length to [%s] ", con.LocalAddr(), con.RemoteAddr())
if lens != 4 || err != nil {
fmt.Printf("failed, len = %d, err = %v\n", lens, err)
return
}
//fmt.Printf("successful, len = %d, content = %+v\n\n", lens, bytebuf)
//2.2 发送数据内容
_, err = con.Write(data)
fmt.Printf("[%s] send message data to [%s] ", con.LocalAddr(), con.RemoteAddr())
if err != nil {
fmt.Println("failed, len = ", lens, "err = ", err)
return
}
fmt.Printf("sucessful, len = %d, content = %+v\n", lens, data)
return
}
server/dealMsg.go 根据客户端发送的消息,走不同的分支验证,并返回结果
package main
import (
"Test0/IMS/utils"
"encoding/json"
"errors"
"fmt"
"net"
)
func parseLoginMsg(con net.Conn, msg utils.Message) (err error) {
//1. 获取客户端发送的登录消息结构体
var login utils.LoginMsg
err = json.Unmarshal([]byte(msg.MsgData), &login)
if err != nil {
fmt.Println("Server received login message unmarshal failed, err = ", err)
return
}
//2. 验证用户名和密码,并且服务端返回的状态消息
var returnMsg utils.LoginReturn
if login.UsrId == 1234 && login.UsrPwd == "root" {
returnMsg = utils.LoginReturn{ErrCode: 200, ErrInfo: "login success"}
} else {
returnMsg = utils.LoginReturn{ErrCode: 401, ErrInfo: "login failed"}
}
//3. 将服务端返回的状态消息序列化为消息结构体
buf, err := json.Marshal(returnMsg)
if err != nil {
fmt.Println("Server return status data marshal failed, err = ", err)
return
}
return utils.SendMsg(con, buf, utils.ServerReturnMsg)
}
func parseStruct(con net.Conn, msg utils.Message) (err error) {
switch msg.MsgType {
case utils.ClientLoginMsg:
return parseLoginMsg(con, msg)
case utils.ServerReturnMsg:
return nil
default:
fmt.Println("message format error!")
return errors.New("message format error")
}
}
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 := utils.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(), err)
}
fmt.Println()
return
}
fmt.Printf("Server receive message data from client[%s] sucessful, msg = %+v\n",
con.RemoteAddr(), msg)
err = parseStruct(con, msg)
if err != nil {
fmt.Printf("Server send return message to client[%s] failed, err = %v\n", con.RemoteAddr(), err)
}
fmt.Println()
}
}
clent/login.go 将发送消息的过程替换为工具函数,增加接收消息的分支判断,并根据服务端的返回值判断登录是否成功
package main
import (
"Test0/IMS/utils"
"encoding/json"
"errors"
"fmt"
"net"
)
func parseLoginReturnMsg(con net.Conn, msg utils.Message) (err error) {
//1. 获取服务端发送的登录返回消息结构体
var loginReturn utils.LoginReturn
err = json.Unmarshal([]byte(msg.MsgData), &loginReturn)
if err != nil {
fmt.Println("Client received login returned message unmarshal failed, err = ", err)
return
}
fmt.Println(loginReturn.ErrInfo)
if loginReturn.ErrCode == 200 {
return nil
} else {
return errors.New(loginReturn.ErrInfo)
}
}
func parseStruct(con net.Conn, msg utils.Message) (err error) {
switch msg.MsgType {
case utils.ClientLoginMsg:
return nil
case utils.ServerReturnMsg:
return parseLoginReturnMsg(con, msg)
default:
fmt.Println("message format error!")
return errors.New("message format error")
}
}
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. 根据LoginMsg序列化后的切片,构造Message结构体,并将数据发给服务端
err = utils.SendMsg(con, loginbuf, utils.ClientLoginMsg)
if err != nil {
fmt.Printf("Client send login message to Server[%s] failed, err = %v\n", con.RemoteAddr(), err)
return
}
//4. 接收服务端返回的消息
msg, err := utils.ReadMsg(con)
if err != nil {
fmt.Printf("Client receive login returned message from Server[%s] failed, err = %v\n", con.RemoteAddr(), err)
return
}
return parseStruct(con, msg)
}
PS Test0\IMS> go build -o server.exe .\server\
PS Test0\IMS> .\server.exe
Server[0.0.0.0:8088] continuously listening for connections
Server receive message data from client[127.0.0.1:64005] sucessful, msg = {MsgType:LoginMsg MsgData:{"UsrId":1245,"UsrPwd":"root"}}
[127.0.0.1:8088] send message data to [127.0.0.1:64005] sucessful, len = 4, content = [123 34 77 115 103 84 121 112 101 34 58 34 76 111 103 105 110 82
101 116 117 114 110 34 44 34 77 115 103 68 97 116 97 34 58 34 123 92 34 69 114 114 67 111 100 101 92 34 58 52 48 49 44 92 34 69 114 114 73 110 102 111
92 34 58 92 34 108 111 103 105 110 32 102 97 105 108 101 100 92 34 125 34 125]
[127.0.0.1:8088] receive message length from [127.0.0.1:64005] failed, err = EOF
client[127.0.0.1:64005] closed, err = EOF
Server receive message data from client[127.0.0.1:64006] sucessful, msg = {MsgType:LoginMsg MsgData:{"UsrId":1234,"UsrPwd":"root"}}
[127.0.0.1:8088] send message data to [127.0.0.1:64006] sucessful, len = 4, content = [123 34 77 115 103 84 121 112 101 34 58 34 76 111 103 105 110 82
101 116 117 114 110 34 44 34 77 115 103 68 97 116 97 34 58 34 123 92 34 69 114 114 67 111 100 101 92 34 58 50 48 48 44 92 34 69 114 114 73 110 102 111
92 34 58 92 34 108 111 103 105 110 32 115 117 99 99 101 115 115 92 34 125 34 125]
[127.0.0.1:8088] receive message length from [127.0.0.1:64006] failed, err = EOF
client[127.0.0.1:64006] closed, err = EOF
Test0\IMS>go build -o client.exe ./client/
Test0\IMS>client.exe
-------------欢迎来到简易及时通讯系统-------------
1. 用户登录
2. 注销用户
3. 退出系统
请选择(1~3): 1
请输入用户ID: 1245
请输入用户密码: root
[127.0.0.1:64005] send message data to [127.0.0.1:8088] sucessful, len = 4, content = [123 34 77 115 103 84 121 112 101 34 58 34 76 111 103 105 110 77 115 103 34 44 34 77 115 103 68 97 116 97 34 58 34 123 92 34 85 115 114 73 100 92 34 58 49 50 52 53 44 92 34 85 115 114 80 119 100 92 34 58 92 34 114 111 111 116 92 34 125 34 125]
login failed
client login failed
Test0\IMS>client.exe
-------------欢迎来到简易及时通讯系统-------------
1. 用户登录
2. 注销用户
3. 退出系统
请选择(1~3): 1
请输入用户ID: 1234
请输入用户密码: root
[127.0.0.1:64006] send message data to [127.0.0.1:8088] sucessful, len = 4, content = [123 34 77 115 103 84 121 112 101 34 58 34 76 111 103 105 110 77 115 103 34 44 34 77 115 103 68 97 116 97 34 58 34 123 92 34 85 115 114 73 100 92 34 58 49 50 51 52 44 92 34 85 115 114 80 119 100 92 34 58 92 34 114 111 111 116 92 34 125 34 125]
login success
client login successful
主要修改点:
(1)在服务端添加了redis_dao.go
:用于向redis注册用户信息,从redis取密码校对登录是否成功;添加了线程池的内容
(2)在服务端的server.go
的main
函数中添加redis线程池的初始化
(3)在客户端添加登录成功后进入二级菜单的功能
utils/msg_def.go
const (
ClientLoginMsg = "LoginMsg"
ServerReturnMsg = "LoginReturn"
UsrRegisterMsg = "UsrRegister"
)
var (
UsrNotExist = LoginReturn{403, "user not exist"}
UsrAlreadyExist = LoginReturn{402, "user already exist"}
PwdNotMatch = LoginReturn{401, "password not match"}
HandleSuccess = LoginReturn{200, "handle success"}
)
type Message struct {
MsgType string
MsgData string
}
type LoginMsg struct {
UsrId int
UsrPwd string
}
type LoginReturn struct {
ErrCode int
ErrInfo string
}
utils/msg_utils.go
import (
"encoding/binary"
"encoding/json"
"fmt"
"net"
)
func ReadMsg(con net.Conn) (msg *Message, err error) {
//1. 读取前4个字节,即数据长度
buf := make([]byte, 8096)
_, err = con.Read(buf[:4])
if err != nil {
fmt.Printf("[%s] receive message length from [%s] failed, err = %v\n", con.LocalAddr(), 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("[%s] receive message data from [%s] failed, err = %v\n", con.LocalAddr(), con.RemoteAddr(), err)
return
}
msg = &Message{}
//3. 读到的数据反序列化为Message结构体
err = json.Unmarshal(buf[:pkgLens], &msg)
if err != nil {
fmt.Printf("[%s] received message unmarshal failed, err = %v\n", con.LocalAddr(), err)
}
return
}
func SendMsg(con net.Conn, buf []byte, msgType string) (err error) {
//1. 根据服务端返回消息/客户端登录消息(如LoginReturn、LoginMsg)序列化的切片来创建消息Message
var msg Message
msg.MsgData = string(buf)
msg.MsgType = msgType
data, err := json.Marshal(&msg)
if err != nil {
fmt.Printf("[%s] message data unmarshal failed, err = %v\n", con.LocalAddr(), err)
return
}
//2. 将Message序列化后的数据长度、内容发送给客户端
//2.1 发送数据长度
bytebuf := make([]byte, 4)
binary.BigEndian.PutUint32(bytebuf, uint32(len(data)))
lens, err := con.Write(bytebuf)
fmt.Printf("[%s] send message length to [%s] ", con.LocalAddr(), con.RemoteAddr())
if lens != 4 || err != nil {
fmt.Printf("failed, len = %d, err = %v\n", lens, err)
return
}
fmt.Printf("successful, len = %d, content = %+v\n", lens, bytebuf)
//2.2 发送数据内容
lens, err = con.Write(data)
fmt.Printf("[%s] send message data to [%s] ", con.LocalAddr(), con.RemoteAddr())
if err != nil {
fmt.Println("failed, len = ", lens, "err = ", err)
return
}
fmt.Printf("sucessful, len = %d, content = %+v\n", lens, string(data))
return
}
client/menu/menu.go
package menu
import (
"fmt"
)
func ShowMenu() {
var key int
for {
fmt.Println("-------------恭喜xxx登录成功-------------")
fmt.Println("\t\t\t 1. 显示在线用户列表")
fmt.Println("\t\t\t 2. 发送群聊消息")
fmt.Println("\t\t\t 3. 显示消息列表")
fmt.Println("\t\t\t 4. 退 出 系 统")
fmt.Printf("\t\t\t请选择(1~4): ")
fmt.Scanln(&key)
switch key {
case 1:
case 2:
case 3:
case 4:
default:
fmt.Println("序号输入有误,请重新输入!")
}
if key == 4 {
break
}
}
}
client/proc/login.go
import (
"Test0/IMS/client/menu"
"Test0/IMS/utils"
"encoding/json"
"errors"
"fmt"
"net"
)
func parseServerReturnMsg(con net.Conn, msg *utils.Message) (err error) {
//1. 获取服务端发送的登录返回消息结构体
var loginReturn utils.LoginReturn
err = json.Unmarshal([]byte(msg.MsgData), &loginReturn)
if err != nil {
fmt.Println("Client received login returned message unmarshal failed, err = ", err)
return
}
//登录成功,则进入二级菜单
if loginReturn.ErrCode == utils.HandleSuccess.ErrCode {
menu.ShowMenu()
return nil
} else {
return errors.New(loginReturn.ErrInfo)
}
}
func parseStruct(con net.Conn, msg *utils.Message) (err error) {
switch msg.MsgType {
case utils.ServerReturnMsg:
return parseServerReturnMsg(con, msg)
default:
return errors.New("message format error")
}
}
func Login_or_Register(usrId int, usrPwd string, op_type 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. 根据LoginMsg序列化后的切片,构造Message结构体,并将数据发给服务端
err = utils.SendMsg(con, loginbuf, op_type)
if err != nil {
fmt.Printf("Client send login message to Server[%s] failed, err = %v\n", con.RemoteAddr(), err)
return
}
//4. 接收服务端返回的消息
msg, err := utils.ReadMsg(con)
if err != nil {
fmt.Printf("Client receive login returned message from Server[%s] failed, err = %v\n", con.RemoteAddr(), err)
return
}
return parseStruct(con, msg)
}
client/main/client.go
import (
"Test0/IMS/client/proc"
"Test0/IMS/utils"
"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.Println("\t\t\t 4. 退出系统")
fmt.Printf("\t\t\t请选择(1~4): ")
fmt.Scanln(&key)
switch key {
case 1:
fmt.Printf("请输入用户ID: ")
fmt.Scanln(&id)
fmt.Printf("请输入用户密码: ")
fmt.Scanln(&pwd)
err := proc.Login_or_Register(id, pwd, utils.ClientLoginMsg)
if err != nil {
fmt.Printf("%s", err.Error())
if err.Error() == utils.UsrNotExist.ErrInfo {
fmt.Println(", 请先注册")
} else if err.Error() == utils.PwdNotMatch.ErrInfo {
fmt.Println(", 请先重新输入")
} else {
fmt.Println()
}
} else {
fmt.Println("login success")
}
case 2:
fmt.Printf("请输入用户ID: ")
fmt.Scanln(&id)
fmt.Printf("请输入用户密码: ")
fmt.Scanln(&pwd)
err := proc.Login_or_Register(id, pwd, utils.UsrRegisterMsg)
if err != nil {
fmt.Println(err.Error())
} else {
fmt.Println("register success")
}
case 3:
case 4:
default:
fmt.Println("序号输入有误,请重新输入!")
}
if key == 1 || key == 2 || key == 3 {
break
}
}
}
server/model/redis_dao.go
import (
"encoding/json"
"errors"
"fmt"
"time"
"Test0/IMS/utils"
"github.com/gomodule/redigo/redis"
)
var pool *redis.Pool
const redis_hash_key = "users"
func Redis_init(maxidle, maxActive int, idleTimeout time.Duration, network, address string) {
pool = &redis.Pool{
MaxIdle: maxidle,
MaxActive: maxActive,
IdleTimeout: idleTimeout,
Dial: func() (redis.Conn, error) {
return redis.Dial(network, address)
},
}
}
//哈希表中,存储用户信息的方式:key:users field:1234 value:{usrid:1234, usrpwd:root}
func LoginRedisCheck(usrId int, usrPwd string) (err error) {
//1. 连接池中获取连接,并查询usrId对应的value
con := pool.Get()
defer con.Close()
value, err := redis.String(con.Do("hget", redis_hash_key, usrId))
if err != nil {
if err == redis.ErrNil {
err = errors.New(utils.UsrNotExist.ErrInfo)
}
return
}
//2. 根据value反序列化出LoginMsg结构体
login := &utils.LoginMsg{}
err = json.Unmarshal([]byte(value), login)
if err != nil {
fmt.Println("Server: LoginMsg struct unmarshal failed, err = ", err)
return
}
//3. 校验密码是否正确
if login.UsrPwd != usrPwd {
err = errors.New(utils.PwdNotMatch.ErrInfo)
}
return
}
func UsrRegister(usrId int, usrPwd string) (err error) {
//1. 连接池中获取连接,并查询usrId是否已存在
con := pool.Get()
defer con.Close()
_, err = redis.String(con.Do("hget", redis_hash_key, usrId))
if err == nil {
err = errors.New(utils.UsrAlreadyExist.ErrInfo)
return
}
//2. 向redis注册用户
value_byte, err := json.Marshal(&utils.LoginMsg{UsrId: usrId, UsrPwd: usrPwd})
if err != nil {
fmt.Println("Server: LoginMsg struct marshal failed, err = ", err)
return
}
_, err = con.Do("hset", redis_hash_key, usrId, string(value_byte))
if err != nil {
fmt.Println("Redis: hset usrid failed, err = ", err)
}
return
}
server/proc/deal_msg.go
package proc
import (
"Test0/IMS/server/model"
"Test0/IMS/utils"
"encoding/json"
"errors"
"fmt"
"net"
)
func parseMsgLogin_or_Register(con net.Conn, msg *utils.Message) (err error) {
//1. 获取客户端发送的登录消息结构体
var login utils.LoginMsg
err = json.Unmarshal([]byte(msg.MsgData), &login)
if err != nil {
fmt.Println("Server received login message unmarshal failed, err = ", err)
return
}
fmt.Println("Server received login message unmarshal success, login = ", login)
var returnMsg utils.LoginReturn
if msg.MsgType == utils.ClientLoginMsg {
//2. 验证用户名和密码,并且服务端返回的状态消息
if err = model.LoginRedisCheck(login.UsrId, login.UsrPwd); err != nil {
if err.Error() == utils.UsrNotExist.ErrInfo {
returnMsg = utils.UsrNotExist
} else if err.Error() == utils.PwdNotMatch.ErrInfo {
returnMsg = utils.PwdNotMatch
} else {
returnMsg = utils.LoginReturn{ErrCode: 404, ErrInfo: err.Error()}
}
} else {
returnMsg = utils.HandleSuccess
}
} else {
//2. 注册用户
if err = model.UsrRegister(login.UsrId, login.UsrPwd); err != nil {
if err.Error() == utils.UsrAlreadyExist.ErrInfo {
returnMsg = utils.UsrAlreadyExist
} else {
returnMsg = utils.LoginReturn{ErrCode: 404, ErrInfo: err.Error()}
}
} else {
returnMsg = utils.HandleSuccess
}
}
//3. 将服务端返回的状态消息序列化为消息结构体
buf, err := json.Marshal(returnMsg)
if err != nil {
fmt.Println("Server return status data marshal failed, err = ", err)
return
}
return utils.SendMsg(con, buf, utils.ServerReturnMsg)
}
func ParseStruct(con net.Conn, msg *utils.Message) (err error) {
switch msg.MsgType {
case utils.ClientLoginMsg, utils.UsrRegisterMsg:
err = parseMsgLogin_or_Register(con, msg)
case utils.ServerReturnMsg:
default:
fmt.Println("message format error!")
err = errors.New("message format error")
}
return
}
server/main/server.go
package main
import (
"Test0/IMS/server/model"
"Test0/IMS/server/proc"
"Test0/IMS/utils"
"fmt"
"io"
"net"
"time"
)
func msgProcess(con net.Conn) {
//1. 获取客户端反序列化的消息结构体
defer con.Close()
for {
msg, err := utils.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(), err)
}
fmt.Println()
return
}
fmt.Printf("Server receive message data from client[%s] sucessful, msg = %+v\n",
con.RemoteAddr(), msg)
err = proc.ParseStruct(con, msg)
if err != nil {
fmt.Printf("Server send return message to client[%s] failed, err = %v\n", con.RemoteAddr(), err)
}
fmt.Println()
}
}
func main() {
//1. 初始化redis线程池
network, redis_server_addr, im_server_addr := "tcp", "localhost:6379", "0.0.0.0:8088"
model.Redis_init(8, 0, time.Minute*5, network, redis_server_addr)
listen, err := net.Listen(network, im_server_addr)
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)
}
}
Test0\IMS>go build -o client.exe ./client/main
Test0\IMS>client.exe
-------------欢迎来到简易及时通讯系统-------------
1. 用户登录
2. 用户注册
3. 注销用户
4. 退出系统
请选择(1~4): 1
请输入用户ID: 1234
请输入用户密码: root
[127.0.0.1:50848] send message length to [127.0.0.1:8088] successful, len = 4, content = [0 0 0 71]
[127.0.0.1:50848] send message data to [127.0.0.1:8088] sucessful, len = 71, content = {"MsgType":"LoginMsg","MsgData":"{\"UsrId\":1234,\"UsrPwd\":\"root\"}"}
user not exist, 请先注册
Test0\IMS>client.exe
-------------欢迎来到简易及时通讯系统-------------
1. 用户登录
2. 用户注册
3. 注销用户
4. 退出系统
请选择(1~4): 2
请输入用户ID: 1234
请输入用户密码: root
[127.0.0.1:50849] send message length to [127.0.0.1:8088] successful, len = 4, content = [0 0 0 74]
[127.0.0.1:50849] send message data to [127.0.0.1:8088] sucessful, len = 74, content = {"MsgType":"UsrRegister","MsgData":"{\"UsrId\":1234,\"UsrPwd\":\"root\"}"}
-------------恭喜xxx登录成功-------------
1. 显示在线用户列表
2. 发送群聊消息
3. 显示消息列表
4. 退 出 系 统
请选择(1~4): 4
login success
PS Test0\IMS> go build -o server.exe .\server\main\
PS Test0\IMS> .\server.exe
Server[0.0.0.0:8088] continuously listening for connections
Server receive message data from client[127.0.0.1:50848] sucessful, msg = &{MsgType:LoginMsg MsgData:{"UsrId":1234,"UsrPwd":"root"}}
Server received login message unmarshal success, login = {1234 root}
[127.0.0.1:8088] send message length to [127.0.0.1:50848] successful, len = 4, content = [0 0 0 86]
[127.0.0.1:8088] send message data to [127.0.0.1:50848] sucessful, len = 86, content = {"MsgType":"LoginReturn","MsgData":"{\"ErrCode\":403,\"ErrInfo\":\"user not exist\"}"}
[127.0.0.1:8088] receive message length from [127.0.0.1:50848] failed, err = EOF
client[127.0.0.1:50848] closed, err = EOF
Server receive message data from client[127.0.0.1:50870] sucessful, msg = &{MsgType:LoginMsg MsgData:{"UsrId":1234,"UsrPwd":"root"}}
Server received login message unmarshal success, login = {1234 root}
[127.0.0.1:8088] send message length to [127.0.0.1:50870] successful, len = 4, content = [0 0 0 86]
[127.0.0.1:8088] send message data to [127.0.0.1:50870] sucessful, len = 86, content = {"MsgType":"LoginReturn","MsgData":"{\"ErrCode\":200,\"ErrInfo\":\"handle success\"}"}
[127.0.0.1:8088] receive message length from [127.0.0.1:50870] failed, err = EOF
client[127.0.0.1:50870] closed, err = EOF