Mysql Resultset 解析记录
结果集消息头
消息头由消息体长度+消息序列号+消息体组成;消息头长度为3字节,消息序列号长度为1字节。
结果集的消息头消息体内容为结果集的列数。
结果集消息头的spicy^1^格式如下:
type header = unit {
osize : uint8[3];
seq : uint8;
on %done {
self.size = self.osize[2];
self.size = self.size << 8;
self.size = self.size + self.osize[1];
self.size = self.size << 8;
self.size = self.size + self.osize[0];
}
var size : uint32;
};
消息体的内容是结果集的列数,是一个整数;但是为了适配整数的范围,该参数采用了INT_ENC的表现形式,其定义格式如下:
type INT_ENC = unit {
osize : uint8;
i2 : uint16 &byte-order=spicy::ByteOrder::Little if ((self.osize & 0xff) == 252);
i3 : uint8[3] if ((self.osize & 0xff) == 253);
i8 : uint64 &byte-order=spicy::ByteOrder::Little if ((self.osize & 0xff) == 254);
inull : uint8[0] if ((self.osize & 0xff) == 251);
on osize {
self.value = self.osize;
}
on i2 {
self.value = self.i2;
}
on i3 {
self.value = self.i3[2];
self.value = self.value << 8;
self.value = self.value + self.i3[1];
self.value = self.value << 8;
self.value = self.value + self.i3[0];
}
on i8 {
self.value = self.i8;
}
on inull {
self.value = 0;
}
var value : uint64;
};
从以上定义可知,当列数小于251时,该类型数据占用的字节即为1字节;但是更大后,会采用大于一个字节的方式进行处理;当中有一个特殊情况,当暂用一个字节,且值为251时,表示的是一个无效值;(后面再定义结果集内容时会用到这个值 )
对于消息头的读取可以进行组合如下:
type COLUMN_SIZE = unit(inout rs: mysql_rs) {
size : INT_ENC { rs.column_size = self.size.value; }
};
public type mysql_rs = unit {
head : header;
hdata : bytes &size=self.head.size { self.s_col_size.write($$); }
on %init {
self.s_col_size.connect(new COLUMN_SIZE(self));
}
var column_size : uint64;
sink s_col_size;
在组合中,用到了unit的参数传递了mysql_rs,同时采用了sink的方式对数据进行了一次传递;如此做主要是为了适配消息体后续可能得扩展;整体的格式不需要变化太大,只需要针对消息体进行更改即可;同时兼容性也会更强。
紧接着的是字段定义。
字段定义
每个字段的定义包括,字段头+字段体,字段头的定义与前面的header定义相同,而后定义的是字段的各个内容,包括catalog、database_name,table_name,orig_table_name,column_name, orig_column_name,字符集索引,字符集长度,列类型,列标识及列精度;其中catalog、database_name,table_name,orig_table_name,column_name, orig_column_name都是数据长度+数据内容的方式进行存储。所以字段的读取定义如下:
type column = unit {
catalog_len : INT_ENC;
:skip bytes &size=self.catalog_len.value;
db_len : INT_ENC;
db_name : bytes &size = self.db_len.value;
tbl_len : INT_ENC;
tbl_name : bytes &size = self.tbl_len.value;
otbl_len : INT_ENC;
otbl_name : bytes &size = self.otbl_len.value;
col_len : INT_ENC;
col_name : bytes &size = self.col_len.value;
ocol_len : INT_ENC;
ocol_name : bytes &size = self.ocol_len.value;
: skip int8;
collation_idx : int16;
coll_len : int32;
col_type : int8;
col_flag : int16;
col_decimals : int8;
: skip bytes &eod;
on %done {
print "{database:%s, tbl_name:%s, otbl_name:%s, col_name:%s, ocol_name:%s, collation_idx:%x, col_type:%x, col_flag:%x, col_decimals:%x}" %
(self.db_name, self.tbl_name, self.otbl_name, self.col_name, self.ocol_name, self.collation_idx, self.col_type, self.col_flag, self.col_decimals);
}
};
其中因为catalog的内容定义恒为def,所以通过skip方式进行了忽略。同时其中col_flag的读取字段可能会是1字节也可能是2字节(会根据认证过程中包含的客户端的参数进行变换、此处为了简化直接定义成了2字节);
包含文件头的定义为:
type column_with_header = unit {
head :header;
data : bytes &size=self.head.size { self.b.write($$); }
on %init {
self.b.connect(new column);
}
sink b;
};
因为在前面的解析总,已经获取了字段数,所以需要将该结构定义成数组的形式
public type mysql_rs = unit {
head : header;
hdata : bytes &size=self.head.size { self.s_col_size.write($$); }
columns : column_with_header[self.column_size ];
on %init {
self.s_col_size.connect(new COLUMN_SIZE(self));
}
var column_size : uint64;
sink s_col_size;
定义完字段后,接下来接收的就是实际的结果数据了
结果数据
resultset的结果数据以每行的形式进行传输。
每行的开头是header结构体,后面的数据内容即为一行数据,由N(N为结果集的列数)个数据单元组成,每个数据单元的组成形式为INT_ENC+数据实体组成。其定义如下:
type element_value = unit(inout r: row) {
size : INT_ENC;
data : bytes &size = self.size.value;
on %done {
print "idx: (%d, %d), size:%d, values:%s" % (r.row_idx, r.col_idx, self.size.value, self.data);
}
};
此处为了方便的识别当前元素所处的位置,将行列索引进行了输出。
此处对element_value实际的值为NULL、空字符串的差异进行简要的说明;如果为空字符串,则INT_ENC内容为0,表示长度为0;而如果实际值为NULL,正常内容长度也为0,但是不能区分是否为NULL,所以mysql使用了251这个特殊的数字,将元素定义为了NULL。所以为INT_ENC的中,如果返现第一个字节的内容为251,则会将最终的size置为0,同时其结果也是NULL,此处未做特殊处理,实际应用时,可以继续这个条件进行修正。
行数据定义如下:
type row = unit(r_idx: uint32, column_size : uint64) {
eles : element_value(self)[column_size] foreach { self.col_idx = self.col_idx + 1; }
on %init {
self.row_idx = r_idx;
self.col_idx = 0;
}
var row_idx : uint32;
var col_idx : uint32;
};
行数据头+行数据的定义如下:
type row_with_head = unit(inout rs: mysql_rs, column_size :uint64) {
head : header;
data : bytes &size=self.head.size {
if ( *self.data.at(0) == 0xfe) {
rs.is_done = True;
}
if (!rs.is_done)
self.b.write($$);
}
on head {
print "head size: %d" % self.head.size;
}
on %init {
self.b.connect(new row(rs.row_idx, column_size));
}
sink b;
};
因为行数据传输的时候,未包含实际的行数信息;所以需要有标识定义何时结束结果集的传输;此处演示我们采用了相对比较简单的方式,即判断数据开始的值为0xfe则认为数据传输截止了(实际上还有数据大小的判断进行组合判断对结果集是否已经完成得判断)。
所以最终结果集的定义如下:
public type mysql_rs = unit {
head : header;
hdata : bytes &size=self.head.size { self.s_col_size.write($$); }
columns : column_with_header[self.column_size ];
rows : row_with_head(self, self.column_size)[] foreach {
if (self.is_done == True) {
stop;
}
self.row_idx = self.row_idx + 1;
}
on %init {
self.is_done = False;
self.row_idx = 0;
self.s_col_size.connect(new COLUMN_SIZE(self));
}
var column_size : uint64;
var is_done : bool;
var row_idx : uint32;
sink s_col_size;
};
完整spicy文件
完整spicy文件内容如下:
module mysql;
import spicy;
type INT_ENC = unit {
osize : uint8;
i2 : uint16 &byte-order=spicy::ByteOrder::Little if ((self.osize & 0xff) == 252);
i3 : uint8[3] if ((self.osize & 0xff) == 253);
i8 : uint64 &byte-order=spicy::ByteOrder::Little if ((self.osize & 0xff) == 254);
inull : uint8[0] if ((self.osize & 0xff) == 251);
on osize {
self.value = self.osize;
}
on i2 {
self.value = self.i2;
}
on i3 {
self.value = self.i3[2];
self.value = self.value << 8;
self.value = self.value + self.i3[1];
self.value = self.value << 8;
self.value = self.value + self.i3[0];
}
on i8 {
self.value = self.i8;
}
on inull {
self.value = 0;
}
var value : uint64;
};
type header = unit {
osize : uint8[3];
seq : uint8;
on %done {
self.size = self.osize[2];
self.size = self.size << 8;
self.size = self.size + self.osize[1];
self.size = self.size << 8;
self.size = self.size + self.osize[0];
}
var size : uint32;
};
type column = unit {
catalog_len : INT_ENC;
:skip bytes &size=self.catalog_len.value;
db_len : INT_ENC;
db_name : bytes &size = self.db_len.value;
tbl_len : INT_ENC;
tbl_name : bytes &size = self.tbl_len.value;
otbl_len : INT_ENC;
otbl_name : bytes &size = self.otbl_len.value;
col_len : INT_ENC;
col_name : bytes &size = self.col_len.value;
ocol_len : INT_ENC;
ocol_name : bytes &size = self.ocol_len.value;
: skip int8;
collation_idx : int16;
coll_len : int32;
col_type : int8;
col_flag : int16;
col_decimals : int8;
: skip bytes &eod;
on %done {
print "{database:%s, tbl_name:%s, otbl_name:%s, col_name:%s, ocol_name:%s, collation_idx:%x, col_type:%x, col_flag:%x, col_decimals:%x}" %
(self.db_name, self.tbl_name, self.otbl_name, self.col_name, self.ocol_name, self.collation_idx, self.col_type, self.col_flag, self.col_decimals);
}
};
type column_with_header = unit {
head :header;
data : bytes &size=self.head.size { self.b.write($$); }
on %init {
self.b.connect(new column);
}
sink b;
};
type element_value = unit(inout r: row) {
size : INT_ENC;
data : bytes &size = self.size.value;
on %done {
print "idx: (%d, %d), size:%d, values:%s" % (r.row_idx, r.col_idx, self.size.value, self.data);
}
};
type row = unit(r_idx: uint32, column_size : uint64) {
eles : element_value(self)[column_size] foreach { self.col_idx = self.col_idx + 1; }
on %init {
self.row_idx = r_idx;
self.col_idx = 0;
}
var row_idx : uint32;
var col_idx : uint32;
};
type row_with_head = unit(inout rs: mysql_rs, column_size :uint64) {
head : header;
data : bytes &size=self.head.size {
if ( *self.data.at(0) == 0xfe) {
rs.is_done = True;
}
if (!rs.is_done)
self.b.write($$);
}
on head {
print "head size: %d" % self.head.size;
}
on %init {
self.b.connect(new row(rs.row_idx, column_size));
}
sink b;
};
type COLUMN_SIZE = unit(inout rs: mysql_rs) {
size : INT_ENC { rs.column_size = self.size.value; }
};
public type mysql_rs = unit {
head : header;
hdata : bytes &size=self.head.size { self.s_col_size.write($$); }
columns : column_with_header[self.column_size ];
rows : row_with_head(self, self.column_size)[] foreach {
if (self.is_done == True) {
stop;
}
self.row_idx = self.row_idx + 1;
}
on %init {
self.is_done = False;
self.row_idx = 0;
self.s_col_size.connect(new COLUMN_SIZE(self));
}
var column_size : uint64;
var is_done : bool;
var row_idx : uint32;
sink s_col_size;
};
假设文件存储名为mysql_rs.spicy,则可通过spicy-driver mysql_rs.spicy进行语法校验及调测。调测运行可以采用
printf "0x070x000x00..." | xxd -r -p | spicy-driver mysql_rs.spicy
进行调测输出。
其中xxd命令,主要是将16进制的字符串转换为二进制数。
- 1:spicy是zeek用于定义协议解析的语言,可参考https://zeek.org ↩︎