第五章 顺序型编程进阶

5.1 BIF

BIF:Erlang的内建函数, 默认已经导入, 可以直接使用。
相关文档参见:
http://erlang.org/doc/man/erlang.html

5.2 二进制数据

书写和打印时二进制数据以整数或字符序列包在”和’ ‘之间的形式出现。
相比于元组和列表, 二进制更节省内存, 且因系统已对其进行了优化, 使得二进制数据的输入输出更加高效。

list_to_binary(IoList) -> binary()
接收一个列表参数, 将其转换为二进制数据

1> Bin1 = <<1, 2, 3>>.
<<1,2,3>>
2> Bin2 = <<4, 5>>.
<<4,5>>
3> list_to_binary([Bin1, 1, [2, 3, Bin2], 4]).
<<1,2,3,1,2,3,4,5,4>>

split_binary(Bin, Pos) -> {Bin1, Bin2}
接收一个二进制数据和位置参数, 将其分割为两部分

1> split_binary(<<1,2,3,1,2,3,4,5,4>>, 3).
{<<1,2,3>>,<<1,2,3,4,5,4>>}

term_to_binary(Term) -> Bin
将任意Erlang值转化为二进制数据
连同binary_to_term(Bin) -> Term函数即可完成类似java中序列化与反序列化的功能。

1> B = term_to_binary({test, "are", none}).
<<131,104,3,100,0,4,116,101,115,116,107,0,3,97,114,101,
  100,0,4,110,111,110,101>>
2> binary_to_term(B).
{test,"are",none}

size(Bin) -> Int
返回二进制数据的字节数

5.3 比特语法

Erlang为二进制数据的操作提供了比特语法这样便捷的工具, 特别适合与针对协议编程的底层代码。
比如有n个变量, 需要封装在 X bit字长的内存区域中M中, 其中每个变量各占Li bit(L1+L2+…+Ln = X), 则对于M的定义可以写为:
M =X1:L1, X2:L2, X3:L3, …, Xn:Ln

5.3.1 16bit色彩的封包与解包

# 首先定义二进制数据的组成
11> Red = 2.
2
12> Green = 61.
61
13> Blue = 20.
20
14> Mem = <<Red:5, Green:6, Blue:5>>.
<<23,180>>

# 然后即可安装其组成来使用模式匹配获取其中的值
15> <<R1:5, G1:6, B1:5>> = Mem.
<<23,180>>
16> R1.
2
17> G1.
61
18> B1.
20

5.3.2 比特语法表达式

比特语法表达式有如下形式:
E1, E2, …, En
其中Ei具有四种形式。

  • Value
# 即直接赋值
1> E1 = <<18, 52, 86, 120>>.
<<18,52,86,120>>
  • Value:Size
1> Red = 2.
2
2> Green = 61.
61
3> Blue = 20.
20

# 指定每个变量所占的大小, 大小之和需为8的倍数
14> Mem = <<Red:5, Green:6, Blue:5>>.
  • Value/TypeSpecifierList
    TypeSpecifierList是一个形如End-Sign-Type-Unit的列表。
    其中
    End为字节序的方式, 有big、little、native三种取值, 默认为大端序, 与网络序相同, 因此在网络编程时可以不用转换。
    Sign为是否为有符号整数, 有signed、unsigned两种取值, 默认为无符号整数。
    Type为数值类型, 有integer、float、binary三种, 默认为integer
    Unit为字节单位, 默认值依赖于Type。
1> <<16#12345678:32/big>>.
<<18,52,86,120>>
2> <<16#12345678:32/little>>.
<<120,86,52,18>>
3> <<16#12345678:32/native>>.
<<120,86,52,18>>
4> <<16#12345678:32/native-signed>>.
<<120,86,52,18>>
5> <<16#12345678:32/native-signed-float>>.
<<180,162,145,77>>

5.3.3 高级比特语法样例

在MPEG数据中查找同步帧
MPEG数据由帧组成, 每一帧又包括帧头和数据, 其中帧头共32bit=4字节。
AAAAAAAA AAABBCCD EEEEFFGH IIJJKLMM

组成 含义 取值
AAAAAAAA AAA 同步字 全1
BB MPEG版本号 00为MPEG2.5, 01为保留, 10为MPEG2, 11为MPEG1
CC Layer索引 00为保留, 01为Layer III, 10为Layer II, 11为Layer I
D 保护位 0为用16位的CRC保护下面的帧头, 1为无CRC
EEEE 比特率索引 参见MPEG比特率索引表
FF 采样率索引 参见MPEG采样率索引表
G 填充位, 如果设置此位就会对每帧数据填充一个slot 0

