基于WebSocket通信的H5小游戏总结

线上地址:欢迎各位小伙伴们试玩,并在游戏中提供反馈 ! !

仓库地址: Klotski: 数字华容道+拼图游戏

1.项目介绍:

数字华容道是一款经典的益智游戏,旨在挑战玩家的逻辑思维和空间想象能力。玩家需要通过移动数字方块的位置,按照特定的顺序将数字排列成正确的顺序,从而完成整个拼图。本项目不仅提供了离线单机版还提供双人PK功能,让您和好友在紧张刺激的氛围下一起头脑风暴。

2.项目逻辑:

  1. 用户点击进入房间后,前端获取访问者的设备号,随机昵称和头像,并存储到本地,用做后续的用户识别。
  2. 房间内只能存在两个玩家,第一个进行房间的人默认成为房主,向后端发送请求,建立WebSocket通信,后端通过map将用户的唯一Id和WebSocket相绑定。房主点击邀请好友后获得邀请码,分享给好友。
  3. 好友通过房主分享的邀请码进行房间,生成昵称和头像并向后端发送请求,建立WebSocket通信,同样地后端通过map将好友Id和WebSocket连接进行绑定。数据结构见Client.go
  4. 此时后端通过roomId将房主和好友进行绑定,方便后期查找,进行各种数据的处理。数据结构见Hub.go
  5. 好友点击准备,房主点击开始游戏后,双方进入华容道游戏。
  6. 在游戏过程中,实时交换双方的游戏数据:步数和完成进度。
  7. 如果一方完成游戏,另一方被告知对方已完成,游戏结束。
  8. 特殊情况处理:
    1. 如果在游戏的过程中,一方主动退出游戏,则另一方直接胜利;
    2. 如果在游戏过程中,玩家出现弱网、断网的情况,则会进行3次短线重连的提示,超过三次后失败后,则直接算作退出房间,留在房间内的玩家胜利。

游戏设计示意图

3.代码实现:

本部分只说明项目中比较重要的代码实现,其余代码实现请查看git仓库

3.1项目结构

项目接口分为两部分,http请求(httpLink包)和websocket通信(socket包)。

在router包指定路径和特定的请求处理器。

在pojo包中定义了常用的数据接口和websocket信息交互格式。

3.2WebSocket通信建立和跨域处理

Go 复制代码
func WsHandle(hub *pojo.HupCenter) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		//将短连接 升级成 长连接-建立WebSocket通信
		upgrade := websocket.Upgrader{
			ReadBufferSize:  1024,
			WriteBufferSize: 1024,
			CheckOrigin: func(r *http.Request) bool {
				return true
			},
		}

		conn, err := upgrade.Upgrade(w, r, nil)
		if err != nil {
			log.Println("conn错误", err)
			return
		}

		fmt.Println("远程主机连接成功 IP为", conn.RemoteAddr())
		//进行client的初始化操作
		client := &pojo.Client{User: &pojo.User{}, Hub: &pojo.HupCenter{}} //非字段不要为nil
		client.Hub = hub
		client.User.UserConn = conn
		client.User.HealthCheck = time.Now().Add(time.Duration(pkg.HeartCheckSecond) * time.Second) //健康时间

		//计时器 : 如果用户规定秒内没有完成用户认证,则直接断开连接
		time.AfterFunc(time.Duration(pkg.UserAuthSecond)*time.Second, func() {
			if !client.User.UserCer {
				fmt.Println("用户认证失败,关闭连接")
				client.User.Close()
			}
		})

		//接受信息 根据信息类型进行分别处理
		go Controller(client)
	}
}

3.3WebSocket游戏处理中枢

Go 复制代码
func Controller(client *pojo.Client) {
	defer func() {
		client.Hub.UnRegister <- client
	}()

	for {
		_, p, err := client.User.UserConn.ReadMessage()
		if err != nil {
			if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) {
				//用户主动给关闭连接后的输出
				log.Println("WebSocket closed")
			} else {
				//服务器主动断开走这个,从一个断开的连接中读取信息
				log.Println("server.go conn.ReadMessage 读取信息错误", err)
			}
			return
		}

		var requestPkg pojo.RequestPkg
		err = json.Unmarshal(p, &requestPkg)
		if err != nil {
			fmt.Println("websocket反序列化失败", err)
			return
		}

		//2. 在信息中枢处根据消息类型进行特定的处理
		switch requestPkg.Type {
		case pojo.CertificationType:
			//用户认证
			client.CertificationProcess(requestPkg)

		case pojo.CreateRoomType:
			//创建房间号,并将创建者加入房间
			fmt.Println("发起创建房间的请求")
			client.CreateRoomProcess()

		case pojo.JoinRoomType:
			//1.加入房间的前提,先建立连接
			//2.完成用户认证
			//3.发送消息类型和房间号 Type uuid
			//只有完成上述步骤,才可以加入房间
			var data map[string]interface{}
			err = json.Unmarshal([]byte(requestPkg.Data), &data)
			if err != nil {
				fmt.Println("解析 JSON 失败:", err)
				return
			}
			uuidValue, ok := data["uuid"].(string)
			if !ok {
				fmt.Println("uuid 字段不存在或不是字符串类型")
				return
			}
			client.JoinRoomProcess(uuidValue)

		case pojo.RefreshScoreType:
			//什么是否进行分数更新,前端判断 type:RefreshScoreType, data:step、step、score
			//当用户的行为触发前端游戏机制的更新时,前端调用此接口,后端进行分数的转发 不需要做业务处理,直接转发即可
			fmt.Println("游戏交换中数据", client)
			client.RefreshScoreProcess(requestPkg)

		case pojo.DiscontinueQuitType:
			client.DiscontinueQuitProcess()

		case pojo.GameOverType:
			//游戏结束类型好像没有太大用,游戏结束的时候的提醒,通过分数更新就可以实现了
			fmt.Println("GameOverType")

		case pojo.HeartCheckType:
			//开启一个协程遍历hub中的Client,进行健康检测,生命时间是否会过期,如果过期进行逻辑删除和关闭连接
			if requestPkg.Data == "PING" {
				client.HeartCheckProcess()
			}
		}

	}

}

