libmxml数据结构(源码分析)
libmxml是一个开源、小巧的C语言xml库。这里简单分析一下它是用什么样的数据结构来保存分析过的xml文档。
mxml关键的结构体mxml_node_t是这样的实现的:
struct mxml_node_s /**** An XML node. @private@ ****/ { mxml_type_t type; /* Node type */ struct mxml_node_s *next; /* Next node under same parent */ struct mxml_node_s *prev; /* Previous node under same parent */ struct mxml_node_s *parent; /* Parent node */ struct mxml_node_s *child; /* First child node */ struct mxml_node_s *last_child; /* Last child node */ mxml_value_t value; /* Node value */ int ref_count; /* Use count */ void *user_data; /* User data */ }; typedef struct mxml_node_s mxml_node_t; /**** An XML node. ****/
它使用左孩子右兄弟的树形结构来描述xml报文:即下层节点登记在child链表,兄弟节点登记在next链表。 如果某个节点下面有N个子节点,则child指向第一个子节点,该子节点的next指向下一个同父节点的子节点。 比较特殊的是,mxml把xml节点值也认为是一个子节点。例如<group>value</group>, 其中value(type是MXML_OPAQUE)是一个独立的子节点,挂载在group节点(type是MXML_ELEMENT)下面。 另外,空白符(空格,回车换行,制表符)和注释,虽然对xml报文无实质意义,但mxml还是把它们做为一个节点存储起来。
由于mxml只是使用简单的链表存储xml元素,所以元素节点个数比较多时,mxml查找元素效率是比较低的。所以libmxml提供了一个索引查找的函数,它需要先遍历xml元素树,生成一个排序过的数组,加快查找速度。
为了方便大家理解,我写了一个函数打印xml结构体。
void printNode(mxml_node_t *node, int nNodeSn, int level) { static int currNodeSn = 0; if (node == NULL) { return; } ++currNodeSn; //每遇到一个新节点 则将节点序号递增,做为本节点序号 printf("[%- 3d -> %- 3d] ", currNodeSn, nNodeSn); switch (node->type) { case MXML_ELEMENT: { int i; printf("level %d MXML_ELEMENT [%s]", level, node->value.element.name); for (i = 0; i < node->value.element.num_attrs; ++i) { printf(" %s=%s", node->value.element.attrs[i].name, node->value.element.attrs[i].value); } printf("\n"); } break; case MXML_INTEGER: printf("level %d MXML_INTEGER %d\n", level, node->value.integer); break; case MXML_OPAQUE: printf("level %d MXML_OPAQUE [%s]\n", level, node->value.opaque); break; case MXML_REAL: printf("level %d MXML_REAL %lf\n", level, node->value.real); break; case MXML_TEXT: printf("level %d MXML_TEXT [%s]\n", level, node->value.text.string); break; case MXML_CUSTOM: printf("level %d MXML_CUSTOM\n", level); break; default: printf("unknown node type %d\n", node->type); } //深度优先遍历 if (node->child) { //访问子节点时把本节点序号做为父节点序号 层级加1 printNode(node->child, currNodeSn, level + 1); } if (node->next) { //访问兄弟节点,直接传父节点序号即可 层级也不用加1 printNode(node->next, nNodeSn, level); } }
运行示例如下:
xml源如下:
<?xml version="1.0" encoding="GBK" ?> <group> <option>122334 我们 <string>我们</string>45677 <keyword type="opaque">InputSlot</keyword> <default type="opaque">Auto</default> <text>Media Source</text> <order type="real">10.000000</order> <choice> <keyword type="opaque">Auto</keyword> <text>Auto Tray Selection</text> <code type="opaque" /> </choice> <choice> <keyword type="opaque">Upper</keyword> <text>Tray 1</text> <code type="opaque"><</MediaPosition 0>>setpagedevice</code> </choice> <choice> <keyword type="opaque">Lower</keyword> <text>Tray 2</text> <code type="opaque"><</MediaPosition 1>>setpagedevice</code> </choice> </option> 我 12334545 050504550 <integer>123</integer> <string>Now is the time for all good men to come to the aid of their country.</string> <!-- this is a comment --> <![CDATA[this is CDATA 0123456789ABCDEF]]> </group>
用我这个printNode分析结果如下:
说明:[ 1 -> 0 ],代表本节点序号是1,其父节点序号是0,level 0代表本节点是最顶层节点。 [ 1 -> 0 ] level 0 MXML_ELEMENT [?xml version="1.0" encoding="GBK" ?] [ 2 -> 1 ] level 1 MXML_OPAQUE [ ] [ 3 -> 1 ] level 1 MXML_ELEMENT [group] [ 4 -> 3 ] level 2 MXML_OPAQUE [ ] [ 5 -> 3 ] level 2 MXML_ELEMENT [option] [ 6 -> 5 ] level 3 MXML_OPAQUE [122334 我们 ] [ 7 -> 5 ] level 3 MXML_ELEMENT [string] [ 8 -> 7 ] level 4 MXML_OPAQUE [我们] [ 9 -> 5 ] level 3 MXML_OPAQUE [45677 ] [ 10 -> 5 ] level 3 MXML_ELEMENT [keyword] type=opaque [ 11 -> 10] level 4 MXML_OPAQUE [InputSlot] [ 12 -> 5 ] level 3 MXML_OPAQUE [ ] [ 13 -> 5 ] level 3 MXML_ELEMENT [default] type=opaque [ 14 -> 13] level 4 MXML_OPAQUE [Auto] [ 15 -> 5 ] level 3 MXML_OPAQUE [ ] [ 16 -> 5 ] level 3 MXML_ELEMENT [text] [ 17 -> 16] level 4 MXML_OPAQUE [Media Source] [ 18 -> 5 ] level 3 MXML_OPAQUE [ ] [ 19 -> 5 ] level 3 MXML_ELEMENT [order] type=real [ 20 -> 19] level 4 MXML_OPAQUE [10.000000] [ 21 -> 5 ] level 3 MXML_OPAQUE [ ] [ 22 -> 5 ] level 3 MXML_ELEMENT [choice] [ 23 -> 22] level 4 MXML_OPAQUE [ ] [ 24 -> 22] level 4 MXML_ELEMENT [keyword] type=opaque [ 25 -> 24] level 5 MXML_OPAQUE [Auto] [ 26 -> 22] level 4 MXML_OPAQUE [ ] [ 27 -> 22] level 4 MXML_ELEMENT [text] [ 28 -> 27] level 5 MXML_OPAQUE [Auto Tray Selection] [ 29 -> 22] level 4 MXML_OPAQUE [ ] [ 30 -> 22] level 4 MXML_ELEMENT [code] type=opaque [ 31 -> 22] level 4 MXML_OPAQUE [ ] [ 32 -> 5 ] level 3 MXML_OPAQUE [ ] [ 33 -> 5 ] level 3 MXML_ELEMENT [choice] [ 34 -> 33] level 4 MXML_OPAQUE [ ] [ 35 -> 33] level 4 MXML_ELEMENT [keyword] type=opaque [ 36 -> 35] level 5 MXML_OPAQUE [Upper] [ 37 -> 33] level 4 MXML_OPAQUE [ ] [ 38 -> 33] level 4 MXML_ELEMENT [text] [ 39 -> 38] level 5 MXML_OPAQUE [Tray 1] [ 40 -> 33] level 4 MXML_OPAQUE [ ] [ 41 -> 33] level 4 MXML_ELEMENT [code] type=opaque [ 42 -> 41] level 5 MXML_OPAQUE [<</MediaPosition 0>>setpagedevice] [ 43 -> 33] level 4 MXML_OPAQUE [ ] [ 44 -> 5 ] level 3 MXML_OPAQUE [ ] [ 45 -> 5 ] level 3 MXML_ELEMENT [choice] [ 46 -> 45] level 4 MXML_OPAQUE [ ] [ 47 -> 45] level 4 MXML_ELEMENT [keyword] type=opaque [ 48 -> 47] level 5 MXML_OPAQUE [Lower] [ 49 -> 45] level 4 MXML_OPAQUE [ ] [ 50 -> 45] level 4 MXML_ELEMENT [text] [ 51 -> 50] level 5 MXML_OPAQUE [Tray 2] [ 52 -> 45] level 4 MXML_OPAQUE [ ] [ 53 -> 45] level 4 MXML_ELEMENT [code] type=opaque [ 54 -> 53] level 5 MXML_OPAQUE [<</MediaPosition 1>>setpagedevice] [ 55 -> 45] level 4 MXML_OPAQUE [ ] [ 56 -> 5 ] level 3 MXML_OPAQUE [ ] [ 57 -> 3 ] level 2 MXML_OPAQUE [ 我12334545 050504550 ] [ 58 -> 3 ] level 2 MXML_ELEMENT [integer] [ 59 -> 58] level 3 MXML_OPAQUE [123] [ 60 -> 3 ] level 2 MXML_OPAQUE [ ] [ 61 -> 3 ] level 2 MXML_ELEMENT [string] [ 62 -> 61] level 3 MXML_OPAQUE [Now is the time for all good men to come to the aid of their country.] [ 63 -> 3 ] level 2 MXML_OPAQUE [ ] [ 64 -> 3 ] level 2 MXML_ELEMENT [!-- this is a comment --] [ 65 -> 3 ] level 2 MXML_OPAQUE [ ] [ 66 -> 3 ] level 2 MXML_ELEMENT [![CDATA[this is CDATA 0123456789ABCDEF]]] [ 67 -> 3 ] level 2 MXML_OPAQUE [ ]
xml报文与结构体转换优化 项目中每个交易都有一个必须的步骤:把请求报文的内容转换到流水结构体。目前的做法是对于流水结构里面的字段,逐个到xml报文(调用XmlGetTextByPath),根据路径在xml报文里面找出对应的值。 根据callgrind分析,此类操作在占用了交易的30%以上cpu时间,值得优化。为此我提出另一个做法: 开发新的函数,XmlGetTextByPathMutiple。改变目前每取一个字段就遍历一次xml的操作,一次性将所有需要取出的字段对应的xml路径传给新函数。在遍历过程中检查所需路径是否存在,如果存在则取出。 如果遍历完成后还没有遇到的路径,则视为不存在。 由于目前我们的请求xml报文都相对比较小,遍历xml开销不大。并且现在每个交易需要从请求xml获取的字段至少十几个以上,新函数理论上可以比原函数更节约时间与资源。 为加快遍历过程中检查xml路径是否存在的过程,需要在函数开始前先对所有字段的xml路径做哈希计算。然后把哈希结果放到C99变长数组,再进行排序。 数组元素结构说明如下: typedef struct tagMemInfo { const char *xmlPath; //字段对应的xml路径 void *destBuf; //结果存放区 size_t destBufLen; //存放区长度 int destType; //结果类型,目前只支持char数组和double int isNullAble; //是否可为空 }MemInfo; typedef struct tagXmlGetInfo { int pathHashCode; //xml路径对应的哈希值 const char *xmlPath; //字段对应的xml路径 int isNullFlag; //是否为空,初始化都是1 MemInfo *memInfo; }XmlGetInfo; 遍历过程中,对于每个xml节点,我们先计算其路径的哈希值。根据哈希值二分查找数组,看是否有与该值相同的目标字段。 如果找到哈希值相同,并且路径也相同的,则把xml节点值取出来放到指定的缓冲区。 计算哈希的函数推荐使用glib的g_str_hash,其使用的是DJB算法。这样我们在遍历子路径可以在父节点的哈希值基础上做增量计算,减少哈希值计算的开销。 //伪代码如下: //parentHashCode:外部调用统一传5381,递归调用传本节点哈希值 void XmlGetTextByPathMutipleInternal(const XmlGetInfo *xmlGetArr, size_t arrLen, mxml_t *currNode, int parentHashCode); { int nHashCode = parentHashCode; 根据parentHashCode及本节点名称计算nHashCode 使用nHashCode在xmlGetArr里面二分查找 如果找到符合要求的路径,则将本节点值取出来(即第一个孩子节点) for (第一个孩子节点; 兄弟节点 != NULL; 取出兄弟节点) { 如果发现该节点是xml路径节点 //可能是文本节点 则调用函数XmlGetTextByPathMutipleInternal } } int XmlGetTextByPathMutipleRel(const XmlGetInfo *xmlGetArr, size_t arrLen, mxml_t *rootNode) { int nHashCode = 5381; for (第一个孩子节点; 兄弟节点 != NULL; 取出兄弟节点) { 如果发现该节点是xml路径节点 //可能是文本节点 则调用函数XmlGetTextByPathMutipleInternal } 检查xmlGetArr所有不允许为空的成员是否找到对应的路径 } 对于组装报文,我们也可以做类似优化:先将需要修改的xml报文路径及其对应的值缓存起来, 等到把xml报文值设置好后再统一遍历一次xml报文,根据缓存信息,进行实际的xml报文修改。 修改逻辑如下:对缓存以xml路径进行排序,排序后顺序处理。由于路径相似的xml路径排序后肯定在一起, 可以减少修改过程中对于xml报文的查找。(可以根据待修改值的数量决定是否对当前xml节点构建索引,为保持与之前代码兼容性,建索引时可以考虑使用归并排序,或者使用冒泡法即可) 相关函数设计: XmlSetTextByPathCopy //xml报文值是存在临时变量,需要把值复制出来,暂存。 XmlSetTextByPathNoCopy //xml报文值是存在非临时变量 XmlSetTextByPathDual //真正对xml报文进行修改
~~积土成山,风雨兴焉;积水成渊,蛟龙生焉;~~~