Erlang 位语法与二进制处理:高效数据序列化与协议解析

Erlang 位语法与二进制处理:二进制数据类型(bitstring/binary)、位语法表达式(<<Value:Size/TypeSpecifierList>>)、模式匹配二进制(头部/长度/剩余)、二进制推导(binary comprehensions)、高效拼接与拆分(iolist/iodata)、位标志位掩码操作、协议解析实战(TCP/IP 包头/Bitcoin 区块/Protobuf 二进制)、性能优化与内存布局、与 C/NIF 的互操作。

引言

Erlang 的位语法(Bit Syntax)是其最独特且被低估的特性之一——它让二进制数据的操作像模式匹配元组一样直观。解析 TCP 包头、处理比特币区块、编码 Protobuf 消息、操作位掩码,这些在其他语言中需要复杂库支持的工程,在 Erlang 中只需一个 << >> 表达式。本文从位语法基础出发,深入到协议解析实战、高效二进制操作与 NIF 互操作,让你掌握 Erlang「二进制即数据」的思维方式。

前置:/erlang-pattern-matching/(模式匹配)、/erlang-language-basics/(基础语法)。


目录


1. 二进制数据类型:Bitstring 与 Binary

1.1 基础定义

% binary:8 位倍数的位序列(最常用)
B1 = <<1, 2, 3>>.           % 3 字节二进制
B2 = <<"hello">>.           % 字符串转为二进制

% bitstring:任意位数的位序列
BS = <<1:1, 0:1, 1:1>>.    % 3 位,不是 binary(不是 8 的倍数)

% is_binary/1 和 is_bitstring/1
is_binary(<<1,2>>).          % true
is_binary(<<1:1>>).          % false(是 bitstring 但不是 binary)

1.2 内存特性

# Binary 在堆外分配(refc binary),进程间共享
# 小 binary(< 64 字节)在进程堆内分配(heap binary)
# 大 binary 引用计数释放 → 避免进程间复制大块数据
# 参考:erts 的 refc binary 和 sub binary 机制

记忆 binary 是 8 位倍数、bitstring 是任意位数;is_binary 检查 8 位对齐;大 binary 堆外引用计数共享、小 binary 堆内分配(<64 字节)。


2. 位语法表达式:构造与拆分

2.1 构造语法

<<Value:Size/TypeSpecifierList>>

% 示例
<<256:16>>.                  % 2 字节大端: <<1, 0>>
<<256:16/little>>.           % 2 字节小端: <<0, 1>>
<<3.14:32/float>>.           % 32 位 IEEE 754 浮点
<<-1:8/signed>>.             % 有符号: <<255>>
<<"abc">>.                   % 字符串转为二进制

2.2 Type Specifier 速查

Specifier含义
integer整型(默认)
floatIEEE 754 浮点
binary嵌入二进制
bitstring嵌入位字符串
bytes字节对齐的 binary
bits位对齐的 bitstring
signed有符号
unsigned无符号(默认)
big大端(默认)
little小端
native本机字节序
utf8/utf16/utf32Unicode 编码

记忆 位语法 «Value:Size/TypeSpecifierList»——Size 默认看上下文、Type 默认 integer、Sign 默认 unsigned、Endian 默认 big;常用 /16/little、/float、/binary、/utf8。


3. 模式匹配二进制:头部、长度与剩余

3.1 基础匹配

% 匹配头部字节
<<Header:8, Rest/binary>> = <<1, 2, 3, 4>>.
% Header = 1, Rest = <<2, 3, 4>>

% 匹配固定长度
<<A:8, B:16, C:8>> = <<1, 0, 255, 3>>.
% A=1, B=255 (0*256+255), C=3

% 匹配字符串前缀
<<"GIF", Rest/binary>> = <<"GIF89a", 1, 2, 3>>.

3.2 动态长度

% Length 前缀 + Payload
<<Len:8, Payload:Len/binary, Rest/binary>> = <<3, "abc", "xyz">>.
% Len=3, Payload=<<"abc">>, Rest=<<"xyz">>

% 嵌套长度(如 TCP 选项)
<<Type:8, Len:8, OptData:Len/binary, Rest/binary>> = Data.

3.3 二进制遍历

parse_packets(<<>>) -> [];
parse_packets(<<Len:16, Payload:Len/binary, Rest/binary>>) ->
    [Payload | parse_packets(Rest)].

