且听疯吟 如此生活三十年

6. 编译并运行程序

  • (略)

7. 并发

  • 机制
    • Erlang 程序由成百上千个 Process 组成,这些 Process 之间可以互发消息
    • Process 能否接收到或者理解消息是不确定的,要知道结果必须向该 Process 发送消息询问并等待
    • Process 之间可以互相 Link ,当 Process 消亡时与之相连的 Process 会收到消息

8. 并发编程

  • 并发原语

    • Pid = spawn(Module, FuncName, Args)
      创建一个新的 Process,用于对 Func 求值,并返回该 Process 的 pid
    • Pid ! Message
      • 向指定 Pid 的 Process 发送消息,返回值为 Message 本身
      • 消息发送是异步的,无需等待即可进行其他操作
    • receive ... end
      • 接收一个发给当前进程的消息
    • self()
      获取自己的 pid
  • 注册进程

    • register(AnAtom, Pid)
    • unregiseter(AnAtom)
      进程死亡时会自动 unregiseter
  • 编写并发程序的一般模式

    -module (concurrency).
    -compile(export_all).
    
    start() ->
        spawn(?MODULE, loop, []).
    
    rpc(Pid, Request) ->
        Pid ! {self(), Request},
        receive
             {Pid, Response} ->
                 {Pid, Response}
         end.
    
    loop(X) ->
        receive
            {From, Any} ->
                From ! {self(), "Received!"},
                io:format("Received: ~p~n", [Any]),
                loop(X)
        end.
    
    %% run in erlang shell
    Pid = concurrency:start().
    %% <0.33.0>
    concurrency:rpc(Pid, "test!")
    %% Received: "test"
    %% {<0.56.0>,"Received!"}
    

4. 异常

  • 抛出异常

    • 显式的 exit(Why)
      • 终止当前 Process
      • 如果当前 Process 未捕获这个异常,则系统会向所有 link 该 Process 的 Process 广播 {'EXIT', Pid, Why} 消息
    • 显式的 throw(Why)
      • 抛出供其调用者捕获的异常
      • 最好是添加注释说明抛出的异常
      • 如何处理由调用者选择,包括忽略
    • 系统错误 erlang:error(Why)
  • try..catch 捕获异常

    • try/catch 本身会消耗一点性能

    • 推荐的 try/catch 风格

      %% 首先对 FuncOrExpressionSequence 求值
      %% 如果没有产生异常则顺序进行 Patterm 匹配, 匹配成功后执行后面的表达式
      %% 如果有异常抛出, 则顺序匹配 ExPattern(ExceptionType 是 throw、exit、error 中的一个, 默认为 throw)
      %% after 块中的代码用于清理工作,绝对会执行
      %% after 可以省略
      try FuncOrExpressionSequence of
          Pattern1 [when Guard1] ->Expressions1;
          Pattern2 [when Guard2] ->Expressions2;
          ...
      catch
          ExceptionType: ExPattern1 [when ExGuard1] ->ExExpressions1;
          ExceptionType: ExPattern2 [when ExGuard2] ->ExExpressions2;
          ...
      after
          AfterExpressions
      end.
      
  • 栈跟踪

    • 在触发异常的时候可以调用 erlang:get_stacktrace/0 来查看最近的栈跟踪信息,可以获得异常函数的调用路径(尾递归调用除外)

      try func()
      catch
      	error: X ->
      		{X, erlang:get_stacktrace()}
      end.
      

