MeteorCat / H5游戏服务端(五)

Created Sat, 27 Jan 2024 12:53:47 +0800 Modified Wed, 29 Oct 2025 23:24:59 +0800

H5游戏服务端(五)

消息通讯功能已经完成了, 现在需要优化对应常量配置保存, 现在重构下之前所需方法:

/**
 * 访问状态常量表
 */
public class LogicStatus {

    /**
     * 无状态
     */
    public static final int None = 0;

    /**
     * 已授权状态
     */
    public static final int Authorized = 1;

    /**
     * 程序内部传递, 不对外暴露
     */
    public static final int Program = 2;


    /**
     * 游戏中状态
     */
    public static final int Gaming = 3;
}

之前的玩家信息重新构建, 改为状态常量处理:

/**
 * 玩家信息 Actor
 */
@EnableActor(owner = PlayerLogic.class)
public class PlayerLogic extends ActorConfigurer {
    /// 其他代码, 略


    /**
     * 玩家追加游戏货币 - 用于测试
     * 现在这里 state 该有常量处理, 只有登录和游戏中才能被访问
     * Example: { "value":302,"args":{ }}
     *
     * @param runtime 运行时
     * @param session 会话
     * @param args    参数
     */
    @ActorMapping(value = 302, state = {LogicStatus.Authorized, LogicStatus.Gaming})
    public void addGold(WebsocketApplication runtime, WebSocketSession session, JsonNode args) {
        // 测试追加100金币
    }
}

优化成常量后续如果调整只需要在常量表修改而不用全局查询代码处理.

协议共享

上面看到了 state 配置常量而 value 却还留着硬编码写在上面, 主要就是为了引出接下来问题.

开发到目前都是由服务端单方面开发, 协议号应该是服务端和客户端共享维护, 所以这里编写 python+json 来做协议生成工具:

#!/usr/bin/python
# -*- coding: UTF-8 -*-

import pathlib
import sys
import getopt
import json


# 识别是否为数字
def is_number(s):
    try:
        float(s)
        return True
    except ValueError:
        pass

    try:
        import unicodedata
        unicodedata.numeric(s)
        return True
    except (TypeError, ValueError):
        pass
    return False


# 查询目录所有JSON文件
def search_json_file(path):
    return [f for f in pathlib.Path(path).iterdir() if f.is_file() and f.glob(".json")]


# 构建Java
def out_java_proto(output, data, name="Protocols", file="Protocols.java"):
    f = pathlib.Path(output)
    f = f.joinpath(file)
    text = '''
/**
 * 请求响应数据协议
 */
'''
    text += "public class %s {\n" % name

    for v in data:
        text += "    public static final int %s = %d; // %s\n" % (v["name"], v["value"], v["desc"])

    text += "}"

    f.open(mode="w+", encoding="utf-8")
    f.write_text(data=text, encoding="utf-8")


# 构建Godot
def out_godot_proto(output, data, name="Protocols", file="Protocols.gd"):
    f = pathlib.Path(output)
    f = f.joinpath(file)
    text = '# 请求响应数据协议: %s\n' % name
    text += 'extends Node\n'
    for v in data:
        text += "const %s:int = %d; # %s\n" % (v["name"], v["value"], v["desc"])

    f.open(mode="w+", encoding="utf-8")
    f.write_text(data=text, encoding="utf-8")


# 构建Lua
def out_lua_proto(output, data, name="Protocols", file="Protocols.lua"):
    f = pathlib.Path(output)
    f = f.joinpath(file)
    text = '---\n'
    text += '--- 请求响应数据协议\n'
    text += '---\n'
    text += '%s = %s or  {\n' % (name, name)
    for v in data:
        text += "    %s = %d; -- %s\n" % (v["name"], v["value"], v["desc"])
    text += '}\n'
    f.open(mode="w+", encoding="utf-8")
    f.write_text(data=text, encoding="utf-8")


# 构建C#
def out_csharp_proto(output, data, name="Protocols", file="Protocols.cs"):
    f = pathlib.Path(output)
    f = f.joinpath(file)
    text = '''
/**
 * 请求响应数据协议
 */
'''
    text += "public class %s {\n" % name

    for v in data:
        text += "    public static readonly int %s = %d; // %s\n" % (v["name"], v["value"], v["desc"])

    text += "}"

    f.open(mode="w+", encoding="utf-8")
    f.write_text(data=text, encoding="utf-8")


