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:foldllists: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 地图是否有意义。