5. 顺序型编程进阶

  • BIF (built-in function)

    • binary

      • 相对于 List 活着 tuple , binary 更节省内存,输入输出更为高效

      • binary_to_term/term_to_binary 可将任何的 binary 和 Erlang 值(List、atom、tuple 甚至 binary 等)转换成为 binary,但是这个 binary 是以所谓的 「外部数据格式」 存储的,类似于其他语言的序列化与反序列化)

        A = <<1,2,3>>.
        B = term_to_binary(A).
        %% <<131,109,0,0,0,3,1,2,3>>
        
      • binary 中的数字,每一个都在 0~255 之间(因为每一个数字表示一个 byte,其值在 0000 0000 ~ 1111 1111 [0 ~ 29 -1] 之间)

      • GB2312 的汉字转成 UTF-8 时,绝大多数是 3 个字节

        A = <<"且听疯吟"/utf8>>.
        %% <<228,184,148,229,144,172,231,150,175,229,144,159>>
        
    • bit 语法

      • 封包与解包

        • 以 RGB 色彩为例:
          创建一个 16 bit (2 byte)的内存块,存放一个 RGB 三元组,并为 Red 分配 5 bit,为 Green 分配 6 bit,为 Blue 分配 5 bit

          Red = 2,
          Green = 60,
          Blue = 29,
          RGB = <<Red:5, Green:6, Blue:5>>.
          %% <<23, 157>>
          <<R1:5, G1:6, B1:5>> = RGB.
          {R1, G1, B1}.
          %% {2, 60, 29}
          
        • 注意取值范围。比如我们为 Blue 分配了 5 bit,但是为 Blue 赋值 61,转换成二进制为 111101,超过了 5 bit,则会发生截断,取低五位,即实际值为 29

          integer_to_list(61,2).
          %% "111101"
          integer_to_list(29,2).
          %% "11101"
          
          <<2:5,60:6,61:5>> == <<2:5,60:6,29:5>>.
          %% true
          
      • bit 语法表达式
        • 形如 <<E1, E2, E3, ... , En>>
        • 每一个元素都是二进制数据中的一个区块
        • 元素的值表示有下面几种方式
          • Value
          • Value:Size
          • Value/TypeSpecifierList
          • Value:Size/TypeSpecifierList
        • 其中 TypeSpecifierList 是形如 End-Sign-Type-Unit 组成的字符串,其中不同的 Type 选项如下,每一个都可以忽略且没有顺序要求:
          • End = big / native / little
            决定字节序,默认 big
          • Sign = signerd / unsigned
            仅用于模式匹配,默认 unsigned
          • Type = integer/ float / binary
            默认 integer
          • Unit = 1 / 2 / 3 / ... / 255
            • 整个区块长度为 Size * Unit,其值必须大于或等于 0 且是 8 的倍数
            • 其值依赖于 Type,如果 Type 是 integer 或者 float,则值为 1 ,是 binary 则值为 8
      • binary 模式匹配
    • apply

      • apply(Module, Func, [Arg1, Arg2, ... , Argn])
      • 对于参数个数已知的函数,M:Func([Arg1, Arg2, ... , Argn]) 的调用优于 apply
      • 使用 apply 调用的函数编译器无法优化,同时许多分析工具也无法分析其细节
      • 如无必要尽量少用 apply
    • 属性

      • module
      • import
      • export
      • compile
        • 编译器选项,常用的 -compile(export_all).
      • vsn
        无语法意义,用于文档分析
      • 用户定义属性 -SomeTag(Value)
        • 该值会被编译进模块
        • 该值可以在运行时使用 attrs:module_info() 提取
        • beam_lib 提供了一系列不加载模块代码就能分析的函数
    • 块表达式
      把多个表达式组织为单个表达式

      begin
      	Expression1,
      	Expression2,
      	...
      	ExpressionN
      end
      
    • 布尔

      • Erlang 中不存在布尔类型,通常使用原子 true/false 代替作为布尔符号使用
      • 布尔表达式:and / not / or / xor
    • 字符集

      • Erlang 默认编码为 ISO-8859-1 (Latin-1)
      • Erlang 内部没有字符串数据类型,而是用一串整数列表表示
      • 因此要注意 Unicode 字符的处理
      • 使用 unicode 模块中如 unicode:characters_to_list 的函数处理 unicode 字符
    • epp

      • Erlang 预处理器,扩展宏,插入必须的包含文件等
    • 转义字符

    • 表达式/表达式序列

      • 表达式序列的值为序列中最后一个表达式的值
    • 函数引用

    • 包含文件

      • include
      • include_lib
    • 列表操作符

      • ++
        • 列表添加 ListA ++ ListB
          即把 ListB 附加到 ListA 尾部
        • 模式匹配中的 ++
          f("begin" ++ T) -> ... 即相当于模式匹配 [$b, $e, $g, $i, $n | T]
      • --
        列表删除 ListA -- ListN ,即从 ListA 中删除 ListB 中的所有元素,若元素 E 在 B 中重复出现 k 次,则只会从 ListA 中按顺序删除 k 个 E
      • 定义
        • 不带参数 -define(Macro, ...)
        • 带参数 -define(Macro(X,Y), ...)
        • 预定义宏
          • ?FILE 当前文件名
          • ?MODULE 当前模块名
          • ?LINE 当前行号
      • 宏的流程控制
        • -undef(Macro) 取消宏定义,在此语句后无法使用该宏
        • ifdef / ifndef / else / end
        • 结合调试标志,扩展 debug 日志输出等
    • 数值类型

      • 整型
        • 整型的计算是精确的
        • 其计算结果代表的数据长度只受限于可用内存
        • 表示法
          • 传统表示法,1, 1989, -1337
          • K 进制整数,2#01001011,16#fe34
      • 浮点型
        • 内部以 IEEE 754 的 64 bit 格式表示
    • 操作符优先级

    • 进程字典

      • 每一个 Erlang Process 都有自己的进程字典

      • 实际上提供的功能类似于全局变量

      • 由于不是非破坏性赋值,会导致代码有副作用

      • 尽量少用

      • 添加/获取进程字典

        @spec put(Key, Value) -> OldValue | undefined.
        @spec get(Key) -> Value | undefined.
        @spec get() -> [{Key, Value}].
        ...
        
    • 引用
      使用 BIF erlang:make_ref() 创建引用

