任务

什么是任务?

任务是进程, 是用于执行特定动作的进程, 通常较少或不与其他进程通信. 任务最常用的情况是异步地计算一个值.

1
task = Task.async(fn -> do_some_work() end)
res  = do_some_other_work()
res + Task.await(task)

任务通常不返回值, 但是有时候我们需要让任务计算一个值,随后读取其计算结果.

async/await 提供了一个非常简单的机制并发地计算.

1
iex(foo@44adb2a6d305)2> task = Task.async(fn -> :math.pow(7,2) + :math.pow(8,2) end)
%Task{pid: #PID<0.97.0>, ref: #Reference<0.0.0.238>}
iex(foo@44adb2a6d305)3> Task.await(task)
113.0

首先来说明异步, Task.async/1创建一个任务,并执行传递给它的函数, 如上述代码第2行所示, Task.async/1调用返回一个对任务的引用.

这行输出很好地阐述了文章开头这么一句话任务是进程.该输出实际上是一个%Task Map, 包含两个元素 进程的Pid, 以及一个引用对象.

随后可以通过调用Task.await/1获取其计算结果. 调用Task.async/1会创建一个新的进程,该进程连接到调用者进程. 任务结果以消息的形式返回给调用者进程.
Task.await/2用于读取由任务发送的消息.

创建任务task1

1
iex(foo@44adb2a6d305)4> task1 = Task.async(fn -> :math.pow(7,2) + :math.pow(8,2) end)
%Task{pid: #PID<0.100.0>, ref: #Reference<0.0.0.245>}

创建任务task2

1
iex(foo@44adb2a6d305)6> task2 = Task.async(fn -> 2+2 end)
%Task{pid: #PID<0.103.0>, ref: #Reference<0.0.0.252>}

分别获取两个任务的计算结果 v1, v2

1
v1 = Task.await(task1)
v2 = Task.await(task2)

再通过第三个任务task3合并计算两个任务的结果

1
iex(foo@44adb2a6d305)15> task3 = Task.async(fn -> v1 + v2 end)
%Task{pid: #PID<0.117.0>, ref: #Reference<0.0.0.288>}
iex(foo@44adb2a6d305)16> Task.await(task3)
117.0

还可以通过start_link/1start_link/3创建任务并挂在到Supervition树中.

Task.async/1不同,Task.start_link/1返回的是一个{:ok,pid}而不是一个任务引用,因此我无法通过Task.await/2获取其计算结果.

1
iex(foo@44adb2a6d305)21> Task.start_link(fn -> 1 + 1 end)
{:ok, #PID<0.128.0>}

这样的任务可以挂在到supervision树, 下面的代码片段显示了如何挂载一个任务到一个supervision树中:

1
import Supervisor.Spec
children = [
    worker(Task, [fn -> IO.puts "ok" end])
]

通过上述几个步骤,我们实现了并发处理. 下面阐述如何把任务分布到集群中的多个节点从而实现分布式.

Supervision 树

Task.Supervisor模块允许开发者创建可动态第添加任务的监控进程(Supervisors):

1
{:ok, pid} = Task.Supervisor.start_link()
Task.Supervisor.async(pid, Module, :function, [arg1, arg2, arg3])

Task.Supervisor还可以在远程节点上创建任务, 只要该监控进程在本地或全局注册过:

1
# 在远程节点
Task.Supervisor.start_link(name: :tasks_sup)
# 在客户端
Task.Supervisor.async({:tasks_sup, :'bar@192.168.8.8'}, Module, :function, [arg1, arg2, arg3])

Task.Supervisor通常在监控树中以下列方式启动

1
import Supervisor.Spec
children = [
    supervisor(Task.Supervisor, [[name: :tasks_sup]
]

需要注意的是, 当处理分布式任务时, 应该使用async/3, 分别传递模块,函数,参数列表, 而不是以一个匿名函数作为参数的async/1.

分布 (Distrbuted)

TODO::

分布式任务和配置

启动节点A, 并定义一个模块

1
root@c87c9967219c:~# iex --sname A
iex(foo@c87c9967219c)1> defmodule Hell do
...(foo@c87c9967219c)1>   def world, do: IO.puts "hello world"
...(foo@c87c9967219c)1> end

启动节点C,并在节点C上Spawn一个在节点A上运行的进程, 并放回消息到C节点的终端

1
root@c87c9967219c:~# iex --sname C
iex(bar@c87c9967219c)1> Node.spawn_link :"foo@c87c9967219c", fn -> Hello.world end
hello world
#PID<9010.79.0>

在节点C上定义要执行的代码, 并在A上运行, 然后把运行结果返回给C

1
iex> Node.spawn_link :"A@c87c9967219c", fn -> Hello.world end

收发消息:

1
iex(B@c87c9967219c)1> pid = Node.spawn_link :"A@c87c9967219c", fn ->
...(B@c87c9967219c)1>   receive do
...(B@c87c9967219c)1>     {:ping, client} ->
...(B@c87c9967219c)1>       send client, :pong
...(B@c87c9967219c)1>   end
...(B@c87c9967219c)1> end
#PID<8005.65.0>
iex(B@c87c9967219c)2> send pid, {:ping, self}
{:ping, #PID<0.60.0>}
iex(B@c87c9967219c)3> flush
:pong
:ok

每次我们想要执行分布式计算的时候, 我们可以通过Node.spawn_link/2在其他节点上spawn一个进程, 但是我们应该避免在 supervision 树的外部去spawn子进程.

对于Node.spawn_link2, 有更好的替代

  • 可以使用:rpc模块执行远程节点上的函数, 比如:

    1
    # 在A@c87c9967219c节点上, 执行Hello模块的:world函数, 没有参数
    :rpc.call(:"A@c87c9967219c", Hello, :world, [])
  • 通过GenServer API请求运行在其他节点上的服务器, 比如:

    1
    GenServer.call({name, node}, arg)
  • 可以使用任务,任务可以在本地或远程节点上Spawn进程

参考资料

  1. Distributed tasks and configuration
    http://elixir-lang.org/getting_started/mix_otp/10.html

连接同一个局域网的Elixir节点

文章目录
  1. 1. 本机两个终端连接两个节点
  2. 2. 在同一个局域网中连接两个节点
  3. 3. 参考资料

本机两个终端连接两个节点

要点: 使用 --sname 启动选项

终端A:

iex --sname A

终端C:

iex --sname C

在终端A中运行

1
iex(A@localhost)1> Node.connect :'C@localhost' %% 连接C节点
true
iex(A@localhost)2> node %% 显示当前节点名称
:"A@localhost"
iex(A@localhost)3> Node.list %% 列出连接到当前节点的其他节点
[:"C@localhost"]
iex(A@localhost)4> [node | Node.list] %% 列出当前节点和连接节点
[:"A@localhost", :"C@localhost"]
iex(A@localhost)5>

在同一个局域网中连接两个节点

要点: 使用 --name 启动选项

启动A(192.168.8.100)节点

iex --name A@192.168.8.100

启动C(192.168.8.200)节点

iex --name C@192.168.8.200

192.168.8.100连接到192.168.8.200, 在192.168.8.100(A)的终端执行:

iex(A@192.168.8.100)1> Node.connect :'C@192.168.8.200'

验证连接的节点

iex(A@192.168.8.100)2> Node.list

连接到多个节点原理相同, 如果增加D(192.168.8.201),E(192.168.8.202),F(192.168.8.203)节点到集群中, 在A节点上(192.168.8.100)依次再执行

1
iex(A@192.168.8.100)1> Node.connect :'D@192.168.8.201'
iex(A@192.168.8.100)2> Node.connect :'E@192.168.8.202'
iex(A@192.168.8.100)3> Node.connect :'F@192.168.8.203'

参考资料

  1. Connecting Elixir Nodes on the Same LAN
    http://benjamintanweihao.github.io/blog/2014/05/25/connecting-elixir-nodes-on-the-same-lan/

从ID3中解析Mp3元数据

TODO:: 判断MP3文件是否包含TAG标签

MP3和ID3

ID3是一种在MP3中使用的描述MP3音频文件的元数据结构, 如下图所示:

ID3元数据

ID3v1 字段结构

字段结构

ID3v1 布局

读取Mp3文件

1
defmodule Mp3 do
  def get(filename) do
    case File.read(filename) do
        {:ok, binary} ->
            binary
        _ ->
        IO.puts "Can't not open #{filename}"
    end
  end
end

提取ID3v1 标记

ID3标记是MP3文件的最后128字节, 因此我们可以计算出ID3 Tag的起始偏移量

1
mp3_byte_size = (byte_size(binary) - 128)

然后通过Bit Syntax类匹配出我们需要的ID3 Tag部分的Bit字符串.

1
<< _ :: binary-size(mp3_byte_size), id3_tag :: binary >> = binary

完整代码

1
defmodule Mp3 do
  def parse(filename) do
    case File.read(filename) do
      {:ok, binary} ->
        IO.puts byte_size(binary)
        # 获取Mp3音频数据的大小,用于计算ID3的起始偏移量
        mp3_byte_size = (byte_size(binary) - 128) * 8
        # 把我们需要的部分解析出来
        << _ :: size(mp3_byte_size), id3_tag :: binary >> = binary
        case id3_tag do
          <<"TAG",tags::binary>> ->
            # 从id3_tag中匹配出标题,艺术家名称,专辑名称,发行年份, 评论, 等
            << tag     :: binary-size(3),
               title   :: binary-size(30),
               artist  :: binary-size(30),
               album   :: binary-size(30),
               year    :: binary-size(4),
               comment :: binary-size(30),
               genre   :: binary-size(1)
            >>  = id3_tag
            # 输出
            IO.puts tags
            IO.puts "TAG: #{tag}"
            IO.puts "标题名: #{title}"
            IO.puts "艺术家: #{artist}"
            IO.puts "专辑名: #{album}"
            IO.puts "年份: #{year}"
            IO.puts "评论: #{comment}"
            IO.puts "流派: #{genre}"
          _ ->
            :NO_TAG_INFORMATION
        end
      _ ->
        IO.puts "Can't not open #{filename}"
    end
  end
end

参考资料

  1. 本文原文
    http://benjamintanweihao.github.io/blog/2014/06/10/elixir-bit-syntax-and-id3/
  2. ID3v1 结构说明
    http://id3.org/ID3v1
  3. Erlang 比特语法和ID3
    http://www.citizen428.net/blog/2010/09/04/erlang-bit-syntax-and-id3
  4. Erlang 比特语法
    http://www.erlang.org/doc/programming_examples/bit_syntax.html
  5. Erlang Programming - Francesco Cesarini and Simon Thompson 电子版PDF, 206页

自定义行为

1
2
3
4
-module(some_behaviour).
-callback init(Args :: list(term())) -> 'ok'|tuple('error', Reason :: string()).
-callback handle(Event :: atom()) -> NextEvent :: atom().
-callback sync(Node :: node(), Timeout :: non_neg_integer()) -> 'ok'|tuple('error', Reason :: string()).

该行为要求在回调模块中定义 init/1, handle/1sync/2三个函数, 如下:

1
-module(usage).
-behaviour(some_behaviour).
-export([init/1, handle/1, sync/2, foo/0]).
init(Config) ->
    Config.
sync(_Entry, Config) ->
    Config.
handle(Message) ->
    Message.
foo() ->
    foo_atom_returned.

资源池

文章目录
  1. 1. 简介
  2. 2. 设计
  3. 3. 操作
    1. 3.1. borrow
    2. 3.2. return
    3. 3.3. add
    4. 3.4. invalidate
    5. 3.5. 典型用例
  4. 4. 大小限制
    1. 4.1. max_active
    2. 4.2. max_idle
    3. 4.3. min_idle
  5. 5. 行为选项
    1. 5.1. 资源池耗尽时的borrow行为
    2. 5.2. 资源检查
    3. 5.3. 资源在Idle列表中的顺序
    4. 5.4. 计时
  6. 6. 资源池实例的维护
    1. 6.1. new
    2. 6.2. clear
    3. 6.3. close
    4. 6.4. 资源池统计
  7. 7. 资源工厂
  8. 8. 示例
    1. 8.1. MySQL Driver连接池
    2. 8.2. Rabbit MQ 连接通道池
  9. 9. 参考资料

资源池是解决并发问题的一种常见模式

Versions:

  • 2014-11-13 Version 0.1

简介

软件资源的创建需要消耗大量的时间和内存, 如果能重用, 将极大地改善应用程序的性能. 资源池是一个在不同的平台和语言中广泛的使用的方法.本文所述Erlang资源池的设计灵感来源于Apache的Common Pool库. API和主要的功能是从其借用的, 但内部实现完全不同, 并使用了Erlang OTP设计原则, 以及Erlang并发模型.

设计

资源池由两个列表组成: Active(活动列表)和Idle(空闲列表). Active列表包含活跃的资源. Idle列表包含不活跃的资源

图-1: 资源池为空的状态,活动列表和空闲列表都为空

+-Pool-----------{0,0}-+
|                      |
| Active--+  Idle----+ |
| |       |  |       | |
| |       |  |       | |
| |       |  |       | |
| +-------+  +-------+ |
+----------------------+

操作

borrow

要从资源池中获得一个资源,需要调用函数borrow

1
Resource = resource_pool:borrow(test_pool)

如果资源池的Idle列表为空(在没有资源可用的情况下),资源池会直接在Active列表中创建一个资源并授予调用进程.

图-2: 创建一个新的资源,并把该资源放到 Active 资源列表

+-Pool-----------{1,0}-+          +-Pool-----------{2,0}-+
|                      |          |                      |
| Active--+  Idle----+ |          | Active--+  Idle----+ |
| |       |  |       | |          | |       |  |       | |
| |       |  |       | |    =>    | | <R.2> |  |       | |
| | <R.1> |  |       | |          | | <R.1> |  |       | |
| +-------+  +-------+ |          | +-------+  +-------+ |
+----------------------+          +----------------------+

如果在资源池的Idle列表中存在可用的资源. 将从Idle列表中取出一个资源,并转移到Active列表,然后授予调用者进程.

图-3: 从空闲资源列表中得到一个资源,并把它转移到活动列表

+-Pool-----------{1,2}-+          +-Pool-----------{2,1}-+
|                      |          |                      |
| Active--+  Idle----+ |          | Active--+  Idle----+ |
| |       |  |       | |          | |       |  |       | |
| |       |  | <R.2> | |    =>    | | <R.2> |  |       | |
| | <R.1> |  | <R.3> | |          | | <R.1> |  | <R.3> | |
| +-------+  +-------+ |          | +-------+  +-------+ |
+----------------------+          +----------------------+

return

一旦进程完成了对资源的使用,它必须把资源返回到资源池中

1
resource_pool:return(test_pool,Resource)

换句话说,就是该资源从Active列表移动到Idle列表(图-4). 以让其他进程能够从资源池中获取可用的资源.

图-4 进程把资源返还给资源池

+-Pool-----------{2,1}-+          +-Pool-----------{1,2}-+
|                      |          |                      |
| Active--+  Idle----+ |          | Active--+  Idle----+ |
| |       |  |       | |          | |       |  |       | |
| | <R.2> |  |       | |    =>    | |       |  | <R.2> | |
| | <R.1> |  | <R.3> | |          | | <R.1> |  | <R.3> | |
| +-------+  +-------+ |          | +-------+  +-------+ |
+----------------------+          +----------------------+

add

有时候我们需要添加新的资源

1
resource_pool:add(test_pool)

函数add创建一个新的资源,并添加到Idle列表中

+-Pool-----------{2,1}-+          +-Pool-----------{2,2}-+
|                      |          |                      |
| Active--+  Idle----+ |          | Active--+  Idle----+ |
| |       |  |       | |          | |       |  |       | |
| | <R.2> |  |       | |    =>    | | <R.2> |  | <R.4> | |
| | <R.1> |  | <R.3> | |          | | <R.1> |  | <R.3> | |
| +-------+  +-------+ |          | +-------+  +-------+ |
+----------------------+          +----------------------+

invalidate

1
resource_pool:invalidate(test_pool,Resource)

invalidate 函数把一个失败的资源标记为不可用,资源池然后会销毁这个资源.

+-Pool-----------{2,1}-+          +-Pool-----------{1,1}-+
|                      |          |                      |
| Active--+  Idle----+ |          | Active--+  Idle----+ |
| |       |  |       | |          | |       |  |       | |
| | <R.2> |  |       | |    =>    | |       |  |       | |
| | <R.1> |  | <R.3> | |          | | <R.1> |  | <R.3> | |
| +-------+  +-------+ |          | +-------+  +-------+ |
+----------------------+          +----------------------+

典型用例

1
2
3
4
5
6
7
8
9
10
case resource_pool:borrow(test_pool) of
{error, E} -> io:format("Error while borrow from pool, reason: ~p", [E]);
Resource ->
try
resource:operation(Resource),
resource_pool:return(test_pool, Resource)
catch
_:_ -> resource_pool:invalidate(test_pool, Resource)
end,
end

大小限制

1
2
3
{ok,Pid} = resource_pool:new(
test_pool,resource_factory,resource_metadata,options
)
1
max_active,
max_idle,
min_idle
            +-Pool-----------{0,0}-+
            |                      |
            | Active--+  Idle----+ |
            | |       |  |_______|_|__ max_idle
max_active__|_|_______|  |       | |
            | |       |  |       | |
            | |       |  |_______|_|__ min_idle
            | |       |  |       | |
            | +-------+  +-------+ |
            +----------------------+

max_active

Active列表默认最大值为8. 如果达到限制borrow操作将会阻塞或失败. 值 -1 (或任何负值) 表示Active列表没有大小限制.

1
2
{ok,Pid} =
resource_pool:new(test_pool,resource_factory,[],[{max_active,20}])

max_idle

Idle列表的最大大小,默认和max_active相同. 如果达到限制,后续return的资源会被销毁,值 -1 (或任何负值) 表示Idle列表没有大小限制.

1
2
3
4
5
6
{ok,Pid} = resource_pool:new(
test_pool, %% 资源池实例标识
resource_factory, %% 资源工厂
[], %% 资源元数据
[{max_active,20},{max_idle,10}] %% 资源池选项
)

min_idle

Idle列表默认的最小大小为0.

If it reaches the limit then following borrow operation will successfully supplies a resource to invoker and then pool will additionally create new resource in Idle container to provide min_idle condition.

1
2
3
4
5
6
{ok,Pid} = resource_pool:new(
test_pool, %% 资源池标识
resource_factory, %% 资源工厂
[], %% 资源元数据
[{max_active,20},{max_idle,10},{min_idle,3}] %% 资源池选项
)

关于空闲资源的最大和最小值应该根据实际需求控制在一个合理的区间. 太小会导致频繁的创建资源,太大又会消耗过多的内存.

行为选项

资源池耗尽时的borrow行为

  • {when_exhausted_action,fail}
    在耗尽的资源池上调用borrow将返回{error,pool_exhausted}

  • {when_exhausted_action,block}
    在耗尽的资源池上调用borrow将阻塞,知道有空闲的资源可用,等待的最大时间可以通过选项max_wait控制

  • {when_exhausted_action,grow}
    在耗尽的资源池上调用borrow将创建新资源,并增大Active列表的大小,此时max_Idle选项被忽略.

1
2
3
4
5
6
{ok,Pid} = resource_pool:new(
test_pool, %% 资源池实例标识
resource_factory, %% 资源工厂
[], %% 资源元数据
[{max_active,20},{when_exhausted_action,fail}] %% 资源池选项
)

资源检查

Resource pool can check status of managed resources. Options test_on_borrow and test_on_return control how pool tests resources: before providing resource to invoker {test_on_borrow, true} and after a resource was returned to pool {test_on_return, true}. If pool finds that the resource is not alive during test then the resource will be destroyed.

资源在Idle列表中的顺序

Option fifo (first-input-first-output) controls order of extracting a resources from Idle list. Diagrams below illustrate this. Suppose we fill out Idle list in order: was first, is next, then . Resource is active in given moment. If {fifo, true} is set the borrow operation leads to situation below: resource was came first and it becames active now (first input).

+-Pool-----------{1,2}-+          +-Pool-----------{2,1}-+
|                      |          |                      |
| Active--+  Idle----+ |          | Active--+  Idle----+ |
| |       |  | <R.3> | |          | |       |  |       | |
| |       |  | <R.2> | |    =>    | | <R.1> |  | <R.3> | |
| | <R.4> |  | <R.1> | |          | | <R.4> |  | <R.2> | |
| +-------+  +-------+ |          | +-------+  +-------+ |
+----------------------+          +----------------------+

If {fifo, false} is set it means that order will be last-input-first-output. borrow operation makes active resource (last input).

+-Pool-----------{1,2}-+          +-Pool-----------{2,1}-+
|                      |          |                      |
| Active--+  Idle----+ |          | Active--+  Idle----+ |
| |       |  | <R.3> | |          | |       |  |       | |
| |       |  | <R.2> | |    =>    | | <R.3> |  | <R.2> | |
| | <R.4> |  | <R.1> | |          | | <R.4> |  | <R.1> | |
| +-------+  +-------+ |          | +-------+  +-------+ |
+----------------------+          +----------------------+

Default value for fifo is false.

计时

max_wait选项定义了一个时间, 当在资源池耗尽时调用borrow函数,并且 when_exhausted_action 设置为block时所等待的最大时间.

max_idle_time ,用来配置资源的最大空闲时间. 如果一个资源空闲的超过这个时间, 就会被销毁. 但至少要在资源池中保留min_idle个资源. 如果max_idle_time设置为infinity, 不会销毁任何空闲的资源.

资源池实例的维护

pool_name是一个原子, 多个进程可以使用该名字来访问资源池. resource_factory是一个负责创建和维护资源的模块名称. resource_metadata是一个包含资源初始化信息的对象. 该对象作为参数传递给resource_factory的每个函数来帮助维护一个资源.

new

创建资源池

1
2
3
4
5
{ok, Pid} = resource_pool:new(
pool_name, %% 资源池名称
resource_factory, %% 资源工厂
resource_metadata %% 资源元数据
)

clear

清空资源池

1
resource_pool:clear(pool_name)

close

关闭资源池, 该函数终止资源池进程, 并销毁池中的所有资源

1
ok = resource_pool:close(pool_name)

资源池统计

get_num_active,get_num_idle,get_number

资源工厂

示例

MySQL Driver连接池

http://sourceforge.net/projects/erlmysql/

Rabbit MQ 连接通道池

http://sourceforge.net/projects/erlpool/files/1.0.x/erl.resource.pool.example.zip/download

参考资料

  1. https://erlangcentral.org/wiki/index.php?title=Resource_Pool

部署Phoenix应用程序到Ubuntu服务器

安装部署工具

1
apt-get install -y capistrano

部署

步骤1: 安装 capistrano 和 capify

1
gem install capistrano --version=2.15.5
capify .
mkdir config/deploy

步骤2: 添加 exrm 依赖

1
defp deps do
  [
    ...
    {:exrm, "~> 0.14.11"}
  ]
end

安装

1
mix deps.get

步骤3: 改变应用程序的启动方式

1
defmodule BookStore do
  use Application
  # See http://elixir-lang.org/docs/stable/elixir/Application.html
  # for more information on OTP Applications
  def start(_type, _args) do
    import Supervisor.Spec, warn: false
    children = [
      # Define workers and child supervisors to be supervised
      # worker(BookStore.Worker, [arg1, arg2, arg3])
      worker(BookStore.Repo, [])
    ]
    BookStore.Router.start
    # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: BookStore.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

第12行, 添加了BookStore.Router.start

在开发模式中,应用程序不再能够以正常的方式启动. mix phoenix.start会立即崩溃, 并显示如下错误:

1
Running MyAwesomeApp.Router with Cowboy on port 4000
** (CaseClauseError) no case clause matching: {:error, {:already_started, #PID<0.149.0>}}
    (phoenix) lib/phoenix/router.ex:78: Phoenix.Router.start_adapter/2
    (phoenix) lib/mix/tasks/phoenix/start.ex:12: Mix.Tasks.Phoenix.Start.run/1
    (mix) lib/mix/cli.ex:55: Mix.CLI.run_task/2
    (elixir) src/elixir_lexical.erl:17: :elixir_lexical.run/3
    (elixir) lib/code.ex:316: Code.require_file/2

在开发模式中, 你需要使用下面的方式启动服务器

1
iex -S mix phoenix.start

步骤4: 把代码Push到Git仓库

1
git init
git remote add origin git@github.com:developerworks/book_store.git

提交代码到远程仓库

1
git add . && git commit -am "initial commit"
git push origin master

步骤5: 在Ubuntu服务器上安装Erlang和Elixir

1
$ wget http://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb
$ sudo dpkg -i erlang-solutions_1.0_all.deb
$ sudo apt-get update
$ sudo apt-get install erlang

由于Phoenix依赖Elixir 1.0.1, 需要手动安装合适的Elixir版本.

下载,解压,编译

1
cd /root
wget https://github.com/elixir-lang/elixir/archive/v1.0.2.zip
unzip v1.0.2.zip
cd elixir-1.0.2
make

把程序路径添加到PATH中

1
echo 'export PATH=/root/elixir-1.0.2/bin:$PATH' >> ~/.profile
source ~/.profile

检查版本

1
# elixir --version
Elixir 1.0.2

步骤6: 调整Locale

1
export LANGUAGE=en_US.UTF-8
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
locale-gen en_US.UTF-8
sudo apt-get install locales
sudo dpkg-reconfigure locales

~/.profile中添加下面的配置

1
export LANGUAGE=en_US.UTF-8
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8

/etc/environment添加如下配置

1
LC_ALL=en_US.UTF-8
LANG=en_US.UTF-8

步骤6: 编辑config/deploy.rb

粘贴下面的代码

1
require 'capistrano/ext/multistage'
set :stages, ["staging", "production"]
set :default_stage, "production"
set :keep_releases, 5
set :application, "My Awesome App"
set :repository,  "git@github.com:learnelixir/my-awesome-app.git"
set :scm, :git
set :branch, :master
set :use_sudo, false
set :normalize_asset_timestamps, false
set :deploy_via, :remote_cache
after "deploy:update", "deploy:cleanup"
after "deploy:update", "deploy:build", "deploy:cleanup"
namespace :assets do
  task :precompile, roles: :web do
    # do nothing
  end
end
def is_application_running?(current_path)
  pid = capture(%Q{ps ax -o pid= -o command=|
      grep "/home/app/www/book_store/current/rel/book_store/.*/[b]eam"|awk '{print $1}'})
  return pid != ""
end
namespace :deploy do
  task :is_running, roles: :web do
    is_running = is_application_running?(current_path)
    if is_running
      puts "Application is running"
    else
      puts "Application is NOT running"
    end
  end
  task :build, roles: :web do
    run "cd #{current_path} && mix deps.get && MIX_ENV=#{mix_env} mix release"
  end
  task :restart, roles: :web do
    if is_application_running?(current_path)
      run "cd #{current_path}/rel/book_store/bin && ./book_store stop"
    end
    run "cd #{current_path}/rel/book_store/bin && ./book_store start"
  end
  task :start, roles: :web do
    run "cd #{current_path}/rel/book_store/bin && ./book_store start"
  end
  task :stop, roles: :web do
    run "cd #{current_path}/rel/book_store/bin && ./book_store stop"
  end
end

步骤7: 创建production.rb

1
vim config/deploy/production.rb

内容如下

1
server "xx.xx.xx.xx", :app, :web, :db, :primary => true
set :user, '<user>'
set :branch, :master
set :mix_env, :prod
set :deploy_to, "/home/<user>/www/book_store"
set :default_environment, {
  'PATH' => "$PATH:/home/app/src/elixir/bin" # --> replace by path to your elixir bin folder
}

其中xx.xx.xx.xx为服务器IP地址, <user>为用户目录, 可以用同样的方法创建staging.rb文件用于staging环境.

步骤8: 运行setup和deploy

为部署初始化目录结构

1
cap deploy:setup

执行实际部署, 如果是第一次部署, 需要下载和安装依赖包

1
cap deploy

步骤9: 把Nginx作为反向代理

1
# Phoenix服务器地址和短裤
upstream book_store {
  server 127.0.0.1:4000;
}
server {
  listen 0.0.0.0:80;
  server_name localhost;
  try_files $uri/index.html $uri @book_store;
  location @book_store {
    proxy_set_header Host $http_host;
    if (!-f $request_filename) {
      proxy_pass http://book_store;
      break;
    }
  }
  error_page 500 502 503 504 /500.html;
  access_log  /var/log/nginx/book_store.log;
  error_log  /var/log/nginx/book_store.log;
}

Phoenix的运行端口可以在配置文件config/prod.exs中修改

在Phoenix控制台中使用模型

启动Phoenix控制台

1
iex -S mix

查询

查询Id为1的图书

1
iex(16)> BookStore.Repo.get(BookStore.Books, 1)
%BookStore.Books{author: "Dave Thomas",
 description: "Programming Elixir: Functional |> Concurrent |> Pragmatic |> Fun",
 id: 1, publisher: "The Pragmatic Bookshelf", title: "Programming Elixir"}

查询所有图书, 返回的是一个数组

1
iex(17)> BookStore.Repo.all(BookStore.Books)
[%BookStore.Books{author: "Dave Thomas",
  description: "Programming Elixir: Functional |> Concurrent |> Pragmatic |> Fun",
  id: 1, publisher: "The Pragmatic Bookshelf", title: "Programming Elixir"}]

定义别名:

1
alias BookStore.Repo, as: Repo
alias BookStore.Books, as: Books

如果没有as:部分, alias将使用模型名称的最后一部分作为别名

1
alias BookStore.Repo
alias BookStore.Books

使用别名查询

1
iex> Repo.all(Books)
[%BookStore.Books{author: "Dave Thomas",
 description: "Programming Elixir: Functional |> Concurrent |> Pragmatic |> Fun",
  id: 1, publisher: "The Pragmatic Bookshelf", title: "Programming Elixir"}]
iex> Repo.get(Books, 1)
%BookStore.Books{author: "Dave Thomas",
 description: "Programming Elixir: Functional |> Concurrent |> Pragmatic |> Fun",
  id: 1, publisher: "The Pragmatic Bookshelf", title: "Programming Elixir"}

赋值给一个变量和访问模型字段

1
iex(10)> book = Repo.get(Books, 1)
%BookStore.Books{author: "Dave Thomas",
 description: "Programming Elixir: Functional |> Concurrent |> Pragmatic |> Fun",
 id: 1, publisher: "The Pragmatic Bookshelf", title: "Programming Elixir"}
iex(11)> book.author
"Dave Thomas"
iex(12)> book.description
"Programming Elixir: Functional |> Concurrent |> Pragmatic |> Fun"
iex(13)> book.id
1
iex(14)> book.publisher
"The Pragmatic Bookshelf"
iex(15)> book.title
"Programming Elixir"

更新

1
iex>  book = %{book | description: "Programming Elixir: a lot more fun", \
                      title: "Programming Elixir with fun"}
%BookStore.Books{author: "Dave Thomas",
 description: "Programming Elixir: a lot more fun", id: 1,
  publisher: "The Pragmatic Bookshelf", title: "Programming Elixir with fun"}
iex> Repo.update(book)
:ok
iex> Repo.get(Books, 1)
%BookStore.Books{author: "Dave Thomas",
 description: "Programming Elixir: a lot more fun", id: 1,
  publisher: "The Pragmatic Bookshelf", title: "Programming Elixir with fun"}

插入

1
iex> Repo.insert(%Books{author: "Simon St. Laurent, J. David Eisenberg", \
                        description: "Elixir is an excellent language if you want to \
                                      learn about functional programming, and with this hands-on \
                                      introduction",
                        publisher: "O'Reilly", title: "Introducing Elixir"})
%BookStore.Books{author: "Simon St. Laurent, J. David Eisenberg",
                 description: "Elixir is an excellent language if you want to learn about \
                               functional programming, and with this hands-on introduction", \
                 id: 18, publisher: "O'Reilly", title: "Introducing Elixir"}

删除

1
iex> introducing_elixir_book = Repo.get(Books, 2)
%BookStore.Books{author: "Simon St. Laurent, J. David Eisenberg",
 description: "Elixir is an excellent language if you want to learn \
               about functional programming, and with this hands-on introduction",
  id: 2, publisher: "O'Reilly", title: "Introducing Elixir"}
iex> Repo.delete(introducing_elixir_book)
:ok

参考资料

  1. http://learnelixir.com/blog/2014/10/08/playing-with-model-in-elixir-phoenix-console/

编码问题

iex进入Elixir Shell时出现提示

Elixir默认编码问题

临时有效

1
export LANG=en_US.UTF-8

永久有效

1
update-locale LANG=en_US.UTF-8

用Phoenix,Postgresql和Ecto创建一个书单应用

文章目录
  1. 1. 修订
  2. 2. 安装Phoenix
  3. 3. 创建书单项目
  4. 4. 添加依赖库
  5. 5. 创建一个仓库(Repo)
  6. 6. 创建模型
  7. 7. 创建数据库移植脚本
  8. 8. 创建查询
  9. 9. 配置路由
  10. 10. 创建控制器
  11. 11. 创建书单视图
  12. 12. 参考资料

本文通过一个书单应用简要介绍使用Phoenix框架创建一个Web应用程序的基本步骤. 从这些基本步骤我们来逐步学习如何使用phoenix框架开发一个Web应用程序的基本过程.

修订

  • 2014-12-17
    • Phoenix 从0.8.0开始 phoenix任务phoenix.start重命名为phoenix.server
    • 修改项目文件mix.exs的依赖版本号,更新依赖库
      • ecto 0.2.0 -> 0.2.8
      • postgrex 0.5.0 -> 0.6.0
      • phoenix 0.5.0 -> master
    • 增加命令注释说明

安装Phoenix

1
git clone https://github.com/phoenixframework/phoenix.git
cd phoenix
mix do deps.get, compile

创建书单项目

phoenix源代码目录中运行如下命令创建一个phoenix项目目录结构

1
mix phoenix.new book_store ../book_store
=== =========== ========== =============
|         |            |              |
命令     任务        项目名称    新创建的项目保存位置

目录../book_store不必事先存在, phoenix会自动创建.

添加依赖库

编辑mix.exs文件, 修改后如下:

1
defmodule BookStore.Mixfile do
  use Mix.Project
  def project do
    [app: :book_store,
     version: "0.0.1",
     elixir: "~> 1.0",
     elixirc_paths: ["lib", "web"],
     compilers: [:phoenix] ++ Mix.compilers,
     deps: deps]
  end
  # Configuration for the OTP application
  #
  # Type `mix help compile.app` for more information
  def application do
    [mod: {BookStore, []},
     applications: [:phoenix, :cowboy, :logger, :postgrex, :ecto]]
  end
  # Specifies your project dependencies
  #
  # Type `mix help deps` for examples and options
  defp deps do
    [ {:phoenix, github: "phoenixframework/phoenix"},
      {:cowboy, "~> 1.0"},
      {:postgrex, "~> 0.6.0"},
      {:ecto, "~> 0.2.8"} ]
  end
end

和修改之前的mix.exs文件相比有两个变更处:

  • application函数中增加了两个依赖的应用程序 :postgres:ecto (16行)
  • deps函数增加两个依赖库{:postgrex, "~> 0.6.0"}{:ecto, "~> 0.2.8"}(24,25行)

运行

1
mix do deps.get, compile

创建一个仓库(Repo)

创建文件web/models/repo.ex,内容如下:

1
defmodule BookStore.Repo do
  use Ecto.Repo, adapter: Ecto.Adapters.Postgres
  def conf do
    parse_url "ecto://postgres:postgres@localhost/book_store"
  end
  def priv do
    app_dir(:book_store, "priv/repo")
  end
end

创建数据库:

1
createdb book_store -U postgres --encoding='utf-8' --locale=en_US.UTF-8 --template=template0

修改lib/book_store.ex为, 如下:

1
defmodule BookStore do
  use Application
  # See http://elixir-lang.org/docs/stable/elixir/Application.html
  # for more information on OTP Applications
  def start(_type, _args) do
    import Supervisor.Spec, warn: false
    children = [
      # Define workers and child supervisors to be supervised
      worker(BookStore.Repo, [])
    ]
    opts = [strategy: :one_for_one, name: BookStore.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

编译

1
mix compile

创建模型

创建文件web/models/books.ex, 内容如下:

1
defmodule BookStore.Books do
  use Ecto.Model
  schema "books" do
    field :title, :string
    field :description, :string
    field :author, :string
    field :publisher, :string
  end
end

创建数据库移植脚本

1
$ mix ecto.gen.migration Bookstore.Repo create_book
Compiled web/models/books.ex
Generated bookstore.app
* creating priv/repo/migrations
* creating priv/repo/migrations/20141112170140_create_book.exs

编辑生成的priv/repo/migrations/20141112170140_create_book.exs脚本, 内容如下:

defmodule BookStore.Repo.Migrations.CreateBook do
  use Ecto.Migration

  def up do
    ["CREATE TABLE books(\
        id serial primary key, \
        title varchar(125), \
        description text, \
        author varchar(255), \
        publisher varchar(255))",\

     "INSERT INTO books(title, description, author, publisher) \
             VALUES ( \
                'Programming Elixir', \
                'Programming Elixir: Functional |> Concurrent |> Pragmatic |> Fun', \
                'Dave Thomas', \
                'The Pragmatic Bookshelf')"
    ]
  end
  def down do
    "DROP TABLE books"
  end
end

运行移植脚本

1
mix ecto.migrate BookStore.Repo

创建查询

创建文件web/models/queries.ex, 内容如下:

1
defmodule BookStore.Queries do
  import Ecto.Query
  def books_query do
    query = from book in BookStore.Books,
            select: book
    BookStore.Repo.all(query)
  end
end

配置路由

打开文件web/router.ex, 修改为如下:

1
defmodule BookStore.Router do
  use Phoenix.Router
  scope "/" do
    # Use the default browser stack.
    pipe_through :browser
    #get "/", BookStore.PageController, :index, as: :pages
    get "/", BookStore.BookController, :index, as: :books
  end
  # Other scopes may use custom stacks.
  # scope "/api" do
  #   pipe_through :api
  # end
end

创建控制器

创建文件web/controllers/book_controller.ex, 内容如下:

1
defmodule BookStore.BookController do
  use Phoenix.Controller
  plug :action
  def index(conn, _params) do
    books = BookStore.Queries.books_query
    render conn, "index", books: books
  end
end

创建书单视图

创建文件web/views/book_view.ex, 内容如下:

1
defmodule BookStore.BookView do
  use BookStore.Views
end

创建目录

1
mkdir web/templates/book

并添加文件web/templates/book/index.html.eex, 内容如下:

1
<h1>我的图书</h1>
<table class='table table-bodered table-striped'>
  <thead>
    <tr>
      <th>#</th>
      <th>标题</th>
      <th>描述</th>
      <th>作者</th>
      <th>出版社</th>
    </tr>
  </thead>
  <tbody>
    <%= for book <- @books do %>
      <tr>
        <td><%= book.id %></td>
        <td><%= book.title %></td>
        <td><%= book.description %></td>
        <td><%= book.author %></td>
        <td><%= book.publisher %></td>
      </tr>
    <% end %>
  </tbody>
</table>

启动应用,并刷新页面

1
mix phoenix.start

我的书单应用

完成!

参考资料

  1. Book Listing App With Elixir, Phoenix, Postgres and Ecto
    http://learnelixir.com/blog/2014/10/05/build-web-app-with-elixir/

PostgreSQL Ubuntu上的安装和配置

文章目录
  1. 1. 安装
  2. 2. 配置

安装

执行如下命令安装PostgreSQL Server

1
apt-get install -y postgresql

配置

  • 让其他计算机可以连接到Postgresql Server

    编辑文件 vi /etc/postgresql/9.3/main/postgresql.conf, 定位到#listen_addresses = 'localhost', 去掉注释,并修改为:

    1
    listen_addresses = '*'
  • 重启服务器

    /etc/init.d/postgresql restart
    
  • 登录服务器并修改密码

    1
    # 登录
    sudo -u postgres psql template1
    # 修改密码
    ALTER USER postgres with encrypted password 'postgres';
  • 修改登录认证

    编辑vi /etc/postgresql/9.3/main/pg_hba.conf, 添加如下行:

    1
    local   all         postgres                          md5

    再次重启:

    1
    service postgresql restart
  • 安装命令行客户端和登录验证

    1
    sudo apt-get install postgresql-client
    psql -h localhost -U postgres -W

元编程Quote和Unquote

概念介绍

  • Elixir中的AST是什么?
    • 是一个Elixir Term
    • 是深度嵌套的一种表示Elixir代码结构的方法
  • 如何理解?

    1
    iex(1)> quoted = quote do 1 + 2 end
    {:+, [context: Elixir, import: Kernel], [1, 2]}

    quote用于把一个Elixir表达式转换为Elixir的AST片段

  • 抽象语法树片段(AST Fragment)

才接触Quote和Unquote的时候会让人迷惑. 当你很好的理解了它后, 却是非常好用的.

Quote

Quote是一个Elixir函数,用于把一个Elixir表达式转换为AST(抽象语法树). AST是一种编译器的内部表示,用于对表达式进行求值, 比如:

1
iex> quote do: 1 + 2
{:+, [context: Elixir, import: Kernel], [1, 2]}

表达式1 + 2在Elixir编译器中被表示为一个有三个元素的元组:

  • 操作符 (:+)
  • 关键字列表元数据([context: Elixir, import: Kernel])
  • 参数列表 ([1, 2])

如果不考虑关键字列表元数据, 其可以表示为一棵抽象语法树, 以操作符为根节点, 2个参数为树叶

表达式1 + 2的抽象语法树

再如, 一个稍微复杂一点的表达式1 + 2 * 3, 将生成一个更加复杂的抽象语法树

1
iex> quote do: 1 + 2 * 3
{:+, [context: Elixir, import: Kernel],[1, {:*, [context: Elixir, import: Kernel], [2, 3]}]}

表达式1 + 2 * 3的抽象语法树

当对表达式求值的时候, Elixir编译器将会从最左边的叶子节点开始遍历. 例如,该AST会求值为: 1 + (2 * 3)

为了对Quote表达式求值, 需要使用Code.eval_quoted函数, 例如:

1
iex> Code.eval_quoted(quote do: 1 + 2 * 3)
{7, []}

Code.eval_quoted函数调用返回一个最终求得的值和一个从求值表达式产生的所有变量列表. 上述求得的值为7, 因为没有变量绑定, 所以返回一个空的列表.

Quote还可以用于函数调用, 比如:

1
iex> quote do: sum(1, 2, 3)
{:sum, [], [1, 2, 3]}

Quote function is like a function which is used to put an expression between a quote so that it can be used later on.

Next let’s try to define some variables and use those in quote body:

1
iex> a = 1
1
iex> b = 2
2
iex> Code.eval_quoted(quote do: a + b)
** (CompileError) nofile:1: undefined function a/0

The eval_quoted function call will give you an error on undefined function a. This happens because when
Code.eval_quoted is called, it does not know any a value, because the a here is not the same variable
that we defined outside ealier. In order to refer a variable defined outside quote, unquote function needs to be used

Unquote

So here, how it should be written if a variable is referred to outside of the scope of quote:

1
iex> a = 1
1
iex> b = 2
b
iex> quote do: unquote(a) + unquote(b)
{:+, [context: Elixir, import: Kernel], [1, 2]}

As you can see, the value of a and b are now evaluated correctly before Elixir construct the abstract syntax tree and
these values are actually computed at compiled time and not runtime. Now, let say, we define a function:

1
iex> a = 1
1
iex> b = 2
b
iex> fun = fn -> quote do
   unquote(a) + unquote(b)
  end
end
{:+, [context: Elixir, import: Kernel], [1, 2]}

Now we try to change a value and call the function again to see if the presentation will change with the new a value:

1
iex> a = 10
10
iex> fun.call()
{:+, [context: Elixir, import: Kernel], [1, 2]}

As you can see, although a’s value is change but the funtioncal representing a + b is still reflecting the original
value of a and b. The way that we use quote and unquote in Elixir can be very creative and dynamic, for instance, we
can define like following to play with the real function definition at runtime.

1
iex> num1 = 5
iex> num2 = 2
iex> perform = fn fun -> Code.eval_quoted(quote do: unquote(fun)(unquote(num1), unquote(num2))) end
iex> perform.(:rem) # calculate remaining of 5 and 2
{1, []}
iex> perform.(:div) # calculate division result of 5 and 2
{2, []}

匿名函数

快速定义

进入Elixir Shell输入:

1
iex(1)> sum = fn a, b -> a + b end
#Function<12.90072148/2 in :erl_eval.expr/5>

匿名函数的定义以关键字fn开始, 然后紧跟参数列表, 再接符号->, 其后为函数体, 并以关键字end结束匿名函数的定义

要调用匿名函数,需要使用dot(点)语法.

1
iex(2)> sum.(1,2)
3

匿名函数作为参数

1
defmodule MyList do
    def filter([], _func) do
        []
    end
    def filter([head|tail], func) do
        if func.(head) do
            [head | filter(tail, func)]
        else
            filter(tail, func)
        end
    end
end
## 奇数
MyList.filter([1,2,3,4,5,6], fn num -> rem(num,2) == 1 end)

控制台输出:

1
iex(5)> MyList.filter([1,2,3,4,5,6], fn num -> rem(num,2) == 1 end)
[1, 3, 5]

匿名函数简写

我们可以把匿名函数fn n -> rem(n, 2) == 1 end简写为&(rem(&1, 2) == 1)

1
sum = fn a, b -> a + b end
# 等同于
sum = &(&1 + &2)

其中第一个&表示匿名函数本身&1表示匿名函数的第一个参数, &2表示匿名函数的第二个参数, 以此类推. 小括号内为函数体表达式.

Ejabberd 多人聊天协议分析-配置房间

文章目录
  1. 1. 概述
  2. 2. 获取配置信息
  3. 3. 配置聊天室表单IQ-result
  4. 4. 提交房间配置表单 IQ-set
  5. 5. 获取服务器上的聊天室列表

概述

MUC(Multiple User Chat), 即多人聊天, 多个用户可以在一个房间(群)里面相互交流. 一个成员发送的信息能够被所有群中的成员看到. XMPP的MUC和QQ群在功能上很多都是相似的. 可以对比两者的区别来学习和理解XMPP的多人聊天扩展.

群里面又各种角色, 不同的角色拥有不同的权限, 例如:

  • 群的创建者具有所有权限
  • 群成员只能聊天
  • 管理员可以踢人, 邀请等管理任务.

获取配置信息

1
<iq type='get' xml:lang='zh' to='xmpp@conference.xmpp.hezhiqiang.info' xmlns='jabber:client'>
  <query xmlns='http://jabber.org/protocol/muc#owner'/>
</iq>

配置聊天室表单IQ-result

1
<iq from="xmpp@conference.xmpp.hezhiqiang.info"
    to="root@xmpp.hezhiqiang.info/37262574821415332900124309"
    type="result"
    xmlns="jabber:client"
    xmlns:stream="http://etherx.jabber.org/streams"
    version="1.0">
  <query xmlns="http://jabber.org/protocol/muc#owner">
    <instructions>您需要一个兼容 x:data 的客户端来配置房间</instructions>
    <x xmlns="jabber:x:data" type="form">
      <title>房间 xmpp@conference.xmpp.hezhiqiang.info 的配置 </title>
      <field type="hidden" var="FORM_TYPE">
        <value>http://jabber.org/protocol/muc#roomconfig</value>
      </field>
      <field type="text-single" label="房间标题" var="muc#roomconfig_roomname">
        <value>群标题</value>
      </field>
      <field type="text-single" label="房间描述" var="muc#roomconfig_roomdesc">
        <value>群描述</value>
      </field>
      <field type="boolean" label="永久保存该房间" var="muc#roomconfig_persistentroom">
        <value>1</value>
      </field>
      <field type="boolean" label="使房间可被公开搜索" var="muc#roomconfig_publicroom">
        <value>1</value>
      </field>
      <field type="boolean" label="公开参与人列表" var="public_list">
        <value>1</value>
      </field>
      <field type="boolean" label="进入此房间需要密码" var="muc#roomconfig_passwordprotectedroom">
        <value>0</value>
      </field>
      <field type="text-private" label="密码" var="muc#roomconfig_roomsecret">
        <value/>
      </field>
      <field type="list-single" label="允许的与会人最大数" var="muc#roomconfig_maxusers">
        <value>200</value>
        <option label="5">
          <value>5</value>
        </option>
        <option label="10">
          <value>10</value>
        </option>
        <option label="20">
          <value>20</value>
        </option>
        <option label="30">
          <value>30</value>
        </option>
        <option label="50">
          <value>50</value>
        </option>
        <option label="100">
          <value>100</value>
        </option>
        <option label="200">
          <value>200</value>
        </option>
      </field>
      <field type="list-single" label="将真实 Jabber ID 显示给" var="muc#roomconfig_whois">
        <value>moderators</value>
        <option label="仅主持人">
          <value>moderators</value>
        </option>
        <option label="任何人">
          <value>anyone</value>
        </option>
      </field>
      <field type="boolean" label="设置房间只接收会员" var="muc#roomconfig_membersonly">
        <value>1</value>
      </field>
      <field type="boolean" label="设置房间只接收主持人" var="muc#roomconfig_moderatedroom">
        <value>1</value>
      </field>
      <field type="boolean" label="用户默认被视为参与人" var="members_by_default">
        <value>1</value>
      </field>
      <field type="boolean" label="允许用户更改主题" var="muc#roomconfig_changesubject">
        <value>1</value>
      </field>
      <field type="boolean" label="允许用户发送私聊消息" var="allow_private_messages">
        <value>1</value>
      </field>
      <field type="list-single" label="允许访客发送私聊消息至" var="allow_private_messages_from_visitors">
        <value>anyone</value>
        <option label="没有人">
          <value>nobody</value>
        </option>
        <option label="仅主持人">
          <value>moderators</value>
        </option>
        <option label="任何人">
          <value>anyone</value>
        </option>
      </field>
      <field type="boolean" label="允许用户查询其它用户" var="allow_query_users">
        <value>1</value>
      </field>
      <field type="boolean" label="允许用户发送邀请" var="muc#roomconfig_allowinvites">
        <value>0</value>
      </field>
      <field type="boolean"
        label="更新在线状态时允许用户发送状态文本" var="muc#roomconfig_allowvisitorstatus">
        <value>1</value>
      </field>
      <field type="boolean" label="允许用户更改昵称" var="muc#roomconfig_allowvisitornickchange">
        <value>1</value>
      </field>
      <field type="boolean" label="允许访客发送声音请求" var="muc#roomconfig_allowvoicerequests">
        <value>1</value>
      </field>
      <field type="text-single"
        label="声音请求的最小间隔(以秒为单位)" var="muc#roomconfig_voicerequestmininterval">
        <value>1800</value>
      </field>
      <!-- 验证码白名单: 该名单中的Jid可以绕过验证码验证-->
      <field type="jid-multi"
        label="从验证码挑战中排除 Jabber ID" var="muc#roomconfig_captcha_whitelist"/>
      <field type="boolean" label="启用服务器端聊天记录" var="muc#roomconfig_enablelogging">
        <value>0</value>
      </field>
    </x>
  </query>
</iq>

提交房间配置表单 IQ-set

1
<!-- 提交房间配置表单 -->
<iq type='set' id='purple6b0f6f1d' to='xmpp@conference.xmpp.hezhiqiang.info'>
    <query xmlns='http://jabber.org/protocol/muc#owner'>
        <x xmlns='jabber:x:data' type='submit'>
            <field var='FORM_TYPE'>
                <value>http://jabber.org/protocol/muc#roomconfig</value>
            </field>
            <field var='muc#roomconfig_roomname'>
                <value>群标题</value>
            </field>
            <field var='muc#roomconfig_roomdesc'>
                <value>群描述</value>
            </field>
            <field var='muc#roomconfig_persistentroom'>
                <value>1</value>
            </field>
            <field var='muc#roomconfig_publicroom'>
                <value>1</value>
            </field>
            <field var='public_list'>
                <value>1</value>
            </field>
            <field var='muc#roomconfig_passwordprotectedroom'>
                <value>0</value>
            </field>
            <field var='muc#roomconfig_roomsecret'>
                <value/>
            </field>
            <field var='muc#roomconfig_maxusers'>
                <value>200</value>
            </field>
            <field var='muc#roomconfig_whois'>
                <value>moderators</value>
            </field>
            <field var='muc#roomconfig_membersonly'>
                <value>1</value>
            </field>
            <field var='muc#roomconfig_moderatedroom'>
                <value>1</value>
            </field>
            <field var='members_by_default'>
                <value>1</value>
            </field>
            <field var='muc#roomconfig_changesubject'>
                <value>1</value>
            </field>
            <!-- 允许私有消息-->
            <field var='allow_private_messages'>
                <value>1</value>
            </field>
            <field var='allow_private_messages_from_visitors'>
                <value>anyone</value>
            </field>
            <!-- 允许查询用户 -->
            <field var='allow_query_users'>
                <value>1</value>
            </field>
            <!-- 是否允许邀请 -->
            <field var='muc#roomconfig_allowinvites'>
                <value>0</value>
            </field>
            <field var='muc#roomconfig_allowvisitorstatus'>
                <value>1</value>
            </field>
            <field var='muc#roomconfig_allowvisitornickchange'>
                <value>1</value>
            </field>
            <field var='muc#roomconfig_allowvoicerequests'>
                <value>1</value>
            </field>
            <field var='muc#roomconfig_voicerequestmininterval'>
                <value>1800</value>
            </field>
            <field var='muc#roomconfig_captcha_whitelist'/>
            <field var='muc#roomconfig_enablelogging'>
                <value>0</value>
            </field>
        </x>
    </query>
</iq>

获取服务器上的聊天室列表

IQ-get

1
<iq type='get' to='conference.xmpp.hezhiqiang.info' xmlns='jabber:client'>
  <query xmlns='http://jabber.org/protocol/disco#items'/>
</iq>

IQ-result

1
<iq from="conference.xmpp.hezhiqiang.info"
    to="root@xmpp.hezhiqiang.info/36782688381415333849585695"
    type="result"
    xmlns="jabber:client"
    xmlns:stream="http://etherx.jabber.org/streams" version="1.0">
  <query xmlns="http://jabber.org/protocol/disco#items">
    <item jid="test@conference.xmpp.hezhiqiang.info" name="聊天室标题 (0)"/>
    <item jid="xmpp@conference.xmpp.hezhiqiang.info" name="群标题 (0)"/>
  </query>
</iq>

TODO::需要扩展支持分页显示

译文 | Elixir 物联网-1

1
defmodule IoT do
    defp do_listen(list_socket) do
        # 接受连接
        {:ok, socket} = :ssl.transport_accept(listen_socket)
        :ok = :ssl.accept_socket(socket)
        endpoint = TcpSupervisor.start_endpoint(socket)
        :ssl.controlling_process(socket,endpoint)
        # 异步处理
        :gen_server.cast(endpoint, {:start})
        # 重新进入Accept
        do_listen(listen_socket)
    end
end

多个Acceptor处理每秒1000连接:

Tcp监听器Supervisor

1
defmodule Owsla.TcpListenerSupervisor do
    use Supervisor.Behaviour
    def start_link(port, acceptor_count, backlog) do
        :supervisor.start_link({ :local, :listener_sup}, __MODULE__, [port, acceptor_count, backlog])
    end
    def init([port, acceptor_count, backlog]) do
        :ssl.start()
        {:ok, listen_socket} = create_listen_socket(port, backlog)
        spawn(fn ->
            Enum.each(1..acceptor_count,
                fn (_) -> start_listener() end
                )
            end)
            tree = [ worker(Owsla.TcpAcceptor, [listen_socket], restart: :permanent) ]
            supervise(tree, strategy: :simple_one_for_one)
    end
    def create_listen_socket(port, backlog) do
         tcp_options = [
            :binary,
            {:packet, :line},
            {:reuseaddr, true},
            {:active, false},
            {:backlog, backlog}
            ]
        :gen_tcp.listen(port, tcp_options)
    end
    def start_listener() do
        :supervisor.start_child(:listener_sup, [])
    end
end
1
defmodule Owsla.TcpAcceptor do
    use GenServer.Behaviour

    @ssl_options
        [{:certfile, "deviceserver.crt"}, {:keyfile, "deviceserver.key"},
        {:ciphers, [
            {:dhe_rsa,:aes_256_cbc,:sha256},
            {:dhe_dss,:aes_256_cbc,:sha256},
            {:rsa,:aes_256_cbc,:sha256},
            {:dhe_rsa,:aes_128_cbc,:sha256},
            {:dhe_dss,:aes_128_cbc,:sha256},
            {:rsa,:aes_128_cbc,:sha256},
            {:dhe_rsa,:aes_256_cbc,:sha},
            {:dhe_dss,:aes_256_cbc,:sha},
            {:rsa,:aes_256_cbc,:sha},
            {:dhe_rsa,:'3des_ede_cbc',:sha},
            {:dhe_dss,:'3des_ede_cbc',:sha},
            {:rsa,:'3des_ede_cbc',:sha},
            {:dhe_rsa,:aes_128_cbc,:sha},
            {:dhe_dss,:aes_128_cbc,:sha},
            {:rsa,:aes_128_cbc,:sha},
            {:rsa,:rc4_128,:sha},
            {:rsa,:rc4_128,:md5},
            {:dhe_rsa,:des_cbc,:sha},
            {:rsa,:des_cbc,:sha}
        ]}]

    def start_link(listen_socket) do
        :gen_server.start_link(__MODULE__, listen_socket, [])
    end
    def init(listen_socket) do
        :gen_server.cast self, {:listen}
        {:ok, listen_socket}
    end

    def handle_cast( {:listen}, listen_socket) do
        do_listen(listen_socket)
    end

    defp do_listen(listen_socket) do
        case :gen_tcp.accept(listen_socket) do
       {:ok, socket} ->
                case :ssl.ssl_accept(socket, @ssl_options) do
               {:ok, ssl_socket} ->
                        endpoint = Owsla.TcpSupervisor.start_endpoint(ssl_socket)
                        :ssl.controlling_process(ssl_socket, endpoint)
                        :gen_server.cast endpoint, {:start}
                        do_listen(listen_socket)
               {:error, :closed} ->
                        do_listen(listen_socket)
                end
       {:error, :closed} -> do_listen(listen_socket)
       {:error, _} -> { :stop, :error, [] }
        end
    end
end

使用linguist处理国际化字符串

使用Mix创建一个项目:

1
mix new i18n_example
cd i18n_example

编辑mix.exs项目文件, 添加linguist依赖

1
defp deps do
    [{:linguist,  github: "chrismccord/linguist"}]
end

获取依赖

1
mix deps.get

增加国际化字符串文件

1
# locales/zh.exs
[
  flash: [
    notice: [
      hello: "你好 %{first} %{last}"
    ]
  ]
]
# locales/fr.exs
[
  flash: [
    notice: [
      hello: "salut %{first} %{last}"
    ]
  ]
]

实现模块

1
defmodule I18n do
    use Linguist.Vocabulary
    # 默认英文, 注意,这里的locale是一个宏,而不是一个函数
    locale "en", [
        flash: [
            notice: [
                hello: "hello %{first} %{last}",
                bye: "bye now, %{name}!"
            ]
        ],
        users: [
            title: "Users",
            profiles: [
                title: "Profiles"
            ]
        ]
    ]
    # locale宏还可以直接读取文件
    locale "fr", Path.join([Path.dirname(__DIR__), "locales","fr.exs"])
    locale "zh", Path.join([Path.dirname(__DIR__), "locales","zh.exs"])
end

测试

1
$ iex -S mix
iex(1)> I18n.t!("en", "flash.notice.hello", first: "chris", last: "mccord")
"hello chris mccord"
iex(2)> I18n.t!("zh", "flash.notice.hello", first: "chris", last: "mccord")
"你好 chris mccord"

简介

文章目录
  1. 1. Bootstrapping
    1. 1.1. mix.exs
    2. 1.2. lib/my_project.ex
    3. 1.3. test/my_project_test.exs
    4. 1.4. test/test_helper.exs
  2. 2. 探索
  3. 3. 编译
  4. 4. 依赖
    1. 4.1. 源代码管理(scm)
    2. 4.2. 编译依赖
    3. 4.3. 重复性
    4. 4.4. 依赖任务
    5. 4.5. 依赖的依赖
  5. 5. 伞形项目
  6. 6. 环境

Elixir自带了几个应用使得编写和部署Elixir项目更加容易,其中Mix是关键.

Mix是一个提供了用于创建,编译,测试(很快就会有发布功能)的编译工具.Mix的灵感来自于Clojure的编译工具Leiningen,并且作者本人就是它的其中一个开发者.

在这一章,我们将学习如何用mix来创建项目,安装依赖.在之后的部分,我们将雪鞋如何创建OTP应用,和定制mix的任务.

Bootstrapping

要开始你的第一个项目,你只需要输入mix new 并把项目的路径作为参数.现在,我们将在当前目录创建一个被称为my_project的项目:

1
$ mix new my_project --bare

Mix将创建一个名为my_project的目录,包含一下这些内容:

1
.gitignore
README.md
mix.exs
lib/my_project.ex
test/test_helper.exs
test/my_project_test.exs

让我们来简单地看一下其中的一些.

  • 注意:Mix是一个Elixir的可执行文件.这意味着为了能够运行mix,elixir的可执行文件许需要在你的PATH里面.如果不是,你可以直接把脚本作为参数传递给elixir来执行:

    $ bin/elixir bin/mix new ./my_project

  • 注意你也能通过-S选项来让Elixir运行任何PATH里的脚本:

    $ bin/elixir -S mix new ./my_project

当用了-S, elixir会遍历PATH,寻找并运行那个脚本

mix.exs

这是包含了你项目配置的文件.它看起来是这样的:

1
defmodule MyProject.Mixfile do
  use Mix.Project

  def project do
    [app: :my_project,
     version: "0.0.1",
     deps: deps]
  end

  # Configuration for the OTP application
  def application do
    []
  end

  # Returns the list of dependencies in the format:
  # {:foobar, git: "https://github.com/elixir-lang/foobar.git", tag: "0.1"}
  #
  # To specify particular versions, regardless of the tag, do:
  # {:barbat, "~> 0.1", github: "elixir-lang/barbat"}
  defp deps do
    []
  end
end

我们的mix.exs定义了两个函数:

project, 用来返回项目的配置比如项目名称和版本,和application,用来产生一个被Erlang运行时管理的Erlang应用.在这一章,我们将谈一谈函数project.我们将在下一章详谈application函数.

lib/my_project.ex

这个文件包含了一个简单的模块,它定义了我们代码的基本结构:

1
defmodule MyProject do
end

test/my_project_test.exs

这个文件包含了项目的一个测试用例:

1
defmodule MyProjectTest do
  use ExUnit.Case

  test "the truth" do
    assert true
  end
end

有几点请注意:

  • 注意这个文件是一个Elixir的脚本文件(.exs).作为一个约定,我们不需要在运行之前编译测试.
  • 我们定义了一个测试模块MyProjectTest,用MyProjectTest来注入默认的行为,并定义了一个简单测试.你可以在ExUnit那一章学习到有关测试框架的更多内容.

test/test_helper.exs

我们将查看的最后一个文件是test_helper.exs,它的任务是启动测试框架:

1
ExUnit.start

探索

现在我们已经创建了新项目,接下去做什么?要了解还有其他什么命令可以使用的话,运行help任务:

1
$ mix help

它将会打印出所有可用的任务,运行mix help TASK可获取更多的信息.

运行其中一些命令试试,比如mix compilemix test,在你的项目里运行看看会发生什么.

编译

Mix可以为我们编译项目.默认的设置是用lib/放源代码,ebin/放编译后的beam文件.你无需提供任何的编译相关的设置,但如果你决定这么做,有一些选项可以用.例如,如果你打算把你的编译后的beam文件放在ebin之外的文件夹里,只需要在mix.exs里设置:compile_path:

1
def project do
  [compile_path: "ebin"]
end

总的来说,Mix会尽力表现的聪明一些,只在必须的时候编译.

注意在你第一次编译之后,Mix会在你的ebin文件夹里产生了一个my_project.app文件.这个文件里定义的Erlang应用是用到了你的项目中的application函数里的内容.

这个.app文件存储在和应用有关的信息,它的依赖,它所依赖的模块㩐等.每次你用mix运行命令的时候,这个应用会自动被启动,我们将在下一章学习如何配置它.

依赖

Mix也能用来管理依赖.依赖应被列在项目配置中,例如:

1
def project do
  [app: :my_project,
   version: "0.0.1",
   deps: deps]
end

defp deps do
  [{:some_project, ">= 0.3.0"},
   {:another_project, git: "https://example.com/another/repo.git", tag: "v1.0.2"}]
end

注意: 虽然并非必须,常见的做法是把依赖分散到它们自己的函数里.

某个依赖有一个原子来表示,跟着是一个需求和一些选项.在默认情况下,Mix使用hex.pm来获取依赖,但它也能从git库或直接从文件系统来获取.

当我们使用Hex, 你必须在需求里指定所接受的依赖的版本.它支持一些基本的操作符,例如>=,<=,>,==:

1
# Only version 2.0.0
"== 2.0.0"

# Anything later than 2.0.0
"> 2.0.0"

需求也支持用andor表达复杂的情况:

1
# 2.0.0 and later until 2.1.0
">= 2.0.0 and < 2.1.0"

类似上面的例子非常地常见,所以它也能用简单的方式表达:

1
"~> 2.0.0"

注意为git库设置版本需求不会影响到取出的分支和标签,所以类似下面这样的定义是合法的:

1
{ :some_project, "~> 0.5.0", github: "some_project/other", tag: "0.3.0" }

但它会导致一个依赖永远无法满足,因为被取出的标签总不能和需求的版本匹配.

源代码管理(scm)

Mix的设计就考虑到了支持多种的SCM工具,Hex包是默认,但:git:path是可选项.常见的一些选项是:

  • :git - 依赖是一个git版本库,Mix可以来获取和升级.
  • :path - 依赖是文件系统中的一个路径
  • :compile - 如何编译依赖
  • :app - 依赖所定义的应用的路径
  • :env - 依赖所用的环境(详情在后),默认是:prod;

每个SCM可以支持自定义选项,比如:git,支持下面的选项:

  • :ref - 用来检出git仓库的一个可选的引用(一次提交);
  • :tag - 用来检出git仓库的一个可选的tag
  • :branch - 用来检出git仓库的一个可选的分支
  • :submodules - 当为true,在依赖中递归地初始化子模块;

编译依赖

为了编译依赖,Mix会选择最适合的方式.依赖所包含的文件不同,编译的方式也不一样:

  • mix.exs
    • 直接用Mix的compile任务编译依赖;
  • rebar.configrebar.config.script
    • rebar compile deps_dir=DEPS编译,DEPS是项目依赖的安装目录;
  • Makefile
    • 简单地调用make;

如果编译的代码里没有包含以上的任何,你可以在``选项里直接指定一个命令:

{:some_dep, git: "...", compile: "./configure && make"}

如果:compile被设为false, 不做任何事情.

重复性

任何一个依赖管理工具的重要特性是可重复性.因此当你初次获取依赖,Mix将创建一个文件``,用来包含每个依赖所取出的索引.

当另一个开发者得到这个项目的拷贝,Mix将取出相同的那个索引,保证其他的开发者能“重复”同样的设置.

运行deps.update能自动升级锁,用deps.unlock任务来移除锁.

依赖任务

Elixir自带了许多用来管理项目依赖的任务:

  • mix deps - 列出所有的依赖和它的情况;
  • mix deps.get - 获取所有可得的依赖
  • mix deps.compile - 编译依赖
  • mix deps.update - 更新依赖;
  • mix deps.clean - 删除依赖文件;
  • mix deps.unlock - 解锁依赖

mix help来获取更多信息.

依赖的依赖

如果你的依赖是一个Mix或rebar的项目,Mix知道如何应付:它将自动获取和处理你的依赖的所有的依赖.然而,如果你的项目中有两个依赖共享了同一个依赖,但它们的SCM信息又无法互相匹配的话,Mix将标明这个依赖是分裂的,并发出警告.要解决而这个问题,你可以在你项目中声明选项override: true, Mix将根据这个信息来获取依赖.

伞形项目

你是否想过,如果能将几个Mix项目打包在一起,只需一个命令就可以运行各自的Mix任务, 该有多方便?.这种将项目打包在一起使用的情况被称为伞形项目.一个伞形项目可以用下面的命令来创建:

1
$ mix new project --umbrella

这将会创建一个包含以下内容的``文件:

1
defmodule Project.Mixfile do
  use Mix.Project

  def project do
    [apps_path: "apps"]
  end
end

apps_path选项指定了子项目所在的文件夹.在伞形项目中运行的Mix任务,会对apps_path文件夹中的每一个子项目起作用.例如mix compilemix test将编译或测试文件夹下的每一个项目.值得注意的是,伞形项目既不是一个普通的Mix项目,也不是一个OTP应用,也不能修改其中的源码.

如果在子项目之间有互相依赖的存在,需要指定它们的顺序,这样Mix才能正确编译.如果项目A依赖于项目B,这个依赖关系必须在项目A的mix.exs文件里指定.修改文件mix.exs来指定这个依赖:

1
defmodule A.Mixfile do
  use Mix.Project

  def project do
    [app: :a,
     deps_path: "../../deps",
     lockfile: "../../mix.lock",
     deps: deps]
  end

  defp deps do
    [{ :b, in_umbrella: true }]
  end
end

注意上面的子项目中deps_pathlockfile选项.如果在你的伞形项目的所有子项中都有它们,它们将共享依赖.apps文件夹中的mix new将自动用这些预设的选型创建一个项目.

环境

Mix有环境的概念,能让一个开发者去基于一个外部的设定来定制编译和其他的选项.默认下,Mix能理解项目三类环境:

  • dev - mix任务的默认环境;
  • test - 用于mix test;
  • prod - 在这个环境下,依赖将被载入和编译;

在默认情况下,这些环境的行为没有不同,我们之前看到的所有配置都将会影响到这三个环境.针对某个环境的定制,可以通过访问Mix.env来实现:

1
def project do
  [deps_path: deps_path(Mix.env)]
end

defp deps_path(:prod), do: "prod_deps"
defp deps_path(_), do: "deps"

Mix默认为dev环境(除了测试).可以通过修改环境变量MIX_ENV来改变环境.

1
$ MIX_ENV=prod mix compile

在下一章,我们将学习如何用Mix编写OTP应用和如何创建你自己的任务.

伞形项目

关于伞形项目, 什么是伞形项目, 定义在这里

创建一个伞形项目

1
root@0b85dcd174f2:~/ejabberd/elixir# mix new umbrella_project --umbrella
* creating .gitignore
* creating README.md
* creating mix.exs
* creating apps
* creating config
* creating config/config.exs
Your umbrella project was created successfully.
Inside your project, you will find an apps/ directory
where you can create and host many apps:
    cd umbrella_project
    cd apps
    mix new my_app
Commands like `mix compile` and `mix test` when executed
in the umbrella project root will automatically run
for each application in the apps/ directory.

在伞形项目中运行的Mix任务,会对文件夹中的每一个子项目起作用.

Elixir 使用Mix构建OTP应用(译)

文章目录
  1. 1. 2 用Mix编写OPT应用
  2. 2. 2.1 Stacker服务器
  3. 3. 2.1.1 深入学习回调函数
  4. 4. 2.1.2 干掉一个服务器
  5. 5. 2.2 监控服务器
  6. 6. 2.3 谁来监控监工?
  7. 7. 2.4 启动应用
  8. 8. 2.5 配置应用

2 用Mix编写OPT应用

在Elixir里,我们如何保存状态?

我们的软件需要在运行时系统里保存状态,配置,数据。在之前的章节里我们已学会了如何用进程/Actor保持状态,在一个循环中如何接受以及回复消息,但这种方式似乎不够可靠。如果我们的进程被一个错误退出了怎么办?难道我们真的需要仅仅为一个配置而去创建一个新的进程?

在这一章,我们将用OTP的方式来回答这些问题。在实践中,我们不必使用Mix来编写这样的应用,然而借此机会正好让我们了解一些Mix提供的一些方便之处。

2.1 Stacker服务器

我们的应用将会是一个运行我们推进/推出的简单堆栈。我们管它叫stacker:

1
$ mix new stacker --bare

我们的应用将包含一个堆栈,允许同一时间被许多的进程访问。为此,我们将创建一个服务器来负责管理这个堆栈。客户端随时可以向服务器发送消息来从服务器推进或取出某物。

因为在Erlang和Elixir里,创建这样的一个服务器是常见的一个范式,在OTP里有一个被称为GenServer的行为封装了这些常见的服务器功能。让我们创建一个文件lib/stacker/server.ex, 这就是我们的第一个服务器:

1
defmodule Stacker.Server do
  use GenServer.Behaviour

  def init(stack) do
    { :ok, stack }
  end

  def handle_call(:pop, _from, [h|stack]) do
    { :reply, h, stack }
  end

  def handle_cast({ :push, new }, stack) do
    { :noreply, [new|stack] }
  end
end

我们的服务器定义了三个函数: init/1handle_call/3handle_cast/2。我们不会直接调用这些函数,它们是当我们请求服务器的时候由OTP来使用的函数。我们很快会了解详情,现在我们无需关心更多。为此,在你的命令行里运行iex -S mix来开始用mix来启动iex,输入下面的指令:

1
# Let's start the server using Erlang's :gen_server module.
# It expects 3 arguments: the server module, the initial
# stack and some options (if desired):
iex> { :ok, pid } = :gen_server.start_link(Stacker.Server, [], [])
{:ok,<...>}

# Now let's push something onto the stack
iex> :gen_server.cast(pid, { :push, 13 })
:ok

# Now let's get it out from the stack
# Notice we are using *call* instead of *cast*
iex> :gen_server.call(pid, :pop)
13

非常好,我们的服务器工作正常!然而在幕后其实发生了很多的事情,然我们来一一探究。

首先,我们用OTP中的:gen_server模块启动服务器。注意我们使用了start_link, 它启动了服务器并且把当前的进程与之相连。在这种情况下,如果服务器死了,它将会向我们的当前进程发送一个退出消息,使它也退出。我们将在后面看到这个行为。函数start_link返回的是新创建的进程的识别符(pid)。

之后,我们向服务器发送了一个cast消息。消息的内容是{ :push, 13 },与我们之前定义在Stacker.Server中的回调函数handle_cast/2里的一致。无论何时我们发送一个cast消息,函数handle_cast/2会被调用来处理这个消息。

接着,我们最终用发送call消息地方时,看到了堆栈里的情况,它触发了回调handle_call/3。那么,castcall到底有什么不同呢?

cast消息是异步的:我们向服务器发送一个消息,然而并不期待回复。这也是为什么我们的handle_cast/2回调返回的是{ :noreply, [new|stack] }的缘故。这个元组中的第一个元素表明了无需回复,而第二个元素是包含了新物件的经过升级的堆栈。

相反,call消息是同步的。当我们发送一个call消息,客户端期待一个回复。在这个例子中,回调handle_call/3返回了{ :reply, h, stack },其中第二个元素就是用来返回的内容,而第三个是我们不包含头的堆栈。因为call能够向客户端返还消息,所以它的也多了一个有关客户端情况的参数(_from)。

2.1.1 深入学习回调函数

在GenServer的例子中,类似handle_callhandle_cast的函数可能返回8种不同的数值:

1
{ :reply, reply, new_state }
{ :reply, reply, new_state, timeout }
{ :reply, reply, new_state, :hibernate }
{ :noreply, new_state }
{ :noreply, new_state, timeout }
{ :noreply, new_state, :hibernate }
{ :stop, reason, new_state }
{ :stop, reason, reply, new_state }

一个GenServer的实现必须实现6种不同的回调函数。模块GenServer.Behaviour自动定义了这些函数,但又允许我们根据需要来修改。下面是这些函数的列表:

  • init(args) - 当服务器启动时调用
  • handle_call(msg, from, state) - 被调用来处理call消息
  • handle_cast(msg, state) - 被调用来处理cast消息
  • handle_info(msg, state) - 被调用来处理进程所收到的其他消息
  • terminate(reason, state) - 在服务器当机之前被调用,对清理很有用
  • code_change(old_vsn, state, extra) - 在应用代码热升级的时候被调用

2.1.2 干掉一个服务器

Of what use is a server if we cannot crash it?

实际上要使一个服务器当机并不难。我们的回调handle_call/3只有当堆栈不是空的时候才工作正常(想起来了吗,[h|t]不能匹配空列表)。让我们在堆栈为空的情况下发送一个消息看看:

1
# Start another server, but with an initial :hello item
iex> { :ok, pid } = :gen_server.start_link(Stacker.Server, [:hello], [])
{:ok,<...>}

# Let's get our initial item:
iex> :gen_server.call(pid, :pop)
:hello

# And now let's call pop again
iex> :gen_server.call(pid, :pop)

=ERROR REPORT==== 6-Dec-2012::19:15:33 ===
...
** (exit)
...

你可以看到,这里有两个错误报告。第一个因为当机由服务器产生。因为服务器是同我们的进程相连的,它也发送了一个退出的消息,IEx as ** (exit) ....

因为我们的服务器总会崩溃,我们需要监控它们,这也是下面的内容。GenServer.Behaviour不仅仅包含我们已经学到的这些。请查看GenServer.Behaviour的文档来发现更多。

2.2 监控服务器

当在Eralng和Elixir里编写应用,一个常用到的哲学是fail first。也许是因为资源不可得,也许是服务之间的超时,或许是其他的什么原因。这也是为什么有能力对这些崩溃作出反应并回复过来,是非常重要的。把这些牢记在心,我们为我们的服务器编写一个supervisor。

用下面的内容创建一个文件,lib/stacker/supervisor.ex

1
defmodule Stacker.Supervisor do
  use Supervisor.Behaviour

  # A convenience to start the supervisor
  def start_link(stack) do
    :supervisor.start_link(__MODULE__, stack)
  end

  # The callback invoked when the supervisor starts
  def init(stack) do
    children = [ worker(Stacker.Server, [stack]) ]
    supervise children, strategy: :one_for_one
  end
end

在监控中,唯一需要实现的回调函数是init(args)。这个回调必须返回监工的规格,在上面的例子中实际调用了帮助函数supervise/2

我们的监工非常简单:它必须监控我们的工人Stacker.Server, 工人的启动需要一个参数,在这是默认的堆栈。完成定义的工人被用:one_for_one的策略监控,这意味着每一次工人死亡之后都会被重启。

由于我们的工人由Stacker.Server模块指定,并且需要传递stack作为参数,在默认下,监工将调用函数Stacker.Server.start_link(stack)来启动工人,所以让我们来实现它:

1
defmodule Stacker.Server do
  use GenServer.Behaviour

  def start_link(stack) do
    :gen_server.start_link({ :local, :stacker }, __MODULE__, stack, [])
  end

  def init(stack) do
    { :ok, stack }
  end

  def handle_call(:pop, _from, [h|stack]) do
    { :reply, h, stack }
  end

  def handle_cast({ :push, new }, stack) do
    { :noreply, [new|stack] }
  end
end

函数start_link同我们之前启动服务器的方式很相似,除了在这里我们需要多传递一个参数{ :local, :stacker }。这个参数在本地节点上注册我们的服务器,运行用一个名字(在这里,是:stacker)来调用它,而无需直接使用pid

不借助监工,让我们运行iex -S mix来再一次打开控制台,这也会再一次编译我们的文件:

1
# Now we will start the supervisor with a
# default stack containing :hello
iex> Stacker.Supervisor.start_link([:hello])
{:ok,<...>}

# And we will access the server by name since
# we registered it
iex> :gen_server.call(:stacker, :pop)
:hello

注意监工自动为我们启动了服务器,现在我们可以用名字:stacker向服务器发送消息了。如果我们使得服务器当机,会发送甚什么?

1
iex> :gen_server.call(:stacker, :pop)

=ERROR REPORT==== 6-Dec-2012::19:15:33 ===
...
** (exit)
...

iex> :gen_server.call(:stacker, :pop)
:hello

它和前面一样当机了,当监工立刻用默认的堆栈重启了它,使得我们可以再一次接受到:hello。太棒了!

默认下,监工运行一个工人在5秒内最多当机5次。如果工人当机的频率超过了这个限制,监工就会放弃它,不在重启。让我们尝试接连发送一个未知的消息看看(要快!):

1
iex> :gen_server.call(:stacker, :unknown)
... 5 times ...

iex> :gen_server.call(:stacker, :unknown)
** (exit) {:noproc,{:gen_server,:call,[:stacker,:unknown]}}
    gen_server.erl:180: :gen_server.call/2

第六个消息不在产生错误报告,因为我们的服务器不在自动重启了。Elixir返回:noprocno process的简写形式),意味着那里已经没有一个被称为:stacker的进程了。重启的次数和时间间隔,可以通过向函数supervise传递参数进行修改。除了上面例子中的:one_for_one, 监工也能选用不同的重启策略。如果向知道还有那些支持的策略,检查Supervisor.Behaviour文档

2.3 谁来监控监工?

我们已经编写了我们的监工,但有一个问题:谁来监控监工?为了回答这个问题,OTP有一个概念,应用(application)。应用可以被作为一个整体启动或关闭,当这些发生的时候,它们通常和一个监工相连。

在之前的章节中,我们已经看到了Mix如何用文件mix.exs中的application函数中包含的信息来自动地产生一个.app文件,每次编译我们的项目。

这个.app文件被称为应用规格,它必须包含我们的应用的依赖,它定义的模块,注册名和其他的许多。其中的一些信息由Mix来自动完成,但另外的一些数据需要手动添加。

在我们的例子里面,我们的应用有一个监工,而且它用名字:stacker注册了监工。也就是说,为了避免冲突,我们需要在应用规格里加入所有的注册名。如果两个应用注册了同一个名字,我们能更快地找到冲突。所以,让我们打开文件`,用下面的内容编辑application`函数:

1
def application do
  [ registered: [:stacker],
    mod: { Stacker, [:hello] } ]
end

:registered键中我们指定了我们的应用注册的所有名字。而:mod键的用途是,一旦应用启动,它必须去调用应用模块回调函数(appliation module callback)。在我们的例子里面,应用模块回调函数是Stack模块,而且它将会接受到默认的参数,堆栈[:hello]。这个回调函数必须返回同应用相关的监工的pid

有了这些在心里,让我们打开文件lib/stacker.ex,添加以下的内容:

1
defmodule Stacker do
  use Application.Behaviour

  def start(_type, stack) do
    Stacker.Supervisor.start_link(stack)
  end
end

Application.Behaviour期待两个回调,start(type, args)stop(state)。我们需要来实现start(type, args), 而stop(state)可以先放在一边不管。

在添加了上面的应用行为之后,你只需要在一次启动iex -S mix。我们的文件将被再一次重编译,监工(包括我们的服务器)会自动启动:

1
iex> :gen_server.call(:stacker, :pop)
:hello

太棒了,它能性!也许你已经注意到了,应用回调函数start/2接受了一个类型参数,虽然我们忽略了它。这个类型控制当我们的监工,自然也包括应用,崩溃的时候,虚拟机应该如何应对。你可以通过阅读Application.Behaviour文档学到更多。

最后,注意mix new支持一个--sup选项,它告诉Mix产生一个包含应用模块回调的监工,自动地完成了一些上面的工作。你一定要试试!

2.4 启动应用

在任何时候,我们都不必自己去启动我们定义的应用。那是因为默认下Mix会启动所有的应用,包括所依赖的应用。我们能通过调用OTP提供的:application模块中的函数来手动地启动应用:

1
iex> :application.start(:stacker)
{ :error, { :already_started, :stacker } }

在上面的例子中,因为应用已经事先启动了,它返回了一个错误信息。

Mix不仅启动你的应用,而且包括所有的你的应用的依赖。请注意你的项目的依赖(我们在前面几章中讨论过的,定义在键deps里的)和应用依赖是不同的。

项目依赖也许会包含你的测试框架或者一个编译时的依赖。应用依赖是运行时你的应用依赖的一切。一个应用依赖需要明确地被加入appliation函数里:

1
def application do
  [ registered: [:stacker],
    applications: [:some_dep],
    mod: { Stacker, [:hello] } ]
end

当在Mix里运行任务,它将确保应用以及应用的依赖都会被启动。

2.5 配置应用

除了我们已经看到的键:registered:applications:mod,应用也支持被读取和设置的配置数值。

在命令行里,尝试:

1
iex> :application.get_env(:stacker, :foo)
:undefined
iex> :application.set_env(:stacker, :foo, :bar)
:ok
iex> :application.get_env(:stacker, :foo)
{ :ok, :bar }

这个机制非常有用,它无需创建一整个监工链就能为你的应用提供配置数值。应用的默认的配置数值可以通过如下的方式在文件mix.exs中定义:

1
def application do
  [ registered: [:stacker],
    mod: { Stacker, [:hello] },
    env: [foo: :bar] ]
end

现在,从控制台里退出,然后用iex -S mix重启它:

1
iex> :application.get_env(:stacker, :foo)
{ :ok, :bar }

例如,IEx和ExUnit是两个Elixir包含的应用,它们的mix.exs文件就是使用了这样的配置,文件IExExUnit。这样的应用于是提供了提供了对读写这些数值的封装

到这里,我们结束了这一章。我们已经学习了如何创建服务器,监控它们,把它们和我们的应用整合和提供简单的配置选项。在下一章,我们将学习如何创建一个Mix中的定制任务。

创建自定义Mix任务(译)

文章目录
  1. 1. 常见API
  2. 2. Namespaced Tasks
  3. 3. 选项解析
  4. 4. 分享任务
    1. 4.1. 作为依赖
    2. 4.2. 把任务打包
    3. 4.3. MIX_PATH

原文: http://elixir-lang.readthedocs.org/en/latest/mix/3/

在Mix中,一个任务实际上是一个具有名称空间Mix.Tasks并实现了run/1函数的模块.例如,compile任务是一个名称为Mix.Tasks.Compile的模块.

创建一个简单的任务:

1
defmodule Mix.Tasks.Hello do
    use Mix.Task
    @shortdoc "这是一个短文档, 看"
    @moduledoc """
    一个测试任务
    """
    def run(_) do
        IO.puts "你好,世界!"
    end
end

保存文hello.ex, 并编译:

1
$ elixirc hello.ex
$ mix hello
你好,世界

上述模块定义了一个名称为hello的任务. 函数run/1接受一个二进制字符串参数, 该参数是从命令行传递给此任务的.

当调用命令mix hello时, 该任务被执行, 并输出你好,世界. Mix使用其第一个参数(hello)查找任务模块并执行run函数.

为什么有一个@moduledoc@shortdoc. 这两个文档标记是被help任务使用来显示任务的说明文档. @shortdoc用在执行mix help的时候显示任务的简短描述, @moduledoc用于执行mix help hello是显示hello任务的详细描述.

除了这两个文档标签外, 还有一个@hidden标签, 当其设置为true时,该任务不显示在mix help的输出中, 任何没有@shortdoc标签的任务也不会显示.

1
defmodule ModuleName do
    @hidden true
end

常见API

当编写任务时, 需要访问一些常见的Mix功能, 如下:

  • Mix.Project.config 返回项目配置(mix.exsproject函数), 如果当前目录不存在mix.exs文件, 该函数返回一个空的配置.
  • Mix.Project.get! 访问当前项目的模块, 需要访问项目中的特殊函数是非常有用. 如果项目未定义,将抛出异常.
  • Mix.shell
  • Mix.Task.run(task,args) 从其他Mix任务中调用另一个任务; 如果该任务已经被调用, 不做任何操作.

Namespaced Tasks

简单的任务可用于完成复杂的事情. 任务实际上也是Elixir代码,任何Elixir可以做的事情都可以放到任务中去做. 可以像分发其他库一样分发任务, 让任务可以在其他项目中重用.

要在多个项目中重用任务, 为了避免名称冲突, Mix任务支持名称空间.

示例:

1
defmodule Mix.Tasks.Mytasks do
    @shortdoc "任务集合模块"
    @moduledoc """
    用于构建和部署的任务集合
    """
    defmodule Build do
        use Mix.Task
        @shortdoc "构建任务"
        @moduledoc """
        构建一个软件组件模块
        """
        def run(_) do
            IO.puts "运行子任务Build"
        end
    end
    defmodule Deploy do
        use Mix.Task

        @shortdoc "部署一个软件组件"
        @moduledoc """
        把一个软件组件部署到服务器
        """
        def run(_) do
            IO.puts "运行子任务Deploy"
        end
    end
end

任务模块写好了后, 可以像这样调用任务: mix mytasks.build, mix mytasks.deploy, 这功能很酷对吧?

任务帮助

选项解析

OptionParser.parse(["--debug"])
#=> { [debug: true], [] }

OptionParser.parse(["--source", "lib"])
#=> { [source: "lib"], [] }

OptionParser.parse(["--source", "lib", "test/enum_test.exs", "--verbose"])
#=> { [source: "lib", verbose: true], ["test/enum_test.exs"] }

分享任务

创建了任务后,如果需要在团队内分享给其他人, 或在其他项目中重用, 这章描述了几种不同的分享法师

作为依赖

假设创建了一个my_tasks项目, 其提供了众多有用的功能, 把该项目添加为其他项目的依赖, 所有在my_tasks项目中的任务可以被其他引用了my_tasks的项目使用.

把任务打包

Mix允许你安装和卸载本地归档包. 要为当前项目生成一个归档包, 运行:

root@0b85dcd174f2:~/elixir/commandlinetools# mix do archive.build
Generated archive commandlinetools-0.0.1.ez with MIX_ENV=dev

把任务打包

打包的任务可以通过文件系统路径或URL安装:

mix archive.install http://localhost/commandlinetools-0.0.1.ez

mix archive命令的的详细说明可通过 mix help archive 查看

MIX_PATH

最有一个中方法是使用MIX_PATH. 设置了MIX_PATH环境变量之后, 所有在其中的任务对Mix都可用, 比如:

$ export MIX_PATH="/elixir/ebin"

这种方式可以单独维护需要在多个项目中使用的任务.