Erlang 中的二进制协议解析
Binary Protocol Parsing in Erlang
我有点难以从二进制消息中提取字段。原始消息如下所示:
<<1,0,97,98,99,100,0,0,0,3,0,0,0,0,0,0,0,0,0,3,32,3,0,0,88,2,0,0>>
我知道字段的顺序、类型和静态大小,有些字段的大小是任意的,所以我正在尝试执行以下操作:
newobj(Data) ->
io:fwrite("NewObj RAW ~p~n",[Data]),
NewObj = {obj,rest(uint16(string(uint16({[],Data},id),type),parent),unparsed)},
io:fwrite("NewObj ~p~n",[NewObj]),
NewObj.
uint16/2, string/2, rest/2 实际上是提取函数,看起来像这样:
uint16(ListData, Name) ->
{List, Data} = ListData,
case Data of
<<Int:2/little-unsigned-unit:8, Rest/binary>> ->
{List ++ [{Name,Int}], Rest};
<<Int:2/little-unsigned-unit:8>> ->
List ++ [{Name,Int}]
end.
string(ListData, Name) ->
{List, Data} = ListData,
Split = binary:split(Data,<<0>>),
String = lists:nth(1, Split),
if
length(Split) == 2 ->
{List ++ [{Name, String}], lists:nth(2, Split)};
true ->
List ++ [{Name, String}]
end.
rest(ListData, Name) ->
{List, Data} = ListData,
List ++ [{Name, Data}].
这看起来像:
NewObj RAW <<1,0,97,98,99,100,0,0,0,3,0,0,0,0,0,0,0,0,0,3,32,3,0,0,88,2,0,0>>
NewObj {obj,[{id,1},
{type,<<"abcd">>},
{parent,0},
{unparsed,<<3,0,0,0,0,0,0,0,0,0,3,32,3,0,0,88,2,0,0>>}]}
这个问题的原因是将 {List, Data} 作为 ListData 传递,然后在函数中用 {List, Data} = ListData 拆分它感觉很笨拙 - 那么有更好的方法吗?我想我不能使用静态匹配,因为 "unparsed" 和 "type" 部分是任意长度的,因此无法定义它们各自的大小。
谢谢!
----------------更新----------------
尝试考虑以下评论 - 代码现在如下所示:
newobj(Data) ->
io:fwrite("NewObj RAW ~p~n",[Data]),
NewObj = {obj,field(
field(
field({[], Data},id,fun uint16/1),
type, fun string/1),
unparsed,fun rest/1)},
io:fwrite("NewObj ~p~n",[NewObj]).
field({List, Data}, Name, Func) ->
{Value,Size} = Func(Data),
case Data of
<<_:Size/binary-unit:8>> ->
[{Name,Value}|List];
<<_:Size/binary-unit:8, Rest/binary>> ->
{[{Name,Value}|List], Rest}
end.
uint16(Data) ->
case Data of
<<UInt16:2/little-unsigned-unit:8, _/binary>> ->
{UInt16,2};
<<UInt16:2/little-unsigned-unit:8>> ->
{UInt16,2}
end.
string(Data) ->
Split = binary:split(Data,<<0>>),
case Split of
[String, Rest] ->
{String,byte_size(String)+1};
[String] ->
{String,byte_size(String)+1}
end.
rest(Data) ->
{Data,byte_size(Data)}.
代码不符合语言习惯,有些部分无法按原样编译 :-) 以下是一些评论:
newobj/1
函数引用了未绑定的 NewObj
变量。可能真正的代码是 NewObj = {obj,rest(...
?
代码多次使用list append (++
)。应尽可能避免这种情况,因为它会执行过多的内存复制。惯用的方法是根据需要多次添加到列表的头部(即:L2 = [NewThing | L1]
)并在最后调用 lists:reverse/1
。有关详细信息,请参阅任何 Erlang 书籍或免费学习一些 Erlang。
类似地,应该避免使用 lists:nth/2
并用模式匹配或其他方式来构造列表或解析二进制文件
Dogbert 关于直接在函数参数中进行模式匹配的建议是一种很好的惯用方法,可以从代码中删除一些行。
关于调试方法的最后建议,考虑用适当的单元测试替换 fwrite 函数。
希望这能为查看内容提供一些提示。请随意将代码更改附加到您的问题,我们可以从那里继续。
编辑
看起来好多了。让我们看看是否可以简化。请注意,我们是倒着做的,因为我们是在编写生产代码后添加测试,而不是进行测试驱动开发。
第 1 步:添加测试。
我还颠倒了列表的顺序,因为它看起来更自然。
-include_lib("eunit/include/eunit.hrl").
happy_input_test() ->
Rest = <<3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 32, 3, 0, 0, 88, 2, 0, 0>>,
Input = <<1, 0,
97, 98, 99, 100, 0,
0, 0,
Rest/binary>>,
Expected = {obj, [{id, 1}, {type, <<"abcd">>}, {parent, 0}, {unparsed, Rest}]},
?assertEqual(Expected, binparse:newobj(Input)).
我们可以 运行 这,除此之外,还有 rebar3 eunit
(请参阅 rebar3 文档;我建议从 rebar3 new lib mylib
开始创建骨架)。
第 2 步:绝对最小值
您的描述不足以理解哪些字段是必填字段,哪些字段是可选字段以及 obj
之后是否总是有更多内容。
在最简单的情况下,所有您的代码可以简化为:
newobj(Bin) ->
<<Id:16/little-unsigned, Rest/binary>> = Bin,
[Type, Rest2] = binary:split(Rest, <<0>>),
<<Parent:16/little-unsigned, Rest3/binary>> = Rest2,
{obj, [{id, Id}, {type, Type}, {parent, Parent}, {unparsed, Rest3}]}.
非常紧凑:-)
我发现字符串的编码非常奇怪:字符串以 NUL 结尾的二进制编码(因此强制遍历二进制文件)而不是用 2 或 4 个字节来表示长度和然后是字符串本身。
第 3 步:输入验证
由于我们正在解析二进制文件,这可能来自我们系统的外部。因此,让它崩溃的理念不适用,我们必须执行完整的输入验证。
我假设所有字段都是必填的,除了 unparsed
,它可以是空的。
missing_unparsed_is_ok_test() ->
Input = <<1, 0,
97, 98, 99, 100, 0,
0, 0>>,
Expected = {obj, [{id, 1}, {type, <<"abcd">>}, {parent, 0}, {unparsed, <<>>}]},
?assertEqual(Expected, binparse:newobj(Input)).
上面的简单实现就通过了。
第 4 步:父格式错误
我们添加测试并做出 API 决定:该函数将 return 一个错误元组。
missing_parent_is_error_test() ->
Input = <<1, 0,
97, 98, 99, 100, 0>>,
?assertEqual({error, bad_parent}, binparse:newobj(Input)).
malformed_parent_is_error_test() ->
Input = <<1, 0,
97, 98, 99, 100, 0,
0>>,
?assertEqual({error, bad_parent}, binparse:newobj(Input)).
我们更改实现以通过测试:
newobj(Bin) ->
<<Id:16/little-unsigned, Rest/binary>> = Bin,
[Type, Rest2] = binary:split(Rest, <<0>>),
case Rest2 of
<<Parent:16/little-unsigned, Rest3/binary>> ->
{obj, [{id, Id}, {type, Type}, {parent, Parent}, {unparsed, Rest3}]};
Rest2 ->
{error, bad_parent}
end.
第 5 步:格式错误的类型
新测试:
missing_type_is_error_test() ->
Input = <<1, 0>>,
?assertEqual({error, bad_type}, binparse:newobj(Input)).
malformed_type_is_error_test() ->
Input = <<1, 0,
97, 98, 99, 100>>,
?assertEqual({error, bad_type}, binparse:newobj(Input)).
我们可能会想按如下方式更改实现:
newobj(Bin) ->
<<Id:16/little-unsigned, Rest/binary>> = Bin,
case binary:split(Rest, <<0>>) of
[Type, Rest2] ->
case Rest2 of
<<Parent:16/little-unsigned, Rest3/binary>> ->
{obj, [
{id, Id}, {type, Type},
{parent, Parent}, {unparsed, Rest3}
]};
Rest2 ->
{error, bad_parent}
end;
[Rest] -> {error, bad_type}
end.
这是一团乱七八糟的东西。仅仅添加功能对我们没有帮助:
newobj(Bin) ->
<<Id:16/little-unsigned, Rest/binary>> = Bin,
case parse_type(Rest) of
{ok, {Type, Rest2}} ->
case parse_parent(Rest2) of
{ok, Parent, Rest3} ->
{obj, [
{id, Id}, {type, Type},
{parent, Parent}, {unparsed, Rest3}
]};
{error, Reason} -> {error, Reason}
end;
{error, Reason} -> {error, Reason}
end.
parse_type(Bin) ->
case binary:split(Bin, <<0>>) of
[Type, Rest] -> {ok, {Type, Rest}};
[Bin] -> {error, bad_type}
end.
parse_parent(Bin) ->
case Bin of
<<Parent:16/little-unsigned, Rest/binary>> -> {ok, Parent, Rest};
Bin -> {error, bad_parent}
end.
这是 Erlang 中带有嵌套条件的经典问题。
第 6 步:恢复理智
这是我的方法,非常通用,适用于(我认为)许多领域。总体思路取自回溯,如 http://rvirding.blogspot.com/2009/03/backtracking-in-erlang-part-1-control.html
中所述
我们为每个解析步骤创建一个函数,并将它们作为列表传递给 call_while_ok/3
:
newobj(Bin) ->
Parsers = [fun parse_id/1,
fun parse_type/1,
fun parse_parent/1,
fun(X) -> {ok, {unparsed, X}, <<>>} end
],
case call_while_ok(Parsers, Bin, []) of
{error, Reason} -> {error, Reason};
PropList -> {obj, PropList}
end.
函数call_while_ok/3
在某种程度上与lists:foldl
和lists:filter
相关:
call_while_ok([F], Seed, Acc) ->
case F(Seed) of
{ok, Value, _NextSeed} -> lists:reverse([Value | Acc]);
{error, Reason} -> {error, Reason}
end;
call_while_ok([F | Fs], Seed, Acc) ->
case F(Seed) of
{ok, Value, NextSeed} -> call_while_ok(Fs, NextSeed, [Value | Acc]);
{error, Reason} -> {error, Reason}
end.
这里是解析函数。请注意,他们的签名始终相同:
parse_id(Bin) ->
<<Id:16/little-unsigned, Rest/binary>> = Bin,
{ok, {id, Id}, Rest}.
parse_type(Bin) ->
case binary:split(Bin, <<0>>) of
[Type, Rest] -> {ok, {type, Type}, Rest};
[Bin] -> {error, bad_type}
end.
parse_parent(Bin) ->
case Bin of
<<Parent:16/little-unsigned, Rest/binary>> ->
{ok, {parent, Parent}, Rest};
Bin -> {error, bad_parent}
end.
第 7 步:作业
列表 [{id, 1}, {type, <<"abcd">>}, {parent, 0}, {unparsed, Rest}]
是一个 proplist
(参见 Erlang 文档),早于 Erlang 映射。
查看地图文档,看看 return 地图是否有意义。
我有点难以从二进制消息中提取字段。原始消息如下所示:
<<1,0,97,98,99,100,0,0,0,3,0,0,0,0,0,0,0,0,0,3,32,3,0,0,88,2,0,0>>
我知道字段的顺序、类型和静态大小,有些字段的大小是任意的,所以我正在尝试执行以下操作:
newobj(Data) ->
io:fwrite("NewObj RAW ~p~n",[Data]),
NewObj = {obj,rest(uint16(string(uint16({[],Data},id),type),parent),unparsed)},
io:fwrite("NewObj ~p~n",[NewObj]),
NewObj.
uint16/2, string/2, rest/2 实际上是提取函数,看起来像这样:
uint16(ListData, Name) ->
{List, Data} = ListData,
case Data of
<<Int:2/little-unsigned-unit:8, Rest/binary>> ->
{List ++ [{Name,Int}], Rest};
<<Int:2/little-unsigned-unit:8>> ->
List ++ [{Name,Int}]
end.
string(ListData, Name) ->
{List, Data} = ListData,
Split = binary:split(Data,<<0>>),
String = lists:nth(1, Split),
if
length(Split) == 2 ->
{List ++ [{Name, String}], lists:nth(2, Split)};
true ->
List ++ [{Name, String}]
end.
rest(ListData, Name) ->
{List, Data} = ListData,
List ++ [{Name, Data}].
这看起来像:
NewObj RAW <<1,0,97,98,99,100,0,0,0,3,0,0,0,0,0,0,0,0,0,3,32,3,0,0,88,2,0,0>>
NewObj {obj,[{id,1},
{type,<<"abcd">>},
{parent,0},
{unparsed,<<3,0,0,0,0,0,0,0,0,0,3,32,3,0,0,88,2,0,0>>}]}
这个问题的原因是将 {List, Data} 作为 ListData 传递,然后在函数中用 {List, Data} = ListData 拆分它感觉很笨拙 - 那么有更好的方法吗?我想我不能使用静态匹配,因为 "unparsed" 和 "type" 部分是任意长度的,因此无法定义它们各自的大小。
谢谢!
----------------更新----------------
尝试考虑以下评论 - 代码现在如下所示:
newobj(Data) ->
io:fwrite("NewObj RAW ~p~n",[Data]),
NewObj = {obj,field(
field(
field({[], Data},id,fun uint16/1),
type, fun string/1),
unparsed,fun rest/1)},
io:fwrite("NewObj ~p~n",[NewObj]).
field({List, Data}, Name, Func) ->
{Value,Size} = Func(Data),
case Data of
<<_:Size/binary-unit:8>> ->
[{Name,Value}|List];
<<_:Size/binary-unit:8, Rest/binary>> ->
{[{Name,Value}|List], Rest}
end.
uint16(Data) ->
case Data of
<<UInt16:2/little-unsigned-unit:8, _/binary>> ->
{UInt16,2};
<<UInt16:2/little-unsigned-unit:8>> ->
{UInt16,2}
end.
string(Data) ->
Split = binary:split(Data,<<0>>),
case Split of
[String, Rest] ->
{String,byte_size(String)+1};
[String] ->
{String,byte_size(String)+1}
end.
rest(Data) ->
{Data,byte_size(Data)}.
代码不符合语言习惯,有些部分无法按原样编译 :-) 以下是一些评论:
newobj/1
函数引用了未绑定的NewObj
变量。可能真正的代码是NewObj = {obj,rest(...
?代码多次使用list append (
++
)。应尽可能避免这种情况,因为它会执行过多的内存复制。惯用的方法是根据需要多次添加到列表的头部(即:L2 = [NewThing | L1]
)并在最后调用lists:reverse/1
。有关详细信息,请参阅任何 Erlang 书籍或免费学习一些 Erlang。类似地,应该避免使用
lists:nth/2
并用模式匹配或其他方式来构造列表或解析二进制文件Dogbert 关于直接在函数参数中进行模式匹配的建议是一种很好的惯用方法,可以从代码中删除一些行。
关于调试方法的最后建议,考虑用适当的单元测试替换 fwrite 函数。
希望这能为查看内容提供一些提示。请随意将代码更改附加到您的问题,我们可以从那里继续。
编辑
看起来好多了。让我们看看是否可以简化。请注意,我们是倒着做的,因为我们是在编写生产代码后添加测试,而不是进行测试驱动开发。
第 1 步:添加测试。
我还颠倒了列表的顺序,因为它看起来更自然。
-include_lib("eunit/include/eunit.hrl").
happy_input_test() ->
Rest = <<3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 32, 3, 0, 0, 88, 2, 0, 0>>,
Input = <<1, 0,
97, 98, 99, 100, 0,
0, 0,
Rest/binary>>,
Expected = {obj, [{id, 1}, {type, <<"abcd">>}, {parent, 0}, {unparsed, Rest}]},
?assertEqual(Expected, binparse:newobj(Input)).
我们可以 运行 这,除此之外,还有 rebar3 eunit
(请参阅 rebar3 文档;我建议从 rebar3 new lib mylib
开始创建骨架)。
第 2 步:绝对最小值
您的描述不足以理解哪些字段是必填字段,哪些字段是可选字段以及 obj
之后是否总是有更多内容。
在最简单的情况下,所有您的代码可以简化为:
newobj(Bin) ->
<<Id:16/little-unsigned, Rest/binary>> = Bin,
[Type, Rest2] = binary:split(Rest, <<0>>),
<<Parent:16/little-unsigned, Rest3/binary>> = Rest2,
{obj, [{id, Id}, {type, Type}, {parent, Parent}, {unparsed, Rest3}]}.
非常紧凑:-)
我发现字符串的编码非常奇怪:字符串以 NUL 结尾的二进制编码(因此强制遍历二进制文件)而不是用 2 或 4 个字节来表示长度和然后是字符串本身。
第 3 步:输入验证
由于我们正在解析二进制文件,这可能来自我们系统的外部。因此,让它崩溃的理念不适用,我们必须执行完整的输入验证。
我假设所有字段都是必填的,除了 unparsed
,它可以是空的。
missing_unparsed_is_ok_test() ->
Input = <<1, 0,
97, 98, 99, 100, 0,
0, 0>>,
Expected = {obj, [{id, 1}, {type, <<"abcd">>}, {parent, 0}, {unparsed, <<>>}]},
?assertEqual(Expected, binparse:newobj(Input)).
上面的简单实现就通过了。
第 4 步:父格式错误
我们添加测试并做出 API 决定:该函数将 return 一个错误元组。
missing_parent_is_error_test() ->
Input = <<1, 0,
97, 98, 99, 100, 0>>,
?assertEqual({error, bad_parent}, binparse:newobj(Input)).
malformed_parent_is_error_test() ->
Input = <<1, 0,
97, 98, 99, 100, 0,
0>>,
?assertEqual({error, bad_parent}, binparse:newobj(Input)).
我们更改实现以通过测试:
newobj(Bin) ->
<<Id:16/little-unsigned, Rest/binary>> = Bin,
[Type, Rest2] = binary:split(Rest, <<0>>),
case Rest2 of
<<Parent:16/little-unsigned, Rest3/binary>> ->
{obj, [{id, Id}, {type, Type}, {parent, Parent}, {unparsed, Rest3}]};
Rest2 ->
{error, bad_parent}
end.
第 5 步:格式错误的类型
新测试:
missing_type_is_error_test() ->
Input = <<1, 0>>,
?assertEqual({error, bad_type}, binparse:newobj(Input)).
malformed_type_is_error_test() ->
Input = <<1, 0,
97, 98, 99, 100>>,
?assertEqual({error, bad_type}, binparse:newobj(Input)).
我们可能会想按如下方式更改实现:
newobj(Bin) ->
<<Id:16/little-unsigned, Rest/binary>> = Bin,
case binary:split(Rest, <<0>>) of
[Type, Rest2] ->
case Rest2 of
<<Parent:16/little-unsigned, Rest3/binary>> ->
{obj, [
{id, Id}, {type, Type},
{parent, Parent}, {unparsed, Rest3}
]};
Rest2 ->
{error, bad_parent}
end;
[Rest] -> {error, bad_type}
end.
这是一团乱七八糟的东西。仅仅添加功能对我们没有帮助:
newobj(Bin) ->
<<Id:16/little-unsigned, Rest/binary>> = Bin,
case parse_type(Rest) of
{ok, {Type, Rest2}} ->
case parse_parent(Rest2) of
{ok, Parent, Rest3} ->
{obj, [
{id, Id}, {type, Type},
{parent, Parent}, {unparsed, Rest3}
]};
{error, Reason} -> {error, Reason}
end;
{error, Reason} -> {error, Reason}
end.
parse_type(Bin) ->
case binary:split(Bin, <<0>>) of
[Type, Rest] -> {ok, {Type, Rest}};
[Bin] -> {error, bad_type}
end.
parse_parent(Bin) ->
case Bin of
<<Parent:16/little-unsigned, Rest/binary>> -> {ok, Parent, Rest};
Bin -> {error, bad_parent}
end.
这是 Erlang 中带有嵌套条件的经典问题。
第 6 步:恢复理智
这是我的方法,非常通用,适用于(我认为)许多领域。总体思路取自回溯,如 http://rvirding.blogspot.com/2009/03/backtracking-in-erlang-part-1-control.html
中所述我们为每个解析步骤创建一个函数,并将它们作为列表传递给 call_while_ok/3
:
newobj(Bin) ->
Parsers = [fun parse_id/1,
fun parse_type/1,
fun parse_parent/1,
fun(X) -> {ok, {unparsed, X}, <<>>} end
],
case call_while_ok(Parsers, Bin, []) of
{error, Reason} -> {error, Reason};
PropList -> {obj, PropList}
end.
函数call_while_ok/3
在某种程度上与lists:foldl
和lists:filter
相关:
call_while_ok([F], Seed, Acc) ->
case F(Seed) of
{ok, Value, _NextSeed} -> lists:reverse([Value | Acc]);
{error, Reason} -> {error, Reason}
end;
call_while_ok([F | Fs], Seed, Acc) ->
case F(Seed) of
{ok, Value, NextSeed} -> call_while_ok(Fs, NextSeed, [Value | Acc]);
{error, Reason} -> {error, Reason}
end.
这里是解析函数。请注意,他们的签名始终相同:
parse_id(Bin) ->
<<Id:16/little-unsigned, Rest/binary>> = Bin,
{ok, {id, Id}, Rest}.
parse_type(Bin) ->
case binary:split(Bin, <<0>>) of
[Type, Rest] -> {ok, {type, Type}, Rest};
[Bin] -> {error, bad_type}
end.
parse_parent(Bin) ->
case Bin of
<<Parent:16/little-unsigned, Rest/binary>> ->
{ok, {parent, Parent}, Rest};
Bin -> {error, bad_parent}
end.
第 7 步:作业
列表 [{id, 1}, {type, <<"abcd">>}, {parent, 0}, {unparsed, Rest}]
是一个 proplist
(参见 Erlang 文档),早于 Erlang 映射。
查看地图文档,看看 return 地图是否有意义。