1. Erlang 优势

  1. 并发和分布式
    • 主流语言使用共享内存模型,类似于 x = x + n 的代码导致了在多核环境下需要小心的处理锁的问题
    • Erlang 使用消息模型,Process 间不共享数据,从而避免了锁的问题
    • 无锁避免了顺序瓶颈,添加节点到网络更容易
  2. 错误处理
    • 多数语言默认认为程序不会出错
    • Erlang 采用不同的设计决策——注定要出错,那就让他出错,出错后恢复就行了。即 Erlang 程序出错后,会交由更高级的 Process 来处理(重启 Child Process、系列全部终止、重启相关 Process 等等),从而实现对错误的分级和容错处理
    • 同时带来了热更新的好处,进一步保证了可用性

2. 入门

  • Shell

    • f(). 会释放所有绑定的变量
    • 崩溃文件分析
      webtool:start().
  • 原子

    • 使用单引号括起来的字符也是原子

    • 这使得原子可以以大写字母开头,或者带有空格

      'a' = a.  %a
      'Monday'.
      'an atom with spaces'.
      
  • 列表

    • 可以包含不同类型

    • 访问列表的头是高效的,所以通常函数处理也从列表头取起

    • 插入元素到列表头部是高效的

      A = ["a","b"].
      C = ["c","d" | A ]. % ["c","d","a","b"]
      
      • 尽量避免使用 List ++ [H] 这样的操作,通常情况下添加元素到列表尾部是极为低效的(重新生成新的列表),只有在列表非常短的时候可以这样用
      • 添加在头部然后使用 lists:reverse/1 反转通常比添加在列表尾部效率要高
      • 尽量使用经过高度优化的 BIF ,比如反转列表 lists:reverse/1,可以从源码中找到它的定义,但是这个定义通常是作为简单的声明,实际上编译器会使用这个函数在系统内部更为高效的版本
  • 字符串

    • 严格说来 Erlang 中并没有字符串

    • 字符串实际上是整数列表的一种「速记/代表」形式

    • 可用 $ 来表示字符串的整数值

      A = $a.  % 97
      

