软解析器编程指南:表达式、变量与网络数据包处理实战
1. 项目概述深入理解软解析器的“编程”能力在网络数据包处理的底层世界里硬件解析器Hard Parser就像一台精密的、但指令集固定的专用机器它能以极高的速度识别和处理标准协议比如以太网、IP、TCP/UDP。然而当网络世界出现新的封装协议、私有协议或者需要对特定字段进行复杂校验和计算时这台“硬”机器就显得力不从心了。这时软解析器Soft Parser就扮演了“可编程协处理器”的角色。它不是要取代硬件而是作为其强大而灵活的延伸。我常把软解析器的工作理解为给硬件解析器写“插件”或“脚本”。当硬件解析到一个它无法直接处理的协议时或者需要在解析过程中执行一些自定义逻辑时它就会将控制权“移交”给软解析器。软解析器执行我们预先定义好的指令集这些指令的核心就是表达式和变量操作。你可以把它们想象成这个“脚本语言”的语法和内存空间。表达式决定了“如何计算”变量则决定了“计算什么”和“结果存到哪里”。理解这两者是编写高效、可靠自定义解析逻辑的基石。无论是为了支持一种新的隧道协议如Geneve、VXLAN还是实现基于特定字段的复杂流量分类都离不开对表达式和变量的娴熟运用。2. 软解析器表达式构建解析逻辑的“语法”表达式是软解析器逻辑的原子单位它由操作数和运算符组合而成最终会计算出一个值。这个值可以是用于条件判断的“真/假”逻辑表达式也可以是用于赋值或计算的数值算术表达式。理解表达式的构成和运算规则是避免编写出错误或低效解析逻辑的第一步。2.1 操作数数据的来源操作数是表达式的基本构成单元代表了参与运算的数据本身。软解析器支持四种主要类型的操作数数字、变量、字段和子表达式。2.1.1 数字常量值的表示数字是最直接的操作数。它支持十进制无前缀、二进制前缀0b和十六进制前缀0x三种格式。所有数字在内部都被视为64位无符号整数。这里有一个非常重要的细节软解析器原生不支持负数的直接表示。例如你不能直接写-2。但这并不意味着不能进行负数运算。你可以通过算术表达式来“创造”一个负值例如1 - 3的结果是0xFFFFFFFE在32位视角下这就是-2的补码表示。这提醒我们在进行可能产生负数的计算时如某些偏移量计算需要特别注意处理溢出和补码问题。2.1.2 字段协议头部的结构化访问字段是在自定义协议或标准协议定义文件中通过field元素明确定义的数据单元。访问字段有两种方式直接访问如果上下文明确可以直接使用字段名。例如在某个协议的after块中field_name。全限定访问通过协议名.字段名的形式访问这在需要明确指定来源时非常有用例如ethernet.type。访问的上下文至关重要在before元素中你只能访问上一个协议由prevproto属性指定头部中定义的字段。此时帧窗口$FW也指向这个头部。在after元素中你只能访问当前自定义协议头部中定义的字段。此时帧窗口$FW也指向这个头部。一个关键限制是单个字段的长度不能超过8字节64位。如果你定义的协议头中有更长的字段比如一个128位的标识符你需要将其拆分为多个不超过8字节的子字段或者通过帧窗口变量$FW进行位级别的直接访问。2.1.3 变量解析状态与结果的载体所有变量名都以$开头并且区分大小写。它们是软解析器运行时状态的核心主要分为以下几类结果数组变量这是最重要的一类变量它映射到一块称为“结果数组”的共享内存区。硬件和软解析器都通过这个数组来交换信息。例如$l2r存储了L2链路层解析结果$ipv4sa存储了IPv4源地址。你可以通过$变量名[起始字节偏移: 字节数]的语法来访问变量的部分内容。例如$actiondescriptor[2:4]访问的是该变量从第2字节偏移量开始的4个字节。注意$GPR2变量被FMC工具内部用于复杂表达式如校验和的计算。虽然理论上你可以使用它但这不被支持且强烈不推荐因为可能干扰工具的内部逻辑。$GPR1则常被建议用作存储临时计算结果的通用寄存器。参数数组变量通过$PA[偏移:长度]访问用于从外部传入的参数数组中读取数据。由于参数数组通常较长你必须指定要读取的具体字节范围。帧窗口变量这是直接访问原始数据包内容的“窗口”语法为$FW[起始位偏移: 位数]。注意它的单位是位而非字节。这在处理非字节对齐的位字段时极其有用。例如$FW[9:2]读取从第9位开始的2个比特。头部大小变量$headerSize返回协议头部的实际大小。在after块中它由headersize属性定义若未定义则等于$defaultHeaderSize。在before块中它返回上一个协议头部的大小。$defaultHeaderSize仅在after块中可用返回当前自定义协议所有format字段定义的总字节数。$prevprotoOffset变量这是一个“快捷方式”变量它返回上一个协议头部在帧数据中的起始偏移量字节。其值实际上来源于结果数组中的某个偏移量变量如$ethoffset,$ipoffset_n等。在before块中帧窗口位置等于$prevprotoOffset在after块中帧窗口位置等于$prevprotoOffset $headerSize。2.2 运算符数据的加工方式运算符决定了如何对操作数进行计算。软解析器支持丰富的运算符从基础的算术比较到专用的网络操作。2.2.1 基础算术与逻辑运算符这部分与大多数编程语言类似比较运算符gt(),ge(),lt(),le(),,!。用于构建逻辑表达式。逻辑运算符and,or,not。用于组合多个逻辑条件。算术运算符,-。特别注意和-是32位运算。如果结果超出32位高位会被截断只保留低32位和进位标志。例如0xFFFFFFFF 2的结果是0x1进位被忽略低32位为1。位运算符bitwand(),bitwor(|),bitwxor(^),bitwnot(~)。移位运算符shl(),shr()。移位值最多64位。2.2.2 专用运算符concat与checksum这两个是软解析器中非常强大且具有网络处理特色的运算符。concat运算符用于拼接数据。其行为是将第一个操作数左移然后将第二个操作数放置在右侧空出的位上。第二个操作数可以是变量或整数。如果是变量左移的位数由该变量的已知大小决定如果是整数则左移到最近的16、32、48或64位边界。它比手动使用shl和bitwor更高效且生成的代码更紧凑。!-- 示例将多个值拼接成一个64位值 -- assign-variable name$shimr value2/ assign-variable name$GPR1[6:2] value3/ if expr1 concat $shimr concat $GPR1[6:2] concat 0x40000 0x102000300040000 !-- 表达式为真 -- /if重要限制concat的第二个操作数不能是复杂表达式因为工具无法确定表达式结果的大小。此时你必须用shl和bitwor手动实现拼接。checksum运算符用于计算互联网校验和。语法类似函数调用checksum(初始值, 起始偏移, 数据长度)。初始值通常为0也可以是之前计算的部分和。必须小于0xFFFF。起始偏移相对于当前帧窗口位置的字节偏移量0-255。数据长度需要计算校验和的字节数0-256。 运算符会从指定偏移开始将数据按16位2字节为单位进行累加使用addc指令即带进位的加法。如果数据长度是奇数最后一个字节右侧补零构成16位字。最终的总和再与初始值进行一次addc运算返回结果。!-- 示例计算IP头部的校验和 -- after !-- 假设帧窗口已指向IP头部开始计算20字节IP头部的校验和结果应为0xFFFF -- if exprchecksum(0, 0, 20) 0xFFFF assign-variable name$ipvalid value1/ !-- 标记IP头部有效 -- /if /after2.2.3 运算符优先级与表达式大小限制当表达式中存在多个运算符时计算顺序遵循优先级规则从高到低not,bitwnot,checksum,-,addcbitwand,bitwor,bitwxorshr,shl,concatgt,ge,lt,le,,!and,or强烈建议使用括号来明确运算顺序避免因优先级记忆错误导致的逻辑bug。关于大小限制大多数运算以64位为限。addc是16位运算操作数和结果都限制在16位内。和-是32位运算超出部分会产生进位/借位但结果只保留低32位。移位操作的移位量必须小于等于64。3. 变量操作详解与解析引擎的深度交互变量是软解析器与硬件解析器、以及解析器不同阶段之间通信的桥梁。对变量的操作直接影响了解析结果和后续处理流程。3.1 结果数组变量的核心作用与操作结果数组是一个预定义结构的共享内存区硬件解析器在解析过程中会填充它软解析器可以读取并修改它后续的流量分发、策略执行等模块也会读取它。理解每个变量的含义至关重要。3.1.1 关键变量解读与操作示例偏移量变量如$ethoffset,$ipoffset_n,$l4offset等。它们存储了各协议头部在数据帧中的起始字节位置。软解析器在解析自定义协议时可能需要手动设置下一个协议的偏移量。!-- 示例在自定义隧道协议解析后手动设置下一层协议为IP并更新IP偏移量 -- after !-- 假设自定义协议头部大小为 8 字节 -- assign-variable name$headerSize value8/ !-- 计算IP头部偏移量上一个协议偏移 当前头部大小 -- assign-variable name$ipoffset_n value$prevprotoOffset $headerSize/ !-- 设置下一层协议类型为IPv4 -- assign-variable name$nxtHdr value0x0800/ !-- 以太网类型字段0x0800代表IPv4 -- /after协议结果变量如$l2r,$l3r,$l4r。它们存储了硬件解析器对L2/L3/L4协议的解析结果通常是协议类型或端口号。软解析器可以读取它们来做决策。before !-- 在解析自定义协议之前检查上一层是否是IPv4 -- if expr$l3r 0x0800 !-- 是IPv4执行特定逻辑 -- assign-variable name$GPR1 value1/ /if /before描述符与动作变量如$actiondescriptor,$framedescriptor1。这些变量通常用于携带帧的元数据或动作指令传递给下游的处理单元如队列管理器、加密引擎等。操作它们需要非常小心必须与硬件架构定义严格对齐。!-- 示例根据解析结果设置动作描述符的特定比特位来指示需要特殊处理 -- after if expr$FW[24:8] 0x88A8 !-- 检查是否是802.1ad (QinQ) 帧 -- assign-variable name$actiondescriptor[0:1] value0x1/ !-- 设置一个标志位 -- /if /after3.1.2 必须手动更新的字段软解析器不会自动为你更新所有结果数组字段。对于自定义协议以下字段通常需要你根据解析逻辑手动设置否则可能导致后续解析错误或流量分发失败$classificationplanid分类计划ID用于后续流量分类。$nxtHdr下一个协议的类型标识如以太网类型、IP协议号。除非你跳转到after_ip或after_ethernet否则必须设置。$nxtHdrOffset下一个协议头部的偏移量。$runningsum运行总和常用于增量校验和计算。各种HXS偏移量如$shimoffset_1,$mplsoffset_n等用于记录特定协议头部的偏移供后续解析阶段使用。$lastetypeoffset最后一个以太网类型字段的偏移量。更新这些字段的典型位置是在自定义协议的after元素中或者在before元素中当解析器不推进帧窗口就退出时。3.1.3 禁止修改的字段有些字段是软解析器或硬件内部使用的修改它们会导致未定义行为$GPR1虽然文档说可用于临时存储但某些复杂操作如工具内部计算也可能使用它。最佳实践是如果需要临时变量优先使用结果数组中未明确用途的预留字段如果有或者仔细规划$GPR1的使用确保在需要工具内部计算前保存并之后恢复其值。$GPR2绝对不要使用。它是FMC工具内部用于复杂表达式计算的专用寄存器。当nextproto属性为next_ethernet或next_ip时不要修改$nxtHdr因为此时软解析器会使用$nxtHdr的值来计算下一个协议的位置。在before元素中除非解析器不推进帧窗口就退出否则不要修改$prevprotoOffset以及它所关联的偏移量变量如$ethoffset,$ipoffset_n等因为这些变量用于在before和after之间或通过action元素推进帧窗口。3.2 帧窗口变量的位级精确操作$FW变量提供了对原始数据包比特级的直接访问能力这是处理非标准封装或位字段的关键。3.2.1 访问与计算示例假设我们有一个自定义协议头前3比特是版本号接着5比特是类型然后是16比特的长度字段。format fields field typebit nameversion size3/ field typebit nametype size5/ field typefixed namelength size2/ !-- 2字节 -- /fields /format after !-- 方法1使用定义的字段名 -- if exprversion 1 assign-variable name$GPR1 valuetype/ !-- 读取类型字段 -- /if !-- 方法2直接使用帧窗口访问效果相同 -- if expr$FW[0:3] 1 !-- 访问版本字段 -- assign-variable name$GPR1 value$FW[3:5]/ !-- 访问类字段 -- /if !-- 访问2字节的长度字段 -- assign-variable name$packet_len value$FW[8:16]/ /after直接使用$FW在处理非字节对齐数据或需要动态计算偏移时更为灵活。3.2.2 结合checksum进行验证$FW常与checksum运算符配合用于验证协议头部的完整性。after !-- 计算自定义头部假设共10字节的校验和初始值为0从帧窗口偏移0开始计算10字节 -- assign-variable name$my_checksum valuechecksum(0, 0, 10)/ !-- 假设校验和字段位于头部的最后2字节偏移8字节处 -- if expr$my_checksum ! $FW[64:16] !-- 8字节 * 8比特 64比特 -- !-- 校验和错误可以设置错误标志或丢弃动作 -- assign-variable name$flags valuebitwor($flags, 0x01)/ !-- 设置错误标志位 -- /if /after3.3 参数数组变量的外部输入$PA变量允许解析逻辑接收外部配置参数极大地增强了灵活性。例如你可以通过参数数组传入一个密钥、一个特征值列表或一个配置掩码。before !-- 从参数数组的第0字节开始读取一个4字节的魔术字Magic Number -- assign-variable name$magic_from_pa value$PA[0:4]/ !-- 从帧窗口读取数据包中的魔术字 -- assign-variable name$magic_from_frame value$FW[0:32]/ if expr$magic_from_pa $magic_from_frame !-- 魔术字匹配执行特定处理流程 -- assign-variable name$classificationplanid value$PA[4:1]/ !-- 从参数数组读取分类ID -- /if /before这种方式使得同一份解析逻辑可以通过不同的参数数组来适应多种场景而无需修改解析器代码本身。4. 表达式与变量的实战应用与避坑指南掌握了基本语法后如何将它们组合起来解决实际问题并避开常见的陷阱是提升开发效率的关键。4.1 构建复杂的条件判断逻辑逻辑表达式是控制解析流程的核心。你需要熟练运用比较运算符和逻辑运算符来构建精确的匹配条件。before !-- 示例匹配特定的VLAN和IP协议组合 -- if expr($l2r 0x8100) and ($l3r 0x0800) !-- 带有VLAN标签的IPv4 -- !-- 读取VLAN TCI字段假设帧窗口已指向VLAN头部 -- assign-variable name$vlan_id value$FW[0:12]/ !-- PCPDEIVID共12比特 -- if expr($vlan_id 100) and ($FW[16:8] 0x11) !-- VLAN ID为100且IP协议为UDP (0x11) -- !-- 进一步处理例如提取UDP端口 -- assign-variable name$udp_src_port value$FW[64:16]/ !-- 假设IP头部20字节后是UDP头部 -- if expr$udp_src_port 53 !-- 匹配到VLAN 100内发往DNS服务器的IPv4 UDP流量 -- assign-variable name$target_queue value100/ /if /if /if /before技巧复杂的条件判断可以拆分成多个嵌套的if语句或者使用and/or组合但要注意运算符优先级。使用括号可以消除歧义。4.2 实现动态的帧窗口推进与协议跳转action typeexit元素中的advance和nextproto属性与变量操作紧密相关共同决定了解析路径。advanceyes在退出当前解析阶段前将帧窗口向前推进$headerSize字节。这通常用在after块中表示当前自定义协议头部已解析完毕窗口应移到下一个协议的开始。advanceno不推进帧窗口。这通常用在before块中表示软解析器只是对前一协议进行补充处理并未消耗新的头部。nextproto指定接下来由谁继续解析。return默认交还给硬解析器从当前帧窗口位置继续解析标准协议。after_ip/after_ethernet跳转到IP层或以太网层之后的解析逻辑。此时硬解析器会使用结果数组中的$nxtHdr变量值来决定具体的下一个协议。因此如果你设置nextproto为这两个值必须确保$nxtHdr已被正确设置。具体的协议名如ipv6,udp直接跳转到指定的协议解析。关键规则与避坑点当nextproto设为after_ethernet或after_ip时advance必须为yes。因为跳转到这些通用阶段意味着已经解析完了一个完整的头部可能是自定义的帧窗口必须推进。当nextproto设为return或为空默认为return时advance必须为no。因为你要将控制权交还给硬解析器让它从当前未推进的位置继续解析。手动设置偏移量的时机如果你在after块中手动计算并设置了下一个协议的偏移量如$ipoffset_n那么通常你会将nextproto设置为具体的协议如ipv4。如果你设置nextproto为after_ip则硬解析器会忽略你手动设置的$ipoffset_n而是根据$nxtHdr和标准逻辑自行计算偏移。!-- 场景1自定义协议作为L2.5层解析后跳转到IP层 -- protocol namemy_shim prevprotoethernet format.../format execute-code after !-- 计算IP头部偏移假设自定义头部长8字节 -- assign-variable name$ipoffset_n value$prevprotoOffset 8/ !-- 设置下一协议为IPv4 -- assign-variable name$nxtHdr value0x0800/ !-- 推进帧窗口并跳转到IP层之后的处理硬解析器会根据$nxtHdr0x0800识别为IPv4 -- action typeexit advanceyes nextprotoafter_ip/ /after /execute-code /protocol !-- 场景2在VLAN解析前进行一些检查或修改before块不消耗头部 -- protocol nameenhanced_vlan prevprotoethernet execute-code before !-- 检查是否是VLAN并可能修改某些值 -- if expr$l2r 0x8100 assign-variable name$GPR1 value1/ /if !-- 不推进窗口返回硬解析器继续解析VLAN -- action typeexit advanceno nextprotoreturn/ /before /execute-code /protocol4.3 处理复杂表达式与性能优化软解析器的表达式求值能力有限过于复杂的表达式会导致编译错误“expression too complex”。4.3.1 拆分复杂表达式将复杂的计算拆分成多个步骤利用$GPR1存储中间结果。!-- 目标计算 (A B) * C D其中A、B、C、D来自不同字段或变量 -- !-- 错误做法可能过于复杂 -- !-- if expr(($FW[0:16] $shimoffset_1) * $PA[0:2]) shr $GPR1[0:8] 100 -- !-- 正确做法分步计算 -- after !-- 步骤1: 计算 A B -- assign-variable name$GPR1[0:4] value$FW[0:16] $shimoffset_1/ !-- 注意32位限制 -- !-- 步骤2: 计算 (AB) * C可能需要处理溢出这里假设结果在32位内 -- assign-variable name$GPR1[4:4] value$GPR1[0:4] * $PA[0:2]/ !-- 步骤3: 计算右移结果 -- assign-variable name$temp_result value$GPR1[4:4] shr $GPR1[0:1]/ !-- 假设D在GPR1低字节 -- !-- 步骤4: 进行比较 -- if expr$temp_result 100 ... /if /after4.3.2 谨慎使用checksumchecksum表达式本身就可能很复杂。如果校验和的数据区域很长考虑分多次计算或者确保表达式结构尽可能简单。!-- 优化前可能因表达式复杂度过高而失败 -- !-- assign-variable name$chk valuechecksum(checksum(0,0,50), 50, 30)/ -- !-- 优化后分步计算 -- assign-variable name$chk_part1 valuechecksum(0, 0, 50)/ assign-variable name$chk_final valuechecksum($chk_part1, 50, 30)/4.4 调试与验证技巧编写软解析器逻辑类似于编写底层汇编代码调试困难。以下是一些实践技巧充分利用assign-variable进行“打印”调试将关键的计算结果、字段值赋值到结果数组中某个你约定俗成的、后续不会使用的变量例如$GPR1的高位字节。然后在系统的其他部分如驱动或应用层可以读取并打印这些值以验证解析逻辑是否正确。边界条件测试重点测试字段长度为0、偏移量超出范围、数值溢出32位/16位限制等情况。例如对$FW[offset:size]的访问要确保offsetsize不超过当前帧窗口的有效范围通常是256字节。协议交互测试不仅测试自定义协议本身还要测试它与前后协议由prevproto和nextproto定义的衔接是否正确。特别是偏移量的计算务必通过抓取真实数据包进行逐字节核对。理解硬件依赖不同的NXP DPAA版本或芯片结果数组的结构、变量的精确含义可能略有差异。务必查阅对应芯片型号和软件版本的《Frame Manager Parser》参考手册这是最权威的资料比通用文档更重要。版本控制与文档自定义协议解析文件XML应纳入版本控制系统。在文件中使用comment元素和详细的属性描述解释每个复杂表达式或变量操作的目的。几个月后你自己也可能忘记当时为什么这样写。5. 高级模式与策略分发框架的联动软解析器的输出结果数组是后续策略分发Policy的输入。理解这种联动才能设计出端到端的流量处理方案。5.1 为流量分类准备键值在distribution元素的key子元素中你可以指定用于哈希计算的字段。这些字段可以来自标准协议也可以来自软解析器填充的结果数组变量。!-- 在策略文件中定义一个基于自定义字段和IP五元组的哈希分布 -- distribution namecustom_app_dist queue count64 base0x1000/ key !-- 使用软解析器计算并存储在结果数组中的自定义应用ID -- fieldref nameresult_array.app_id/ !-- 假设app_id是自定义结果数组字段 -- !-- 结合标准的IP五元组 -- fieldref nameipv4.src/ fieldref nameipv4.dst/ fieldref nameipv4.protocol/ fieldref nameudp.src/ !-- 或 tcp.src -- fieldref nameudp.dst/ !-- 或 tcp.dst -- /key /distribution为了实现这个你需要在软解析器逻辑中将计算得到的应用ID例如从自定义协议头部提取写入到结果数组的一个特定位置并在策略文件中通过fieldref引用它。这要求你精确地知道该变量在结果数组中的字节偏移。5.2 使用combine元素进行精细控制combine元素允许你将端口号、帧内的特定数据或其他信息通过掩码和偏移操作直接“组合”到最终计算出的队列IDFQID中。这避免了所有流量都经过哈希可以实现确定性的分发。distribution nameport_based_dist queue count16 base0x2000/ !-- 基础队列ID -- key fieldref nameipv4.src/ fieldref nameipv4.dst/ /key !-- 将接收端口号假设从帧描述符或特定变量获得的低4位OR到哈希结果的高位 -- !-- frame112 可能表示从帧描述符的特定位置提取数据 -- combine frame112 offset2 size16 mask0xF/ !-- 提取16位数据应用掩码0xF然后OR到队列ID -- /distributioncombine的配置非常硬件相关需要查阅具体手册来理解frame、offset等参数的确切含义。5.3 动作集成解析后处理action元素不仅可以用于退出解析还可以在distribution中用于触发分类或策略动作。distribution namehigh_prio_vlan queue count4 base0x3000/ key fieldref nameethernet.src/ fieldref namevlan.id/ !-- 使用VLAN ID作为哈希键的一部分 -- /key !-- 在哈希分发前先经过一个名为“vlan_policer”的策略器进行限速 -- action typeclassification namevlan_policer/ /distribution在这个例子中匹配该分布规则的帧会先被送到vlan_policer策略器进行处理可能标记、丢弃或限速然后再进行哈希计算并进入相应的队列。软解析器之前填充的$classificationplanid或其他字段可以被策略器用来做更复杂的决策。6. 总结与核心思维模型经过对软解析器表达式和变量的深入剖析我们可以将其核心思维模型归纳为以下几点状态机视角将解析过程视为一个状态机。结果数组是全局状态帧窗口是当前读取指针before/after是状态转换函数action是状态跳转指令。表达式和变量操作就是在这些函数中读取和修改状态。数据流视角数据包是输入流结果数组是输出流和中间工作区。软解析器的任务是从输入流中提取、计算信息并填充到输出流中同时准备好让硬件继续处理所需的状态如偏移量、下一协议类型。受限编程环境这不是一个完整的编程语言。你面对的是有限的寄存器变量、受限的运算位/算术和严格的内存访问模式帧窗口、结果数组。编程时必须时刻考虑这些限制32/16位运算、表达式复杂度、变量作用域。协同工作软解析器从未独立工作。它总是与硬解析器紧密配合。你的代码必须遵守硬解析器设定的“契约”例如正确设置$nxtHdr和偏移量在适当的时候推进或不推进帧窗口。理解这个契约即硬件的工作流程比掌握语法更重要。最后也是最实际的经验从简单的协议开始。先实现一个只读取固定长度头部、不做复杂计算的解析器验证其能与前后协议正确衔接。然后逐步增加条件判断、字段提取、校验和计算等复杂功能。每增加一个功能都用真实或仿真的数据包进行测试。网络数据包处理无小事一个比特的偏移错误就可能导致整个数据流被错误地路由或丢弃。耐心、细致和对协议的深刻理解是玩转软解析器的不二法门。