windy path

凌虚

服务器开发工程师
勤学苦练,年复一年

MQTT协议中可变长度的具体计算方式(有计算过程解析)

本文介绍MQTT协议中,固定报头中的可变长度部分的计算方式。通过提供一些例子,将其他介绍MQTT协议的文章中没有仔细说明的计算部分进行解释。 参考文章 MQTT 5.0 报文(Packets)入门指南 这篇文章简要介绍了MQTT,但是没有明确提供计算过程。 MQTT简介之三(3) MQTT协议 报文的剩余长度如何计算和编码 这篇文章也介绍了MQTT,且有提供计算过程,但是是用一大段文字进行表述的,理解起来有些困难。 至于New Bing(GPT4.0)和ChatGPT 3.5,他们在做这种特殊规则下的二进制计算时,效果并不好。 如果在读完本文后你对二进制有兴趣,可以阅读这篇文章:CSAPP第二章-信息的表示与处理 如何计算可变长度? 以下是各大文章的相似描述: MQTT协议的剩余长度字段使用了一种变长编码方案,每个字节的最高位是一个进位标志位,如果为1,表示还有后续的字节;如果为0,表示这是最后一个字节。每个字节的低七位是实际的数据位,用来表示剩余长度的一部分。因此,为了得到剩余长度的完整值,需要循环读取每个字节,并用一个乘法器来计算出总和。 并且,提供了一张表,介绍剩余长度在不同的区间中时,需要使用多少个字节数: 字节数 最小值 最大值 1 0(0x00) 127(0x7F) 2 128(0x80,0x01) 16383(0xFF,0x7F) 3 16384(0x80,0x80,0x01) 2097151(0xFF,0xFF,0x7F) 4 2097152(0x80,0x80,0x80,0x01) 268435455(0xFF,0xFF,0xFF,0x7F) 读取过程分析 首先,我们明确MQTT协议读取数据的过程,是一个字节一个字节依次读取的。即读取完一个字节之后,再处理下一个字节。 那么为了可以动态控制可变长度部分的字节数,MQTT协议约定该部分的字节的第一位来标记“后续是否还有字节”。 明确这一点之后,我们抛开第一位,读取后七位的值,可以得到一个数。 想象一下,如果我们的可变长度res特别长,在2097152~2684354455之间,那么我们需要4个字节进行传输。此时我们将每个字节的第一位剥离,将剩余7位取出: 用变量a表示第一个字节后7位的数,用变量b表示第二个字节后7位的数,c,d以此类推。 那么我们会有:res = a * 1 + b * 128 + c * 128 * 128 + d * 128 * 128 * 128 或者咱们换一种写法:res = a * 1280 + b * 1281 + c * 1282 + d * 1283...

十二月 26, 2023 · JohnathanLin

关于游戏服务器配置表功能的探讨