3. 顺序型编程

  • beam
    beam 是 Bogdan’s Erlang Abstract Machine 的缩写

  • 匿名函数 fun

    • fun 也可以拥有多个子句

      TempFun = fun ({square, X}) -> X*X;
      	          ({double,X}) -> X+X
        	  end.
      
  • 列表解析

    • 列表解析实现最简单的 Map

      map(F, L) = [F(X) || X <- L].
      
    • 列表解析中的生成器实际上也可以起到过滤的作用

      [ X || {X, _} <- [ a,{c, d}, "aa", {"c", e}]].
      % [ c, "c"]
      
    • 快排算法

      qsort([]) ->
          [];
      qsort([Pivot | T]) ->
      			qsort([X || X <- T, X < Pivot])
      			++ [H] ++
      			qsort([X || X <- T, X >= Pivot]).
      
    • 毕达哥拉斯三元组

      pythag(N) ->
      	[{A, B, C} || A <- lists:seq(1, N),
      			  B <- lists:seq(1, N),
      			  C <- lists:seq(1, N),
      			  A + B + C =< N,
      			  A*A + B*B =:= C*C
      	].
      
    • 全排列

      perms([]) ->
      	[[]];
      perms(List) ->
      	[ [H | T] || H <- L, T <- perms(L -- [H]) ].
      
  • 断言

    • 以 when 开头

    • ; 隔开的断言,只要有一个为 true 则断言序列成立

    • , 隔开的断言,只有全部为 true 断言才会成立

    • 合法的断言必须保证没有副作用,可以包含一些无副作用的 BIF ,但无法使用用户自定义的布尔表达式函数

    • and, orandalso, orelse
      Erlang Guard: and/andalso, or/orelse

    • ===:=/==/=

      • =:= 代表精确等于,在比较的时候不会对数据类型进行转换

      • =/= 代表精确不等于

      • 99% 的情况下应使用 =:==/=

      • 模式匹配中实际上是 =:=,即 f(12) 不会匹配到 f(12.0)

        1 == 1.0. % true
        1 =:= 1.0 %false
        
    • 比较运算符的优先级

      • 不同数据类型也可以比较大小,其优先级是:

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

        <<"1">> > ["2"].   % true
        ["2"] > {"3"}. % true
        {"3"} > 'atom'. % true
        atom > 1. % true
        
  • record

    • Record 实际上只是 tuple 的「伪装」,其本质上是一个 tuple
  • case/if

    • Erlang 中不存在多个 case 匹配一个执行块的语法
    • Erlang 中任何表达式都是有值的,包括 case,因此可以有 X = case ... end
    • Case 最后使用 _ , if 最后使用 true 是保证所有分支得到匹配的方法
  • 累加器

    • 存储迭代过程中的临时容器
Premature optimization is the root of all evil. -- D.E. Knuth
过早的优化是万恶之源. -- D.E. Knuth

渣翻译,且作读书笔记 :)
原文:The Eight Myths of Erlang Performance


迷思 1:Funs 很慢

funs 曾经是比较慢,不,应该说是特别慢,甚至比那个 apply/3 还慢。因为以前我们都是用一堆的语法糖啦,普通的元组啦,还有 apply/3 啦加上我们的奇技淫巧来实现的。
不过这些都是老黄历了,在 R6B 我们给了它专有的数据类型,并且在 R7B 做了更牛逼的优化,现在它的调用消耗已经降低到
本地调用和 apply/3 之间了。

不靠谱的 Note :

  1. 这里的 funs 应该是包含了 anonymous function 和 F = fun FunctionName/Arity; F(Arg1, Arg2, ..., Argn) 这些调用方式,在调用效率上这两种应该是基本等价的
  2. 在 OTP R5 和更早之前的版本中,funs 使用元组来表示,在之后的版本中有了专有的数据结构和优化1
  3. 对于参数个数已知的函数,M:F([Arg1, Arg2, … , Argn]) 的调用优于 apply
  4. 使用 apply 调用的函数编译器无法优化,同时许多分析工具也无法分析其细节2

迷思 2:列表推导很慢

列表推导曾经是用 funs 来实现的,当然参照第一点,你懂的。
现在编译器把列表推导重写成一个普通的递归函数。当然,写成尾递归加反转的方式看起来会更快。真的吗?我们下回分解。

迷思 3:尾递归函数快于普通递归函数

我们都知道普通递归函数会在堆栈上留下一堆可能不再使用的数据,而垃圾回收器没有这么聪明,它会不停的拷贝这些数据。而尾递归中这些数据会很快消亡。

在 R7B 之前这么说没错。在 R7B 中,编译器会把从来不会被用到的数据引用重写成空列表,这样垃圾回器收器就不需要做无用功了。

当然即使是在优化过后,尾递归函数还是在大多数时候还是比一个普通递归要快。
实际上这跟每次递归调用中消耗的堆栈空间有关。一般来说普通递归每次消耗的堆栈空间比尾递归所分配的堆空间要多。更多的内存消耗意味着更多的垃圾回收,以及更多的堆栈遍历。

在 R12B 以及后续版本中我们又进行了优化,减少了递归调用需要的堆栈空间,普通递归和一个尾递归需要得内存空间在大部分情况下都差不多了。lists:map/2lists:filter/2,列表推导,和其他一些普通递归函数占用的内存空间和尾递归实现一样了。

回到我们的老问题,哪一个更快?

