3.0 封包数据类型的存储结构

3.1 基本内存模型

/*假定封包中有两个参数(int, short)*/
FB:{
    header:{
        0: 参数个数(4字节)
        4: 第1个参数(所在起始封包)相对位置(4字节)
        8: 第2个参数相对位置(4字节)
        12: 表示结束尾的相对位置(4字节)
    }
    16: 第1个参数值(这里int为4字节)
    20: 第2个参数值(这里short为2字节)
}
该sizeof(FB)=22字节

这里先给个建议(尽管不是必须的):
编程语言的开发者在实现完整版FB之前,可以先实现基于'字节集数组'作为参数的FBmake版本函数,该函数仅仅只是一个最简化封包实现的过渡,来测试各自字节数据的参数在封包后的完整字节集存储分布情况

字节集:34
{3,0,0,0,20,0,0,0,25,0,0,0,29,0,0,0,34,0,0,0,104,101,108,108,111,123,0,0,0,119,111,114,108,100}

 

3.2 答疑其中设计的用意

  1. Q:如何进行解析参数的提取?
    A:先提取首4个字节得知所有参数个数,从4开始分别偏移地去读4个字节得知各个参数所在的相对位置,用后一个参数的位置减去本次遍历中参数的位置即可得知本次参数的长度、然后就可以从本参数的偏移位置读那么多个字节长度用赋值或内存复制方式输出给deFB中引用传递的变量

  2. Q:设计为相对位置好还是给出size好?
    A:如果是一个封包在使用时都一定进行全部解析那么size的优势确实更好(因为可以少一次做减法才得知长度的步骤);
    但这样一来想访问任何一个参数位置的话都必须计算累加在它之前的所有参数的大小才能去知道,而gFB函数可以支持进行随机索引的快速访问

  3. Q:为什么会多出"结束尾的相对位置"这个4字节占用
    A:原则上若知道FB的整体大小则该值确实可以不需要,但为了实现gFB的获取连贯性,它总是会提取当前指定索引参数的下一个位置去作减法,所以从性能实行的角度来看并不需要任何多余判断,这是一种牺牲空间去换时间的做法(当然仅仅只是增加了4字节认为是可以接受的)
    此外,在支持访问内存地址的语言中还可以允许在只给出一个封包数据首指针的情况下通过先提前count外加它就能够确定整个封包的完整大小了!

 

3.3 有符号/无符号整型存储和解析的差别

在计算机中无论是有符号还是无符号整型,其内存的存储结构都是原则上等价一致的,
比如在C语言中将-1分别存储为char和uchar它在内存底层都是8位容器的全1

#include "stdafx.h"
#include <字节集.hpp>

void main()
{
char a = -1;
uchar b = -1;
printf(jzjj(字节集(&a,1))); //{255}
printf(jzjj(字节集(&b,1))); //{255}
}

此外对char容器存入130这种超过了自身容器半数的有符号数与uchar的内存模型也是一致的

void main()
{
char a = 130;
uchar b = 130;
printf("%d", a); //-126
printf("%d", b); //130

printf(jzjj(字节集(&a,1))); //{130}
printf(jzjj(字节集(&b,1))); //{130}
}

也就是说重点不在于字面量所给的数它会对应于有符号或无符号的同等类型下的影响(因为字面量给的值通常认为不会超过所给容器最大无符号上限的值,否则会溢出则无意义,这也是较多现代脚本语言选用8字节作为基本整型存储的依据——当然也有可能使用变长形式存储,最大不会超过8字节),而在于整型值的解析上!

 

3.4 举例PHP中数值类型的封包实现

原先<1.2>我们说过对于PHP这种无法用精确类型表示整型的脚本语言,我们的方案默认是将其存储为有符号的4字节整数(尽管PHP内部是把它存储为8字节整数了),如下例子展示:

<?php
include 'FB.php';

$a = 123;
$fb = FB($a);
echo jzjj($fb);
Bytes:16{1,0,0,0,12,0,0,0,16,0,0,0,123,0,0,0}

