前言
只需要了解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).