如何用nom解析器构建面向比特的AIS消息解码接口?
我正在用Rust的nom库解码AIS消息,先明确下AIS消息的核心编码规则:
- AIS消息基于任意长度的比特向量,字段长度不固定,且通常不对齐字节边界
- 该比特向量通过ASCII编码嵌入NMEA语句,解码规则如下:
每个ASCII字符对应6比特数据;恢复6比特值的方法:从ASCII字符值减48,若结果大于40则再减8。
有效ASCII字符范围是"0"(ASCII值64)到"w"(ASCII值87),其中"X"(88)到"_"(95)的字符不会被使用。
举个实际例子:
NMEA语句:
!AIVDM,1,1,,A,D03Ovk1T1N>5N8ffqMhNfp0,0*68
编码后的AIS数据:D03Ovk1T1N>5N8ffqMhNfp0
解码后的比特向量:010100000000000011011111111110110011000001100100000001011110001110000101011110001000101110101110111001011101110000011110101110111000000000
遇到的两个相关问题
问题1:在nom内完成ASCII到比特向量的转换,无需中间字节数组
我能手动实现ASCII到比特流的转换,但希望直接在nom的解析器链中完成这一步,把ASCII字符流直接转换成可处理的比特流,避免生成中间字节数组,和后续的AIS字段解析器无缝链式调用。
问题2:读取任意比特数的字段,避免nom强制字节对齐
使用nom的bits!宏读取比特时,发现如果用take_bits!(u8, n),似乎会一次性消费整字节的比特?比如我定义:
named!(take_3_bits<u8>, bits!(take_bits!(u8, 3)));
调用两次这个解析器,实际会消耗16比特流而非预期的6比特;即使把多个读取操作组合,nom还是会强制前进到整字节边界,手动管理比特指针又会增加额外工作量。
解决方案
针对问题1:在nom中直接将ASCII字符流转换为比特流
我们可以编写自定义nom解析器,把输入的ASCII字符逐个转换成6比特值,再拼接成连续比特流,作为bits!宏的输入,全程无需额外中间字节数组(除了必要的比特打包步骤)。
首先实现单个ASCII字符转6比特值的解析器:
use nom::{ bytes::complete::take, combinator::map_res, error::{Error, ErrorKind}, IResult, }; /// 将单个AIS编码的ASCII字符转换为6比特数值 fn decode_ais_char(c: char) -> Result<u8, Error<&str>> { let ascii_val = c as u8; let mut val = ascii_val - 48; if val > 40 { val -= 8; } // 验证是否在6比特有效范围(0-63)内 if val > 63 { Err(Error::new("", ErrorKind::Char)) } else { Ok(val) } } /// 解析单个AIS编码的ASCII字符 fn parse_ais_char(input: &str) -> IResult<&str, u8> { map_res(take(1usize), |s: &str| { s.chars().next() .ok_or_else(|| Error::new("", ErrorKind::Char)) .and_then(decode_ais_char) })(input) }
接着把所有ASCII字符解码后的6比特值打包成完整字节流,供bits!宏处理:
use nom::{bits::bits, multi::fold_many0}; /// 把AIS的ASCII编码字符串转换为可被bits!处理的字节流 fn parse_ais_payload(input: &str) -> IResult<&str, &[u8]> { let (remaining, six_bit_vals) = fold_many0( parse_ais_char, Vec::new(), |mut acc, val| { acc.push(val); acc } )(input)?; // 将6比特值数组打包成连续字节流 let mut bytes = Vec::new(); let mut current_byte = 0u8; let mut bit_pos = 0; for &val in &six_bit_vals { let bits_remaining = 8 - bit_pos; if bits_remaining >= 6 { current_byte |= val << (bits_remaining - 6); bit_pos += 6; } else { current_byte |= val >> (6 - bits_remaining); bytes.push(current_byte); current_byte = (val << bits_remaining) as u8; bit_pos = 6 - bits_remaining; } } // 处理剩余未填满字节的比特 if bit_pos > 0 { bytes.push(current_byte); } Ok((remaining, bytes.as_slice())) }
针对问题2:读取任意比特数的字段,避免字节对齐
其实nom的bits!宏本身支持任意比特数的连续读取,问题大概率出在用法上——要把所有比特读取操作放在同一个bits!宏内部,这样nom会维护连续的比特偏移量,不会强制字节对齐。
比如我们可以定义解析器读取两次3比特,总共消耗6比特:
use nom::bits::complete::take_bits; // 读取3比特的解析器 fn take_3_bits(input: (&[u8], usize)) -> IResult<(&[u8], usize), u8> { take_bits(3usize)(input) } // 连续读取两次3比特的解析器 fn take_two_3_bits(input: &[u8]) -> IResult<&[u8], (u8, u8)> { bits(|input| { let (input, first) = take_3_bits(input)?; let (input, second) = take_3_bits(input)?; Ok((input, (first, second))) })(input) }
结合上面的payload解析器,我们可以完整解析AIS消息的字段,比如解析消息类型(6比特)和重复指示器(2比特):
// 解析AIS消息头部:消息类型(6比特)+ 重复指示器(2比特) fn parse_ais_header(input: &[u8]) -> IResult<&[u8], (u8, u8)> { bits(|input| { let (input, msg_type) = take_bits(6usize)(input)?; let (input, repeat_indicator) = take_bits(2usize)(input)?; Ok((input, (msg_type, repeat_indicator))) })(input) } // 完整的AIS消息解析链:ASCII payload → 比特流 → 字段解析 fn parse_ais_message(input: &str) -> IResult<&str, (u8, u8)> { let (remaining, payload_bytes) = parse_ais_payload(input)?; let (_, header) = parse_ais_header(payload_bytes)?; Ok((remaining, header)) }
这样整个流程都在nom的解析器链中完成,既避免了多余的中间数组,又能自由读取任意比特数的字段,不会出现强制字节对齐的问题。
内容的提问来源于stack exchange,提问作者squidpickles