那如果我就是要把整型的存储换为其他字节该怎么办?
我们直接构造该字节集就可以了,在PHP中使用pack()函数可以进行构造——基于某一参数为原型构造相关类型的二进制数据(注:PHP中String与Bytes的实现是等价的);

下面摘抄自FB.php文件中的实现:

// region PHP不支持的数值类型提供临时封包方案
function i8($a)
{
  return pack('c', $a);
}
function i16($a)
{
  return pack('s', $a);
}
function i32($a)
{
  return pack('l', $a);
}
function i64($a)
{
  return pack('q', $a);
}
function f32($a)
{
  return pack('f', $a);
}

应用例子:

<?php
include 'FB.php';

$a = 123;
$fb = FB(i16($a));
echo jzjj($fb);
Bytes:14{1,0,0,0,12,0,0,0,14,0,0,0,123,0}

PS:之前我们讨论分析过有符号跟无符号整型的存储从底层二进制来看都是一样的,所以尽管PHP中有大写版的"C、S、L"可以作为无符号类型,但它是适用于解析端的,跟这里搞存储上其实没有区别。

然后说说整型的解析方面,
首先由于PHP底层用于8字节存储,故所有比它位数之下的类型都能完美表示;
在封包的存储过程中由于没有定制任何关于表示确切类型的存储(仅有大小),则读取后的解析是无法判别该数究竟表示为有符号还是无符号,而如果编程语言本身支持确切类型容器了则按照这个类型去解析即可,那对于PHP该怎么搞个方案来实现呢?

先看看例子:

<?php
include 'FB.php';

$a = 255;
$fb = FB(i8($a));

$a_ = I_sign;
deFB($fb, $a_);
echo $a_;      //-1

在FB.php中我的实现是:

只需提供两个值的标签、预先赋值给要输出的引用变量可确定用户究竟想解析为有符号还是无符号,而容器大小本身由封包数据得知,

PS:如果是有符号默认按l、c、s、q来解析,如果是无符号则把解析类型改为大写也就是L、C、S、Q,
另外无符号的64位(Q)我也测了,PHP内部确实是不支持的——只不过人家手册上故意写只为了对称示意

下面几个例子可以充分证明不支持无符号64位:

【例子1】——这里边换为deFB也是可以只不过换成PHP更加原生的实现好充分证明这点
include 'FB.php';

$a = 18446744073709551615;
$fb = FB(pack('Q',$a));
echo jzjj($fb); //Bytes:20{1,0,0,0,12,0,0,0,20,0,0,0,0,0,0,0,0,0,0,0}
print_r(unpack('Q', gFB($fb, 0))); //0

【例子2】
include
'FB.php';

$a = 9223372036854775807;
$fb = FB(pack('Q',$a));
echo jzjj($fb); //Bytes:20{1,0,0,0,12,0,0,0,20,0,0,0,255,255,255,255,255,255,255,127}

print_r(unpack('Q', gFB($fb, 0))); //9223372036854775807

【例子3】
include 'FB.php';

$a = 9223372036854775807+1;
$fb = FB(pack('Q',$a));
echo jzjj($fb); //Bytes:20{1,0,0,0,12,0,0,0,20,0,0,0,0,0,0,0,0,0,0,128}

print_r(unpack('Q', gFB($fb, 0))); //-9223372036854775808

最后顺带说说PHP中的浮点数:
虽然手册中有提到is_float、is_double,但官方说它们是同义词,

也就是PHP内部默认是以double作为对小数类型存储的,跟我们封包存储整型同理,我这里单独给出了f32函数来进行转换让它内部直接按该字节集进行存储即可。

而解析方面,由于浮点型要么是float要么是double,故只需给出其大小就能够完全区分这两了(大小本身在封包中就已知),且在PHP中也有单独pack类型'f'和'd'进行解析。

对于其他可知其确切类型的语言,实际上并不那么麻烦的处理,直接投入FB、deFB一次性就能存储跟解析到位了!

 

3.5 关于文本型与字节集的封包处理

