18.4 海量用户即时通讯系统
18.4.1 需求分析
1、用户注册
2、用户登陆
3、显示在线用户列表
4、群聊
5、点对点聊天
6、离线留言
18.4.2 界面设计
部分:
18.4.3 项目开发前技术准备
项目要保存用户信息和消息数据,因此需要学习数据库(redis或者mysql),这里先选择redis
18.4.4 实现功能-显示客户端登陆菜单
功能:能够正确显示客户端的菜单
代码实现:
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 1 登陆聊天室")
fmt.Println("\t\t\t 2 注册用户")
fmt.Println("\t\t\t 3 退出系统")
fmt.Println("\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)
// 先把登陆的函数,写到另一个文件
err := login(userID, userPwd)
if err != nil {
fmt.Println("登陆失败")
} else {
fmt.Println("登陆成功")
}
} else if key ==2 {
fmt.Println("进行用户注册的逻辑。。。。")
}
}
client/login.go
package main
import (
"fmt"
)
// 写一个函数,完成登陆
func login(userID int, userPwd string) (err error) {
// 下一步就要开始定协议。。。
fmt.Printf(" userID=%d userPwd=%s", userID, userPwd)
return nil
}
18.4.5 实现功能-完成用户登陆
1、先完成指定用户的验证,用户id=100,密码pwd=123456 可以登陆,其他用户不能登陆。
Message 的组成,并发送一个Message的流程图:
1)客户端和服务器端发送消息长度测试
完成客户端可以放消息长度,服务器端可以正常收到该长度
分析思路:
① 先确定Message的格式和结构
② 然后根据上图的分析完成代码
③ 示意图
代码规划
代码实现
server/main.go
package main
import (
"fmt"
"net"
)
//处理和客户端的通讯
func process(conn net.Conn) {
//这里需要延迟关闭conn
defer conn.Close()
buf := make([]byte, 8096)
//循环读取读客户端发送的消息
for {
fmt.Println("读取客户端发送的数据")
_, err := conn.Read(buf[:4])
if err != nil {
fmt.Println("conn.Read err=", err)
return
}
fmt.Println("读到的buf=", buf[:4])
}
}
func main() {
//提示信息
fmt.Println("服务器在8899监听。。。。")
listen, err := net.Listen("tcp", "0.0.0.0:8899")
defer listen.Close()
if err != nil {
fmt.Println("net listen err=", err)
return
}
//一旦监听成功,就等待客户端链接服务器
for {
fmt.Println("等待客户端来接服务器。。。")
conn, err := listen.Accept()
if err != nil {
fmt.Println("liten Accept err=", err)
}
//一旦连接成功,则启动一个协程和客户端保持通讯。
go process(conn)
}
}
client/login.go
package main
import (
"fmt"
"net"
"encoding/binary"
"encoding/json"
"chatroom/common/message"
)
// 写一个函数,完成登陆
func login(userId int, userPwd string) (err error) {
// 下一步就要开始定协议。。。
// fmt.Printf(" userID=%d userPwd=%s", userID, userPwd)
// return nil
//1、连接到feu武器
conn, err := net.Dial("tcp", "localhost:8899")
if err != nil {
fmt.Println("net.Dial err", 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.Println("json.Marshal err=", err)
return
}
//5、将data赋给 mes.Data字段
mes.Data = string(data)
// 6、将mes进行序列化
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("json.Marshal err=", err)
return
}
//7、此时data就是我们要发送的消息
// 7.1 先把data的长度发生给服务器
// 先获取到data的长度->转换成一个表示长度的byte切片
var pkgLen uint32
pkgLen = uint32(len(data))
var buf [4]byte
binary.BigEndian.PutUint32(buf[0:4], pkgLen)
// 发送长度
n, err := conn.Write(buf[0:4])
if n != 4 || err != nil {
fmt.Println("conn.Write fail", err)
return
}
fmt.Printf("客户端,发送消息长度成功=%d 内容是=%s", len(data), string(data))
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 1 登陆聊天室")
fmt.Println("\t\t\t 2 注册用户")
fmt.Println("\t\t\t 3 退出系统")
fmt.Println("\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)
// 先把登陆的函数,写到另一个文件
err := login(userId, userPwd)
if err != nil {
fmt.Println("登陆失败")
} else {
fmt.Println("登陆成功")
}
} else if key ==2 {
fmt.Println("进行用户注册的逻辑。。。。")
}
}
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"`//返回状态码 500表示该用户未注册 200 用户登陆成功
Error string `json:"error"`//返回错误信息
}
测试结果:
2) 客户端和服务器端互通,接收客户端发送的消息
完成客户端可以发送消息本身,服务器端可以正常接收到消息,并根据客户端发送的消息(LoginMes),判断用户的合法性,并返回相应的LoginResMes
思路分析:
① 让客户端发送消息本身
② 服务器端接收到消息,反序列化成对应的消息结构体
③ 服务器端根据反序列化成对应的消息,判断是否登陆用户是合法用户,返回LoginResMes
④ 客户端解析返回的LoginResMes显示对应的界面
⑤ 这里需要做函数的封装
⑥ 代码实现
client/login.go
改动代码如下图
完整的代码
package main
import (
"fmt"
"net"
"encoding/binary"
"encoding/json"
"chatroom/common/message"
"time"
)
// 写一个函数,完成登陆
func login(userId int, userPwd string) (err error) {
// 下一步就要开始定协议。。。
// fmt.Printf(" userID=%d userPwd=%s", userID, userPwd)
// return nil
//1、连接到feu武器
conn, err := net.Dial("tcp", "localhost:8899")
if err != nil {
fmt.Println("net.Dial err", 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.Println("json.Marshal err=", err)
return
}
//5、将data赋给 mes.Data字段
mes.Data = string(data)
// 6、将mes进行序列化
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("json.Marshal err=", err)
return
}
//7、此时data就是我们要发送的消息
// 7.1 先把data的长度发生给服务器
// 先获取到data的长度->转换成一个表示长度的byte切片
var pkgLen uint32
pkgLen = uint32(len(data))
var buf [4]byte
binary.BigEndian.PutUint32(buf[0:4], pkgLen)
// 发送长度
n, err := conn.Write(buf[0:4])
if n != 4 || err != nil {
fmt.Println("conn.Write fail", err)
return
}
// fmt.Printf("客户端,发送消息长度成功=%d 内容是=%s", len(data), string(data))
// 发送消息本身
_, err = conn.Write(data)
if err != nil {
fmt.Println("conn.Write(data) fail", err)
return
}
//休眠20
time.Sleep(20 * time.Second)
fmt.Println("休眠了20秒")
// 这里还需要处理服务器端的消息
return
}
server/main.go
代码改动如下图:
将读取包的任务封装到了一个函数中 readPkg()
完整代码:
package main
import (
"fmt"
"net"
"encoding/binary"
"encoding/json"
"chatroom/common/message"
"io"
)
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 {
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 {
return
}
// 把pkgLen反序列化成 -> message.Message
err = json.Unmarshal(buf[:pkgLen], &mes)
if err != nil {
fmt.Println("json.Unmarshal fail err=", err)
return
}
return
}
//处理和客户端的通讯
func process(conn net.Conn) {
//这里需要延迟关闭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("readPkg fail err=", err)
return
}
}
fmt.Println("mes=", mes)
}
}
func main() {
//提示信息
fmt.Println("服务器在8899监听。。。。")
listen, err := net.Listen("tcp", "0.0.0.0:8899")
defer listen.Close()
if err != nil {
fmt.Println("net listen err=", err)
return
}
//一旦监听成功,就等待客户端链接服务器
for {
fmt.Println("等待客户端来接服务器。。。")
conn, err := listen.Accept()
if err != nil {
fmt.Println("liten Accept err=", err)
}
//一旦连接成功,则启动一个协程和客户端保持通讯。
go process(conn)
}
}
运行结果
3)指定用户登陆
能够完成登陆,并提示相应信息(指定用户100 密码123456)
server/main.go
新增代码
server/main.go整体代码
package main
import (
"fmt"
"net"
"encoding/binary"
"encoding/json"
"chatroom/common/message"
"io"
)
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 {
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 {
return
}
// 把pkgLen反序列化成 -> message.Message
err = json.Unmarshal(buf[:pkgLen], &mes)
if err != nil {
fmt.Println("json.Unmarshal fail err=", err)
return
}
return
}
func writePkg(conn net.Conn, data []byte) (err error){
// 先发送一个长度给对方
var pkgLen uint32
pkgLen = uint32(len(data))
var buf [4]byte
binary.BigEndian.PutUint32(buf[0:4], pkgLen)
// 发送长度
n, err := conn.Write(buf[0:4])
if n != 4 || err != nil {
fmt.Println("conn.Write fail", err)
return
}
// 发送data本身
n, err = conn.Write(data)
if n != int(pkgLen) || err != nil {
fmt.Println("conn.Write fail", err)
return
}
return
}
// 编写一个函数serverProcessLogin函数,专门处理登陆请求
func serverProcessLogin(conn net.Conn, mes *message.Message) (err error){
// 核心代码。。。
// 1、先从mes中取出mes.Data,并直接反序列化成LoginMes
var loginMes message.LoginMes
err = json.Unmarshal([]byte(mes.Data), &loginMes)
if err != nil {
fmt.Println("json.Unmashal fail err=", err)
return
}
// ① 先声明一个resMes
var resMes message.Message
resMes.Type = message.LoginResMesType
// ② 再声明一个 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 = "该用户不存在,请注册后再使用"
}
// ③ 将 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函数
err = writePkg(conn, data)
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
}
//处理和客户端的通讯
func process(conn net.Conn) {
//这里需要延迟关闭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("readPkg fail err=", err)
return
}
}
err = serverProcessMes(conn, &mes)
if err != nil {
return
}
}
}
func main() {
//提示信息
fmt.Println("服务器在8899监听。。。。")
listen, err := net.Listen("tcp", "0.0.0.0:8899")
defer listen.Close()
if err != nil {
fmt.Println("net listen err=", err)
return
}
//一旦监听成功,就等待客户端链接服务器
for {
fmt.Println("等待客户端来接服务器。。。")
conn, err := listen.Accept()
if err != nil {
fmt.Println("liten Accept err=", err)
}
//一旦连接成功,则启动一个协程和客户端保持通讯。
go process(conn)
}
}
client/utils.go 新增文件整体代码
package main
import (
"fmt"
"net"
"encoding/binary"
"encoding/json"
"chatroom/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 {
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 {
return
}
// 把pkgLen反序列化成 -> message.Message
err = json.Unmarshal(buf[:pkgLen], &mes)
if err != nil {
fmt.Println("json.Unmarshal fail err=", err)
return
}
return
}
func writePkg(conn net.Conn, data []byte) (err error){
// 先发送一个长度给对方
var pkgLen uint32
pkgLen = uint32(len(data))
var buf [4]byte
binary.BigEndian.PutUint32(buf[0:4], pkgLen)
// 发送长度
n, err := conn.Write(buf[0:4])
if n != 4 || err != nil {
fmt.Println("conn.Write fail", err)
return
}
// 发送data本身
n, err = conn.Write(data)
if n != int(pkgLen) || err != nil {
fmt.Println("conn.Write fail", err)
return
}
return
}
client/login.go
改动代码
整体代码
package main
import (
"fmt"
"net"
"encoding/binary"
"encoding/json"
"chatroom/common/message"
)
// 写一个函数,完成登陆
func login(userId int, userPwd string) (err error) {
// 下一步就要开始定协议。。。
// fmt.Printf(" userID=%d userPwd=%s", userID, userPwd)
// return nil
//1、连接到feu武器
conn, err := net.Dial("tcp", "localhost:8899")
if err != nil {
fmt.Println("net.Dial err", 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.Println("json.Marshal err=", err)
return
}
//5、将data赋给 mes.Data字段
mes.Data = string(data)
// 6、将mes进行序列化
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("json.Marshal err=", err)
return
}
//7、此时data就是我们要发送的消息
// 7.1 先把data的长度发生给服务器
// 先获取到data的长度->转换成一个表示长度的byte切片
var pkgLen uint32
pkgLen = uint32(len(data))
var buf [4]byte
binary.BigEndian.PutUint32(buf[0:4], pkgLen)
// 发送长度
n, err := conn.Write(buf[0:4])
if n != 4 || err != nil {
fmt.Println("conn.Write fail", err)
return
}
// fmt.Printf("客户端,发送消息长度成功=%d 内容是=%s", len(data), string(data))
// 发送消息本身
_, err = conn.Write(data)
if err != nil {
fmt.Println("conn.Write(data) fail", err)
return
}
//休眠20
// time.Sleep(20 * time.Second)
// fmt.Println("休眠了20秒")
// 这里还需要处理服务器端的消息
mes, err = readPkg(conn) //mes就是
if err != nil {
fmt.Println("readPkg err=", err)
return
}
// 将mes的Data部分反序列化成 LoginResMes
var loginResMes message.LoginResMes
err = json.Unmarshal([]byte(mes.Data), &loginResMes)
if loginResMes.Code == 200 {
fmt.Println("用户登陆成功")
} else if loginResMes.Code == 500 {
fmt.Println(loginResMes.Error)
}
return
}
common/messag/message.go 改动代码
运行结果
4)程序改进
程序结构的改进,前面的程序虽然完成了功能,但是没有结构,系统的可读性,扩展性和维护性都不好,因此需要对程序进行改进。
改进1
① 先改进服务器端,先画出程序的框架图
② 步骤
-
先把分析出来的文件,创建好,然后放到相应的文件夹[包],下图为代码规划图:
-
现在根据各个文件,完成的任务不同,将server/main/main.go的代码剥离到对应的文件中即可。
服务器端
server/main/main.go
package main
import (
"fmt"
"net"
)
// 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 {
// 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 {
// return
// }
// // 把pkgLen反序列化成 -> message.Message
// err = json.Unmarshal(buf[:pkgLen], &mes)
// if err != nil {
// fmt.Println("json.Unmarshal fail err=", err)
// return
// }
// return
// }
// func writePkg(conn net.Conn, data []byte) (err error){
// // 先发送一个长度给对方
// var pkgLen uint32
// pkgLen = uint32(len(data))
// var buf [4]byte
// binary.BigEndian.PutUint32(buf[0:4], pkgLen)
// // 发送长度
// n, err := conn.Write(buf[0:4])
// if n != 4 || err != nil {
// fmt.Println("conn.Write fail", err)
// return
// }
// // 发送data本身
// n, err = conn.Write(data)
// if n != int(pkgLen) || err != nil {
// fmt.Println("conn.Write fail", err)
// return
// }
// return
// }
// 编写一个函数serverProcessLogin函数,专门处理登陆请求
// func serverProcessLogin(conn net.Conn, mes *message.Message) (err error){
// // 核心代码。。。
// // 1、先从mes中取出mes.Data,并直接反序列化成LoginMes
// var loginMes message.LoginMes
// err = json.Unmarshal([]byte(mes.Data), &loginMes)
// if err != nil {
// fmt.Println("json.Unmashal fail err=", err)
// return
// }
// // ① 先声明一个resMes
// var resMes message.Message
// resMes.Type = message.LoginResMesType
// // ② 再声明一个 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 = "该用户不存在,请注册后再使用"
// }
// // ③ 将 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函数
// err = writePkg(conn, data)
// 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
// }
//处理和客户端的通讯
func process(conn net.Conn) {
//这里需要延迟关闭conn
defer conn.Close()
// 这里调用总控创建一个总控
processor := &Processor{
Conn : conn,
}
err := processor.process2()
if err != nil {
fmt.Println("客户端和服务器端的通讯协程出现问题 err=", err)
return
}
}
func main() {
//提示信息
fmt.Println("服务器[新的结构]在8899监听。。。。")
listen, err := net.Listen("tcp", "0.0.0.0:8899")
defer listen.Close()
if err != nil {
fmt.Println("net listen err=", err)
return
}
//一旦监听成功,就等待客户端链接服务器
for {
fmt.Println("等待客户端来接服务器。。。")
conn, err := listen.Accept()
if err != nil {
fmt.Println("liten Accept err=", err)
}
//一旦连接成功,则启动一个协程和客户端保持通讯。
go process(conn)
}
}
server/main/process.go
package main
import (
"fmt"
"net"
"chatroom/common/message"
"chatroom/server/utils"
"chatroom/server/process"
"io"
)
// 先创建一个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 := &process2.UserProcess{
Conn : this.Conn,
}
err = up.ServerProcessLogin(mes)
case message.RegisterMesType :
// 处理注册
default :
fmt.Println("消息类型不存在,无法处理。。。。")
}
return
}
func(this *Processor) process2() (err error){
//循环读取读客户端发送的消息
for {
// 这里将读取数据包,直接封装成一个readPkg(),返回Message,err
// 创建一个Transfer 实例完成读包任务
tf := &utils.Transfer{
Conn : this.Conn,
}
mes, err := tf.ReadPkg()
if err != nil {
if err == io.EOF {
fmt.Println("客户端退出,服务器端也正常退出")
return err
} else {
fmt.Println("readPkg fail err=", err)
return err
}
}
err = this.serverProcessMes(&mes)
if err != nil {
return err
}
}
}
server/process/smsprocess.go
package process2
server/process/userprocess.go
package process2
import (
"fmt"
"net"
"encoding/json"
"chatroom/common/message"
"chatroom/server/utils"
)
type UserProcess struct {
// 字段
Conn net.Conn
}
// 编写一个函数serverProcessLogin函数,专门处理登陆请求
func(this *UserProcess) ServerProcessLogin(mes *message.Message) (err error){
// 核心代码。。。
// 1、先从mes中取出mes.Data,并直接反序列化成LoginMes
var loginMes message.LoginMes
err = json.Unmarshal([]byte(mes.Data), &loginMes)
if err != nil {
fmt.Println("json.Unmashal fail err=", err)
return
}
// ① 先声明一个resMes
var resMes message.Message
resMes.Type = message.LoginResMesType
// ② 再声明一个 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 = "该用户不存在,请注册后再使用"
}
// ③ 将 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函数
// 因为使用了分层的模式,先创建Transfer实例,然后读取
tf := &utils.Transfer{
Conn : this.Conn,
}
err = tf.WritePkg(data)
return
}
server/utils/utils.go
package utils
import (
"fmt"
"net"
"encoding/binary"
"encoding/json"
"chatroom/common/message"
)
// 这里将这些方法关联到结构体中
type Transfer struct {
// 分析他应该有哪些字段
Conn net.Conn
Buf [8064]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 {
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 {
return
}
// 把pkgLen反序列化成 -> message.Message
err = json.Unmarshal(this.Buf[:pkgLen], &mes)
if err != nil {
fmt.Println("json.Unmarshal fail err=", err)
return
}
return
}
func(this *Transfer) WritePkg(data []byte) (err error){
// 先发送一个长度给对方
var pkgLen uint32
pkgLen = uint32(len(data))
// var buf [4]byte
binary.BigEndian.PutUint32(this.Buf[0:4], pkgLen)
// 发送长度
n, err := this.Conn.Write(this.Buf[0:4])
if n != 4 || err != nil {
fmt.Println("conn.Write fail", err)
return
}
// 发送data本身
n, err = this.Conn.Write(data)
if n != int(pkgLen) || err != nil {
fmt.Println("conn.Write fail", err)
return
}
return
}
代码运行结果
改进2
客户端
③ 修改客户端,先画出程序的框架图
④ 先把各个文件放到对应的文件夹/包
代码规划图:
client/main/main.go
package main
import (
"fmt"
"os"
"chatroom/client/process"
)
// 定义两个变量,一个表示用户的ID, 一个表示用户密码
var userId int
var userPwd string
func main() {
// 接收用户选择
var key int
// 判断是否还继续显示菜单
// var loop = true
for true {
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 请选择(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)
// 完成登陆
// 1、创建一个UserProcess实例
up := &process.UserProcess{}
up.Login(userId, userPwd)
case 2 :
fmt.Println("注册用户")
// loop = false
case 3 :
fmt.Println("退出系统")
os.Exit(0)
default :
fmt.Println("你的输入有误,请重新输入")
}
}
// 根据用户的输入,显示新的提示信息
// if key ==1 {
// // 说明用户要登陆
// // 因为使用了分层的结构,
// //先把登陆的函数,写到另一个文件
// // 这里我们会需要重新调用
// // login(userId, userPwd)
// // if err != nil {
// // fmt.Println("登陆失败")
// // } else {
// // fmt.Println("登陆成功")
// // }
// } else if key ==2 {
// fmt.Println("进行用户注册的逻辑。。。。")
// }
}
client/process/server.go
package process
import (
"fmt"
"os"
"net"
"chatroom/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("1、显示在线用户列表")
case 2 :
fmt.Println("2、发送消息")
case 3 :
fmt.Println("3、信息列表")
case 4 :
fmt.Println("4、退出系统")
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("RedPkg err=", err)
return
}
// 如果读取到了消息,下一步处理
fmt.Printf("mes=%v", mes)
}
}
client/process/smsprocess.go
package process
client/process/userprocess.go
package process
import (
"fmt"
"net"
"encoding/binary"
"encoding/json"
"chatroom/common/message"
"chatroom/client/utils"
)
type UserProcess struct {
//暂时不需要字段
}
// 给关联一个用户登陆方法
// 写一个函数,完成登陆
func (this *UserProcess) Login(userId int, userPwd string) (err error) {
// 下一步就要开始定协议。。。
// fmt.Printf(" userID=%d userPwd=%s", userID, userPwd)
// return nil
//1、连接到feu武器
conn, err := net.Dial("tcp", "localhost:8899")
if err != nil {
fmt.Println("net.Dial err", 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.Println("json.Marshal err=", err)
return
}
//5、将data赋给 mes.Data字段
mes.Data = string(data)
// 6、将mes进行序列化
data, err = json.Marshal(mes)
if err != nil {
fmt.Println("json.Marshal err=", err)
return
}
//7、此时data就是我们要发送的消息
// 7.1 先把data的长度发生给服务器
// 先获取到data的长度->转换成一个表示长度的byte切片
var pkgLen uint32
pkgLen = uint32(len(data))
var buf [4]byte
binary.BigEndian.PutUint32(buf[0:4], pkgLen)
// 发送长度
n, err := conn.Write(buf[0:4])
if n != 4 || err != nil {
fmt.Println("conn.Write fail", err)
return
}
// fmt.Printf("客户端,发送消息长度成功=%d 内容是=%s", len(data), string(data))
// 发送消息本身
_, err = conn.Write(data)
if err != nil {
fmt.Println("conn.Write(data) fail", err)
return
}
//休眠20
// time.Sleep(20 * time.Second)
// fmt.Println("休眠了20秒")
// 这里还需要处理服务器端的消息
// 创建Transfer实例
tf := &utils.Transfer{
Conn : conn,
}
mes, err = tf.ReadPkg() //mes就是
if err != nil {
fmt.Println("readPkg err=", err)
return
}
// 将mes的Data部分反序列化成 LoginResMes
var loginResMes message.LoginResMes
err = json.Unmarshal([]byte(mes.Data), &loginResMes)
if loginResMes.Code == 200 {
// fmt.Println("登陆成功")
// 这里需要在客户端启动一个协程
// 该协程保持和服务器端的通讯,如果服务器有数据推送客户端
// 则接收并显示在客户端的终端
go serverProcessMes(conn)
// 1、显示我们登陆成功的菜单[循环]
for {
ShowMenu()
}
} else if loginResMes.Code == 500 {
fmt.Println(loginResMes.Error)
}
return
}
client/utils/utils.go
package utils
import (
"fmt"
"net"
"encoding/binary"
"encoding/json"
"chatroom/common/message"
)
// 这里将这些方法关联到结构体中
type Transfer struct {
// 分析他应该有哪些字段
Conn net.Conn
Buf [8064]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 {
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 {
return
}
// 把pkgLen反序列化成 -> message.Message
err = json.Unmarshal(this.Buf[:pkgLen], &mes)
if err != nil {
fmt.Println("json.Unmarshal fail err=", err)
return
}
return
}
func(this *Transfer) WritePkg(data []byte) (err error){
// 先发送一个长度给对方
var pkgLen uint32
pkgLen = uint32(len(data))
// var buf [4]byte
binary.BigEndian.PutUint32(this.Buf[0:4], pkgLen)
// 发送长度
n, err := this.Conn.Write(this.Buf[0:4])
if n != 4 || err != nil {
fmt.Println("conn.Write fail", err)
return
}
// 发送data本身
n, err = this.Conn.Write(data)
if n != int(pkgLen) || err != nil {
fmt.Println("conn.Write fail", err)
return
}
return
}
运行结果