MeteorCat / H5游戏服务端(八)

Created Thu, 01 Feb 2024 22:14:59 +0800 Modified Wed, 29 Oct 2025 23:24:59 +0800

H5游戏服务端(八)

结合之前的篇章, 目前已经实现:

  • 网络数据传输
  • Actor处理模式
  • 数据库异步落地
  • 策划Excel对接
  • 客户端协议对接
  • 网络状态同步和帧同步
  • Linux系统( 需要分析游戏规模和架构部署游戏服务端, 必须学习项 )

这里还需要补上最后关于 运营 的部分, 最基本上需要实现以下功能:

  • 给玩家发送资源: 玩家直接发放补偿资源
  • 给单人|全体玩家发送邮件: 玩家单独|全服补偿推送
  • 更新上架|下架活动: 游戏内部活动通知和结束发放
  • 单个|全体玩家下线处理: 游戏维护将玩家下线处理

这里有很多方法处理, 如果但是这里我倾向采用的是 Redis 内存数据库处理, 然后生成单个 Actor 来监听以上相关对运营服务, 这里先引入类库处理:

<!-- Redis 数据库组件 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置好 application.properties 的连接配置, 这里不需要采用 redis.lettuce.pool 连接池处理:

# Redis 数据库配置
spring.data.redis.host = 127.0.0.1
spring.data.redis.port = 6379
# spring.data.redis.password=xxx # 密码设置
spring.data.redis.database = 2

最后挂起 Actor 服务来监听:

/**
 * 运营商业指令
 */
@EnableActor(owner = BusinessLogic.class)
public class BusinessLogic extends ActorConfigurer {

    /**
     * 日志对象
     */
    final Logger logger = LoggerFactory.getLogger(BusinessLogic.class);


    /**
     * 全局会话句柄
     */
    WebsocketApplication application;


    /**
     * 定时任务
     */
    ScheduledFuture<?> task = null;


    /**
     * Redis监听队列
     */
    final RedisTemplate<String, String> redis;

    /**
     * 这里需要初始化指定Redis消息队列, 一般是 类型:服务器ID 做KEY
     */
    final static String productName = "business:1";


    /**
     * JSON数据解析器, 这里按照 { "value":xxx, "args":{ } } 转发到自己内部 Actor
     */
    final ObjectMapper mapper = new ObjectMapper();


    /**
     * 初始化
     *
     * @param redis Redis监听队列
     */
    public BusinessLogic(RedisTemplate<String, String> redis) {
        this.redis = redis;
    }


    /**
     * 系统初始化
     */
    @Override
    public void init() {
        if (task == null) {
            // 监听队列不需要太过频繁, 所以只需要1s执行确定下队列是否为空
            task = getMonitor().scheduleAtFixedRate(() -> {
                // 获取目前的最新消息
                String message = redis.opsForList().rightPop(productName);
                if (message == null || message.isBlank()) {
                    return;
                }

                // 确定消息是否能够解析
                JsonNode node;
                try {
                    node = mapper.readTree(message);
                } catch (Exception exception) {
                    logger.error("运营消息解析错误: {}", message);
                    return;
                }
                if (node == null) {
                    return;
                }


                // 解析出 { "value":xxx, "args":{ } } 格式
                JsonNode valueNode = node.get("value");
                JsonNode argNode = node.get("args");
                if (valueNode == null || !valueNode.isInt() || !argNode.isObject()) {
                    logger.error("运营消息解析错误: {}", message);
                    return;
                }


                // 推送到内部执行
                Integer value = valueNode.asInt();
                this.invoke(value, LogicStatus.Program, argNode);
            }, 1L, 1L, TimeUnit.SECONDS);
        }
    }


    /**
     * 系统退出
     */
    @Override
    public void destroy() {
        // 需要监听队列任务
        if (task != null) {
            task.cancel(false);
        }
    }


    /**
     * 挂起目前的启动运行时, 在 WebSocket 的时候构造方法调用该服务
     */
    @ActorMapping(value = 900, state = LogicStatus.Program)
    public void load(WebsocketApplication application) {
        if (this.application == null) {
            this.application = application;
        }
    }


    /**
     * 发送消息给指定玩家
     *
     * @param data 数据内容
     */
    @ActorMapping(value = 901, state = LogicStatus.Program)
    public void mailToPlayer(JsonNode data) throws IOException {
        if (application == null) {
            return;
        }

        logger.info("发送消息给指定玩家: {}", data);
        JsonNode uid = data.get("uid");
        JsonNode title = data.get("title");
        JsonNode content = data.get("content");
        if (uid == null || title == null || content == null || !uid.isNumber()) {
            logger.error("消息格式错误: {}", data);
            return;
        }


        // 发送给客户端, 一般是移交给其他专用 Actor 处理, 这里先自己直接推送
        WebSocketSession session = application.getSession(uid.asLong());
        if (session == null) {
            logger.error("目前玩家不在线");
            return;
        }

        // 在线直接推送邮件
        application.push(session, 901, new HashMap<>(2) {{
            put("title", title.asText());
            put("content", content.asText());
        }});

    }

}

这里可以在命令行或者客户端追加指令通知挂起的服务, 假设运营后台给玩家推送邮件:

// 推送给 UID = 1 邮件
LPUSH business:1 '{ "value":901, "args":{ "uid":1,"title":"hello","content":"bro" } }' 

这里只提供思路, 后续那些接口可以自己学习补充上去; 在过程当中思考下会出现什么问题, 通过解决问题的过程也能对自己有所提升.

运营主要出发点都是以市场运营和游戏客服反馈为主来开发对应游戏功能, 上面只是概括简单的应用场景;
而且基本上只需要实现基础功能, 后续上架按照市场|客服反馈继续补充功能, 所以这里不需要太过着重心力浪费在这里.

而且这里采用 Redis 做外部交互并不是行业标准, 有的喜欢内置 HTTP 服务来进行通用交互也可以.

我个人方面很不喜欢采用游戏进程内再挂个对外服务端口给运营后台提供游戏内交互, 不止影响游戏服务端性能还引入安全性问题.