对绝大部分语言而言文本型和字节集基本都是分家的,比如C语言习惯性用\0作为字符串的终止符(只需提供该内存首地址即可知道字符串的长度),若要表示字节集类型则需要缓冲区中提供长度这么一个字段(一般是首地址的前4个字节),而PHP中String和Bytes的实现则是完全一致的(它均用字节集的实现方案且它本身也支持按二进制的原始内容进行存储和处理),

但其他语言比如python、js这种,文本型和字节集是严格分家的,我们这里不过多讨论它们内部的实现,但凡是数据最终一定能够导出为字节集,关键是我们要约定文本型这种要统一好编码(这样在双方解码为自身字符串类型的时候才能有一个依据)

在FB存储方案中,通通把这两都视为字节集按照<3.1 基本内存模型>进行参数封包,在文本型中建议应当本身统一的编码是UTF-8,然后导出字节集(结尾不强制需或无需空终止)进行存档;
而在FB解析时,只需根据用户投入的变量类型判别本次解包的参数应当是本语言的文本型还是字节集了!

比如下面易语言的封包和解封包实现:

子程序名 返回值类型 公开 备 注
__启动窗口_创建完毕      
变量名 类 型 静态 数组 备 注
fb 字节集      
a 文本型      
b 字节集      
fb = FB (“hello”)
调试输出 (fb)

deFB (fb, a)
deFB (fb, b)
调试输出 (a)
调试输出 (b)
* 字节集:17{1,0,0,0,12,0,0,0,17,0,0,0,104,101,108,108,111}
* “hello”
* 字节集:5{104,101,108,108,111}

而PHP中由于String和Bytes不分家,则只需给出声明是字符串即可

<?php
include 'FB.php';

$fb = FB('hello');
echo jzjj($fb);

$a='';
deFB($fb, $a);
echo $a,"\n";
echo jzjj($a);
Bytes:17{1,0,0,0,12,0,0,0,17,0,0,0,104,101,108,108,111}
hello
Bytes:5{104,101,108,108,111}

而在Python中有b""的则表示是bytes类型,没b的则是普通字符串类型(该类型不能直接处理二进制)

fb = FB('hello');
print(jzjj(fb));
a='';
deFB(fb, a);
print(a);

b=b''
deFB(fb, b);
print(jzjj(b));

 

3.6 关于数组的封包方案

对于可知常量大小的基本数据类型的数组而言,实际上无需按照FB原本的头部是偏移位置方案去搞存储(否则太浪费空间了),我们只需用1字节去约定好当前封包的数组是何种类型的就可以了,下面是封包例子

<?php
include 'FB.php';

$arr = [123, 321, 666];
$fb = FB('h', $arr);
echo jzjj($fb);
Bytes:34{2,0,0,0,16,0,0,0,17,0,0,0,34,0,0,0,104,3,0,0,0,123,0,0,0,65,1,0,0,154,2,0,0,4}

数组这一总体在封包层面还是按照<3.1 基本内存模型>去进行的没有问题,而arr数组单独作为一个序列化后的整体作为一个字节集形式存入FB的一个参数中(其数据类型由数组的第一个成员来确定),而其单个数组内部的序列化存储格式也很简单:

array:{0~3:数组成员数, 4~开始都是该数组的成员按照线性排列的内容, 尾部1字节表示该数组的类型}

若对于不可知常量大小的类型(如String、Bytes)则需按照FB标准封装格式给出每一个数组成员它的偏移量和尾偏移这部分头部(会多占据(成员数+1)*4的额外空间)

下面是FB规范了尾部1字节的类型的常量值

类型

char

-1 (0xFF)

short

-2 (0xFE)

int

-4 (0xFC)

int64

-8 (0xF8)

uchar

1

ushort

2

uint

4

bool

1

float

‘f’ (0x66)

double

‘d’ (0x64)

String

‘s’ (0x73)

Bytes

‘b’ (0x62)

注:String和Bytes更多只是个示意实践中直接根据语言中的声明类型直接判断而无需理会,或者动态语言随意声明一个空数组[],由封包中的最后一个字节类型得知解析并加入正确类型值到数组中!