如何用libxml2检测XML流中单个XML的结束与下一个的开始
如何用libxml2在TCP长连接中正确分割连续的XML命令文档?
问题背景
我们维护一个基于C++、使用libxml2开发的老旧服务器,接收客户端通过长连接发送的多个XML命令(协议无数据长度前缀),每个命令的根节点为<Command>。
之前采用字符串匹配</Command>作为分隔符,遇到注释中包含该字符串时会失效:
<Command User="Bozo">GetBozoData <!-- trip simple parser </Command> --> </Command> <Command User="Joker">GetJokerPlan</Command>
尝试用libxml2的推送解析/IO解析方案,仅能处理第一个XML文档;添加假根节点的方案因部分XML带声明导致格式错误;目前采用逐字符推送解析,效率极低。
正确的libxml2 SAX推送解析方案
核心问题是现有代码未在完成一个XML解析后重置解析器状态,无法处理剩余数据流。正确思路是通过SAX回调跟踪根节点开闭状态,完成单个XML后重置解析器,继续处理后续字节。
实现步骤
- 自定义解析状态:跟踪根节点计数、当前XML是否完成、解析出的命令数据。
- SAX回调绑定:在
startElementNs和endElementNs中更新根节点计数,当计数归零时标记当前XML解析完成。 - 分块推送+解析重置:逐块推送TCP数据,完成单个XML后重置解析器,继续处理剩余数据。
修正后代码示例
// 解析状态结构体 struct ParseState { int rootCmdCount = 0; bool xmlCompleted = false; std::string currentUser; // 存储解析出的User参数 }; // SAX回调:开始元素 void startElementNs(void* ctx, const xmlChar* localname, const xmlChar* prefix, const xmlChar* URI, int nb_namespaces, const xmlChar** namespaces, int nb_attributes, int nb_defaulted, const xmlChar** attributes) { auto state = static_cast<ParseState*>(ctx); if (xmlStrcmp(localname, BAD_CAST "Command") == 0) { state->rootCmdCount++; // 提取User属性 for (int i = 0; i < nb_attributes; i += 5) { if (xmlStrcmp(attributes[i], BAD_CAST "User") == 0) { state->currentUser = reinterpret_cast<const char*>(attributes[i+3]); } } } } // SAX回调:结束元素 void endElementNs(void* ctx, const xmlChar* localname, const xmlChar* prefix, const xmlChar* URI) { auto state = static_cast<ParseState*>(ctx); if (xmlStrcmp(localname, BAD_CAST "Command") == 0) { state->rootCmdCount--; if (state->rootCmdCount == 0) { state->xmlCompleted = true; // 触发命令处理逻辑 processCommand(state->currentUser); state->currentUser.clear(); } } } // 创建SAX处理器 xmlSAXHandler createSaxHandler() { xmlSAXHandler handler = xmlSAXHandlerInit(); handler.startElementNs = startElementNs; handler.endElementNs = endElementNs; return handler; } // 处理TCP接收的分块数据 void processTcpChunks(std::vector<std::string>& chunks) { xmlSAXHandler saxHandler = createSaxHandler(); ParseState state; xmlParserCtxtPtr ctxt = nullptr; size_t chunkIdx = 0; while (chunkIdx < chunks.size()) { if (!ctxt) { // 初始化新的推送解析器 const auto& firstChunk = chunks[chunkIdx]; ctxt = xmlCreatePushParserCtxt(&saxHandler, &state, firstChunk.c_str(), firstChunk.size(), nullptr); chunkIdx++; if (state.xmlCompleted) { xmlFreeParserCtxt(ctxt); ctxt = nullptr; state.xmlCompleted = false; continue; } } // 推送剩余分块 while (chunkIdx < chunks.size() && !state.xmlCompleted) { const auto& chunk = chunks[chunkIdx]; int ret = xmlParseChunk(ctxt, chunk.c_str(), chunk.size(), 0); if (ret != 0) { // 解析错误,重置解析器跳过错误数据 xmlFreeParserCtxt(ctxt); ctxt = nullptr; state.xmlCompleted = false; state.rootCmdCount = 0; chunkIdx++; break; } chunkIdx++; } // 完成当前XML,清理解析器 if (state.xmlCompleted) { xmlFreeParserCtxt(ctxt); ctxt = nullptr; state.xmlCompleted = false; state.rootCmdCount = 0; } } // 保留未完成的解析上下文,等待后续TCP数据 if (ctxt) { saveParserContext(ctxt, state); } }
替代方案:改进型状态机分隔器
如果追求更高性能,可实现轻量状态机,跳过注释、CDATA等特殊节点,仅匹配顶层</Command>:
状态机逻辑
- 正常状态:寻找
<字符 - 注释状态:遇到
<!--后,直到-->才回到正常状态 - CDATA状态:遇到
<![CDATA[后,直到]]>才回到正常状态 - 结束标签状态:读取标签名,仅当顶层
Command节点闭合时标记XML结束
代码示例
bool findTopLevelCmdEnd(const std::string& data, size_t& outEndPos) { enum class State { Normal, TagStart, Comment, CommentDash, CDATA, EndTag }; State state = State::Normal; int rootCmdCount = 0; std::string tagName; for (size_t i = 0; i < data.size(); ) { char c = data[i]; switch (state) { case State::Normal: if (c == '<') state = State::TagStart; i++; break; case State::TagStart: if (c == '!') { if (data.substr(i+1, 3) == "-->") { state = State::Comment; i += 3; } else if (data.substr(i+1, 8) == "[CDATA[") { state = State::CDATA; i += 8; } else i++; } else if (c == '/') { state = State::EndTag; tagName.clear(); i++; } else { tagName.clear(); while (i < data.size() && isalpha(data[i])) tagName += data[i++]; if (tagName == "Command") rootCmdCount++; state = State::Normal; } break; case State::Comment: if (c == '-' && i+1 < data.size() && data[i+1] == '-' && i+2 < data.size() && data[i+2] == '>') { state = State::Normal; i += 3; } else i++; break; case State::CDATA: if (i+2 < data.size() && data[i] == ']' && data[i+1] == ']' && data[i+2] == '>') { state = State::Normal; i += 3; } else i++; break; case State::EndTag: while (i < data.size() && isalpha(data[i])) tagName += data[i++]; if (tagName == "Command") { rootCmdCount--; if (rootCmdCount == 0) { while (i < data.size() && data[i] != '>') i++; outEndPos = i + 1; return true; } } state = State::Normal; break; } } return false; }
总结
- 优先选择libxml2 SAX推送方案:能正确处理所有XML语法,符合libxml2设计规范,稳定性高。
- 状态机分隔器:适合对性能有要求的场景,能处理绝大多数常见XML格式,实现简单。
内容的提问来源于stack exchange,提问作者Daniel Anderson
相关产品推荐
相关产品推荐