3.4维护建立连接的客户端

维护已经建立的客户端连接和进行客户端的之间的配对、查询。这里使用管道对全局唯一的map进行操作,防止出现多个协程操作同一个map。

package pojo

import (
	"fmt"
	"klotski/pkg"
	"time"
)

type HupCenter struct {
	ClientsMap map[string]map[string]*Client `json:"-"` //第一个string-roomId 第二个string-userId
	Register   chan *Client
	UnRegister chan *Client
}

// NewHub 初始化一个hub
func NewHub() *HupCenter {
	return &HupCenter{
		ClientsMap: make(map[string]map[string]*Client),
		Register:   make(chan *Client, 1),
		UnRegister: make(chan *Client, 1),
	}
}

// Run 用户向hub中的逻辑注册、删除、心跳检测全方法
func (h *HupCenter) Run() {
	checkTicker := time.NewTicker(time.Duration(pkg.HeartCheckSecond) * time.Second)
	defer checkTicker.Stop()

	for {
		select {
		case client := <-h.Register:
			//先查询是否存在此一个roomId key
			if myMap, ok := client.Hub.ClientsMap[client.User.RoomId]; ok { //有,加入房间
				//检测人数
				if len(myMap) == 1 {
					myMap[client.User.UserId] = client
				}
			} else { //没有,创建房间
				myMap := make(map[string]*Client)
				myMap[client.User.UserId] = client                //userId
				client.Hub.ClientsMap[client.User.RoomId] = myMap //roomId
			}
			fmt.Println("有人加入房间:", client.Hub.ClientsMap)
		case client := <-h.UnRegister:
			client.User.Close()
			if value, ok1 := client.Hub.ClientsMap[client.User.RoomId]; ok1 {
				if _, ok2 := value[client.User.UserId]; ok2 {
					delete(value, client.User.UserId)
				}
			}
			if len(client.Hub.ClientsMap[client.User.RoomId]) == 0 {
				delete(client.Hub.ClientsMap, client.User.RoomId)
			}
		case <-checkTicker.C:
			for _, roomMap := range h.ClientsMap {
				//fmt.Println(roomMap)
				for _, client := range roomMap {
					//fmt.Println(client)
					//fmt.Println(client.User.HealthCheck)
					if client.User.HealthCheck.Before(time.Now()) {
						h.UnRegister <- client
					}
				}
			}
			fmt.Println(time.Now().Format(time.DateTime), h.ClientsMap)
		}
	}
}

// QueryOtherUser 读操作 -- 根据当前用户寻找另一位用户,返回user对象
func (h *HupCenter) QueryOtherUser(c *Client) *Client {
	if roomMap, ok := h.ClientsMap[c.User.RoomId]; ok { //room
		for userId, user := range roomMap {
			if userId != c.User.UserId {
				return user
			}
		}
	}
	return nil
}

4.项目收获:

  • 前期的设计要明确具体,提前构思好项目的整体交互、处理流程,不要把一切问题推迟到编码阶段解决,要学会使用工具,将自己的项目构思表达出来。
  • WebSocket通信的应用场景。
  • 如何设计WebSocket数据包和客户端、服务端的通信流程
  • 将管道和协程熟练运用到自己的项目中
  • 掌握go语言打包部署流程,使用Jenkin自动化部署,进行产品迭代
相关推荐
hgdlip6 分钟前
使用代理ip和本地网络的区别是什么
网络·网络协议·tcp/ip
垂杨有暮鸦⊙_⊙8 分钟前
阅读《先进引信技术的发展与展望》识别和控制部分_笔记
笔记·学习
^Lim18 分钟前
esp32 JTAG 串口 bootload升级
java·linux·网络
Linux运维老纪1 小时前
交换机配置从IP(Switch Configuration from IP)
linux·服务器·网络·安全·运维开发·ip
加载中loading...1 小时前
C/C++实现tcp客户端和服务端的实现(从零开始写自己的高性能服务器)
linux·运维·服务器·c语言·网络
ccnnlxc1 小时前
https(day30)
网络协议·http·https
埋头编程~1 小时前
【C++】踏上C++学习之旅(十):深入“类和对象“世界,掌握编程黄金法则(五)(最终篇,内含初始化列表、静态成员、友元以及内部类等等)
java·c++·学习
世伟爱吗喽3 小时前
NUXT3学习日记四(路由中间件、导航守卫)
学习
m0_547486663 小时前
计算机网络名词解释汇总
网络·计算机网络
飞凌嵌入式4 小时前
飞凌嵌入式旗下教育品牌ElfBoard与西安科技大学共建「科教融合基地」
嵌入式硬件·学习·嵌入式·飞凌嵌入式