% 解析全部长度前缀的报文

记忆 二进制模式匹配像拆快递——«头部:8, 剩余/binary» 拆头部;«长度:8, 负载:长度/binary, 剩余/binary» 动态长度;递归遍历解析批量报文。


4. 二进制推导与列表互转

4.1 Binary Comprehension

% 将列表转为 UTF-8 二进制
<< <<X/utf8>> || X <- [1049, 1088, 1083, 1072, 1085, 1076] >>.
% => <<"Ирланд">>  (爱尔兰)

% 偶数过滤
<< <<X:8>> || X <- [1,2,3,4,5,6], X rem 2 == 0 >>.
% => <<2, 4, 6>>

% 二进制推导生成
[ X || <<X:8>> <= <<1,2,3>> ].
% => [1, 2, 3]

4.2 列表与二进制互转

binary_to_list(<<1,2,3>>).     % => [1, 2, 3]
list_to_binary([1,2,3]).       % => <<1, 2, 3>>

% 注意:binary_to_list/1 处理大 binary 效率低,保持二进制操作

记忆 二进制推导 « «Expr» || Generator, Filter »——类似列表推导但产出二进制;列表转二进制 list_to_binary、二进制转列表 binary_to_list;大 binary 尽量保持二进制操作不转列表。


5. 高效拼接与拆分:iolist 与 iodata

5.1 为什么不用 <<Bin1/binary, Bin2/binary>>

% 每次拼接创建新 binary → O(n) 拷贝
% 大量拼接时性能灾难
% 解决:iolist——树形结构,延迟拼接

5.2 iolist

% iolist = [iolist() | byte() | binary()]
% 即:嵌套列表、字节、二进制的混合树

IoList = ["HTTP/1.1 200 OK\r\n",
          ["Content-Length: ", integer_to_binary(12), "\r\n"],
          "\r\n",
          <<"Hello World!">>].

% 使用时统一转换
Response = iolist_to_binary(IoList).

% 或直接发给端口(gen_tcp:send 接受 iolist)
gen_tcp:Socket, IoList).

5.3 iodata

iodata = iolist() | binary()
% 函数参数声明为 iodata() → 接受 binary 或 iolist
% 宽口径输入、统一处理

记忆 避免 «A/binary, B/binary» 拼接(O(n) 拷贝),用 iolist 树形延迟拼接;iolist = [嵌套列表|字节|binary];gen_tcp:send 直接收 iolist,最后 iolist_to_binary 一次转;参数用 iodata() 宽口径。


6. 位标志与掩码操作

6.1 设置/清除/检查位

% 设置第 N 位(从 0 开始)
set_bit(Flags, N) -> Flags bor (1 bsl N).

% 清除第 N 位
clear_bit(Flags, N) -> Flags band bnot (1 bsl N).

% 检查第 N 位
is_bit_set(Flags, N) -> (Flags band (1 bsl N)) =/= 0.

% 位掩码提取低 4 位
Low4 = Flags band 2#1111.

6.2 实战:TCP Flags

-define(FIN, 1).  % 0b00000001
-define(SYN, 2).  % 0b00000010
-define(RST, 4).  % 0b00000100
-define(PSH, 8).  % 0b00001000
-define(ACK, 16). % 0b00010000
-define(URG, 32). % 0b00100000

is_syn(Flags) -> (Flags band ?SYN) =/= 0.
is_syn_ack(Flags) -> (Flags band (?SYN bor ?ACK)) =:= (?SYN bor ?ACK).

记忆 位操作——设置 bor、清除 band bnot、检查 band =/= 0;用宏定义语义化位标志(TCP FIN/SYN/RST/ACK);掩码提取 band 2#1111。


7. 协议解析实战:TCP 包头与自定义协议

7.1 TCP 包头解析

parse_tcp(<<SrcPort:16, DstPort:16,
            Seq:32, Ack:32,
            DataOffset:4, _Reserved:4, Flags:8,
            Window:16, Checksum:16, Urgent:16,
            OptionsAndData/binary>>) ->
    HeaderLen = DataOffset * 4,
    <<Options:HeaderLen/binary, Data/binary>> = OptionsAndData,
    #{src_port => SrcPort, dst_port => DstPort,
      seq => Seq, ack => Ack,
      flags => Flags, window => Window,
      options => Options, data => Data}.

