Erlang编译相关模块

compile 模块

compile 模块提供编译接口,主要是 file/2 和 forms/2,分别接受文件和Erlang抽象格式(Abstract Format)——Erlang项式解析树的标准表现形式。第二个参数是选项,大致可以分为几类: 测试编译结果、是否产生二进制数据、调试信息、Makefile条目生成、’P’/‘E’/’S’生成中间格式文件、错误和警告信息、宏定义等。具体内容看文档。

‘P’、’E’、’S’ 选项分别生成预处理和解析变换(基本检查、函数是否存在等)、代码转换(导入导出文件处理、宏替换、生成module_info函数)、汇编代码(.S文件即模块对应的Erlang汇编,可以用file和forms函数生成bin)文件。这也暗示了compile模块的几个过程,下面介绍相关的其他模块。

epp 模块

epp 模块解析Erlang源文件,并生成抽象格式。可以用open打开一个EPP句柄,并迭代处理文件,也可以用 parse_file 一次性生成。也提供文件编码相关的函数。

Erlang和其他语言的交互

Erlang和其他语言(如C和Java)的交互手段一直是我很感兴趣的主题,周末看了下OTP文档,终于大致理清楚了思路。这里先简单总结四种交互手段,也是更进一步学习Erlang的开始。

端口

最简单的方式是调用Erlang模块的 open_port/2 ,创建一个端口。Erlang端口可以被认为是一个外部Erlang进程,交互手段是通过标准IO输入输出,对应C语言里的read和write函数。

-spec open_port(PortName, PortSettings) -> port() when
      PortName :: {spawn, Command :: string() | binary()} |
                  {spawn_driver, Command :: string() | binary()} |
                  {spawn_executable, FileName :: file:name() } |
                  {fd, In :: non_neg_integer(), Out :: non_neg_integer()},
      PortSettings :: [Opt],
      Opt :: {packet, N :: 1 | 2 | 4}
           | stream
           | {line, L :: non_neg_integer()}
           | {cd, Dir :: string() | binary()}
           | {env, Env :: [{Name :: string(), Val :: string() | false}]}
           | {args, [string() | binary()]}
           | {arg0, string() | binary()}
           | exit_status
           | use_stdio
           | nouse_stdio
           | stderr_to_stdout
           | in
           | out
           | binary
           | eof
       | {parallelism, Boolean :: boolean()}
       | hide.

HTTP请求处理cowboy_rest

cowboy_rest以REST方式,允许用户模块介入HTTP请求处理。看过nginx源码的同学都知道,nginx回调模块可以介入http请求处理,依靠请求过程中划分的十几个阶段,实现资源重定向、权限控制等。cowboy的做法略有不同,前一篇提到过中间件的概念。给middlewares设置实现了execute回调的模块,接收到请求头和数据包后介入处理,实现nginx类似功能,官方有个markdown的例子,回头分析。一般来说,我们关注正常请求,在用户回调模块中实现cowboy_rest模块的可选回调即可。

cowboy_handler作为请求处理的最后一环,根据路由规则回调用户模块,如果返回{cowboy_rest, Req, State},就会调用cowboy_rest:upgrade/6:

execute(Req, Env=#{handler := Handler, handler_opts := HandlerOpts}) ->
	try Handler:init(Req, HandlerOpts) of
		...
		{Mod, Req2, State} ->
			Mod:upgrade(Req2, Env, Handler, State, infinity, run);
		...

Erlang HTTP服务器cowboy

cowboy是基于ranch的http服务器框架,提供用户自定义路由、REST接口等便利功能。由于ranch是很完善的TCP池,所以在此之上的cowboy代码很容易支持http/https。虽说如此,http毕竟是很复杂的协议,好多细节不参考RFC根本搞不清楚。借此机会详细了解如何实现完整的HTTP服务器。首先了解cowboy完整的流程以及使用方法。

最简单的例子:

start(_Type, _Args) ->
	Dispatch = cowboy_router:compile([
		{'_', [
			{"/", toppage_handler, []}
		]}
	]),
	{ok, _} = cowboy:start_clear(http, 100, [{port, 8080}], #{
		env => #{dispatch => Dispatch}
	}),

tcp连接池ranch

ranch 是Erlang/OTP实现的 TCP 连接池,我在看cowboy源码的时候意外发现了这个组件。让人欣喜的是它本身就非常强大,只需要短短几行代码,就可以实现强大的tcp服务器功能,而socket管理的细节都被ranch封装起来。

运行anchapp:start(,_)启动服务,然后就可以使用自己实现的服务。晚上看了一下它的源码,非常短,但是很成熟,是学习的好例子。

