Organizations

  • 最近准备重构和整理发行平台项目, 结合项目中的问题边思考和边总结开发当中的流程和问题, 便于后续查漏补缺. 首先对于发行平台核心总共分为以下部分: 后台管理 SDK对接 静态渲染 后台管理 对于发行后台一般提供和设置以下参数: appid: 生成的游戏应用ID appKey: 游戏接入的登录时候需要用到 appSecret: CP回调的时候验证回调参数完整性的参与签名 clientId: 客户端ID(H5应用不需要) clientKey: 客户端Key(H5应用不需要) notifyUrl: 支付完成需要通知CP的回调地址 SDK对接 SDK对接目前仅有 H5 相关内容, 服务端需要处理以下接口来对接: 登录认证(需要CP文档对接,必要) 支付唤起(需要CP文档对接,按照游戏性质区分, 有的免费游戏不需要) 订单状态(需要CP文档对接,用于CP方确认订单状态) SDK初始化(事件上报,必要) 选择服务器(事件上报,非必要) 创建角色(事件上报,必要) 进入游戏(事件上报,必要) 等级提升(事件上报,必要) 退出游戏(事件上报,按照游戏性质区分,H5没有退出相关功能可以忽略) 选择服务器的时候没有角色信息, 所以上报的涉及的玩家和角色相关信息可以留空或者默认值 实际上可以区分为两部分: 研发联调 和 事件上报 事件上报是很关键的接口, 用于追踪玩家行为的转化率和在线留存等信息 研发联调 研发(CP)联调基本上只涉及授权和支付而已, 结构上报为统一的 JSON|FORM 格式, 内部字段如下: // 为了简洁, 对于默认不存在账号会自动在系统帮助创建注册, 省下了手动切换注册页面输入的流程 // 以下就是涉及到需要上报的注册|登录信息 { // 其他必要的登录参数 // 有的第三方可能参数有所不同 // --------------------------- // 后台创建的游戏应用ID appid: 1001, // 对应的推广渠道ID, 该渠道ID就是后台推广员绑定的账号ID // 用于将玩家绑定专属的渠道, 也就是相同游戏会被分包让其玩家归属到指定后台运营管理员 channel: "100002", // 登录平台, 有时候采用跨平台(Android|iOS|H5)登录, 两者平台都是同个账号 platform: 0, // 设备唯一标识码, H5无法获取到会默认留空 imei: "", // 设备信息上报, 比如 AndroidOS | AppleOS 等 device: "", // 设备UA信息上报, 比如 Mozilla/5.
    架构 Created Mon, 14 Jul 2025 23:26:56 +0800
  • 前面比较长时间篇幅去说明 Description, 主要开始对开源项目的复杂性有基础认识, 可以看到开源项目当中做了大量妥协和扩展, 导致有时候很简单的功能都要为了扩展而写的很复杂. 这还仅仅是作为很小的组件就要这么多依赖类, 其他的网络|文件|Actor库依赖更加庞大, 所以需要总结适合自己理解方法 这里回过头来说明配置 Configuration 类的相关工具类: org.apache.flink.configuration.WritableConfig org.apache.flink.configuration.ReadableConfig org.apache.flink.configuration.ConfigOption org.apache.flink.configuration.ConfigOptions: 主要对之前的 ConfigOption 进行构建的 Builder 上个篇章已经说明 ConfigOption 是作为 Configuration 单独配置存放内部集合中, 而 ConfigOptions 就是相当于创建器: // 这里引用官方文档的构建器例子 // simple string-valued option with a default value ConfigOption<String> tempDirs = ConfigOptions .key("tmp.dir") .stringType() .defaultValue("/tmp"); // simple integer-valued option with a default value ConfigOption<Integer> parallelism = ConfigOptions .key("application.parallelism") .intType() .defaultValue(100); // option of list of integers with a default value ConfigOption<Integer> parallelism = ConfigOptions .
    Java Flink Created Thu, 03 Jul 2025 19:58:07 +0800
  • 接下来我们想看下 POM 的 modules 节点上面的子模块, 首先 flink-annotations 可以先排除, 里面基本都是一些实验性注解和版本信息, 不过如果参与开源开发的话这个功能就比较有用. 可以学习下 FlinkVersion 版本文件设计, 这个文件在其他面向对象编程语言都能改动下就能使用(建议学习) 那么可以参照官方文档的例子来一步步解构内部源码: // pojo class WordWithCount public class WordWithCount { public String word; public int count; public WordWithCount() {} public WordWithCount(String word, int count) { this.word = word; this.count = count; } } // main method StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); DataStreamSource<String> text = env.socketTextStream(host, port); DataStream<WordWithCount> windowCounts = text .flatMap( (FlatMapFunction<String, String>) (line, collector) -> Arrays.stream(line.split("\\s")).forEach(collector::collect) ).returns(String.class) .
    Java Flink Created Wed, 02 Jul 2025 02:37:19 +0800
  • 我个人对 akka/pekko 这类 Java-Actor 库十分喜爱, 所以在日常当中也会去为这类 Java 方案做推广; 虽然直接调用内部库函数方法就能满足日常使用, 但是作为后续扩展当中直接声明裸写是不符合工程化规范, 后续多人维护麻烦也挺多的. 如果2-3人的时候可能还不太明显, 随便架个 pom.xml 单项目管理也没什么问题, 但业务扩展出来的时候就很麻烦; 这里说下工作当中可能遇到的问题, 以下就是工作当中实际遇到: 1. 项目刚开始立项国内平台, 直接简单引入 pekko+msgpack 搭建服务端协议 2. 客户端调通之后开始编写业务逻辑, 然后开始跑运营流程追加功能业务 3. 底层依赖没有做封装和业务抽离, 所以直接在内部改动些底层功能, 实际上这时候还没问题 4. 海外需要构建个版本低于国内版本, 这是否问题就开始发作, 出现版本底层割裂问题 5. 国内版本在v1.2的时候底层函数改动参数变动, 但海外版本v1.0依赖这个函数而不依赖逻辑改动 (比如抽奖函数两边最开始一致, 但底层函数更新要求传递随机种子 seed 值, 国内版本已经更新而国外版本不需要更新避免干扰到其他用到函数) ------------------------ 从这里开始就是版本管理崩溃的前兆 ------------------- 6. 虽然这次靠着直接复制底层功能类, 手动修改函数调用方法处理完成, 但是结果终归是好的, 可以满足业务正常运行 7. 原先网络库采用 websocket 做传输, 现在为了底层性能需求需要改动成 tcp|udp 8. 第三方库引入又对两个版本产生巨大差异, 海外版本可能落后两个大版本, 但是内部业务还是要维持更新 9. 国内v2.0大版本更新了, 但是海外依旧保持v1.2版本, 这时候运营要求把v2.0国内某些功能业务移动海外v1.2 10. 最可怕的事情就发生了, 底层有着大量没有同步更新导致两边版本底层可能完全对不上, 只能人肉比对版本然后修改变动 11. 随着这种差异版本对比越来越多, 后续版本合并越来越困难, 两个版本不断人肉比对修改第三方引用和调用方式差别越来越大 12.
    Java Flink Created Tue, 01 Jul 2025 13:11:30 +0800
  • 如果长期使用 spring 全系列的开发者, 对于 依赖注入(Dependency Injection) 能够很明显体现到: /** * RestApi接口类 */ @RestController public class ExampleController { private final IService service; /** * 在构造的时候注入 IService 接口实现 */ @Autowired public ExampleController(IService service) { this.service = service; } } 这种很便利把所需的外部编写服务类注入到类内部, 省下了直接实例化new的过程尽可能把功能做好可控性. 实际工作中发现在项目之中可以随意实例化确实后患无穷, 要么直接用上下文(context)做构建器传递, 要么就直接做依赖注入管理. 但是这种强依赖 spring 在做自己项目集成的时候基本不会考虑而是考虑比较轻量化的方案容易保持项目的简洁, 社区提供的方案如下: Spring: 和 spring 集成绑定最高的依赖注入库 Guice: 谷歌推出的轻量级基于模块方式的依赖注入库 Dagger/Dagger2: Android 官方推荐的基于 Guice 依赖注入库 其他可以跳过…… 这里基本上已经说明了各自所用的场景, 所以最后按轻量化方案采用了 Guice. 如果想尽快上手可以直接引入 spring-core 底层依赖直接使用就行, 毕竟项目规划好实现时间需要尽快搭建好准备对接需求 实际上我基本上对于依赖注入就是把功能句柄注册到全局之中, 起到作为 任务管理器(Task Manager) 作用. 这里基本上引入第三方库, 之后就开始说明一些:
    Java Created Fri, 27 Jun 2025 14:12:18 +0800
  • 一般 Actor 不会是单个项目独享的, 可能会分开大量架构(比如游戏关卡,游戏战斗等)来集群处理, 这种可以作为集群在其他项目之中启动处理. pekko 内部封装其实已经很好了, 工作之中发现基本没什么需要封装, 除非有必要否则可以直接引入构建高级应用 实际上也是考虑到常常会用到搭建 Actor 管理器关系, 所以才打算学习 Erlang 处理方式让其支持 Supervisor-Worker 架构. 默认都是采用 pekko 的强类型 actor 构建, 弱类型后面传输的内容不好做控制 默认做个接口类方便作为数据容器继承: /** * 基础的数据载体, 用于被其他类型做实现衍生 */ public interface ActorCommand { } 动态实现有种泛型静态反射构建的抽象类, 可以用来做些特殊的动态抽象: 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.Behaviors; import java.lang.reflect.Constructor; /** * 自定义 Actor 运行层扩展 */ public abstract class AbstractActorSupervisor extends AbstractBehavior<ActorCommand> { /** * Actor 名称 */ protected String name; /** * 构造方法 * 默认的 */ public AbstractActorSupervisor(ActorContext<ActorCommand> context, String name) { super(context); this.
    Java Created Mon, 16 Jun 2025 16:51:16 +0800
  • 最开始 pekko 本身是起源于 akka, 因为 akka 转为商业许可而演变出来的开源替代版本, 两者在早期的 abi 基本上是类似, 而后续 akka 追加不少商业的功能支持. akka搭建服务需要买授权码, 为了避免商业纠纷问题推荐采用 pekko 学习 两者在使用中没什么差异, 更多差异来源于本身的 类型系统 差别: actor 和 actor-typed 最明显的就是 pekko 当中的声明 actor 不同的抽象定义: // 弱类型定义类 class Worker extends AbstractActor{ // do something } // 强类型泛型类 class Worker extends AbstractBehavior<T>{ // do something } AbstractActor: 基于弱类型的Actor抽象. 允许接收任何类型的消息, 在处理消息时需要通过模式匹配或其他方式来判断消息的具体类型并进行相应的处理. 这种方式在编译时无法对消息类型进行检查, 可能导致运行时出现类型不匹配的错误. AbstractBehavior<T>:基于强类型的Actor抽象. 它通过 类型参数<T> 明确指定了 Actor 所能接收的消息类型并在编译时会进行严格的类型检查, 确保只有正确类型的消息才能发送给 Actor 从而提高了代码的稳定性和可维护性. AbstractActor 方式内部本身采用 Object 装载传输消息, 在编译的时候没办法做类型检查(毕竟最后数据都用 Object 包装), 现在在网上大部分教程都是以这种方式构建 actor.
    Java Created Mon, 09 Jun 2025 14:18:02 +0800
  • 之前已经展示封装成功能 Actor, 可以看到我们封装类和 skynet 类似, 在官方 vert-x 文档可以看到比较简单的 WebSocket 集成设置: import io.vertx.core.Vertx; import io.vertx.core.http.HttpServer; import io.vertx.core.http.HttpServerOptions; import io.vertx.core.http.WebSocket; public class WebSocketServerExample { public static void main(String[] args) { // 创建Vertx实例 Vertx vertx = Vertx.vertx(); // 创建Http服务器选项 HttpServerOptions options = new HttpServerOptions() .setPort(8080); // 创建Http服务器 HttpServer server = vertx.createHttpServer(options); // 监听HTTP请求并升级为WebSocket连接 server.webSocketHandler(webSocket -> { System.out.println("WebSocket连接已建立"); // 处理接收到的WebSocket消息 webSocket.handler(buffer -> { String message = buffer.toString(); System.out.println("收到消息: " + message); // 发送响应消息 webSocket.writeTextMessage("你发送的消息是: " + message); }); // 处理WebSocket连接关闭事件 webSocket.
    VertX Java Created Sun, 01 Jun 2025 18:29:00 +0800
  • WebSocket设计指南 这里主要对常用 TCP 和 WebSocket 做对比, 确认两者的网络传输之间的细节从而在开发当中规避一些可能存在的问题. WebSocket 是基于 HTTP 协议, 而 HTTP 协议也是基于底层 TCP 架构开发, WebSocket 为了弥补 HTTP 的 无状态 特性采取的 双向通讯 协议. 为什么采用 WebSocket 而不是更加底层且效率更好的 TCP? 主要来源于 Web和HTML5 应用的飞速发展, 传统 C/S端 逐渐被 B/S端 赶上, 而 Web 界面上面开始对长链接有所需求, 从而需要在网页应用能够做到简单的 Socket 网络请求. 特别是对于网页游戏来说, 需要专门的网页长链接协议方便做数据交换, 这里还需要说明下几种协议差别: UDP: 基于 报文(Packed) 传输消息, 每个报文都是独立的数据内含 源地址|目的地址|端口号以及数据 等信息, 不保证消息有序性 TCP: 基于 流(Stream) 传输消息, 传输数据无边界的字节流需要对流进行消息分包, 已经内部确保消息有序性 WebSocket: 基于 消息(Message) 传输消息, 每个消息都有明确的边界无须在此消息分包 这三种协议性能上从高到底比较来说是: UDP > TCP > WebSocket.
    架构 Created Fri, 23 May 2025 13:46:32 +0800
  • 在商业游戏当中比较有以下基础角色: 游戏研发(Content Provider, 简称CP): 最基础的游戏开发底层 游戏发行(Publisher, 商业发行): 游戏推广和商业计划执行者 在 Steam|WeGame|Epic 之类平台的平台也能看到不少相关信息, 当然也有研发和发行一体的商业公司. 游戏发行 涉及以下工作( 以下内容不分单机和手游 ): 海外|国内运营的本地文化和本地化处理, 需要按照不同地方规避文化民俗和联系语言本地化翻译(可能要联系其他海外发行合作) 海外|国内授权和支付体系接入, 需要暴露外部Web接口联系 游戏研发 来接入处理 海外|国内广告平台接入, 需要 游戏发行 的 商务 确认平台广告量级从而才能进行广告推送(Facebook|抖音贴片广告位) 海外|国内运营客服的处理, 游戏研发 不会直接和玩家对接, 而是玩家和客服反馈, 通过运营整合活动BUG联系策划出活动或者修复 海外|国内需要按照定下的推广指标, 通过 财务 再按照商定的分账比例由 游戏发行 转账给具体的 游戏研发 并且 游戏发行 还有部分 第三方 来处理, 也就是 游戏研发 没有余力搭建维护整套发行架构的时候就会去寻求 第三方发行. 比较知名的就是直接接入腾讯体系发行, 只需要接入对应内部体系并且联系他们推广就行, 不过分成比例也是很高 这里主要说下 第三方游戏发行 的开发和设计, 最基础的就是必须实现接口: 登陆请求 请求支付 支付回调 还有广告追踪转化SDK等, 这些附属的SDK比较高级和复杂所以不展开说明 注意: 如果作为 第三方游戏发行方, 你对接的是 游戏研发 的技术人员而非玩家用户. # 常规登陆账号体系 [客户端] --> username|password --> [服务器] <---> 数据查询存在并生成授权token # 第三方发行账号体系 [客户端] --> username|password --> [CP服务器] | |--> [appid, username, password] + 发行申请的 secretKey 哈希出 token | |--> [第三方发行服务器] | | --> 数据查询存在并生成授权token | | 如果存在账号返回信息, 不存在就创建账号入库 | [CP服务器] <------------------------------ | | | (可选步骤,有的CP方需要记录入库来保存, 也有的CP方不想维护独立服务器) 验证发行返回的账号是否在 CP 自己维护的数据库用户表存在, 不存在就创建 | | [客户端] <------------- 将发行的验证的授权凭证数据返回客户端 这种架构可以有效把职责独立出来, 发行方面不需要和研发做太多交互, 直接单独维护自己 API 服务就行; 对于 游戏发行, 你只需要提供给 游戏研发 以下数据:
    架构 Created Thu, 15 May 2025 22:33:51 +0800