erlang解析协议

Table of Contents

erlang虽然是一门相对比较冷门的语言,但是它有一个功能我非常喜欢。它有一种叫bit syntax的语法,可以用声明式的写法来解析二进制报文中的字段,非常简单易用,我平时写一些协议相关的demo,最喜欢用它来做这种协议解析的活。

语法

Value:Size/TypeSpecifierList

Value可以是任何表达式,Size*TypeSpecifierList 就是二进制的位数,实际使用可以省略Size或者TypeSpecifier,也可以两者都不写,就像下面这样。

Value
Value:Size
Value/TypeSpecifierList

例子

A = 1, B = 17, C = 42,
Bin2 = <<A, B, C:16>>
<<D:16, E, F/binary>> = Bin2

这是一个bit syntax的二进制模式匹配写法,用<<>>的符号括起来。D:16是Value:Size的写法,表示从二进制数据中获取16位,F/binary则是Value/TypeSpecifierlist的写法,E和F会获取后面两个8位字节。

A = 1, B = 17, C = 42,
Bin2 = <<A, B, C:16>>

这部分代码,生成了[1, 17, 00, 42],也就是

1 17 00 42

这样的字节流,然后用

<<D:16, E, F/binary>> = Bin2

去匹配这个字节流。D会匹配到1 17这前两个字节,E和F则匹配到了00 和 42,以此来达到协议解析的目的。

这就是erlang的bit syntax, 可以像填写表格一样来解析二进制协议

demo1 解析二进制报文

创建一个名为 ip_parser.erl 的文件,用来解析ip头:

-module(ip_parser).
-export([parse_ip_datagram/1, demo/0]).

-define(IP_VERSION, 4).
-define(IP_MIN_HDR_LEN, 5).

%% 解析 IP 数据报的函数
parse_ip_datagram(Dgram) ->
    DgramSize = byte_size(Dgram),
    case Dgram of
        <<?IP_VERSION:4, HLen:4, SrvcType:8, TotLen:16,
          ID:16, Flgs:3, FragOff:13,
          TTL:8, Proto:8, HdrChkSum:16,
          SrcIP:32, DestIP:32, RestDgram/binary>> when HLen >= ?IP_MIN_HDR_LEN, 4 * HLen =< DgramSize ->
            OptsLen = 4 * (HLen - ?IP_MIN_HDR_LEN),
            <<Opts:OptsLen/binary, Data/binary>> = RestDgram,
            {ip_header, HLen, SrvcType, TotLen, ID, Flgs, FragOff, TTL, Proto, HdrChkSum, SrcIP, DestIP, Opts, Data};
        _ ->
            {error, invalid_datagram}
    end.

%% 演示函数
demo() ->
    %% 构造一个示例 IP 数据报
    %% IP 版本 4, 头部长度 5, 服务类型 0, 总长度 20, ID 54321, 标志 0, 片偏移 0,
    %% TTL 64, 协议 TCP, 头部校验和 0, 源地址 127.0.0.1, 目的地址 192.168.0.1
    Datagram = <<4:4, 5:4, 0:8, 20:16, 54321:16, 0:3, 0:13, 64:8, 6:8, 0:16,
                 127, 0, 0, 1, 192, 168, 0, 1,
                 "Hello, world!">>,
    io:format("Parsing Datagram: ~p~n", [Datagram]),
    Result = parse_ip_datagram(Datagram),
    io:format("Result: ~p~n", [Result]),
    Result.

编译运行

$ erlc ip_parser.erl
$ erl
1> ip_parser:demo().
Parsing Datagram: <<69,0,0,20,212,49,0,0,64,6,0,0,127,0,0,1,192,168,0,1,72,101,
                    108,108,111,44,32,119,111,114,108,100,33>>
Result: {ip_header,5,0,20,54321,0,0,64,6,0,2130706433,3232235521,<<>>,
                   <<"Hello, world!">>}
{ip_header,5,0,20,54321,0,0,64,6,0,2130706433,3232235521,
           <<>>,<<"Hello, world!">>}

demo2 从bin文件读取解析

把上面的代码修改一下 ip_parser_from_file.erl

-module(ip_parser_from_file).
-export([parse_ip_datagram/1, demo/0]).

-define(IP_VERSION, 4).
-define(IP_MIN_HDR_LEN, 5).

%% 解析 IP 数据报的函数
parse_ip_datagram(Dgram) ->
    DgramSize = byte_size(Dgram),
    case Dgram of
        <<?IP_VERSION:4, HLen:4, SrvcType:8, TotLen:16,
          ID:16, Flgs:3, FragOff:13,
          TTL:8, Proto:8, HdrChkSum:16,
          SrcIP:32, DestIP:32, RestDgram/binary>> when HLen >= ?IP_MIN_HDR_LEN, 4 * HLen =< DgramSize ->
            OptsLen = 4 * (HLen - ?IP_MIN_HDR_LEN),
            <<Opts:OptsLen/binary, Data/binary>> = RestDgram,
            {ip_header, HLen, SrvcType, TotLen, ID, Flgs, FragOff, TTL, Proto, HdrChkSum, SrcIP, DestIP, Opts, Data};
        _ ->
            {error, invalid_datagram}
    end.

%% 演示函数
demo() ->
    {ok, DatagramBin} = file:read_file("datagram.bin"),
    io:format("Parsing Datagram: ~p~n", [DatagramBin]),
    Result = parse_ip_datagram(DatagramBin),
    io:format("Result: ~p~n", [Result]),
    Result.

构建一个datagram.bin,用十六进制编辑器编辑下面内容:

45000028000040008006a3fe7f000001c0a80001
48656c6c6f2c20776f726c6421

编译运行

$ erlc ip_parser_from_file.erl
$ erl
1> ip_parser_from_file:demo().
Parsing Datagram: <<69,0,0,40,0,0,64,0,128,6,163,254,127,0,0,1,192,168,0,1,72,
                    101,108,108,111,44,32,119,111,114,108,100,33>>
Result: {ip_header,5,0,40,0,2,0,128,6,41982,2130706433,3232235521,<<>>,
                   <<"Hello, world!">>}
{ip_header,5,0,40,0,2,0,128,6,41982,2130706433,3232235521,
           <<>>,<<"Hello, world!">>}