ranch的启动大致如下流程:

ranch_app:start(_,_) %% application
-> ranch_sup:start_link() %% supervisor 
-> ranch_sup:init() %% supervisor callback
-> ets:new(ranch_server, ...) %% one_for_one, ranch_server
-> ranch_server:start_link() %% gen_server

ranch_server提供tcp连接池管理,其它模块通过和ranch_server交互,维护自己的连接信息、socket选项等。

supervisor源码阅读

supervisor是监控树中最重要的组成部分,负责启动、停止和监控子进程,异常时重启等。supervisor模块应该尽可能简单,保持功能单一——负责监控。supervisor模块本身基于gen_server,所以它实现了gen_server的六个主要回调函数,是一个gen_server进程。通过阅读supervisor模块源码,了解实现supervisor回调的过程以及实现进程监控器的步骤。

先写一个简单的worker_sup.erl,实现了supervisor行为模式,配合worker.erl模块使用:

%% supervisor
-module(worker_sup).

-behaviour(supervisor).

-export([start_link/0]).
-export([init/1]).

start_link() ->
    supervisor:start_link(worker_sup, []).

init(_Args) ->
    SupFlags = #{strategy => one_for_one, intensity => 1, period => 5},
    ChildSpecs = [#{id => worker, start => {worker, start_link, []}, restart => permanent, shutdown => brutal_kill, type => worker, modules => [worker]}],
    {ok, {SupFlags, ChildSpecs}}.

gen_server源码阅读

gen_server是Erlang/OTP的通用服务器框架,使用最为广泛。源代码很少,只有几百行。抽空详细梳理了一遍流程,以简单的gen_server应用worker为例。

先看我们自己的worker.erl文件,根据OTP设计规范,既是回调模块,又是用户接口。对外导出 start_link/0,1 , query/1 , set/1 , show/1 , *stop/1*,同时实现gen_server行为模式的6个必要的回调函数:

9 OTP设计原则——第四部分

9.8 应用

这一章节结合 Kernel 的man page中的 app(4)application(3) 部分阅读。

9.8.1 应用概念

当你写完代码实现某个特定功能之后,你希望把它变成一个 应用 —— 可以被启动停止的组件单元,同时也能被其他系统复用。

为了做到这一点,创建一个 应用回调模块 ,还需要描述这个应用如何启动和停止的文件。

因此,需要一个 应用描述说明 ,并放到 应用源文件 中。除了这些,这个文件还表明应用由哪些模块组成以及回调模块的名称。

如果你用 systools —— Erlang/OTP的代码打包工具(查看 Releases ),为每个应用准备的代码会放到特殊的目录,遵从预定义的 *目录结构*。

9 OTP设计原则——第三部分

9.5 gen_event 行为模式

这一章节结合 stdlib man page中的 gen_event(3) 阅读,man page中详细介绍了gen_event行为模式的接口函数和回调函数。

9.5.1 事件处理原则

在OTP中,事件管理器是一个可以接收并记录事件的命名对象。错误、警告和其他信息都是可以被记录的事件。

在事件管理器中,可以安装一个或多个事件处理模块。当事件处理器接收到事件通知时,所有安装的事件处理模块会处理这个事件。例如,处理错误的事件管理器默认有一个事件处理器,它会把错误日志写到终端。如果特定时期的错误信息需要保存到文件中,那么用户可以安装一个事件处理器来做这件事。当不需要写入文件时,这个事件处理器可以被删除。

事件管理器可以用进程的形式实现,而事件管理器可以实现它的回调模块。

事件管理器内部维护一个{Module, State}列表,每个Module是一个事件处理器,State是事件处理器的内部状态。

9 OTP设计原则——第二部分

9.4 gen_statem 行为模式

这一章节结合stdlib man page中的gen_statem(3)阅读,其中详细介绍了gen_statem行为模式的接口函数和回调函数。

注意:这是Erlang/OTP 19.0 开始提供的新行为模式,它已经经过严格的检验和慎重思考,并且稳定的运行在至少两个重量级OTP应用。根据用户反馈,可能会在Erlang/OTP 20.0中做一些不向下兼容的改动,我们希望不会如此。

9.4.1 事件驱动的状态机

现有公认的有效状态机理论并没有涉及状态转移如何触发,我们假设输出是由输入和状态函数运算,并产生一些值。

对于事件驱动的状态机,输入是一个触发状态转移的事件,输出是状态转移过程中执行的动作。有限状态机的数学模型和下面的关系集合类似:

State(S) x Event(E) -> Actions(A), State(S')

这组关系可以这样理解: