erlang 組件 application

erlang 組件 application

我們?nèi)粘S玫降牡谌降膸煲诚欤M件絕大部分是application,所以理解并且掌握applicaiton的一些特性對我們來說非常重要段誊,也非常實用连舍。

1.什么是applicaiton?,為什么要用application

官方的解釋是這個樣子的:

When you have written code implementing some specific functionality you might want to make the code into an application, that is, a component that can be started and stopped as a unit, and which can also be reused in other systems.

http://erlang.org/doc/design_principles/applications.html

在我理解其實就2點:

  1. 為了實現(xiàn)特定功能
  2. 可以復(fù)用

在我們的日常生活中盼玄,單項目多application的場景比較少强岸,我們平時寫的最多的是module準確的說是回調(diào)模塊Callback Module,也會有少量的進程狀態(tài)模塊Residence Module砾赔,前者只是一個回調(diào)函數(shù)青灼,從一個狀態(tài)切換到另一個狀態(tài)杂拨,所以生命周期只在單process,很少與外界的process打交道檀夹,所以使用link,monitor這些進程間關(guān)系相對較少策橘。反觀application要考慮的方面比較多丽已,不僅要考慮狀態(tài)的正確與否,還會考慮到進程運行的異常與否吼畏,甚至?xí)紤]到怎么來設(shè)計監(jiān)控樹來讓程序保持健壯。

2. 如何自己實現(xiàn)一個application?

2.1 目錄結(jié)構(gòu)

─ ${application}
      ├── doc
      │   ├── internal
      │   ├── examples
      │   └── src
      ├── include
      ├── priv
      ├── src
      │   └── ${application}.app.src
      └── test
  • src 必須躲舌,存放源代碼(.erl)
  • priv 非必須没卸,存放自定義的文件毅贮,比如nif的so文件等滩褥,還有資源文件
  • include 非必須,存放一些頭文件(hrl)铺然,方便別的application訪問
  • doc 非必須酒甸,存放一些文檔
  • test 非必須插勤,測試文件,eunit common_test文件都放在這里

2.2 application 回調(diào)模塊(callback module)

默認情況下是$APP_NAME_app,當然自己也可以在${application}.app.src自行定義析恋,定義方法如下:

{application, $APP_NAME,
 [
  {description, ""},
  {vsn, "1"},
  {registered, []},
  {applications, [
                  kernel,
                  stdlib
                 ]},
   % 請在這里自定義回調(diào)模塊
   % 請在這里修改回調(diào)參數(shù)
  {mod, {$CALLBACL_MODULE, Args}},
  {env, []}
 ]}.

下面我來做一個最簡單的例子助隧,然后來分析源代碼的流程,applicaton 的名稱是chapp

% 文件:chapp.app.src
{application, chapp,
 [
  {description, ""},
  {vsn, "1"},
  {registered, []},
  {applications, [
                  kernel,
                  stdlib
                 ]},
  {mod, { chapp_app, [myargs]}},
  {env, []}
 ]}.

%文件:chapp_app,erl
-module(chapp_app).
-behaviour(application).

-export([start/2, stop/1]).
-record(state, {
  mod
}).
%% 啟動的回調(diào)函數(shù)
start(_StartType, _StartArgs) ->
  io:format("~p,~p,~p~n", [?MODULE, ?FUNCTION_NAME, {_StartType, _StartArgs}]),
  {ok, Pid} = chapp_sup:start_link(),
  {ok, Pid, #state{mod = ?MODULE}}.

%% 停止的回調(diào)函數(shù)
stop(_State) ->
  io:format("~p,~p,~p~n", [?MODULE, ?FUNCTION_NAME, _State]),
  ok.

運行結(jié)果如下:

Eshell V10.4  (abort with ^G)
1> application:start
start/1       start/2       start_boot/1  start_boot/2  start_type/0  


1> application:start(chapp).
% 成功打印出 myargs
chapp_app,start,{normal,[myargs]}
ok
2> application:stop(chapp).
% 還可以自定義pre_stop函數(shù)
chapp_app,prep_stop,{state,chapp_app}
% 成功打印出 state
chapp_app,stop,{state,chapp_app}
=INFO REPORT==== 1-Dec-2019::14:58:57.004000 ===
    application: chapp
    exited: stopped
    type: temporary
ok

2.3 $APP_NAME.app.src 文件格式

% application.erl(kernel)
start(Application, RestartType) ->
  case load(Application) of
    ok ->
      % 要先載入 .app.src文件
      Name = get_appl_name(Application),
      application_controller:start_application(Name, RestartType);
    {error, {already_loaded, Name}} ->
      application_controller:start_application(Name, RestartType);
    Error ->
      Error
  end.
  
load1(Application, DistNodes) ->
  % 載入application
  case application_controller:load_application(Application) of
    ...
    Else ->
      Else
  end.
% 我們再來看 application_controller.erl

% application_controller.erl
load_application(Application) ->
    gen_server:call(?AC, {load_application, Application}, infinity).

make_appl(Name) when is_atom(Name) ->
   % 在 path 里尋找 $APP_NANE.app文件并村,這個文件是 $APP_NAME.app.src轉(zhuǎn)化而來
  FName = atom_to_list(Name) ++ ".app",
  case code:where_is_file(FName) of
    non_existing ->
      {error, {file:format_error(enoent), FName}};
    FullName ->
      case prim_consult(FullName) of
        {ok, [Application]} ->
          {ok, make_appl_i(Application)};
        {error, Reason} ->
          {error, {file:format_error(Reason), FName}};
        error ->
          {error, "bad encoding"}
      end
  end;
  
  % 這個是$APP_NAME.app.src個文件格式
  % {application, Name, Opts}
  % 其中 有幾個一定要有的哩牍,如:description,mod,env
  % 我們要特別注意mod里面的參數(shù)殖属,因為這個是我們回調(diào)的參數(shù)
  make_appl_i({application, Name, Opts}) when is_atom(Name), is_list(Opts) ->
  Descr = get_opt(description, Opts, ""),
  Id = get_opt(id, Opts, ""),
  Vsn = get_opt(vsn, Opts, ""),
  Mods = get_opt(modules, Opts, []),
  Regs = get_opt(registered, Opts, []),
  Apps = get_opt(applications, Opts, []),
  Mod =
    case get_opt(mod, Opts, []) of
      {M, _A} = MA when is_atom(M) -> MA;
      [] -> [];
      Other -> throw({error, {badstartspec, Other}})
    end,
  Phases = get_opt(start_phases, Opts, undefined),
  Env = get_opt(env, Opts, []),
  MaxP = get_opt(maxP, Opts, infinity),
  MaxT = get_opt(maxT, Opts, infinity),
  IncApps = get_opt(included_applications, Opts, []),
  {#appl_data{name = Name, regs = Regs, mod = Mod, phases = Phases,
    mods = Mods, inc_apps = IncApps, maxP = MaxP, maxT = MaxT},
    Env, IncApps, Descr, Id, Vsn, Apps};

2.4 application啟動步驟

% application_controller.erl
handle_call({start_application, AppName, RestartType}, From, S) ->
  #state{running = Running, starting = Starting, start_p_false = SPF,
    started = Started, start_req = Start_req} = S,
  %% Check if the commandline environment variables are OK.
  %% Incase of erroneous variables do not start the application,
  %% if the application is permanent crash the node.
  %% Check if the application is already starting.
  case lists:keyfind(AppName, 1, Start_req) of
    false ->
      case catch check_start_cond(AppName, RestartType, Started, Running) of
        {ok, Appl} ->
            ......
            {false, undefined} ->
              % 正常的入口
              spawn_starter(From, Appl, S, normal),
              {noreply, S#state{starting = [{AppName, RestartType, normal, From} |
                Starting],
                start_req = [{AppName, From} | Start_req]}};
            ......
        {error, _R} = Error ->
          {reply, Error, S}
      end;
    {AppName, _FromX} ->
      SS = S#state{start_req = [{AppName, From} | Start_req]},
      {noreply, SS}
  end;

start_appl(Appl, S, Type) ->
  ApplData = Appl#appl.appl_data,
  case ApplData#appl_data.mod of
    [] ->
      {ok, undefined};
    _ ->
      %% Name = ApplData#appl_data.name,
      ......
      % 交給application_master來啟動
      case application_master:start_link(ApplData, Type) of
        {ok, _Pid} = Ok ->
          Ok;
        {error, _Reason} = Error ->
          throw(Error)
      end
  end.
 
 % application_master.erl
start_link(ApplData, Type) ->
  Parent = whereis(application_controller),
  proc_lib:start_link(application_master, init, [Parent, self(), ApplData, Type]).
 
 start_it_old(Tag, From, Type, ApplData) ->
  {M, A} = ApplData#appl_data.mod,
  case catch M:start(Type, A) of
    {ok, Pid} ->
      % 啟動成功了原环,默認狀態(tài) State = []
      link(Pid),
      From ! {Tag, {ok, self()}},
      loop_it(From, Pid, M, []);
    {ok, Pid, AppState} ->
    % 啟動成功了处窥,默認狀態(tài) State = AppState
      link(Pid),
      From ! {Tag, {ok, self()}},
      % 啟動成功滔驾,自己loop進入主循環(huán)
      loop_it(From, Pid, M, AppState);
    {'EXIT', normal} ->
      From ! {Tag, {error, {{'EXIT', normal}, {M, start, [Type, A]}}}};
    {error, Reason} ->
      From ! {Tag, {error, {Reason, {M, start, [Type, A]}}}};
    Other ->
      From ! {Tag, {error, {bad_return, {{M, start, [Type, A]}, Other}}}}
  end.
% 另一個入口,和上面幾乎一樣的邏輯
 start_supervisor(Type, M, A) ->
  case catch M:start(Type, A) of
    {ok, Pid} ->
      {ok, Pid, []};
    {ok, Pid, AppState} ->
      {ok, Pid, AppState};
    {error, Reason} ->
      {error, {Reason, {M, start, [Type, A]}}};
    {'EXIT', normal} ->
      {error, {{'EXIT', normal}, {M, start, [Type, A]}}};
    Other ->
      {error, {bad_return, {{M, start, [Type, A]}, Other}}}
  end.

啟動之后的proc之間的拓撲圖如下:

|application_controller | --- |(application_master:main_loop)| --- | (application_master:loop_it)| --- | chapp_sup| 

到此绕德,我們已經(jīng)將啟動的代碼流程走了一遍耻蛇,一些回調(diào)參數(shù)也已經(jīng)很清楚胞此。總結(jié)成以下幾點

  • application_controller 才是真正啟動appcationprocess,或者叫父進程(process)
  • application啟動是異步的,啟動結(jié)束才將結(jié)果castapplication_controller
  • 啟動之前要先載入(load)

2.5 application如何停止(stop)?

% application_controller.erl
handle_call({stop_application, AppName}, _From, S) ->
  #state{running = Running, started = Started} = S,
  case lists:keyfind(AppName, 1, Running) of
    {_AppName, Id} ->
      {_AppName2, Type} = lists:keyfind(AppName, 1, Started),
      stop_appl(AppName, Id, Type),
      NRunning = keydelete(AppName, 1, Running),
      NStarted = keydelete(AppName, 1, Started),
      cntrl(AppName, S, {ac_application_stopped, AppName}),
      {reply, ok, S#state{running = NRunning, started = NStarted}};
    false ->
      case lists:keymember(AppName, 1, Started) of
        true ->
          NStarted = keydelete(AppName, 1, Started),
          cntrl(AppName, S, {ac_application_stopped, AppName}),
          {reply, ok, S#state{started = NStarted}};
        false ->
          {reply, {error, {not_started, AppName}}, S}
      end
% application_master.erl
stop(AppMaster) -> call(AppMaster, stop).

main_loop(Parent, State) ->
  receive
    ......
    Other ->
      NewState = handle_msg(Other, State),
      main_loop(Parent, NewState)
  end.

handle_msg({stop, Tag, From}, State) ->
  catch terminate(normal, State),
  From ! {Tag, ok},
  % 自己主動退出
  exit(normal);
  
  loop_it(Parent, Child, Mod, AppState) ->
  receive
     ......
    {'EXIT', Parent, Reason} ->
      % 在stop之前還可以自定義pre_stop階段
      % application_master:main_loop退出了, application_master:loop_it收到消息
      % 執(zhí)行退出邏輯
      NewAppState = prep_stop(Mod, AppState),
      exit(Child, Reason),
      receive
        {'EXIT', Child, Reason2} ->
          exit(Reason2)
      end,
      % stop 回調(diào)調(diào)用點
      catch Mod:stop(NewAppState);
       ......
    _ ->
      ......
  end.

至此酣胀,application的停止邏輯已經(jīng)分析完成,我們通過閱讀代碼還能找到一個文檔中沒有的hook:pre_stop

3.總結(jié)

本文通過實例加閱讀源代碼的方式截型,演示了一遍application的實現(xiàn)儒溉,希望讓讀者加深對application的理解发钝,為合理使用application打下堅實的基礎(chǔ)酝豪。

4.參考文獻:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末孵淘,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子揉阎,更是在濱河造成了極大的恐慌,老刑警劉巖洞斯,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件坑赡,死亡現(xiàn)場離奇詭異毅否,居然都是意外死亡,警方通過查閱死者的電腦和手機徘溢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進店門甸昏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來徐许,“玉大人,你說我怎么就攤上這事翻默∏∑穑” “怎么了?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長吨枉。 經(jīng)常有香客問我貌亭,道長,這世上最難降的妖魔是什么锄奢? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮涂屁,結(jié)果婚禮上堪滨,老公的妹妹穿的比我還像新娘袱箱。我一直安慰自己,他們只是感情好盟萨,可當我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布了讨。 她就那樣靜靜地躺著捻激,像睡著了一般前计。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上丈屹,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天旺垒,我揣著相機與錄音肤无,去河邊找鬼宛渐。 笑死,一個胖子當著我的面吹牛窥翩,可吹牛的內(nèi)容都是我干的鳍烁。 我是一名探鬼主播繁扎,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼爹梁!你這毒婦竟也來了姚垃?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤掂墓,失蹤者是張志新(化名)和其女友劉穎君编,沒想到半個月后川慌,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡兑燥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年降瞳,在試婚紗的時候發(fā)現(xiàn)自己被綠了艾蓝。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片赢织。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡于置,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出搓侄,到底是詐尸還是另有隱情话速,我是刑警寧澤,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布泊交,位于F島的核電站乳讥,受9級特大地震影響柱查,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜云石,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一唉工、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧汹忠,春花似錦淋硝、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至参歹,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間隆判,已是汗流浹背犬庇。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留侨嘀,地道東北人臭挽。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像咬腕,于是被迫代替她去往敵國和親欢峰。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內(nèi)容