MeteorCat / 构建游戏网关(三)

Created Sat, 19 Apr 2025 18:53:00 +0800 Modified Wed, 29 Oct 2025 23:24:45 +0800
1384 Words

之前已经创建好第一个属于我们的 Actor 系统, 但是内部完全没有任何交互, 现在就是开始设计业务来模拟日常业务, 这里需要定义设计个 世界boss 功能.

  • 世界boos 每分钟都会刷新血量
  • websocket 每10s都会推送客户端心跳包
  • 每个新连接 websocket 都支持攻击1次, 随机返回攻击数值并扣除血量
  • 如果结算时间到就会给在线会话推送攻击的扣除最高血量

这里就是简单的初步游戏业务功能, 这里需要设计网络运输网关层组件:

<!-- 第三方依赖 -->
<dependencies>
    <!-- 之前的其他组件 -->

    <!-- SpringWebSocket -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
</dependencies>

之后就是改造 main 入口和配置全局属性:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.socket.config.annotation.EnableWebSocket;

/**
 * 游戏项目启动入口, 现在托管给 SpringBoot 来管理
 * 默认启动WebSocket设置
 */
@EnableWebSocket
@SpringBootApplication
public class FusionActorApplication {

    /**
     * 启动项目方法
     *
     * @param args 启动方法
     */
    public static void main(String[] args) {
        SpringApplication.run(FusionActorApplication.class, args);
    }
}

WebSocket 需要导出配置文件可以被识别到, 也就会是不再采用硬编码形式写入而是外部配置:

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.WebSocketHandler;

/**
 * WebSocket 配置属性
 */
@Data
@Configuration
@ConfigurationProperties(prefix = "fusion.websocket")
public class WebSocketProperties {
    /**
     * 请求路径
     */
    private String path = "/";

    /**
     * 传输数据缓存大小
     */
    private Integer bufferSize = 8192;

    /**
     * 待机主动中断时间
     */
    private Long idleTimeout = 30000L;

    /**
     * 跨域地址
     */
    private String allowOrigins = "*";

    /**
     * 运行句柄
     */
    private Class<? extends WebSocketHandler> handler;
}

之后就能在 application.yml 配置挂起的 WebSockt 参数:

fusion:
  websocket:
    handler: com.meteorcat.fusion.websocket.WebSocketApplication
    path: /ws

handler 就是接下来我们需要编写个 TextWebSocketHandler(文本流传输) :

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.pekko.actor.typed.ActorSystem;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

/**
 * Text类型的WebSocket句柄
 */
@Slf4j
@Component
public class WebSocketApplication extends TextWebSocketHandler {


    /**
     * Actor全局系统
     */
    final ActorSystem<String> system;

    /**
     * 全局JSON解析器
     */
    final ObjectMapper objectMapper;


    /**
     * 构建方法
     *
     * @param objectMapper JSON解析器
     */
    @Autowired
    public WebSocketApplication(ActorSystem<String> system, ObjectMapper objectMapper) {
        this.system = system;
        this.objectMapper = objectMapper;
    }


    /**
     * 连接回调
     */
    @Override
    public void afterConnectionEstablished(
            @NonNull WebSocketSession session
    ) throws Exception {
        log.info("Established: {}", session.getId());
    }


    /**
     * 消息回调
     * 以 JSON:{ id: number, data: {} } 来做消息传递
     */
    @Override
    protected void handleTextMessage(
            @NonNull WebSocketSession session,
            @NonNull TextMessage message
    ) throws Exception {
        final String payload = message.getPayload();
        if (payload.isEmpty()) {
            return;
        }

        // 确认必须带 Object 并且格式为 { id: number, data: {} }
        final JsonNode node = objectMapper.readTree(payload);
        if (!node.isObject() || !node.has("id") || !node.has("data")) {
            return;
        }

        // 提取数据
        final JsonNode id = node.get("id");
        final JsonNode data = node.get("data");
        if (!id.isNumber() || !data.isObject()) {
            return;
        }

        log.info("frame received({}): {}", id.asInt(), data);

        // 先简单实现 data 文本推送 actor
        // 后续再扩展出数据自定义格式
        system.tell(data.toString());
    }


    /**
     * 关闭回调
     */
    @Override
    public void afterConnectionClosed(
            WebSocketSession session,
            @NonNull CloseStatus status
    ) throws Exception {
        log.info("Closed({}): {}", status, session.getId());
    }
}