简介 在游戏开发中,配置表是策划与程序员之间针对功能模块开发而搭建的桥梁。配置表在游戏开发中扮演非常重要的角色。策划需要向配置表填入各种数据,以完成功能数值的配置;程序员需要在项目代码中读取配置表,根据配置表和需求决定的业务逻辑来开发功能模块。 本文想探讨接触过的两个项目的配置表功能设计。在本文中,我不会泄露项目相关的具体信息,仅针对方案进行讨论。 两种配置表方案简介 此处先以表格的形式,列举两种方案之间的异同: 条目 大型SLG手游 小型MMO手游 策划配置方式 使用Excel配置 使用Excel配置 代码读取方式 将Excel文件中的内容转换成Csv文件 将Excel文件中的内容转换成Json文件 读取后存储位置 在项目启动前,先读取Csv的内容到内存中,转成二进制文件写入Zookeeper,在项目启动时加载Zookeeper中的配置表信息到内存 在项目加载时读取Json到内存中 热更新方案 修改Zookeeper中的二进制文件,通过Zookeeper发布订阅机制,分布式系统中各个系统从Zookeeper中读取新的配置表信息 先修改Json文件,然后读取Json文件到内存中 配置表检查 在读取Csv到内存后,编写检查代码,检查内存对象之间的相关逻辑依赖 无 大型SLG手游 这是一个比较大型的SLG手游项目,使用了分布式的系统,服务器数量众多,因此需要一个统一的中心服务器来管理所有的配置信息。这个项目使用的是Zookeeper进行配置表的存储。 策划配置方式 策划使用Excel进行配置。每一列包含中文名、英文名、key类型(标记客户端、服务器或是客户端和服务器都用,且标记是否是key)、字段类型(数据data还是文字text)。 一张表最多只能有两个key,一个主key,一个副key。 比如,如果有一个可以领取宝箱的活动类型,活动可能有多个id,以满足不同的活动包装;每个活动都有多个宝箱。那么这张配置表的主key是活动id,副key是宝箱id。 读取配置表 首先将Excel文件中的内容转换成Csv文件,这一步会将客户端的配置列移除,仅保留服务器用到的配置列。然后使用Java代码读取Csv文件,将Csv文件中的数据构造成Java对象,然后再将Java对象序列化成二进制文件,上传到Zookeeper中的某个节点之下。 graph LR A[Excel文件]-->|Python转换|B[Csv文件]-->|Java代码读取|C[Zookeeper节点下]-->|服务器启动读取Zookeeper|D[服务器内存对象] 服务器启动时会从Zookeeper的节点中加载配置表文件。 在后续的开发过程中,发生过因为策划失误,修改了Excel后忘记生成Csv,而导致Excel文件与Csv文件不一致的情况。在这个项目中,Csv并不是读取配置表过程中最后的输出产物,仅仅是一个中间产物,所以内部也讨论过是否可以直接从Excel文件中读取到内存中的可能。 热更新方案 当服务器启动读取完Zookeeper中的配置表后,订阅Zookeeper中配置表节点的变化事件。 当配置表Excel文件被修改后,依次执行读取流程到上传到Zookeeper节点,此时触发节点变化事件,Zookeeper会将变化事件发布给所有订阅此节点的服务器,服务器接收到配置表节点变化后,再次加载新的配置表文件信息。 配置表检查 当Csv读取到内存后,会对每一场表生成一个Java表管理对象,Java表管理对象与配置表一一对应。同时,这个Java表管理对象中,还可以进行多张配置表之间的逻辑校验。比如奖励表中的道具,必须在道具表中有配置,否则就会报错。 小型MMO手游 这是一个比较小的MMO手游,服务器进程数量比较少,配置表的数量也不多。因此它不需要使用类似Zookeeper那样的配置中心,只需要各个服务器进程读取相同内容的Json文件即可。 策划配置方式 策划使用Excel进行配置表的编辑。表头包含中文名,英文名,类型(数字,或者any任意结构)。 一张表必须要有数字id,且必须列在第一列中。其他的部分不做强制要求。 读取配置表 Excel文件会转换为Json文件。 在代码中,服务器会加载所有的Json文件。 程序员一般不需要针对某一张配置表单独编写解析代码。每一张表都有一个id,其他字段都默认读取为Golang的interface{}类型,每一张表都能读取成统一的格式,即:表名-id-其余各列数据。如果这张表是一张只有一个key的表,那么可以直接通过表名+id查找对应的数据。 但如果是不止一个key的表,那么可以在默认加载之后,再新开辟内存空间,使用自定义的结构进行存储。 热更新方案 策划修改配置表Excel文件后,转成Json文件,然后手动上传到服务器中。 通过RPC的方式,向网关服务器发送“重新读取配置表”消息,网关通知服务器集群中的所有服务器进程,重新读取Json文件到内存中,替换之前的配置表内存数据。 配置表检查 没有配置表提前检查,仅在使用配置的时候抛出err错误。 优缺点 类型 大型SLG手游 小型MMO手游 优点 (1)热更新方便,一次发布到Zk上之后,所有服务器都更新 (2)有配置表检查,在加载配置表时校验配置错误信息 (1)所有表都有默认取值的方法,方便开发 (2)Excel转Json后,Json文件方便人类阅读 缺点 (1)Excel生成的Csv可读性差,维护性差,不如XML,Json带有自己的结构信息 (2)开发繁琐,需要为每一张表定义类,并且预测配置表的关联,有可能会因为过度关联导致自由度下降 (3)在这个项目中,一个Excel对应一张配置表,导致Excel表数量过多,难以分类维护 (1)没有配置表校验,配错值只能等业务运行时抛出Error...

十二月 24, 2023 · JohnathanLin

Java并发编程中上锁的几种方式