看情况。在 Solaris/Sparc 上,普通递归看起来稍微快那么一点点,即使对于那些有很多元素的列表。在 x86 架构上,尾递归要比普通递归快近 30%。

剩下的就是个人口味的选择了。如果需要追求极限的速度,就必须要自己衡量了。你再也不能拍着胸脯说尾递归在所有情况下都快于普通递归。

注意:一个尾递归函数,如果最后的结果不需要 lists:reverse/1,显然比一个递归函数要快,因为尾递归函数不需要构造数据项(例如,对列表求和函数)。

迷思 4:++ 总是不好的

++ 操作符的名声总是很糟糕。其实你们都误解了它。主要是跟下面这种写法有关:

  • 不要这样做
    naive_reverse([H|T]) ->
        naive_reverse(T)++[H];
    naive_reverse([]) ->
        [].
    
    这是效率最低的反转列表的方式。因为 ++ 操作符原理是拷贝它左边的列表,结果就是一遍又一遍的拷贝……最终导致 n2 的复杂度。
  • 像这样的使用是没问题的
    naive_but_ok_reverse([H|T], Acc) ->
        naive_but_ok_reverse(T, [H]++Acc);
    naive_but_ok_reverse([], Acc) ->
        Acc.
    
    每一个列表元素只会被拷贝一次。变化的值 Acc++ 操作符的右边,它不会被拷贝。
  • 有经验的 Erlang 程序员会这么写
    vanilla_reverse([H|T], Acc) ->
        vanilla_reverse(T, [H|Acc]);
    vanilla_reverse([], Acc) ->
        Acc.
    
    这个会稍微更有效率一点,因为没有构建列表元素,只是直接拷贝它而已。(如果编译器没有自动将 [H] ++ Acc 重写为 [H | Acc],那这种写法就毫无疑问的胜出了)

迷思 5:字符串很慢

某种程度上来说,操作不当会导致字符串处理起来很慢。
在 Erlang 中,需要在字符串的使用方式上多注意。
另外如果打算用正则表达式,用 re 模块,不要用废弃的 regexp 模块。

不靠谱的 Note :

  1. 实际上确实是「慢」
  2. 这个「慢」倒并不一定体现在运行效率上,不如说是增加了编码的麻烦。
  3. 由于字符串作为列表处理,但字符串是无法避免大量的拼接、裁剪、反转等操作的,要避免额外的列表拷贝,而且在使用的时候必须很小心。

迷思 6:修复一个 Dets 文件很慢

修复时间和 Dets 文件中的记录(records)数量依然成正比,但是过去修复 Dets 非常非常慢,而现在已经改进了。

迷思 7:BEAM 是一个基于堆栈的字节码虚拟机(因此很慢)

实际上 BEAM 是一个 threaded-code 解释器。每条指令直接指向可执行 C 代码,指令调度是非常快的。

迷思 8:用 _ 来表示不使用的变量可以让程序更快

好吧,在古老的某个时候它是对的。
但是从 R6B 版本开始,BEAM 编译器已经足够聪明到发现哪些变量是没有使用的了。

断断续续学习和使用 Erlang 几个月了,感觉跟之前看待这门语言有了点变化,还是挺有意思的。
作为初学者可能理解不太准确,但有些东西还是可以记录一下。

关于函数式编程

  • 刚开始从其他语言转移到函数式语言的时候还害怕所谓的思维转换,实际上担心有点多余

  • 入门并不难,能理解把循环写成尾递归的过程就基本没什么问题了

  • 更少的心智负担,不再需要考虑是传值还是传引用,以及糟糕的副作用

  • 在某些问题上更接近思维过程,不需要关心怎么做,只需要关心做什么

    比如 erlang 的 quicksort 可以这么写(注意这并不是高效的写法):

    qsort([]) ->
        [];
    qsort([Pivot | T]) ->
        qsort([X || X <- T, X < Pivot])
        ++ [H] ++
        qsort([X || X <- T, X >= Pivot]).
    

