Organizations

12 results for 技巧
  • 这是在编写特定简单业务服务端的时候看到大部分都采用 protobuf, 但是需要配置环境利用本身 proto.exe 把数据结构打包引入. 实际上我不太喜欢用 protobuf 作为数据交换方案, 多端功能要同步文件再打包成序列化文件引入才能使用. 比较通用的方案就是采用 JSON 就能多平台直接传输, 但是解析性能和传输数据量对于高性能服务端来说都不能接受, 因此需要新的数据交换方案来支持 多端交换/性能高效/配置简洁. 在这种多种方案之中, 最后发现 MessagePack 这套数据交换方案满足我的要求. 传统的 JSON 方案主要的问题就像官网揭示的一样: // 可以看到传统JSON的占用, 首先 '{' 和 '}' 包裹就占用大量数据(节点越多占用越多) // 而 string 元素则是额外占用两个 '"' 包裹起来(节点越多占用越多) { "id": 100, "msg": "hello" } 这也是 JSON 长期被诟病的地方, 随着数据结构越复杂传输性能和效率也越低; 而 messagePack 则是采用新的压缩方式移除掉内部多余的占用数据. 另外 messagePack 解析处理简单粗暴, 不需要采用额外的二进制编译处理, 打开官网只能能看到所有平台例子 特别是对于 WebSocket 来说, 这种轻量级协议直接引入第三方库就能极其方便的使用; 在更新迭代多次之后 msgpack 越来越具有可用性, 尝试一次过后简直惊为天人. JSON 兼容性直接保证 msgpack 也大部分兼容数据格式, 只需要像 JSON 一样处理. 更加粗暴的就是只需要 Msgpack.encode|Msgpack.decode 就能直接处理数据, 和JSON一样过于简单方便
    技巧 Created Fri, 02 May 2025 22:45:31 +0800
  • 字符串转int32 这里主要场景就是编写 proto 的时候客户端和服务端需要对齐协议 id(int); 客户端常规来说会推送 int32 值, 然后服务端拿着该值匹配本地 proto 文件尝试将后续字节数据转化成对应格式. 所以需要简单的 Map<Int,Proto> 做映射处理, 从而方便调用出字符串所属的对象. 也可以直接传递个字符串标识如 Player.Login 给服务端, 但是每次传递协议都带一大段字符串还要做安全解析处理起来更加复杂 这里利用 Java 编写简单的字符串转 int32 方法, 并且测试下碰撞: public class StringToInt32 { public static void main(String[] args) { // 测试两者的碰撞 int total = 100000; // 十万随机字符碰撞 // 随机构建字符串测试碰撞 Random random = new Random(); List<String> exists = new ArrayList<>(); String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; for (int i = 0; i < total; i++) { int len = 3 + random.
    技巧 Created Fri, 31 May 2024 14:01:13 +0800
  • 后台周期汇总统计 源于大量在线/付费统计的情况, 很多后台开发的时候会赶时间直接连表查询出来, 前期如果数据少且条件少的情况可能没什么问题, 但是后续会出现大量问题. 以下下面简单架构玩家和订单表为例: CREATE TABLE IF NOT EXISTS `user_info` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `username` VARCHAR ( 64 ) NOT NULL, `create_time` INT NOT NULL, PRIMARY KEY ( `id` ) ) COMMENT='玩家信息表' COLLATE ='utf8mb4_unicode_ci'; CREATE TABLE IF NOT EXISTS `order_info` ( `order_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '订单ID', `username` VARCHAR ( 64 ) NOT NULL COMMENT '这里姑且采用账号名做唯一标识,实际这里采用uid做bigint索引', `create_time` INT UNSIGNED NOT NULL COMMENT '支付时间', `amount` BIGINT UNSIGNED NOT NULL COMMENT '充值金额', PRIMARY KEY ( `order_id` ) ) COMMENT='订单信息' COLLATE ='utf8mb4_unicode_ci' 这里说下几个常见数据统计概念:
    技巧 Created Thu, 23 Nov 2023 18:08:41 +0800
  • OAuth2.0授权 这里图片展示下 OAuth 授权流程: 流程基本上简单来说就是: 客户端请求唤起地址, 需要跳转到第三方的授权页面 第三方授权页面等待玩家账号授权, 同意的时候根据 redirect_uri 跳转回调服务器地址 回调到我们自己服务器的时候返回授权的 code 和我们自己带的 token 用户标识 根据返回 code 需要再去验证获取到 accss_token 会话凭据, 这里就是这次请求主要会话凭据 拿到 access_token 相当于本次访问临时密码, 可以请求获取玩家信息: user ( 主要获取唯一标识作为账号记录 ) 现在获取 user 相关信息, 提取当中在第三方做唯一标识的字段去数据库判断 username, 不存在就帮助创建账号 这里就是常规 OAuth 验证流程, 但是在这个过程当中的处理方式有所不同: 接口复用, 请求采用 /login 路径, 判断参数有没有带 code 字段识别, 如果有就直接在原接口进行玩家创建处理(会有接口代码堆叠情况, 单个接口堆叠大量逻辑). 状态判断, 请求依旧采用 /login 路径, 但是返回的是请求 redis 生成会话 token, 客户端需要不断通过 token 检查 /check 接口状态( 分离化处理, 有的 PC 端需要另外打开网页来访问 ). 之前正式项目都是采用接口复用, 这种方式相对来比较简单, 如果本身是 Web 网页跳转的话; 但是有种例外情况, 那就是 PC 客户端打开网页授权, 这时候客户端是没办法知道网页变动的, 所以需要不断轮询监听指定 token 来确认状态.
    技巧 Created Wed, 22 Nov 2023 14:35:04 +0800
  • 小端字节序和大端网络序 端序列基本上是网络开发必须要了解的知识点, 类似已知单个字节为如下: [ int32 ] [ 8 8 8 8 ] 每个 8 代表 1 字节, 所以 int32 占用 4 字节 [11111111 |00000000 |00000000 |00000000 ] 每 1|0 代表二进制占位, 所以这里合计总共 32 位 由多个 字(word) 组成为 字节(bytes), 内部的多个 word 排列顺序就是字节序, 在日常很多都会用到连续字节传输储存: 假设 0x1234567 占用 4 字节, 内部空了 4 个字节放置该变量 [0x100] [0x101] [0x102] [0x103] 日常如果这个顺序应该按照上面顺序排列, 这种就是大端序列: [ 01(0x100) ] [ 23(0x101) ] [ 45(0x102) ] [ 67(0x103) ] 但是除了大端序列还有另外的小端序列: [ 67(0x103) ] [ 45(0x102) ] [ 23(0x101) ] [ 01(0x100) ] 可以看到完全反转的序列, 这就是小端字节序.
    技巧 Created Sat, 14 Oct 2023 14:34:24 +0800
  • Nginx静态资源处理 静态资源一般频繁做访问解析处理, 需要缓存处理下( 在 http 块中生成 ): http{ # 开启gzip gzip on; # 启用gzip压缩的最小文件,小于设置值的文件将不会压缩 gzip_min_length 1k; # gzip 压缩级别,1-10,数字越大压缩的越好,也越占用CPU时间。一般设置1和2 gzip_comp_level 2; # 进行压缩的文件类型。javascript有多种形式。其中的值可以在 mime.types 文件中找到。 gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript image/jpeg image/gif image/png; # 是否在http header中添加Vary: Accept-Encoding,建议开启 gzip_vary on; # 禁用IE 6 gzip gzip_disable "MSIE [1-6]\."; # 设置缓存路径并且使用一块最大100M的共享内存,用于硬盘上的文件索引,包括文件名和请求次数,每个文件在1天内若不活跃(无请求)则从硬盘上淘汰,硬盘缓存最大10G,满了则根据LRU算法自动清除缓存。 # 这里设置 static_cache 为缓冲区名称 # 这里是设置代理转发的配置, 如果没有启动反向代理转发内网可以不设置 proxy_cache_path /tmp/cache levels=1:2 keys_zone=static_cache:100m inactive=1d max_size=10g; } 调用其唤起缓存服务( 放置于 server 块当中 ): location ~* ^.
    技巧 Created Tue, 10 Oct 2023 20:17:20 +0800
  • 预下单机制带来的弊端 当初刚工作受到显示订单模块影响, 那时候默认流程是这样的: 客户端发起支付请求, 推送支付道具信息 服务端接受支付请求, 数据库创建状态 ‘已下单’ 的订单, 构建SDK支付请求链接返回客户端 客户端收到支付链接, 直接响应唤起支付端等待玩家支付 支付完成回调服务端, 调取订单记录将预下单记录修改完成并通知到账 包括现在关于支付的相关流程都是按照这样处理, 但是这样处理并非完全没错. 这要设计的系统确实能够很好保证整个支付的链路流程分析( 预下单机制代表用户有付费的想法, 在后续分析需求可以查看用户对其有想法道具但没有付款的 ). 这样看起来虽好, 但是在运转几年之后问题也就慢慢浮现, 那就是单表容量容易爆炸! 三年运行的订单表当时切换到其他云平台数据库, 导出导入都要花费将近一天时间才完成. 支付成功订单还好, 毕竟付费成功代表有收益; 但是 ‘已下单’ 状态的订单是 ‘支付完成’的十几倍, 这里面都是毫无收益的订单记录. 这里面有个收益比, 如果单条订单如果能够带来哪怕是 6 块钱的收入, 那么就允许数据库多他 6000 条无效记录, 毕竟在现在硬盘空间越来越不值钱了. 有必要预下单吗? 这也是我后来开始考虑的, 有必要请求的时候直接生产预下单的记录吗? 将写入数据库的步骤移到回调过程的时候, 客户端发起支付的时候只做本地日志记录而不做数据库记录. 这也是后续处理的方案, 把客户端请求做日志, 而第三方 SDK 都带有扩展字段, 这扩展字段当作唯一标识做记录验证然后回调验证完成写入数据库再推送. 在游戏SDK当中这种方式代表了能够进库的就是成功回调的订单, 压缩大量无效订单让后台运营做功能的时候也相对方便点( 单表10多G的对查询也是挺可怕的 ). 后续项目当中都采用请求支付做日志记录, 回调记录入库的请求确实能够缓解这问题.
    技巧 Created Tue, 10 Oct 2023 13:03:30 +0800
  • SpringWebsocket聊天室 这里用于从头搭建自己需要搭建私域交流聊天室, 用于可以内部定制处理, 数据备份写入 Redis 等待其他进程落地关系数据库. <dependencies> <!-- WebSocket库 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <!-- spring 开发配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <!-- ....... --> </dependencies> 默认这样处理就行了, 主要是先搭建好基本框架. 先处理下 echo 流程. package com.meteorcat.chats; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.lang.NonNull; 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; /** * 这里先采用 TextWebSocketHandler 来做接收处理 * TextWebSocketHandler 为文本流, 而 BinaryWebSocketHandler 为二进制流 * * @author MeteorCat */ public class ChatRuntime extends TextWebSocketHandler { /** * 日志句柄 */ protected final Log logger = LogFactory.
    技巧 Created Wed, 04 Oct 2023 20:27:01 +0800
  • 异常就要奔溃吗? 这里疑问是由编写状态机 Actor 服务引申出来的, 在日常编写业务代码的时候难免出现异常而且这时候真的需要让其整个服务崩溃吗? // 伪代码, 获取数值除去 function div_value( int val ){ return 10 / val; } // 诱发异常, 除0 div_value(0); 这是手动诱发的异常, 值得让整个程序崩溃退出吗? 当然这里默认这种小异常是不需要退出的, 这里仅仅说明异常并不是一定要崩溃退出. 实际上异常也是分成多种等级: 除0 这种本身属于代码级别异常, 一般代码审阅的时候就会被排查出来, 而且哪怕异常影响也不大 而 内存泄露 这种异常实际上仅仅是作为内存泄露, 在业务上面实际上也没什么大影响, 主要是硬件上内存消耗会更大 虚悬指针/空指针/线程崩溃, 这种才是最应该崩溃的, 因为本身内部逻辑业务已经错乱了, 整体业务无法正常运行
    技巧 Created Wed, 04 Oct 2023 00:22:50 +0800
  • Actor, 状态机, 事件 我一般都是将 Actor 和 状态机 当作关联概念理解, 一般来说状态机都是说的是有限状态机( finite-state machine|FSM ), 可以理解为设备/服务等具有多个状态并且允许切换转让. 也就是可以把服务抽象当作风扇, 对于风扇来说有以下状态: 开启 关闭 微风 中风 大风 这种抽象出来就是完全的状态机, 它具有内置多种状态, 而其中内部允许含有几种术语: state(状态): 内部服务的状态, 这就是状态机内部核心, 所有系统行为都是围绕状态处理. transition(转换): 这里指的是将状态A切换成状态B的过程, 也就是定义状态机切换过程相关动作. transition condition(转换条件): 也就是常常听过的事件切换( Event ), 用于触发转换方法从而切换状态 action(行为): 状态运转的时候的切换动作, 如开始执行( initialize ), 退出执行( exit ), 转换行为( transition)等 这里需要先有状态机概念, 后续基本上都是这种概念的延申 状态机驱动如下: // 这里采用伪造代码驱动, 假设创建状态机 let rt = Runtime // 注册内部事件 rt.register('initialize',{ 驱动初始化事件 }) rt.register('exit', { 驱动退出事件 } ) // 自强定义切换事件 rt.transducers( state { // 确定调用时候状态切换 switch state { case '微风': // 切换成风扇微风 case '中风': // 切换成风扇中风 case '大风': // 切换成风扇大风 } }) // 执行状态机让其运行 loop { let err = rt.
    技巧 Created Mon, 02 Oct 2023 22:29:24 +0800
Next