前言:本文想要介绍Synchronized,ReentrantLock和ReentrantLock的Condition的相关用法。 Synchronized上锁 Synchronized可以修饰实例方法、静态方法和代码块。修饰代码块时,可以对具体的对象上锁,也可以对某个类(.class)上锁。 Synchronized是非公平锁 以下代码是通过给一个多线程能访问到的变量使用synchronized进行上锁,实现有序打印数字的功能。并且在最后会统计不同线程打印数字的次数: package com.windypath.lockcondition; public class Syn { int count = 0; final Object sth = new Object(); void play() { int loopTimes = 1000; SynThread t1 = new SynThread(loopTimes, "t1"); SynThread t2 = new SynThread(loopTimes, "t2"); SynThread t3 = new SynThread(loopTimes, "t3"); SynThread t4 = new SynThread(loopTimes, "t4"); t1.start(); t2.start(); t3.start(); t4.start(); } public static void main(String[] args) { Syn syn = new Syn(); syn.play(); } class SynThread extends Thread { int loopTimes; public SynThread(int loopTimes, String threadName) { super(threadName); this....

十二月 4, 2023 · JohnathanLin

如何用C++分割一个字符串?

前言 在上机面试的时候,遇到了一道题,它的输入是两行字符串,每行字符串有未知数量的数字(两行数字数量一致),用空格分隔开,输入形如: 12 34 567 888 99 100 358 74 58454 742 4469 88 并不提前提供每行的数字数量。而是让用户自己切分。 当时在上机考试时,我没有使用C++实现这一功能,而是使用Java里的split()进行处理。 后来,考试结束后,我上网查询C++切分字符串的写法,发现C++并没有原生提供类似split(某个字符)的写法。 那么有什么方法能替代呢? 方法1:使用string的find等函数()配合substr()进行切分 根据知乎大佬的回答,他提供的第一种解决方案是: C++ 的 string 为什么不提供 split 函数? - 知乎用户的回答 - 知乎 https://www.zhihu.com/question/36642771/answer/865135551 #include <iostream> #include <cstring> #include <vector> void split(const std::string& s, std::vector<std::string>& tokens, const std::string& delimiters = " ") { std::string::size_type lastPos = s.find_first_not_of(delimiters, 0); std::string::size_type pos = s.find_first_of(delimiters, lastPos); while (std::string::npos != pos || std::string::npos != lastPos) { tokens.push_back(s.substr(lastPos, pos - lastPos)); lastPos = s....

七月 2, 2023 · JohnathanLin

CSAPP第二章-信息的表示与处理

23年3月23日,我在公司进行了一次分享会,内容是本文的内容。在分享前,我重新对文章知识点进行了梳理,补充了很多细节。现将补充的细节重新编写到本文中。 什么是二进制数? 我们日常使用的是十进制,数字包括0,1,2,3,4,5,6,7,8,9 再往下数,就得向前进一位,变成10,然后从个位数开始继续增加11,12,13…19 计算机最底层使用的是二进制,数字包括0和1,再往下数,也是前进一位,变成10。注意,这个10并不是十进制的十,而是十进制的二。 如何用二进制来表示一个整数? 二进制 十进制(无符号) 0000 0 0001 1 0010 2 0011 3 0100 4 0101 5 0110 6 0111 7 1000 8 1001 9 1010 10 1011 11 1100 12 1101 13 1110 14 1111 15 二进制如何表示负数? 原码 我们把最高位(最左边的位)作为符号位,后面剩余的位代表的数作为数值具体的大小。 比如:四位原码二进制表示数字 — 3 1 011 开头的1代表负号,后面的011表示3。这样拼起来就是负3了 但是这么表示可能会有什么问题? 原码表示负数存在的问题 0000和1000,都是表示数字0,但是一个是正0,一个是负0。这显然不符合我们对零的理解。 无法进行加减运算:观察以下式子1(0001) + (-3(1011)) = -4(1100) 0001 +1011 -——- 1100 那么如何用二进制表示一个数字,才能处理加减操作呢? 补码 以时钟为例,拨动时钟理解补码 把红色指针从指向“8”拨动到“6”, 有几种方式? 有两种方式,如图所示: 以此图为例,如果指针目前指向8(红色指针),要把它拨到6(绿色指针),有两种方式: 把8往逆时针方向旋转到6(蓝色)这种方式就是进行8-2=6 把8往顺时针方向旋转到6(黄色)这种方式是进行8+10=18,但是时钟只能显示12个数字,所以18-12=6 补码减法的逻辑是:通过加法,给数字加上一个超过表示上限的数,使其最高位“丢失”的方式来实现减法。...

四月 30, 2023 · JohnathanLin