关于 Erlang

  • 语法简单
    • 没有大量的复杂的概念,不像某些《Thinking in XXXX》,看完巨厚一本书你发现自己还是啥也不懂
    • 没有大量的奇技淫巧
    • 大概看了一个多月,就基本可以把 ejabberd 代码从头到尾过一遍了
    • 多看多用,我自己入门看的 《Erlang Programing》
  • pattern match
    • 用过才知道有多好用
    • 据说效率极其惊人
    • 据说 C# 也要加入了
  • 并发
    • process
      • 需要并发执行一个任务的时候,最容易想到的就是新开一个 process 去处理
      • Erlang 的 process 是轻量的,开启和关闭消耗也小,不需要操心各种并发问题
    • 避免锁
      • 实际上开始写命令式代码的时候,我倒是不怎么担心死锁,担心的反而是应该在哪里加锁……
      • Erlang 的并发模型没有试图去解决锁的问题,而是从根源避免了它,那就是根本不允许全局变量共享(当然你需要共享,可以使用 ETS 或者外部数据库)
      • 对我而言,这种做法减轻了不少心智负担
  • 鼓励崩溃和热更新
    • 写代码的时候 Server 不用不停重启刷新的感觉太好
  • 效率
    • 入门快

      比如我这种不合格的程序猿看了一点《Erlang Programing》就开始写 ejabberd 的模块了
    • 很容易用

      比如我这样不合格的程序猿也可以很顺利的手写稳定的服务器了
  • 部分不怎么好的地方
    • 字符串的处理
      • 没有单独的字符串类型,而是用 List,从编码效率到处理效率,都不怎么好看
      • 正则表达式的转义让人蛋疼无比
    • unicode 字符

      可能一不小心就容易坑了,当然这点在慢慢改进了
    • record

      可能 OTP17 后的 map 会方便点
    • 工具链

      从 IDE 到 debug profile 到 compile,体验都比较……怎么说呢,原始吧。
      目前用 Intellij IDEA + Erlang Plugin,还凑合。
    • 出了问题搜索不到

      嗯,这个时候就会嫌弃 Google 不够智能不能帮你写代码了

Supervisor 的四个 Restart Strategy 中,关于 one for one 和 simple one for one , 虽然很多地方都说 simple one for one 是 one for one 的简化版,但两者之间还是有一些不同。

1. 先来看一个简单的 one for one 和 simple one for one 的例子

  • one for one

    init(_Args) ->
        {ok, {{one_for_one, 1, 60},
              [{call, {call, start_link, []},
                permanent, brutal_kill, worker, [call]}]}}.
    
    • simple one for one
    init(_Args) ->
        {ok, {{simple_one_for_one, 0, 1},
              [{call, {call, start_link, []},
                permanent, brutal_kill, worker, [call]}]}}.
    

    初始化方法非常相似

2. 启动 Supervisor

  • one for one
    启动时,会同时启动一个子进程
  • simple one for one
    启动时,supervisor 并不会启动任何子进程,所有的子进程都是通过 supervisor:start_child(Sup, List) 来动态添加的

3. 允许的 Child Type

  • one for one
    允许不同的 child type,所以每次添加子进程都需要传递完整的 child spec
  • simple one for one
    只能有一个 child type,不同的 child instance 可以共享同一个 child spec

4. 添加子进程

  • one for one
    添加子进程的时候传递的是子进程规格

    supervisor:start_child(Sup, ChildSpec)
    
  • simple one for one
    添加子进程的时候传递的是任意的值列表,它将会被添加到子进程规格中的参数列表中,即实际上是通过 apply(call, start_link, [] ++ List) 来启动的

    supervisor:start_child(Sup, List)
    

相同的是,如果 supervisor 挂了,并且被 restart,之前动态添加的子进程将会全部丢失

5. Supervisor 停止

  • one for one
    按照启动 Spec 相反的顺序停止所有子进程,然后停止自身
  • simple one for one
    由于 supervisor 启动的是同一子进程的多个 instance,因此在停止的时候不存在顺序,
    在定义 Shutdown Strategy 的时候应该注意其 terminate 表现的区别。

主要是通过 hook ejabberd 的离线消息处理,从而实现针对离线消息进行推送

1. Hook offline message

  • 记录 device token
    这一步没啥好说的,iOS 连接到 APNs 后获取到 device token,发给服务器,服务器负责维护好对应关系即可。
    注意客户端退出时清理掉相应记录。

  • 在 ejabberd 中新增一个模块,注册 offline_message_hook,大致如下:

    start(Host, Opts) ->
    ?INFO_MSG("Starting mod_push_service", []),
    register(?MODULE, spawn(?MODULE, init, [Host, Opts])),
    ok.
    
    init(Host, _Opts) ->
        ejabberd_hooks:add(offline_message_hook, Host, ?MODULE, send_notification, 10),
        ok.
    
    stop(Host) ->
        ?INFO_MSG("Stopping mod_push_service", []),
        ejabberd_hooks:delete(offline_message_hook, Host,
            ?MODULE, send_notification, 10),
        ok.
    
    send_notification(From, To, Packet) ->
        dosomething.
    