# 入口方法
def main(argv):
    input_dir = ''
    output_dir = ''
    try:
        opts, args = getopt.getopt(argv, "hi:o:", ["input_dir=", "output_dir="])
    except getopt.GetoptError:
        print('protocol.py -i <input_dir> -o <output_dir>')
        sys.exit(2)
    for opt, arg in opts:
        if opt == '-h':
            print("protocol.py -i <input_dir> -o <output_dir>")
            sys.exit()
        elif opt in ("-i", "--input_dir"):
            input_dir = arg
        elif opt in ("-o", "--output_dir"):
            output_dir = arg

    if len(input_dir) <= 0 or len(output_dir) <= 0:
        print("找不到输入|输出目录信息")
        sys.exit(1)

    print("输入目录: ", input_dir)
    print("输出目录: ", output_dir)

    files = search_json_file(input_dir)
    if len(files) <= 0:
        print("找不到协议文件")
        sys.exit()

    # 遍历数据
    proto_list = []
    for file in files:
        prop = file.name.upper().replace(".JSON", "")
        print("协议分类: ", prop)

        text = file.read_text(encoding="utf-8")
        data = json.loads(text)
        if isinstance(data, dict):
            for value in data:
                name = data[value]["name"]
                desc = data[value]["description"]
                proto = "%s_%s" % (prop, name)
                print("解析协议: ", value, "-", proto, "|", desc)
                proto_list.append({
                    "value": int(value),
                    "name": proto,
                    "desc": desc
                })

    # 构建协议
    out_godot_proto(output_dir, proto_list)
    out_lua_proto(output_dir, proto_list)
    out_csharp_proto(output_dir, proto_list)
    out_java_proto(output_dir, proto_list)


# 入口调用
if __name__ == "__main__":
    main(sys.argv[1:])

如果开发的时候独自建立 protocols 目录在版本库共享给客户端和服务端用于对协议, 内部文件格式以 xxx.json 做文件内容, 这里先生成某些分类的协议文件:

  • info.json: 常规服务端发给客户端的信息, 比如心跳包或者错误|调试|警告消息
  • login.json: 登录相关协议数据分类, 本质上是授权相关
  • scene.json: 关卡相关协议, 比如需要切换关卡场景等情况
// info.json
{
  "11": {
    "name": "MESSAGE_LOG",
    "description": "用于服务端返回日志消息"
  },
  "12": {
    "name": "MESSAGE_NOTICE",
    "description": "用于服务端返回提示消息"
  },
  "13": {
    "name": "MESSAGE_WARN",
    "description": "用于服务端返回警告消息"
  },
  "14": {
    "name": "MESSAGE_ERROR",
    "description": "用于服务端返回错误消息"
  }
}

// login.json
{
  "101": {
    "name": "SEND",
    "description": "客户端推送给服务端请求"
  },
  "102": {
    "name": "ERROR_BY_EXISTS",
    "description": "服务端推送授权不存在"
  },
  "103": {
    "name": "ERROR_BY_SECRET",
    "description": "服务端推送授权响应失败"
  },
  "104": {
    "name": "ERROR_BY_OTHER",
    "description": "服务端推送异地登录"
  }
}

// scene.json
{
  "51": {
    "name": "CHANGE",
    "description": "用于服务端返回切换关卡"
  }
}

直接运行 Python 的协议构建工具, 测试是否会生成具体的不同平台语言的协议类:

# 将 protocols 内部协议文件转化为 godot|c#|java|lua 的静态码表工具导出到 .\output\protocols 目录之中
python .\tools\protocol.py -i .\protocols -o .\output\protocols

这样基本上服务端与客户端的工作流对接:

  1. 开会策划确定新分类功能; 假设开发抽卡功能 card, 那么协议定下 card.json 分类
  2. 客户端和服务端确定需要的协议功能数量; 假设功能需要 抽卡提交(Send)|抽卡响应(Reward)
  3. 生成具体码表并且工具打包生成; 那么前后端可以通过 CARD_SEND|CARD_REWARD 常量对接
  4. 后续如果协议 int 变动也仅仅是重新打包份协议常量而已