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報文進行修改