2. Implement APNs

  • 从 Apple 获取推送证书

  • 在服务端使用该证书建立 ssl 连接

  • 按照 Apple 文档生成指定格式的 Payload 并发送

  • Apple 同时提供了 feedback channel 来获取失效 token 便于服务端获取记录,实现方式和发送消息类似,区别在于一个是发送消息,一个是接收消息

  • 连接的粗略代码如下

    -module(apntest).
    -export([connect/0]).
    
    connect() ->
        application:start(asn1),
        application:start(crypto),
        application:start(public_key),
        application:start(ssl),
        Address = "gateway.sandbox.push.apple.com",
        Port = 2195,
        Cert = "cert.pem",
        Options = [{certfile, Cert}, {mode, binary}],
        Timeout = 10000,
        case ssl:connect(Address, Port, Options, Timeout) of
            {ok, Socket} ->
                PayloadString = "{\"aps\":{\"alert\":\"Hello world.\",\"sound\":\"chime\", \"badge\":1}}",
                Payload = list_to_binary(PayloadString),
                PayloadLength = size(Payload),
                Packet = <<0:8,
                32:16/big,                16#ff496f96352abb7c875bedfc755287XXXXXXe14bfadade9ff6ba75360de65441:256/big,
                PayloadLength:16/big,
                Payload/binary>>,
                ssl:send(Socket, Packet),
                ssl:close(Socket),
                PayloadString;
            {error, Reason} ->
                Reason
        end.
    
  • 有一些开源的 Erlang APNs 推送服务实现,比如 apns4erl,不过这个项目实在引用了太多不必要的东西了 XD,还是推荐自己看懂后根据需求重新实现,代码也不复杂

MAM - Message Archive Management

1. Message Archive

  • Archiving

    archived tag
    <message to='[email protected]/balcony'
      from='[email protected]/orchard'
      type='chat'>
      <body>Call me but love, and I'll be new baptized; Henceforth I never will be Romeo.</body>
      <archived by='[email protected]' id='28482-98726-73623' />
    </message>
    

2. Querying

  • Filtering

    • by jid
      <iq type='get' id='juliet1'>
        <query xmlns='urn:xmpp:mam:tmp'>
          <with>[email protected]</with>
        </query>
      </iq>
      
    • by time
      <iq type='get' id='juliet1'>
        <query xmlns='urn:xmpp:mam:tmp'>
            <start>2010-06-07T00:00:00Z</start>
            <end>2010-07-07T13:23:54Z</end>
        </query>
      </iq>
      
    • by max number
    • by uid of message
    <iq type='get' id='q29303'>
      <query xmlns='urn:xmpp:mam:tmp'>
          <start>2010-08-07T00:00:00Z</start>
          <set xmlns='http://jabber.org/protocol/rsm'>
             <max>10</max>
             <after>09af3-cc343-b409f</after>
          </set>
      </query>
    </iq>
    
    <message id='aeb213' to='[email protected]/chamber'>
      <result xmlns='urn:xmpp:mam:tmp' queryid='f27' id='28482-98726-73623'>
        <forwarded xmlns='urn:xmpp:forward:0'>
          <delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:08:25Z'/>
          <message to='[email protected]/balcony'
            from='[email protected]/orchard'
            type='chat'>
            <body>Call me but love, and I'll be new baptized; Henceforth I never will be Romeo.</body>
          </message>
        </forwarded>
      </result>
    </message>
    
    <message id='aeb214' to='[email protected]/chamber'>
      <result xmlns='urn:xmpp:mam:tmp' queryid='f27' id='5d398-28273-f7382'>
        <forwarded xmlns='urn:xmpp:forward:0'>
          <delay xmlns='urn:xmpp:delay' stamp='2010-07-10T23:09:32Z'/>
          <message to='[email protected]/orchard'
             from='[email protected]/balcony'
             type='chat' id='8a54s'>
            <body>What man art thou that thus bescreen'd in night so stumblest on my counsel?</body>
          </message>
        </forwarded>
      </result>
    </message>
    