这里 @Component 仅仅作为全局唯一实例, 还要将参数和这个实例化绑定一起:

import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

/**
 * WebSocket 配置中心
 */
@Data
@Configuration
public class WebSocketConfiguration implements WebSocketConfigurer {


    /**
     * SpringBoot上下文
     */
    private final ApplicationContext context;

    /**
     * 配置属性
     */
    private final WebSocketProperties properties;


    /**
     * 构造方法
     */
    @Autowired
    WebSocketConfiguration(
            ApplicationContext context,
            WebSocketProperties properties
    ) {
        this.context = context;
        this.properties = properties;
    }


    /**
     * WebSocket配置
     *
     * @param registry 设置器
     */
    @Override
    public void registerWebSocketHandlers(@NonNull WebSocketHandlerRegistry registry) {
        registry.addHandler(context.getBean(properties.getHandler()), properties.getPath())
                .setAllowedOrigins(properties.getAllowOrigins());
    }
}

这里就是采用 SpringBoot 当作底层工具好处, 内部可以通过 ApplicationContext 扫描到泛型类并实现自动加载.

现在需要该写 Actor 功能, 同样采用 application.yml 让其能够追加配置:

import lombok.Data;
import org.apache.pekko.actor.typed.ActorSystem;
import org.apache.pekko.actor.typed.javadsl.AbstractBehavior;
import org.apache.pekko.actor.typed.javadsl.ActorContext;
import org.apache.pekko.actor.typed.javadsl.Behaviors;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;


/**
 * 全局 Actor 系统配置中心
 */
@Data
@Configuration
@ConfigurationProperties(prefix = "fusion.actor")
public class ActorProperties {

    /**
     * Actor系统名称
     */
    private String name;


    /**
     * Actor类型
     */
    public Class<? extends AbstractBehavior<?>> behavior;

    /**
     * 实例句柄
     */
    @Bean
    public ActorSystem<?> factory() {
        return ActorSystem.create(
                Behaviors.setup((ctx) -> {
                    AbstractBehavior<?> instance = behavior.getConstructor(ActorContext.class).newInstance(ctx);
                    return instance.unsafeCast();
                }),
                this.name
        );
    }
}

可以通过在 application.yml 添加配置:

fusion:
  actor:
    name: fusion
    behavior: com.meteorcat.fusion.actor.ActorSupervisor

behavior 就是我们再次编写好的 Actor 系统工具类:

import org.apache.pekko.actor.typed.Behavior;
import org.apache.pekko.actor.typed.javadsl.AbstractBehavior;
import org.apache.pekko.actor.typed.javadsl.ActorContext;
import org.apache.pekko.actor.typed.javadsl.Receive;
import org.slf4j.Logger;

/**
 * 简单的 Actor 服务
 */
public class ActorSupervisor extends AbstractBehavior<String> {

    /**
     * 日志句柄
     */
    final Logger log = getContext().getLog();

    /**
     * 构造方法
     *
     * @param context Actor上下文
     */
    public ActorSupervisor(ActorContext<String> context) {
        super(context);
    }

    /**
     * 构建回调匹配
     *
     * @return Receive
     */
    @Override
    public Receive<String> createReceive() {
        return newReceiveBuilder()
                .onMessage(String.class, this::onMessage)
                .build();
    }


    /**
     * 回调内部
     *
     * @param msg 消息
     * @return Behavior
     */
    private Behavior<String> onMessage(String msg) {
        log.info("ECHO : {}!", msg);
        return this;
    }
}

最后就是激动人心启动服务的时刻, 我本地启动打开 Postman 推送一条消息 { "id":1001, "data":{ "flag":100 }} 返回数据:

2025-04-19T23:15:26.105+08:00  INFO 2005432 --- [io-18080-exec-1] c.m.f.websocket.WebSocketApplication     : Established: dea7144b-452d-564f-77de-8dab317f2b1e
2025-04-19T23:15:26.701+08:00  INFO 2005432 --- [io-18080-exec-2] c.m.f.websocket.WebSocketApplication     : frame received(1001): {"flag":100}
2025-04-19T23:15:26.711+08:00  INFO 2005432 --- [lt-dispatcher-3] c.m.fusion.actor.ActorSupervisor         : ECHO : {"flag":100}!

OK, 一个简单的单机Actor服务已经启动完成了, 现在我们可以开始主要功能就在 WebSocket|Actor 运行类, 基本功能已经完成现在就要开始设计最早上面提出的 世界BOSS 战斗服务!