字节运算艺术
编程语言常规都含有 int 类型, 而 int 类型最简单的日常仅仅作为 1+1 来做数值运算; 但是其中还有更多高阶玩法,
在编程当中涵盖大量实用技巧.
以下采用
Rust编写处理示例
最简单的 int 分为:
int32: 取值范围(-2147483648~2147483647),取值位(4字节), 位长度(32)uint32: 取值范围(0~4294967295),取值位(4字节), 位长度(32)
日常语言推算位:
println!(
"Int32 -> RANGE({}~{}), Byte({}) , Length({})",
i32::MIN,
i32::MAX,
std::mem::size_of::<i32>(),
i32::BITS,
);
// Int32 -> RANGE(-2147483648~2147483647), Byte(4) , Length(32)
println!(
"Uint32 -> RANGE({}~{}), Byte({}) , Length({})",
u32::MIN,
u32::MAX,
std::mem::size_of::<u32>(),
u32::BITS,
);
// Uint32 -> RANGE(0~4294967295), Byte(4) , Length(32)
这里就是 Rust 内部的获取数值进制位相关数值对象, 但是接下来后续知识点最好了解 进制换算 相关知识才能看得懂.
还需要说明 isize|size_t|usize|usize_t 这种类型, 该类型是和平台相关的; 如果在 32 位架构中定义为 unsigned int32,
在 64 位架构中定义为 unsigned long, 这是为了应对将来全面步入 64 位架构所必须设置的类型.
println!(
"ISize -> RANGE({}~{}), Byte({}) , Length({})",
isize::MIN,
isize::MAX,
std::mem::size_of::<isize>(),
isize::BITS,
);
// ISize -> RANGE(-9223372036854775808~9223372036854775807), Byte(8) , Length(64)
println!(
"USize -> RANGE({}~{}), Byte({}) , Length({})",
usize::MIN,
usize::MAX,
std::mem::size_of::<usize>(),
usize::BITS,
);
// USize -> RANGE(0~18446744073709551615), Byte(8) , Length(64)
目前现代计算机已经大规模步入 64 位计算时代了, 所以可以考虑基于全部 64 处理.
进制运算
这里之前得出数值最大长度 u32::BITS = 32, 这就是 int32 数值的位长度.
1 0 0 0 .... 0(这里的位长度指的是以二进制来计算的最大长度值, 也就是 1 首位加上后续 0 合计 32 位)
所以常用语言都含有换位运算功能, 这里按照这种方式来处理运算:
fn main() {
// 以 1 这个值偏移到 31 位, 1 首位补充所以 32 -1 就是极限位置
let offset = 1 << (i32::BITS - 1);
println!("Offset: {}", offset);
// Offset: -2147483648, 这里就是最大的极限值
}
这里进制有什么用? 这里的进制位可以将数值编码到其中, 形成自己的编码数据:
use bytes::{BufMut, BytesMut};
fn main() {
// 获取输入值
println!("Input Message Type:");
let mut flag_value = String::new();
std::io::stdin()
.read_line(&mut flag_value)
.expect("failed by read type");
let flag_number = flag_value.trim();
let flag_number: i32 = flag_number.parse().expect("failed by convert value");
// 获取需要编码的消息
println!("Input Message Information:");
let mut flag_message = String::new();
std::io::stdin()
.read_line(&mut flag_message)
.expect("failed by read type");
let flag_message = flag_message.trim();
let flag_message_bytes = flag_message.as_bytes();
let flag_message_bytes_length = flag_message_bytes.len();
// 编码成二进制数据包
// 首先计算相关的数据容器
let shift = (std::mem::size_of::<isize>() - 1) * 8; // (8 - 1) * 8, 首位留给其他
println!("[ENC]Shift = {}", shift);
// 计算偏移
let offset = (flag_number as isize) << shift;
println!("[ENC]Offset = {}", offset);
// 换位填充数据填充封包
let mask = flag_message_bytes_length as isize | offset;
println!("[ENC]Mask = {}", mask);
// 前置的封包数据已经完成, 接下来就是数据封装, 这里用第三方二进制封装库
// 引入即可: bytes = "1"
let mut buf = BytesMut::with_capacity(flag_message_bytes_length + std::mem::size_of::<isize>());
buf.put_i64(mask as i64);
buf.put_slice(flag_message_bytes);
// 展示二进制封装数据
let bin_message = buf.as_ref();
println!("[ENC]Bytes = {:?}", bin_message);
// 反序列化解包
let offset_mask = isize::MAX >> 8; // 偏移屏障
let dec_type = mask >> shift;
let dec_len = mask & offset_mask;
println!("[DEC] Type = {}", dec_type);
println!("[DEC] Length = {}", dec_len);
}
这种就是利用 size 通过位运算来让类型值/数据长度合并成 size 来传递, 传统数据转发就类似这样数据封包处理.
需要说下进制位变动流程:
// 这里是获取进制位(8), 首位默认需要偏移 int32 的位用来分割也就是分割 i32|i32 这样
let shift = (std::mem::size_of::<isize>() - 1) * 8;
// 这里将需要编码类型值左边移动到 i32|i32 -> i32(这就是shift之后的值) | i32(偏移后备用)
let offset = (flag_number as isize) << shift;
// 类型偏移之后默认后续都是 [0,0,0,0...], 之后就是换位填充, 将二进制数据长度填充进去
let mask = flag_message_bytes_length as isize | offset;
// 至此已经完美把两个 int32 合并成 i64(isize), 后续附加上二进制内容
# Input Message Type:
# 10
# Input Message Information:
# hello.world
# [ENC]Shift = 56
# [ENC]Offset = 720575940379279360
# [ENC]Mask = 720575940379279371
# [ENC]Bytes = [10, 0, 0, 0, 0, 0, 0, 11, 104, 101, 108, 108, 111, 46, 119, 111, 114, 108, 100]
[DEC] Type = 10
[DEC] Length = 11
// 当socket解析数据的时候, 首先确定获取 i64 值, 解析提取之后才能知道客户端 socket发送过来是什么类型协议且传递数据需要初始化多少数据容器.
这种方案算是目前比较广泛使用的方式之一, 如果做网络传输的话这种方式可能需要了解,
用这种方法来将协议类型和协议长度黏贴成单个 size_t 推送.
这种推送字节位方式其他语言都是通用, 只要做好进制换算即可.
类型合并
这里的换位计算还引申数据库到内部, 如果想简单处理物品道具类型也可以采用简单的换位处理方式.
C = 1(2^0,2的0次方)
CPP = 2(2^1,2的1次方)
Rust = 4(2^2,2的2次方)
Java = 8(2^3,2的3次方)
Python = 16(2^4,2的4次方)
.... 以此递增的 2 的 N 次方, 以此标识获取的二进制类似于
n0: 1=00000001
n1: 2=00000010
n2: 4=00000100
n3: 8=00001000
n4: 16=00010000
这里设计个 skill_types(技能表) 来做数据类型记录:
-- 导出 表 skill_types 结构
CREATE TABLE IF NOT EXISTS `skill_types`
(
`id` int(10) unsigned NOT NULL,
`name` varchar(32) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_unicode_ci COMMENT ='数据类型记录';
-- 正在导出表 skill_types 的数据:~5 rows (大约)
INSERT INTO `skill_types` (`id`, `name`)
VALUES (1, 'C'),
(2, 'CPP'),
(4, 'Rust'),
(8, 'Java'),
(16, 'Python');
-- 玩家管理的记录表:
CREATE TABLE `skill_users`
(
`username` VARCHAR(32) NOT NULL COLLATE 'utf8mb4_unicode_ci',
`skills` BIGINT(20) UNSIGNED NOT NULL,
PRIMARY KEY (`username`) USING BTREE
) COMMENT ='玩家技能关联'
COLLATE = 'utf8mb4_unicode_ci'
ENGINE = InnoDB;
这里前置的配置已经完成, 这里 skill_users.skills 就是指定玩家关联的所有技能, 采用单个值记录多状态.
-- 创建个用户, 并且该用户含有 CPP + Java 技能
INSERT INTO `skill_users` VALUE ('MeteorCat', 2 | 8);
-- 这里确定玩家是否含有技能 Java
SELECT *
FROM `skill_users`
WHERE `skills` & 8 = 8;
-- 判断玩家是否含有其他 C 技能(实际上没该技能)
SELECT *
FROM `skill_users`
WHERE `skills` & 1 = 1;
-- Ok, 现在不需要 CPP 技能
UPDATE `skill_users`
SET `skills` = `skills` & (~8)
WHERE `username` = 'MeteorCat';
-- 再看下技能是否不存在(可以看到CPP技能被删除)
SELECT *
FROM `skill_users`
WHERE `skills` & 8 = 8;
-- 删除之后追加个 C 技能到玩家
UPDATE `skill_users`
SET `skills` = `skills` | 1
WHERE `username` = 'MeteorCat';
-- 查看 C 技能是否已经存在
SELECT *
FROM `skill_users`
WHERE `skills` & 1 = 1;
这样对于某些简单不需要太复杂的分类可以直接采用这种方式.
实际上, 并不推荐这种方式做数据库分类处理, 因为是按位不断递增后续扩展类型太过庞大会导致带来极大的数值溢出问题.