7.2 自定义协议:长度前缀 + JSON Payload

% 编码
encode(Msg) ->
    Payload = jsx:encode(Msg),
    <<(byte_size(Payload)):32, Payload/binary>>.

% 解码(处理粘包)
decode(Buffer) -> decode(Buffer, []).

decode(<<Len:32, Payload:Len/binary, Rest/binary>>, Acc) ->
    decode(Rest, [jsx:decode(Payload) | Acc]);
decode(Buffer, Acc) ->
    {lists:reverse(Acc), Buffer}.

记忆 协议解析 = 位语法模式匹配结构字段(如 TCP 包头 DataOffset*4 算头长);自定义协议常用「长度前缀:32 + Payload」处理粘包;递归 decode 直到缓冲不足。


8. 与 C 互操作:NIF 与二进制共享

8.1 NIF 共享 Binary

// C NIF 接收 Erlang binary
static ERL_NIF_TERM parse_header(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
    ErlNifBinary bin;
    if (!enif_inspect_binary(env, argv[0], &bin)) return enif_make_badarg(env);
    
    // bin.data 指向共享内存,只读不可修改
    uint16_t magic = (bin.data[0] << 8) | bin.data[1];
    return enif_make_uint(env, magic);
}

8.2 安全边界

# NIF 不能长时间运行(会阻塞 Erlang 调度器)
# dirty NIF(dirty_cpu/dirty_io)用于耗时操作
# Binary 在 NIF 中是只读的,修改需复制到新内存

记忆 NIF 通过 enif_inspect_binary 共享 Erlang binary 内存(只读);耗时操作用 dirty NIF;binary 数据零拷贝传递是 Erlang-C 互操作的高效基础。


9. 性能优化与内存布局

9.1 二进制操作优化

# 1) 避免频繁 binary_to_list → 保持二进制上下文
# 2) 用 iolist 替代多次 << >> 拼接
# 3) 模式匹配提取优于 binary:part/3
# 4) 大文件用 file:read_file 或 raw 模式
# 5) 二进制复制开销:sub binary(引用)vs 新分配

9.2 sub binary 机制

<<_:8, Rest/binary>> = <<1, 2, 3, 4>>.
% Rest 是 sub binary:引用原内存 + 偏移/长度
% 不分配新内存,直到写入才复制(copy-on-write)

9.3 内存诊断

% 查看 binary 内存
erlang:memory(binary).    % 当前 binary 总内存

% 进程持有的 binary
process_info(Pid, binary). % 进程引用的二进制列表

记忆 性能优化——保持二进制上下文(不转列表)、iolist 替代拼接、模式匹配优于 binary:part、sub binary 引用不拷贝;erlang:memory(binary) 看总使用量、process_info 看进程引用。


10. 速查表与一句话记忆

概念一句话
binary8 位倍数,堆外共享
bitstring任意位数
« »位语法构造与匹配
/little /big字节序
/floatIEEE 754
/binary嵌入二进制
iolist树形延迟拼接
iodataiolist | binary
bor/bsl/band/bnot位操作
NIF共享 binary,只读

一句话记忆:Erlang 位语法 «Value:Size/TypeSpecifierList» 让二进制操作像模式匹配一样直观——binary 是 8 位倍数(堆外共享)、bitstring 任意位数;Type 默认 integer/big/unsigned,常用 /little、/float、/binary 嵌入;模式匹配拆二进制«头部:8, Body:Len/binary, 剩余/binary» 是协议解析核心;避免 «A,B» 拼接拷贝用 iolist 树形延迟拼接;位操作 bor 设置、band bnot 清除、band =/= 0 检查;TCP 解析用模式匹配按字段宽度拆;自定义协议用 Len:32 + Payload 处理粘包;NIF 通过 enif_inspect_binary 共享内存零拷贝;sub binary 引用原内存到写入时才复制——「二进制是 Erlang 的母语,位语法是协议工程师的瑞士军刀」。


延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「erlang」更多文章

  1. OTP 应用设计模式:监督树结构、release 打包与热升级
  2. Erlang/Elixir 安全加固:加密、认证与分布式信任
  3. Ecto 高级查询与数据库工程:关联、多态与性能优化