go-websocket小游戏项目技术总结

项目概述

        此项目基于go语言,通过http和websocket来实现前后端分离技术,指定客户端之间的通讯等功能的H5版的”找不同“小游戏。

游戏功能及简述

  1. 登录:本项目采用游客方式,让用户进行游玩,每次进入游戏的用户信息都是随机的,如用户昵称(也可以指定),不对用户信息进行持久性存储。
  2. 创建和加入房间:房主进入游戏后,自动创建房间,然后选择单人或双人游戏游玩;双人游戏需要邀请其他玩家加入房间。
  3. 退出房间:被邀请者退出房间,实时更新房主页面状态;房主退出房间,则房间进行销毁,被邀请者同时退出房间。
  4. 进行游戏:双人游戏时,可实时共享对方的游戏进度,增加竞争和紧张感。
  5. 排行榜:游戏结束后,会根据各自找到不同的个数和时间对双方进行排行。
  6. 心跳检测:房间和游戏页面,客户端每5s向服务端发送心跳包,保证客户端的存活。
  7. 断线重连:当客户端处于弱网或断网情况下,心跳检测不稳定或失联,在客户端网络重新保持健康时,进行重新连接。

数据结构

  • 通过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)

具体接口实现

用户认证

对客户端发来的用户信息进行判断,由于房主的连接是不携带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种情况:

1. 被邀请者退出房间:直接将被邀请者的client从房间删除,并将退出成功的信息告诉房主客户端。

2. 房主单人模式退出房间:直接销毁房间即可。

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链接:https://gitee.com/kanfanxing/dif

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值