引言
Erlang 的位语法(Bit Syntax)是其最独特且被低估的特性之一——它让二进制数据的操作像模式匹配元组一样直观。解析 TCP 包头、处理比特币区块、编码 Protobuf 消息、操作位掩码,这些在其他语言中需要复杂库支持的工程,在 Erlang 中只需一个 << >> 表达式。本文从位语法基础出发,深入到协议解析实战、高效二进制操作与 NIF 互操作,让你掌握 Erlang「二进制即数据」的思维方式。
前置:/erlang-pattern-matching/(模式匹配)、/erlang-language-basics/(基础语法)。
目录
- 1. 二进制数据类型Bitstring与Binary
- 2. 位语法表达式构造与拆分
- 3. 模式匹配二进制头部长度与剩余
- 4. 二进制推导与列表互转
- 5. 高效拼接与拆分iolist与iodata
- 6. 位标志与掩码操作
- 7. 协议解析实战TCP包头与自定义协议
- 8. 与 C 互操作NIF 与二进制共享
- 9. 性能优化与内存布局
- 10. 速查表与一句话记忆
- 延伸阅读
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 | 整型(默认) |
| float | IEEE 754 浮点 |
| binary | 嵌入二进制 |
| bitstring | 嵌入位字符串 |
| bytes | 字节对齐的 binary |
| bits | 位对齐的 bitstring |
| signed | 有符号 |
| unsigned | 无符号(默认) |
| big | 大端(默认) |
| little | 小端 |
| native | 本机字节序 |
| utf8/utf16/utf32 | Unicode 编码 |
记忆 位语法 «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. 速查表与一句话记忆
| 概念 | 一句话 |
|---|---|
| binary | 8 位倍数,堆外共享 |
| bitstring | 任意位数 |
| « » | 位语法构造与匹配 |
| /little /big | 字节序 |
| /float | IEEE 754 |
| /binary | 嵌入二进制 |
| iolist | 树形延迟拼接 |
| iodata | iolist | 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-pattern-matching/ — 模式匹配深入
- /erlang-language-basics/ — 基础语法
- /elixir-otp-supervision-tasks/ — OTP 监督树
- Elixir 二进制处理 — Elixir 中的位语法
- Erlang 位语法官方文档
- erts: Binary Internals
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。