3. Preferences

  • jid list

     <iq type='set' id='juliet2'>
        <prefs xmlns='urn:xmpp:mam:tmp' default='roster'>
          <always>
            <jid>[email protected]</jid>
          </always>
          <never>
            <jid>[email protected]</jid>
          </never>
        </prefs>
      </iq>
    
    • option:
      • always - all messages are archived by default.
      • never - messages are never archived by default.
      • roster - messages are archived only if the contact’s bare JID is in the user’s roster
    • rules:
      • outgoing - use ’to’ attribute
      • incomming - use ‘from’ attribute
      • contains resource - compare
      • no resource - any resource
  • Ad-Hoc Commands

    http://xmpp.org/extensions/xep-0050.html

4. Security

  • archived tag
  • only user authorized have permission
  • privacy

5. MongooseIM Config (odbc)

  • {mod_mam, []}
    enable mam
  • {mod_mam_odbc_prefs, [pm]}
    archiving preferences
    pm : user-to-user messages
    muc : multiple user
  • {mod_mam_odbc_user, [pm, muc]}
    converting an archive id to an integer
  • {mod_mam_odbc_arch, [pm, muc]}
    archiving message
  • mod_mam_odbc_async_writer
    Asynchronous writer, will insert batches of messages.
    option no_writer set in mod_mam_odbc_arch

懒得贴代码,画个渣图看下好了。
ejabberd login module

P.S 从图上可以看出 XMPP 确实是细(fán)致(suǒ)之极了,考虑到在移动设备上的使用,这样的效率肯定是无法接受的。
对特定的服务来说,没有必要做如此多的交互步骤,考虑做一些简化。


Update: 详细的 login 流程

gist d0ec83e9aa3c1899287d

XMPP 协议

XMPP 协议是一种基于 XML 的 IM 协议,其前身是 jabber,后来被 IETF 标准化。其他信息可以参考 wikipedia.

协议架构

  • 通过 TCP socket 与 XMPP 服务器进行通信
  • 传输的是预定格式的 XML 信息
  • 通过解析 XML 提取请求类型和消息

消息格式

  • 一个实体在 XMPP 网络中被称为一个节点,它有唯一的标示符 jabber identifier (JID),实体可以是用户或者聊天室

  • XMPP 中定义了 3 个顶层元素: MessagePresenceIQ

    • Message

      基本的消息通讯格式:

      • To:消息的接收方
      • from : 发送方的 JID
      • body: 要发送的消息
      • type:
        • normal:单纯的消息,不要求响应;
        • chat:IM 消息
        • groupchat:聊天室 group chat
        • headline:发送 alert 和 notification 消息
        • error:发送 message 出错时通知
      <message from='[email protected]/test'
       to='[email protected]' type='chat'>
        <body>Anybody?</body>
      </message>
      
    • Presence

      用来表明用户的状态,如:online、away、dnd (请勿打扰)等。
      当用户离线或改变自己的状态时,就会在 stream 的上下文中插入一个 Presence 元素,来表明自身的状态。要想接受 presence 消息,必须经过一个叫做 presence subscription 的授权过程。

      • type: 非必须
        • subscribe:订阅其他用户的状态
        • probe:请求获取其他用户的状态
        • unavailable:不可用,offline
      • show: 用户当前状态
        • chat:聊天中
        • away:暂离
        • xa:eXtend Away,长时间离开
        • dnd:请勿打扰
      • status:自由文本,即 rich presence 或 extended presence
      <presence from='[email protected]/test' xml:lang='en'>
        <show>dnd</show>
        <status>Busy!</status>
      </presence>
      
    • IQ (Info/Query)
      请求/响应消息,如获取用户的好友列表。

      • type:
        • get: 获取一个值
        • set: 设置或替换 get 查询的值
        • result: 服务器响应了先前的查询,返回结果如好友列表
        • error: 请求或响应出现错误
      <iq from='[email protected]/test'
         id='xxxxxx'
         to='test'
         type='subscribe'>
          <ping xmlns='urn:xmpp:ping'/>
      </iq>
      
      <iq from='test'
         id='xxxxxx'
         to='[email protected]/test'
         type='error'>
          <error type='modify'>
             <bad-request xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
          </error>
      </iq>
      

Ref:
[1] http://xmpp.org/rfcs/rfc6120.html
[2] http://en.wikipedia.org/wiki/XMPP