查找三个连续的帧头

    %% 从给定的二进制数据Bin中的第N位开始查找头
    find_sync(Bin, N) ->
        case is_header(N, Bin) of
            %% 1. 如果是头则跳过帧长继续查找
            {ok, Len1, _} ->
                case is_header(N+Len1, Bin) of
                     %% 2. 如果是头则跳过帧长继续查找
                    {ok, Len2, _} ->
                        case is_header(N+Len1+Len2, Bin) of
                            %% 3. 如果是头则查找完毕, 输出结果 
                            {ok, _, _} ->{ok, N};
                            %% 3. 如果不是头则查找下一位 
                            error      ->find_sync(Bin, N+1)
                        end;
                    %% 2. 如果不是头则查找下一位 
                    error ->find_sync(Bin, N+1)
                end;
            %% 1. 如果不是头则查找下一位 
            error ->find_sync(Bin, N+1)
        end.
  • 从二进制数据指定位置读取32bit以用于分析
    %% 使用split_binary函数将Bin从第N位分成两部分
    %% 对第二部分进行模式匹配, 获取4字节共32bit的数据, 存储到变量C
    get_word(N, Bin) ->
        {_, <<C:4/binary, _/binary>>} = split_binary(Bin, N), C.

    %% 对获取到的数据调用unpack_header判断是否为头
    is_header(N, Bin) ->
        unpack_header(get_word(N, Bin)).

    %% 对数据按照MPEG头进行解码, 如果不匹配则通过catch抛出异常 
    unpack_header(X) ->
        try decode_header(X)
        catch
            _:_ ->error
        end.
  • 对MPEG的头帧进行分析
    %% 首先在参数中指定了MPEG的头帧格式, 不匹配的数据将会执行decode_header(_) -> exit(badHeader)
    decode_header(<<2#11111111111:11, B:2, C:2, _D:1, E:4, F:2, G:1, Bits:9>>) ->
        %% 获取版本号
        Vsn = case B of
                  0 ->{2, 5};
                  1 ->exit(badVsn);
                  2 ->2;
                  3 ->1
              end,
        %% 获取Layer索引
        Layer = case C of
                    0 ->exit(badLayer);
                    1 ->3;
                    2 ->2;
                    3 ->1
                end,
        %% 根据版本、Layer、比特率索引计算比特率
        BitRate = bitrate(Vsn, Layer, E) * 1000,

        %% 根据版本、采样率索引计算采样率 
        SampleRate = samplerate(Vsn, F),

        %% 填充位 
        Padding = G,

        %% 根据Layer、比特率、采样率、填充位计算帧长 
        FrameLength = framelength(Layer, BitRate, SampleRate, Padding),
        if
            %% 获取了32bit数据进行分析, 刨去11位同步字, 帧长不可能小于21
            FrameLength < 21 ->exit(frameSize);
            true ->{ok, FrameLength, {Layer, BitRate, SampleRate, Vsn, Bits}}
        end;
    decode_header(_) ->exit(badHeader).
  • 计算比特率的函数
    参见[MPEG比特率索引表][MPEG]
    bitrate(_,_,15) ->exit(1);
    bitrate(1,1,E) ->            
        element(E+1, {free,32,64,96,128,160,192,224,256,288,320,352,384,416,448});
    bitrate(1,2,E) ->
        element(E+1, {free,32,48,56,64,80,96,112,128,160,192,224,256,320,384});
    bitrate(1,3,E) ->
        element(E+1, {free,32,40,48,56,64,80,96,112,128,160,192,224,256,320});
    bitrate(2,1,E) ->
        element(E+1, {free,32,48,56,64,80,96,112,128,144,160,176,192,224,256});
    bitrate(2,2,E) ->
        element(E+1, {free,8,16,24,32,40,48,56,64,80,96,112,128,144,160});
    bitrate(2,3,E) ->bitrate(2,2,E);
    bitrate({2,5}, L, E) ->bitrate(2, L, E).
  • 计算采样率的函数
    参见[MPEG采样率索引表][MPEG 1]
    samplerate(1, 0) ->44100;
    samplerate(1, 1) ->48000;
    samplerate(1, 2) ->32000;
    samplerate(2, 0) ->22050;
    samplerate(2, 1) ->24000;
    samplerate(2, 2) ->16000;
    samplerate({2,5}, 0) ->11025;
    samplerate({2,5}, 1) ->12000;
    samplerate({2,5}, 2) ->8000.

解包COFF数据
Erlang定义宏的方式

    %% 定义DWORD为32位的无符号小端序整数
    -define(DWORD, 32/unsigned-little-integer).

    %% Erlang中的自定义类型 
    unpack_image_resource_directory(Dir) ->
        <<Characteristics : ?DWORD,
          ...
        >>

    %% 解析数据的方式
    <<ImageFileRelocsStripped:1, ImageFileExecutableImage:1, ...>> = <<Characteristics:32>>

从一个IPv4数据报中解析头
同上例, 仍然使用模式匹配的方式来解析已知其结构的数据

5.4 小问题集锦

5.4.1 apply

动态调用某个模块的某个函数

1> apply(erlang, atom_to_list, [hello]).
"hello"

5.4.2 属性

  • 预定义模块属性
    定义模块名
-module(modname)

导入某个模块的某个函数

-import(Mod, [Name1/Arity1, Name2/Arity2, …]).

导出某个函数

-export([Name1/Arity1, Name2/Arity2, …]).

编译选项

-compile(Options)

模块版本

-vsn(Version)
  • 用户定义属性
    语法规则为 -SomeTag(Value), 其中SomeTag为原子, Value为文字项。

5.4.3 块表达式

begin
    Expr1,
    ...
    ExprN
end.

5.4.4 布尔类型

Erlang没有专门的布尔类型, 其中true、false为赋予了布尔语义的原子。

5.4.5 布尔表达式

含义 表达式
逻辑非 not B1
逻辑与 B1 and B2
逻辑或 B1 or B2
逻辑异或 B1 xor B2

5.4.6 字符集

Erlang默认字符集为ISO-8859-1(Latin-1), 因为其字符串实质是整数列表, 因此对于Unicode字符的解析和生成存在一定的限制。

5.4.7 注释

Erlang注释以”%”开始, “%%”可以在Emacs中被识别以实现自动缩进。

5.4.8 epp

epp预处理程序将会扩展源文件中的宏以及插入包含文件。

5.4.9 转义符

转义符 含义 整数值
\b 退格 8
\d 删除 127
\e 转义 27
\f 换页 12
\n 换行 10
\r 换行 13
\s 空格 32
\t 制表符 9
\v 纵向制表符 11
\NNN \NN \N 八进制码(N是0-7)
a…z or A…Z Ctrl+A到Ctrl+Z 1~26
\’ 单引号 39
\”” 双引号 34
\ 反斜杠 92
\C C的ASCII码(C为字符) 整数

5.4.10 表达式和表达式序列

可以求值的都为表达式, 使用逗号分隔的多个表达式称为表达式序列。

5.4.11 函数引用

本地引用

fun LocalFunc/Arity

模块引用

fun Mod:RemoteFunc/Arity

5.4.12 包含文件

包含记录文件

-include(FileName)

包含库文件

-include_lib(FileName)

5.4.13 列表操作符++和–

  • 列表操作
    A++B表示两个列表组合成一个新的列表
    A–B表示从A中删除B中出现的元素

  • 模式匹配
    匹配以某个字符串开头的字符串

5.4.14 宏

  • 宏的定义
-define(Constant, Replacement).
  • 宏的扩展
?Constant
  • 获取模块信息的预定义宏
?FILE :: 当前文件名
?MODULE :: 当前模块名
?LINE :: 当前行号
  • 宏的流程控制
取消宏定义 :: -undef(Macro)
是否已定义 :: -ifdef(Macro)
是否未定义 :: -ifndef(Macro)
  • else
    只能在ifdef或ifndef之后出现

  • endif
    标记ifdef或ifndef的结束语句

5.4.15 在模式中使用匹配操作符

因为Erlang中变量只能一次赋值, 因此使用匹配操作符可以在需要重复使用参数时提供一种高效的实现。

5.4.16 数值类型

  • 整型
    传统语法
    即直接赋整数值

  • K进制整数
    即以”K#Digits”的形式表示

  • $语法
    即获取字符的ASCII码对应的数值

  • 浮点型
    浮点数由5部分组成: 可选符号位、数值部分、小数点、小数部分、可选指数部分

5.4.17 操作符优先级

5.4.18 进程字典

Erlang的每个进程都有自己的私有数据存储, 即进程字典, 因为对其修改具有副作用, 因此不建议使用, 除非键值对只绑定一次。

  • 插入值
put(Key, Value) -> OldValue
  • 获取键值
get(Key) -> Value
  • 获取整个字典
get() -> [{Key, Value}]
  • 获取值为Value的Key列表
get_keys(Value) -> [Key]
  • 获取值并删除
erase(Key) -> Value
  • 删除整个字典
erase() -> [{Key, Value}]

5.4.19 引用

创建引用用于建立唯一标示, 使用erlang:make-ref()实现。

5.4.20 短路布尔表达式

使用短路布尔表达式可不必对每个判断字句逐一求值。
布尔表达式的”or”和”and”, 分别有”orelse”和”andalso”与之对应。

5.4.21 比较表达式

Erlang中各种类型的大小顺序:

number < atom  < reference < fun < port < pid < tuple < list < binary

5.4.22 下划线变量

使用_VarName来声明常规变量, 主要用于:

  1. 命名一个不准备使用的变量
  2. 为了方便进行调试
上一篇:开发直播App对多协议编解码的支持


下一篇:音视频相关基础知识