Erlang中的二进制协议解析

时间:2017-03-12 05:13:15

标签: parsing binary erlang network-protocols

我在从二进制消息中提取字段时有点挣扎。原始邮件如下所示:

<<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分开,感觉很笨 - 所以有更好的方法吗?我认为我不能使用静态匹配,因为“未解析”和“类型”部分具有任意长度,因此不可能定义它们各自的大小。

谢谢!

---------------更新-----------------

尝试将以下评论纳入考虑范围 - 代码现在如下所示:

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)}.

1 个答案:

答案 0 :(得分:3)

代码是非惯用的,有些部分无法编译:-)以下是一些评论:

  • 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后是否还有其他字段。

在最简单的情况下, all 您的代码可以简化为:

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决定:该函数将返回错误元组。

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/3lists: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地图。

查看地图文档,看看是否有意义返回地图。