#go-websocket小游戏项目技术总结
项目概述
此项目基于go语言,通过http和websocket来实现前后端分离技术,指定客户端之间的通讯等功能的H5版的”找不同“小游戏。
游戏功能及简述
- 登录:本项目采用游客方式,让用户进行游玩,每次进入游戏的用户信息都是随机的,如用户昵称(也可以指定),不对用户信息进行持久性存储。1. 创建和加入房间:房主进入游戏后,自动创建房间,然后选择单人或双人游戏游玩;双人游戏需要邀请其他玩家加入房间。1. 退出房间:被邀请者退出房间,实时更新房主页面状态;房主退出房间,则房间进行销毁,被邀请者同时退出房间。1. 进行游戏:双人游戏时,可实时共享对方的游戏进度,增加竞争和紧张感。1. 排行榜:游戏结束后,会根据各自找到不同的个数和时间对双方进行排行。1. 心跳检测:房间和游戏页面,客户端每5s向服务端发送心跳包,保证客户端的存活。1. 断线重连:当客户端处于弱网或断网情况下,心跳检测不稳定或失联,在客户端网络重新保持健康时,进行重新连接。
数据结构
- 通过client类来封装用户的基本信息、所持的websocket连接以及一些方法。
package client
import (
"github.com/gorilla/websocket"
uuid "github.com/satori/go.uuid"
)
type Client struct {
UserName string `json:"username"`
UserGender string `json:"userGender"`
Uuid string `json:"uuid"`
Score int `json:"score"`
Time int `json:"time"`
IsOwner string `json:"isOwner"`
UserIcon string `json:"userIcon"`
Heart string `json:"heart"`
Hub map[string]map[string]*Client `json:"-"`
RoomId string `json:"roomId"`
Conn *websocket.Conn `json:"-"`
}
type Clients []Client
// GetAuthentication 对client进行认证
func (client *Client) GetAuthentication() {
id := uuid.NewV4()
client.Uuid = id.String()
}
// BuildRoom GetRoomId 创建房间
func (client *Client) BuildRoom() {
id := uuid.NewV4()
client.RoomId = id.String()
temp := make(map[string]*Client)
temp[client.Uuid] = client
client.Hub[client.RoomId] = temp
}
// JoinRoom Join 加入房间
func (client *Client) JoinRoom() {
client.Hub[client.RoomId][client.Uuid] = client
}
// Len 排序
func (clients Clients) Len() int {
return len(clients)
}
func (clients Clients) Less(i, j int) bool {
if clients[i].Score == clients[j].Score {
return clients[i].Time < clients[j].Time
}
return clients[i].Score > clients[j].Score
}
func (clients Clients) Swap(i, j int) {
clients[i], clients[j] = clients[j], clients[i]
}
- 各种定义好的数据包,用于向客户端传递数据。
package pkg
import (
client2 "DifProject/client"
)
// Pkg 用户请求包
type Pkg struct {
Type string `json:"type"`
Data client2.Client `json:"data"`
}
type PostPkg struct {
Type string `json:"type"`
Code string `json:"code"`
Data client2.Client `json:"data"`
}
// PostListPkg 榜单请求包
type PostListPkg struct {
Type string `json:"type"`
Code string `json:"code"`
Data client2.Clients `json:"data"`
}
type StartPkg struct {
Type string `json:"type"`
Code string `json:"code"`
}
// Authentication 用户认证
var Authentication = "authentication"
// BuildRoom 创建房间
var BuildRoom = "buildRoom"
// JoinRoom 加入房间
var JoinRoom = "joinRoom"
// CounterpartyData 对方数据
var CounterpartyData = "CounterpartyData"
// GameProgress 游戏进度
var GameProgress = "gameProgress"
// List 排行榜
var List = "list"
// HeartBeat 心跳
var HeartBeat = "check"
// StartGame 开始游戏
var StartGame = "startGame"
// EndGame 结束游戏
var EndGame = "endGame"
// UpdateConn 更新连接
var UpdateConn = "updateConn"
- 用map维护房间的hub
var hub = make(map[string]map[string]*client2.Client)
<img alt="" height="295" src="https://i-blog.csdnimg.cn/blog_migrate/4d43a322d7ba120b65e0785824a8c3fd.png" width="488">
具体接口实现
用户认证
对客户端发来的用户信息进行判断,由于房主的连接是不携带url参数的,而被邀请者携带,通过此区别来判断他们的身份,并生成他们的唯一uuid。
var client client2.Client
var pkg pkg2.Pkg
// 设置允许跨域的来源,*表示允许所有来源
w.Header().Set("Access-Control-Allow-Origin", "*")
// 设置允许的请求方法
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
// 设置允许的请求头
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
body, err := io.ReadAll(r.Body)
if err != nil {
//fmt.Println("read message error")
return
}
err = json.Unmarshal(body, &pkg)
if err != nil {
//fmt.Println("用户认证请求读取失败")
return
}
err = r.ParseForm()
if err != nil {
return
}
queryParams := r.URL.Query()
var roomId string
if len(queryParams) > 0 {
roomId = queryParams.Get("roomId")
client.RoomId = roomId
}
pkgType := pkg.Type
client = pkg.Data
client.Hub = hub
switch pkgType {
case pkg2.Authentication:
//用户认证
client.GetAuthentication()
if client.RoomId == "" {
client.IsOwner = "1"
} else {
client.IsOwner = "0"
}
if client.IsOwner == "0" && len(hub[client.RoomId]) >= 2 {
postPkg := pkg2.PostPkg{
Type: "joinError",
Code: "300",
}
m, err := json.Marshal(postPkg)
if err != nil {
//fmt.Println("jsonMarshal error")
return
}
w.Write(m)
return
}
//返回数据
postPkg := pkg2.PostPkg{
Type: pkg2.Authentication,
Code: "200",
Data: client,
}
m, err := json.Marshal(postPkg)
if err != nil {
//fmt.Println("jsonMarshal error")
return
}
w.Write(m)
创建和加入房间
接收客户端发来的"buildRoom"数据包,如果IsOwner为1则进行创建房间,否则进行加入房间的逻辑。
通过map来维护和管理房间。
case pkg2.BuildRoom:
if pkg.Data.IsOwner == "1" {
pkg.Data.BuildRoom()
//tokens[pkg.Data.RoomId] = append(tokens[pkg.Data.RoomId], pkg.Data.Uuid)
postPkg := pkg2.PostPkg{
Type: pkg2.BuildRoom,
Code: "200",
Data: pkg.Data,
}
err := pkg.Data.Conn.WriteJSON(postPkg)
if err != nil {
return
}
fmt.Println("build success")
} else {
pkg.Data.JoinRoom()
var conn1 *websocket.Conn
var conn2 *websocket.Conn
var uid1 string
for id := range hub[pkg.Data.RoomId] {
if id == pkg.Data.Uuid {
continue
}
uid1 = id
}
conn1 = hub[pkg.Data.RoomId][uid1].Conn
conn2 = hub[pkg.Data.RoomId][pkg.Data.Uuid].Conn
//房主身份识别
//hub[pkg.Data.RoomId][uid1].IsOwner = "1"
//pkg.Data.IsOwner = "0"
postPkg1 := pkg2.PostPkg{
Type: "roomOwner",
Code: "200",
Data: *hub[pkg.Data.RoomId][uid1],
}
postPkg2 := pkg2.PostPkg{
Type: "invitee",
Code: "200",
Data: pkg.Data,
}
err := conn1.WriteJSON(postPkg2)
if err != nil {
return
}
err = conn2.WriteJSON(postPkg1)
if err != nil {
return
}
fmt.Println("join success")
}
退出房间
退出房间分为3种情况:
被邀请者退出房间:直接将被邀请者的client从房间删除,并将退出成功的信息告诉房主客户端。
房主单人模式退出房间:直接销毁房间即可。
3.房主双人模式退出房间:销毁房间,并告知被邀请者退出房间。
case "exitRoom":
if pkg.Data.IsOwner == "0" {
fmt.Println("退出成功")
//将该用户从房间中退出
var uid string
for key, _ := range hub[pkg.Data.RoomId] {
if key == pkg.Data.Uuid {
continue
}
uid = key
}
postPkg := pkg2.PostPkg{
Type: "otherExit",
Code: "200",
Data: pkg.Data,
}
fmt.Println("@@@", pkg.Data.RoomId)
hub[pkg.Data.RoomId][uid].Conn.WriteJSON(postPkg)
fmt.Println("发送成功")
delete(hub[pkg.Data.RoomId], pkg.Data.Uuid)
} else if pkg.Data.IsOwner == "1" {
fmt.Println("房主退出")
if len(hub[pkg.Data.RoomId]) == 1 {
//退出房间
delete(hub[pkg.Data.RoomId], pkg.Data.Uuid)
//注销房间
delete(hub, pkg.Data.RoomId)
} else if len(hub[pkg.Data.RoomId]) == 2 {
var uid string
for key, _ := range hub[pkg.Data.RoomId] {
if key == pkg.Data.Uuid {
continue
}
uid = key
}
postPkg1 := pkg2.PostPkg{
Type: "exitRoom",
Code: "200",
Data: pkg.Data,
}
hub[pkg.Data.RoomId][uid].Conn.WriteJSON(postPkg1)
//注销房间
delete(hub, pkg.Data.RoomId)
}
开始游戏
收到开始游戏的命令后,告知房间中的另一个人开始游戏。
//开始游戏
case pkg2.StartGame:
var conn1 *websocket.Conn
var conn2 *websocket.Conn
var uid string
for key, _ := range hub[pkg.Data.RoomId] {
if key == pkg.Data.Uuid {
continue
}
uid = key
}
fmt.Println("----conn1", hub[pkg.Data.RoomId][uid])
conn1 = hub[pkg.Data.RoomId][uid].Conn
conn2 = conn
pkg := pkg2.StartPkg{
Type: pkg2.StartGame,
Code: "200",
}
fmt.Println("发送开始游戏1")
conn1.WriteJSON(pkg)
fmt.Println("发送开始游戏2")
conn2.WriteJSON(pkg)
更新游戏进度
实时将得分信息转发给房间中另一个人的客户端。
//更新游戏进度
case pkg2.GameProgress:
//转发分数
var uid2 string
for key, _ := range hub[pkg.Data.RoomId] {
if key == pkg.Data.Uuid {
continue
}
uid2 = key
}
//更新数据
hub[pkg.Data.RoomId][pkg.Data.Uuid].Score = pkg.Data.Score
hub[pkg.Data.RoomId][pkg.Data.Uuid].Time = pkg.Data.Time
//将该客户端数据转发到对方连接
err := hub[pkg.Data.RoomId][uid2].Conn.WriteJSON(pkg)
if err != nil {
fmt.Println("转发失败")
return
}
fmt.Println("转发成功")
结束游戏
游戏结束后,通知双发结束游戏。
case "endGame":
fmt.Println("游戏结束")
var uid string
for key, _ := range hub[pkg.Data.RoomId] {
if key == pkg.Data.Uuid {
continue
}
uid = key
}
hub[pkg.Data.RoomId][pkg.Data.Uuid].Score = pkg.Data.Score
hub[pkg.Data.RoomId][pkg.Data.Uuid].Time = pkg.Data.Time
Postpkg1 := pkg2.PostPkg{
Type: "endGame",
Code: "200",
Data: *hub[pkg.Data.RoomId][pkg.Data.Uuid],
}
Postpkg2 := pkg2.PostPkg{
Type: "endGame",
Code: "200",
Data: *hub[pkg.Data.RoomId][uid],
}
//给双发发送游戏结束的消息
conn.WriteJSON(Postpkg2)
err := hub[pkg.Data.RoomId][uid].Conn.WriteJSON(Postpkg1)
if err != nil {
fmt.Println(err)
return
}
重新连接
当客户端连接断开后,收到断线前的用户数据,并为其更新新的websocke连接。
case pkg2.UpdateConn:
//获取当前客户端的uid
uid := pkg.Data.Uuid
//更新该客户端的连接
hub[pkg.Data.RoomId][uid].Conn = conn
fmt.Println("更新成功")
pkg1 := pkg2.PostPkg{
Type: "updateConn",
Code: "200",
Data: *hub[pkg.Data.RoomId][uid],
}
var uid2 string
for key, _ := range hub[pkg.Data.RoomId] {
if key == pkg.Data.Uuid {
continue
}
uid2 = key
}
pkg2 := pkg2.PostPkg{
Type: "updateConn",
Code: "200",
Data: *hub[pkg.Data.RoomId][uid2],
}
conn.WriteJSON(pkg2)
hub[pkg.Data.RoomId][uid2].Conn.WriteJSON(pkg1)
排行
将接收到的用户数据进行排序,并返回给双方的客户端。
func postList(w http.ResponseWriter, r *http.Request) {
// 设置允许跨域的来源,*表示允许所有来源
w.Header().Set("Access-Control-Allow-Origin", "*")
// 设置允许的请求方法
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
// 设置允许的请求头
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
body, err := io.ReadAll(r.Body)
if err != nil {
fmt.Println("read message error")
return
}
var pkg pkg2.Pkg
err = json.Unmarshal(body, &pkg)
if err != nil {
fmt.Println("序列化失败")
return
}
fmt.Println("排行榜:")
fmt.Println(pkg)
pkgType := pkg.Type
switch pkgType {
case pkg2.List:
clients := client2.Clients{}
var uid string
for key, _ := range hub[pkg.Data.RoomId] {
if key == pkg.Data.Uuid {
continue
}
uid = key
}
clients = append(clients, pkg.Data)
clients = append(clients, *hub[pkg.Data.RoomId][uid])
sort.Sort(clients)
//返回数据
clientListPkg := pkg2.PostListPkg{
Type: pkg2.List,
Code: "200",
Data: clients,
}
marshal, err := json.Marshal(clientListPkg)
if err != nil {
return
}
w.Write(marshal)
}
}
总结
通过本次项目,主要学习了前后端分离技术的流程,以及对http短连接和websocket长连接的理解和应用、以及维护管理客户端连接,共享其他客户端信息、心跳检测客户端存活、重新连接解决用户弱网或断网的情况等。
gitee链接: