文章目录

前言

只需要了解EMQ用到过的语法,以EMQ2.3.11为基础,有任何理解错误请指正。

一、认识HelloWorld

% hello world program
-module(helloworld). 
-export([start/0]). 

start() -> 
   io:fwrite("Hello, world!\n").

%:注释

-module(模块名):定义模块,模块名必须和文件名一致(去掉后缀.erl),否则编译会出错。

-export([函数名/参数个数]) :定义外部可以调用的函数,斜杠后面是参数个数

start():一个函数,名字任意取

io:fwrite:调用io模块的fwrite函数,输出消息到控制台

.:语句结束符

二、数据类型

1、标准数据类型

Erlang有7种标准数据类型,数字、原子、布尔、位字符串、元组、映射、列表

1)数字 number

只有两种,整数integer、浮点数float,例如:40、40.00。

【进制整数】可以用数字+“#”表示2-36的进制整数,例如emqttd_session.erl第824行中16#FFFF

【整数范围】Erlang的则整数精度只受到内存限制,支持大数运算。

测试:

start() ->
  A = 1234567890*99999999999999999999,
  io:fwrite("~B~n",[A]).

  % 输出123456788999999999998765432110

【浮点数的科学计数法】和其他语言类似,用e/E后面带+/-和指数:

start() ->
  A = 3.468e+3,
  io:fwrite("~f~n",[A]).

  % 输出3468.000000

【ASCII】可以用$char来表达ASCII的数字,如$A值为65,$\n值为10。

2)原子 atom

原子是文字,以小写字母开头,后跟字母、数字、下划线、@。也可以使用单引号,就能跟任何的字符,否则会被识别为变量编译报错:


测试:

start() ->
  A = ok,
  B = error,
  C = 'Error',
  D = a@12_3Axf,
  E = 'error ans',
  io:fwrite("~w~n",[A]).

【运算】原子的意义就是文字,只支持比较和赋值运算

【创建】出现即创建,除非系统重启否则不会被清除,单系统最大可用原子数1048576个。

EMQ通常使用原子来定义一些字符串常量,例如emqttd.hrl第43行自定义类型时,取值只能为两个原子:

-type(pubsub() :: publish | subscribe).

emqttd_protocol.hrl第101行定义MQTT报文名称时,使用了原子:

-define(TYPE_NAMES, [
    'CONNECT',
    ……

emqttd_trie.erl第156行删除订阅树边时,返回的原子:

delete_path([]) ->
    ok;

emqttd_trie.erl第82行,从根节点遍历订阅树时,根节点用的原子:

match(Topic) when is_binary(Topic) ->
    TrieNodes = match_node(root, emqttd_topic:words(Topic)),

3)布尔 boolean

伪数据类型,其实是两个保留原子,true、false。

4)位字符串 binary

二进制序列,操作二进制非常方便的数据结构。通常的二进制串是无符号8bit字节序列,但位串可以是任意bit位数。

标准语法:双小于、双大于括起来,用逗号隔开。

<<E1, ..., EN>>

中间的Ei可以是:

Value                                     % 必须是整数、浮点数或另一个位串
Value:Size                             % 指定数据的二进制位数
Value/TypeSpecifierList         % 指定数据的类型
Value:Size/TypeSpecifierList % 指定数据的位数和类型

示例1:<<1,2,3>>、<<12.1,44.45>>、<<$a,$b,$c>>、<<"abc">>

默认的Value,类型可以是整数(8bit)、浮点数(64bit)、位串,自动识别。

整数如果越界(超过8bit表达的十进制数,如256、257),则和其他语言处理类似,进行高位截断(256→0)。

<<"abc">>是<<$a,$b,$c>>的语法糖,在EMQ超级常用,例如emqttd_packet.erl第34行:

protocol_name(?MQTT_PROTO_V5) -> <<"MQTT">>.

示例2:<<1:3, 2:4>>、<<42:12>>

<<1:3, 2:4>>表达式中的位串位数是3+4=7位,二进制被拼接成0b0010010,所以最终得到的值是<<18:7>>。

如果你用<<18:7>> == <<18>>做比较,会得到false,因为默认8位位数不同。

无论<<1:3, 2:4>>还是<<18:7>>哪种表达,它们都是同一段二进制数据,只是截断的位置不一样。  

<<1,17,42:12>>表达式中的位串位数是8+8+12=28位,二进制高位用0填充,整个串为0b 0000 0010 1010,所以最终得到的值是<<2,10:4>>

测试:

start() ->
  A = <<1:3, 2:4>>,
  B = <<18:7>>,
  C = A == B,
  io:fwrite("~w ~w ~w ~n",[A,B,C]).

示例3:<<1024/utf8>>、<<1024:32/unsigned-little-integer>>、<<"abc"/utf8>> 、<<$a/utf8,$b/utf8,$c/utf8>>.

TypeSpecifierList可以控制编码细节,控制参数用横杠隔开:

  • 数据类型(Type):integer、float、bytes/binary、bits/bitstring、utf8、utf16、utf32,其中bytes是binary缩写,bits是bitstring的缩写,作用相同。
  • 符号(Signedness):signed、unsigned,默认无符号。
  • 大小端(Endianness):big、little、native,默认大端。
  • 单位(Unit):unit:1~256,默认整数、浮点数都是1bit为单位,位串8bit为单位。

同样的,<<"abc"/utf8>> 是<<$a/utf8,$b/utf8,$c/utf8>>.的语法糖。

我们可以用来定义常见数据类型,如:

-define( UINT, 32/unsigned-little-integer).
-define( INT, 32/signed-little-integer).
-define( USHORT, 16/unsigned-little-integer).
-define( SHORT, 16/signed-little-integer).
-define( UBYTE, 8/unsigned-little-integer).
-define( BYTE, 8/signed-little-integer).
-define( CHAR, 1/binary-unit:8).

在EMQ中通常被用来定义字符串常量,好处在于,在进行模式匹配的时候速度很快;还被用于解析一些复杂的数据,好处在于,可以控制任意位数,例如emqttd_topic.erl第112行,当在某一层中既含有字符(在validate2已经判断过了),又含有通配符时,判定是非法的格式:

validate3(<<C/utf8, _Rest/binary>>) when C == $#; C == $+; C == 0 ->
    false;

5)元组 tuple

将多个实体组合成一个实体,通常用于参数匹配,可以嵌套任意数据类型,包括元组。

元组定义好就不变了。

标准语法:

A={Term1, ..., TermN}.

参数匹配的含义是:

{A, B, C, _}  =  {apple, banana, orange, hhhhhh}

这样就可以将apple赋值给A、banana赋值给B,orange赋值给C,hhhhhh被丢弃。

在EMQ中也被用来做参数匹配,例如:

% emqttd_protocol.erl第493行,调用函数,发送参数:
    case emqttd_topic:validate({name, Topic}) of
        true  -> ok;
        false -> {error, badtopic}
    end;
% emqttd_topic.erl第91行,接受参数:
validate({filter, Topic}) when is_binary(Topic) ->
    validate2(words(Topic));

6)映射 map

map是k-v映射表。

标准语法:

% 定义map
mymap = #{Key1=>Value1,...,KeyN=>ValueN}
% 获取值
myvalue = maps:get(Key1,mymap)
% 设置数据
maps:put(Value1, Value2, ..., ValueN)
% 更新数据
maps:update(Value1, Value2, ..., ValueN)

例如emqttd_session.erl第397行,更新订阅:

maps:put(Topic, NewQos, SubMap);

7)列表 list

一堆任意类型的数据组合成的表,可以任意增删数据。

标准语法:

[Term1,...,TermN]

使用频率极高,包括像helloworld里面也在使用:

-export([start/0]).

一个重要特性是:[Term1,...,TermN] 等价于 [Term1|[...|[TermN|[]]]]:

[H|T]=[apple, banana]
% H = apple
% T = [banana]

前一个将会匹配独立的元素apple,类型是元素类型,H可以任意取名,这里代表的是Head的含义;

后一个将会匹配整个列表剩余部分(哪怕只剩一个元素),类型是列表,T可以任意取名,这里代表的是Tail的含义。

通常元组被用于递归,例如emqttd_topic.erl第87行到第108行:

validate({_, <<>>}) ->
    false;
validate({_, Topic}) when is_binary(Topic) and (size(Topic) > ?MAX_TOPIC_LEN) ->
    false;
validate({filter, Topic}) when is_binary(Topic) ->
    validate2(words(Topic));
validate({name, Topic}) when is_binary(Topic) ->
    Words = words(Topic),
    validate2(Words) and (not wildcard(Words)).

validate2([]) ->
    true;
validate2(['#']) -> % end with '#'
    true;
validate2(['#'|Words]) when length(Words) > 0 ->
    false;
validate2([''|Words]) ->
    validate2(Words);
validate2(['+'|Words]) ->
    validate2(Words);
validate2([W|Words]) ->
    case validate3(W) of true -> validate2(Words); false -> false end.

第一个validate用元组,因为只是参数传递;

第二个validate2用列表,因为在用递归去不断地匹配Topic按斜杠分割后的每一层的Token。

2、高级数据类型

1)记录 record

记录相当于C语言中的结构体,它是一个伪数据类型,会在编译阶段被转换成元组。

定义记录,通常在.hrl头文件中定义:

-record(Name, {Field1 [= Value1],
               ...
               FieldN [= ValueN]}).

创建记录:

R = #Name{Field1=Expr1,...,FieldK=ExprK}

更新记录(可以只更新部分值):

R2 = R#Name{Field1=Expr1,...,FieldM=ExprM}

读取值(提取法):

#Name.Field

读取值(匹配法):

terminate(_Reason, #state{stats_timer = TRef}) ->
    timer:cancel(TRef),
    ekka:unmonitor(membership).

在EMQ中被大量使用,定义一些数据结构:

% 定义:emqttd.hrl 第148行
-record(mqtt_delivery,
        { sender  :: pid(),          %% Pid of the sender/publisher
          message :: mqtt_message(), %% Message
          flows   :: list()
        }).

% 创建:emqttd_pubsub.erl第89行
delivery(Msg) -> #mqtt_delivery{sender = self(), message = Msg, flows = []}.

% 更新:eqmttd_pubsub.erl第78行
dispatch(To, Delivery#mqtt_delivery{flows = [{route, Node, To} | Flows]});

% 读值:eqmttd_pubsub.erl第72行
route([], #mqtt_delivery{message = #mqtt_message{topic = Topic}}) ->
    dropped(Topic), ignore;

2)进程ID Pid

就是进程的ID,在EMQ对应的使用:

-record(mqtt_session,
        { client_id  :: binary(),
          sess_pid   :: pid(),
          clean_sess :: boolean()
        }).

如果向某个进程发送消息,可以直接用pid+感叹号的形式,如emqttd_pubsub.erl第112行:

SubPid ! {dispatch, Topic, Msg};

消息会放到对应的Mailbox里面。

self() 可以获取自己的进程ID。

3)引用 Reference

引用通过make_ref/0函数来创建,引用在运行时是全局唯一的,常常在创建定时任务时返回一个引用,通过这个引用来取消定时任务。

如EMQ的gen_server2.erl第821行:

Tag = make_ref()

4)函数 function

函数也算是数据类型,函数可以被当作参数去赋值,如EMQ中emqttd_topic.erl第184行:

parse(<<"$local/", Topic1/binary>>, Options) ->
    if_not_contain(local, Options, fun() ->
                       parse(Topic1, [local | Options])
                   end);

对应接收的函数是emqttd_topic.erl第203行:

if_not_contain(local, Options, Fun) ->
    ?IF(lists:member(local, Options), error(invalid_topic), Fun());

三、运算符

1、算数运算符

符号 含义 
 + 加
 - 减
 * 乘
 / 除
 rem 取余(模)
 div 整除


2、关系运算符

符号 含义 备注 
 == 相等 
 /= 不等 注意不等号
 < 小于 
 =< 小于等于 注意先等后小
 > 大于 
 >= 大于等于 
 =:= 恒等于 重点
 =/= 恒不等于 重点

1)恒等于和恒不等于

等于(==)只需要判断数值: 1 == 1.0 → true

恒等于(=:=)除了数值还需要判断数据类型:1 =:= 1.0 → false

不等于(/=)只需要判断数值:1 /= 1.0 → false

恒不等于(=/=)除了数值还需要判断数据类型:1 =/= 1.0 → true

测试:

start() ->
  A = 1 == 1.0,
  B = 1 =:= 1.0,
  C = 1 /= 1.0,
  D = 1 =/= 1.0,
  io:fwrite("1==1.0: ~w~n1=:=1.0: ~w~n1/=1.0: ~w~n1=/=1.0: ~w~n",[A,B,C,D]).

% 1==1.0: true
% 1=:=1.0: false
% 1/=1.0: false
% 1=/=1.0: true

在EMQ中也被使用到,如:

% emqttd_pubsub.erl第77行
Node =:= node()
% emqttd_trie.erl第83行
Name =/= undefined

2)不同数据类型做比较

如果数据类型不同,按照如下的方式比较大小:

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

例如,[]>0 → true,因为list>number

3、位运算符

符号 含义 
 band 按位与
 bor 按位或
 bxor 按位异或
 bnot 按位非

4、逻辑布尔运算符

符号 含义 
 or 或
 and 与
 not 非
 xor 异或
 andalso 短路与
 orelse 短路或

默认逻辑运算符是不短路的,短路与和短路或在EMQ中使用得很频繁,如:

% eqmttd_serializer.erl第43行:
when ?CONNECT =< Type andalso Type =< ?DISCONNECT ->
% emqttd_protocol.erl第427行:
when NullId =:= undefined orelse NullId =:= <<>> ->

四、变量

变量必须以大写字母开头,例如Myvar。

五、函数和递归

函数和递归是Erlang最核心的部分,Erlang没有for循环,没有while循环,全靠函数和递归了。

标准语法:

[Name](Pattern11,...,Pattern1N) [when GuardSeq1] ->
        Body1;
...;
[Name](PatternK1,...,Pa<code>tternKN) [when GuardSeqK] ->
        BodyK.

1)简单函数示例

start() ->
  A = 1<2.

2)多子句函数示例

子句用逗号分割,依次执行:

start() ->
  A = 1<2,
  io:fwrite("~w~n",[A]).

3)带保护序列函数示例

用when关键字在执行函数前判断执行条件,如果为false则这个函数不会被匹配到:

add(X) when X>3 -> 
   io:fwrite("~w~n",[X]). 

start() -> 
   add(4).

4)匿名函数

用fun关键字定义匿名函数,括号里可以带参数如fun(var1,var2),可以被赋值给变量,例如emqttd_client.erl第259行定义了一个匿名函数,并赋值给StatFun,后面就可以把这个StatFun作为参数传递,或者后面加个括号直接作为函数调用:

StatFun = fun() ->
                case Conn:getstat([recv_oct]) of
                    {ok, [{recv_oct, RecvOct}]} -> {ok, RecvOct};
                    {error, Error}              -> {error, Error}
                end
             end

% 作为参数传递:emqttd_keepalive:start(StatFun, Interval, {keepalive, check})
% 直接调用:StatFun()

5)多函数声明执行过程示例

例如EMQ中emqttd_topic.erl第97行:

validate2([]) ->
    true;
validate2(['#']) -> % end with '#'
    true;
validate2(['#'|Words]) when length(Words) > 0 ->
    false;
validate2([''|Words]) ->
    validate2(Words);
validate2(['+'|Words]) ->
    validate2(Words);
validate2([W|Words]) ->
    case validate3(W) of true -> validate2(Words); false -> false end.

在调用validate2之前,已经将主题"/a/b/c/+/d/#"拆分成了list[a,b,c,+,d,#],然后调用validate2。

调用的时候会去比较到底哪个函数的参数类型是匹配的(类似于方法重载的概念),就调用哪个函数。

如果有多个都可能被匹配的函数,则只匹配写在最前面的第一个函数,例如:

validate2(['#']) -> % end with '#'
    true;
validate2(['#']) -> % end with '#'
    false.

则匹配['#']只会返回true。

所以上述逻辑为:

  • 如果为空,返回true,合法主题,
  • 如果为"#"且是最后一个,返回true,合法主题,
  • 如果为"#"且后面还有层级,返回false,非法主题,
  • 如果为空层,继续匹配,
  • 如果为"+",继续匹配,
  • 如果为任意字符串,则调用validate3看里面是否有通配符(如果有则是非法主题,通配符必须独占层级)。

6)递归

如果调用了自己,就会形成递归,例如上述validate2就调用了自己。

六、宏定义

宏定义(Macros)通常在include/xxx.hrl文件里,类似C语言的头文件,语法也类似。

1)宏定义常量示例

% -define(Const, Replacement).
-define(PROTOCOL_VERSION, "MQTT/5.0").

2)宏定义函数示例(支持函数重载)

% -define(Func(Var1,...,VarN), Replacement).
-define(ALLOW_DENY(A), ((A =:= allow) orelse (A =:= deny))).

3)使用宏定义示例

% ?Const
% ?Func(Arg1,...,ArgN)
compile({A, all}) when ?ALLOW_DENY(A) ->
    {A, all};

4)系统预定义宏

% 当前模块名
?MODULE
% 当前模块名,字符串格式
?MODULE_STRING.
% 当前模块的文件名
?FILE.
% 当前行号
?LINE.
% 当前机器名称
?MACHINE.
% 当前函数名称
?FUNCTION_NAME
% 当前函数有多少个参数
?FUNCTION_ARITY
% OTP版本号
?OTP_RELEASE

5)控制宏

和C语言概念一致,多用于debug:

% 如果定义了宏,则
-ifdef(Macro).
% 如果未定义宏,则
-ifndef(Macro).
% 否则
-else.
% 结束控制宏流程
-endif.
% 如果条件为真,则
-if(Condition).
% 否则如果条件2为真,则
-elif(Condition).
% 让宏表现得像没有被定义过一样
-undef(Macro).

七、行为 Behaviour

和behaviour的英语含义一致,就是“表现、行为、外观”,类似java的抽象类,规定了共性的函数操作,所有的实现模块自己实现所有特性的函数操作。

Erlang/OTP有四个经典behaviour:gen_server、gen_fsm、gen_event、supervisor,在前文《EMQ源码分析(二):Erlang相关概念》已经描述过了:

  • gen_server:用于实现C/S结构中的服务端
  • gen_fsm:用于实现有限状态机
  • gen_event:用于实现事件处理功能
  • supervisor:用于实现监督树中的监督进程

例如EMQ中emqttd_router.erl就采用了行为:

-behaviour(gen_server).

八、其他EMQ中常见Erlang语法

1)定义模块名

-module(emqttd_router).

2)定义作者

-author("Feng Lee <feng@emqtt.io>").

3)引入模块或头文件

这样在调用函数就不需要加前缀了。

-include("emqttd.hrl").

4)定义函数声明

在每个函数的前面用-spec编写声明,如emqttd_router.erl第93行:

%% @doc Match Routes.
-spec(match(Topic:: binary()) -> [mqtt_route()]).
match(Topic) when is_binary(Topic) ->
    %% Optimize: ets???
    Matched = mnesia:ets(fun emqttd_trie:match/1, [Topic]),
    %% Optimize: route table will be replicated to all nodes.
    lists:append([ets:lookup(mqtt_route, To) || To <- [Topic | Matched]]).

5)自定义数据类型

-type(mqtt_qos() :: ?QOS0 | ?QOS1 | ?QOS2).

推荐参考资料

1、《Erlang官网英文手册》

2、《易百Erlang教程》


转载请注明出处http://www.bewindoweb.com/257.html | 三颗豆子
分享许可方式知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议
重大发现:转载注明原文网址的同学刚买了彩票就中奖,刚写完代码就跑通,刚转身就遇到了真爱。
你可能还会喜欢
具体问题具体杠