应用erlang的behaviour 框架(gen_server,gen_fsm,gen_event,supervisor)在处理很多情况的时候省下大量不必要的代码,使用的时候在框架基础添加自定义逻辑就可以搭建项目了,可以说是精髓。

gen_server:提供的是client和server之间,请求和回调的处理。

Client --请求-->Server       Server --回应-->Client

具体描述:erlang behaviour小结之gen_server

gen_fsm:提供的是event状态转换和action触发处理。

{State1*Event } ----> {Action,State2}

StateName(Event,StateData) ->

%%action here,

{next_state,StateName',StateData'}.

gen_event:提供的是event manager对event的处理。自定义event manager收到event,进行自定义处理。

event_manager(Event) ->

%%deal with the Event.

{ok}.

这里要说说gen_event和gen_fsm的一点区别:

gen_fsm带有外部状态(自定义状态)的转换,一个状态由于某事件发生,导致状态发生变化,触发action的同时进入下一个状态,可以是个循环。

类似水和冰的转换:水的状态为液态,此时遭遇了“冷却”事件,水变成冰,液态转换成固态;这时,固态的冰遭遇了“受热”事件,回头转换成液态水。

gen_event对事件进行处理,可以理解为单向的。event给过来,event manager处理掉,就这样。

supervisor:提供的是对子进程的启动、停止、监控操作。可以说是一个异常处理框架,但又类似切面,可以干涉到项目的每个进程(启动或停止任一子进程)。如果子进程需要重启,supervisor会根据重启策略和最大重启频率这些参数确定如何重启子进程。

有了这几个behaviour,没太大必要再去封装发布类似功能的组件。

基于erlang behavoiur,构建高可行性的项目是方便简单的。

erlang四大behaviour之一gen_server(转载)

erlang程序设计里面有个设计原则就是把你的进程构造成树,把共用代码提出来,特定功能用自己的module实现,这也就是behaviour了,应用behaviour可以减少与本身事务无关的代码量,设计逻辑更加清晰。老纪边学习边记录吧。

gen_server实现服务器/客户端模型,用于多个客户共用一个资源的这种情况。他由几个接口函数和几个回调函数组成(回调函数必须在你的module里定义)这些可以参考erlang的doc

举个例子:

-module(ch3).  %这是我们的回调模块,也是我们实现业务逻辑的模块-behaviour(gen_server).  % 说明我们应用gen_server这个behaviour-export([start_link/0]).-export([alloc/0, free/1]).
-export([init/1, handle_call/3, handle_cast/2]).  %gen_server 的导出函数
start_link() ->   gen_server:start_link({local, ch3}, ch3, [], []).
alloc() ->  gen_server:call(ch3, alloc).
free(Ch) ->   gen_server:cast(ch3, {free, Ch}).
init(_Args) ->
{ok, channels()}.
handle_call(alloc, _From, Chs) -> {Ch, Chs2} = alloc(Chs),   {reply, Ch, Chs2}.
handle_cast({free, Ch}, Chs) -> Chs2 = free(Ch, Chs),  {noreply, Chs2}.

gen_server:start_link的调用会生成一个服务器进程且连接到进程树,并调用我们的init函数。 gen_server:call(ch3, alloc)的调用导致对handle_call的调用,这是同步的。gen_server:cast(ch3, {free, Ch})的调用导致对handle_cast的调用,这是异步的。很简单。

假如你不想把服务器进程挂入监控树的话,直接用gen_server:start启动进程,这是这个服务器进程就是一个普通进程了。

gen_server的停止规则:

一般情况不需要提供自己的停止函数,监控进程会自动处理,但是如果你想在gen_server进程中自己清理以下资源,那么就必须在init函数里调用process_flag(trap_exit, true)来捕获退出信号,这会导致调用terminate(shutdown, State)函数,所以你也必须实现这个函数

终止他就比较简单,直接调用gen_server:cast(Name, stop),这会导致调用handle_cast(stop, State),它的实现里写入 {stop, normal, State}即可,它最终导致terminate(normal, State)的调用,你的清理工作就可以在这继续了。

  1. 以gen_server:start_link开始的连入监控树的
  2. 以gen_server:start开始的单独gen_server

erlang越看越喜欢,但是要深入要走的路还很长

erlang四大behaviour之二-gen_fsm(转载)

今天介绍erlang的一个非常重要的behaviour,就是gen_fsm-有限状态机,有限状态机的作用非常之多,比如文本解析,模式匹配、游戏逻辑等等方面的处理都是它的强项,所以这个behaviour非常之重要

1. 有限状态机

有限状态机可以用下面这个公式来表达

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

表示的就是在S状态时如果有事件E发生,那么执行动作A后把状态调整到S’。理解很好理解,如果能够熟练应用必须得下苦功,多练习。

2. 一个例子

erlang手册中用这个例子来解释的:开锁问题,有一个密码锁的门,它就可以看作一个状态机,初始状态门是锁着的,任何时候有人按一个密码键就会产生一个事件,这个键值和前面的按键组合后与密码相比较,看是否正确,如果输入的密码顺序是对的,那么将门打开30秒,如果输入密码不完全,则等待下次按钮按下,如果输入密码顺序是错的,则重新开始等待按键按下。

-module(code_lock).-behaviour(gen_fsm).-export([start_link/1]).-export([button/1]).-export([init/1, locked/2, open/2]).start_link(Code) ->    gen_fsm:start_link({local, code_lock}, code_lock, Code, []).button(Digit) ->    gen_fsm:send_event(code_lock, {button, Digit}).init(Code) ->    {ok, locked, {[], Code}}.locked({button, Digit}, {SoFar, Code}) ->    case [Digit|SoFar] of        Code ->            do_unlock(),            {next_state, open, {[], Code}, 3000};        Incomplete when length(Incomplete)<length(Code) ->            {next_state, locked, {Incomplete, Code}};        _Wrong ->            {next_state, locked, {[], Code}}    end.open(timeout, State) ->    do_lock(),    {next_state, locked, State}.

这些代码下面解释

3. 启动状态机

在上一节提到的例子里,我们使用code_lock:start_link(Code)启动gen_fsm

start_link(Code) ->    gen_fsm:start_link({local, code_lock}, code_lock, Code, []).

start_link调用gen_fsm:start_link/4,启动一个新的gen_fsm进程并连接。
1)第一个参数{local, code_lock}指定名字,在本地注册为code_lock
2)第二个参数code_lock是回调模块
3)第三个参数Code是传递给回调模块init函数的参数,就是密码锁的密码
4)第四个[]是状态机的选项
如果进程注册成功,则新的gen_fsm进程调用code_lock:init(Code),返回{ok, StateName, StateData}。StateName是gen_fsm的初始状态,在这里返回的是locked,表示初始状态下门是锁着的,StateData是gen_fsm的内部状态,在这里Statedata是当前的按键顺序(初始时为空)和正确的锁代码,是个列表

init(Code) ->    {ok, locked, {[], Code}}.

注意gen_fsm:start_link是同步的,直到gen_fsm进程初始化并准备好开始接受请求时才会返回。加入gen_fsm是监控树的一部分,那么gen_fsm:start_link必须被使用,也就是被一个监控者调用,gen_fsm:start则是启动单独的gen_fsm进程,也就是gen_fsm不是监控树的一部分

4. 事件通知

使用gen_fsm:send_event/2来实现按建事件的通知

button(Digit) ->    gen_fsm:send_event(code_lock, {button, Digit}).

code_lock是gen_fsm的名字,且必须用这个名字启动进程,{button, Digit}是发送的事件,事件是作为消息发送给gen_fsm的,当事件被接收到,gen_fsm就调用StateName(Event, StateData),它的返回值应该是{next_state, StateName1, StateData1}。StateName是当前状态名称,而StateName1是将转换到的下一状态名称,StateData1是StateData的新值

locked({button, Digit}, {SoFar, Code}) ->    case [Digit|SoFar] of        Code ->            do_unlock(),            {next_state, open, {[], Code}, 30000};        Incomplete when length(Incomplete)<length(Code) ->            {next_state, locked, {Incomplete, Code}};        _Wrong ->            {next_state, locked, {[], Code}};    end.open(timeout, State) ->    do_lock(),    {next_state, locked, State}.

假如门是锁着的且按了一个按键,完整的按键序列和密码相比较,根据比较结果来决定门是打开(状态切到open)还是保持locked状态。

5 超时

假如输入的密码正确,门被打开,locked/2函数返回下面的序列

{next_state, open, {[], Code}, 30000};

30000表示超时30000毫秒,在30秒后,超时发生,调用StateName(timeout, StateData) ,门又重新锁上

open(timeout, State) ->    do_lock(),    {next_state, locked, State}.

6. 所有状态事件

有时候一个事件可以到达gen_fsm进程的任何状态,取代用gen_fsm:send_event/2发送消息和写一段每个状态函数处理事件的代码,这个消息我们可以用gen_fsm:send_all_state_event/2 发送,用Module:handle_event/3处理

-module(code_lock)....-export([stop/0])....stop() ->    gen_fsm:send_all_state_event(code_lock, stop)....handle_event(stop, _StateName, StateData) ->    {stop, normal, StateData}.

7. 停止

假如gen_fsm是监控树的一部分,则不需要停止方法,gen_fsm会自动被监控者停止。如果需要在结束前清理数据,那么shutdown strategy必须为一个timeout,并且必须在gen_fsm的init方法里设置捕获exit信号,然后
gen_fsm进程会调用callback方法terminate(shutdown, StateName, StateData)

init(Args) ->    ...,    process_flag(trap_exit, true),    ...,    {ok, StateName, StateData}....terminate(shutdown, StateName, StateData) ->    ..code for cleaning up here..    ok.

8. 独立gen_fsm进程

加入gen_fsm不是监控树的一部分,stop函数可能有用,如:

...-export([stop/0])....stop() ->    gen_fsm:send_all_state_event(code_lock, stop)....handle_event(stop, _StateName, StateData) ->    {stop, normal, StateData}....terminate(normal, _StateName, _StateData) ->    ok.

回调函数处理stop事件并返回{stop, normal, StateData1},normal表示正常停止,StateData1为gen_fsm的新的StateData值,这将导致gen_fsm调用terminate(normal, StateName, StateData1)然后自然的停止

9. 处理其他信息

收到的其他消息由handle_info(Info, StateName, StateData)处理,其他消息的一个例子就是exit消息,假如gen_fsm进程与其他进程link了并且trace了信号,就要处理exit消息

handle_info({'EXIT', Pid, Reason}, StateName, StateData) ->    ..code to handle exits here..    {next_state, StateName1, StateData1}.

erlang四大behaviour之三-gen_event(转载)

1. 事件处理规则

在OTP中,事件管理器是一个事件可以发送到的命名对象,一个事件可以是一个错误、一个警告、或者一些要写入日志的信息

在事件管理器中,有0个、一个或者多个事件处理器被安装,当事件管理器被一个事件通知时,这个事件将被安装在事件管理器中的事件处理器处理,

事件管理器用一个进程实现,事件处理器用回调模块实现。事件管理器本质上维护一个{Module, State}列表,每一个Module为一个事件处理器,而State为事件处理器的内部状态。

2. 例子

事件处理器的回调模块把错误信息写入终端

-module(terminal_logger).-behaviour(gen_event).-export([init/1, handle_event/2, terminate/2]).init(_Args) ->    {ok, []}.handle_event(ErrorMsg, State) ->    io:format("***Error*** ~p~n", [ErrorMsg]),    {ok, State}.terminate(_Args, _State) ->    ok.

事件处理器的回调模块把错误信息写入文件

-module(file_logger).-behaviour(gen_event).-export([init/1, handle_event/2, terminate/2]).init(File) ->    {ok, Fd} = file:open(File, read),    {ok, Fd}.handle_event(ErrorMsg, Fd) ->    io:format(Fd, "***Error*** ~p~n", [ErrorMsg]),    {ok, Fd}.terminate(_Args, Fd) ->    file:close(Fd).

3. 启动事件管理器

调用

gen_event:start_link({local, error_man})

启动管理器,这个函数生成并连接到一个新进程,参数{local, error_man}指定名称,在这个例子中,事件管理器被局部注册为error_man

假如忽略名称,那么事件管理器不会被注册,它的PID将被使用。名称也可以是这种形式{global, Name},这样,事件管理器的名称是用global:register_name/2注册的。

假如事件管理器是监控树的一部分,那么gen_event:start_link必须被使用,也就是被监控树启动,而gen_event:start启动单独的事件管理器,也就是事件管理器不是监控树的一部分。

4. 添加事件处理器

下面的例子显示怎样启动一个事件管理器和添加一个事件处理器

1> gen_event:start({local, error_man}).{ok,<0.31.0>}2> gen_event:add_handler(error_man, terminal_logger, []).ok

gen_event:add_handler(error_man, terminal_logger, [])为error_man添加处理器terminal_logger,事件管理器调用terminal_logger:init([])这个回调函数, []是参数,init要返回一个{ok, State},State是事件处理器的内部状态

init(_Args) ->    {ok, []}.

这里,init不需要任何输入参数,对于terminal_logger,也没使用内部状态,对于file_logger,内部状态保存了打开的文件描述符

init(File) ->    {ok, Fd} = file:open(File, read),    {ok, Fd}.

5. 关于事件通知

3> gen_event:notify(error_man, no_reply).***Error*** no_replyok

error_man是事件管理器的名称,no_reply是事件,事件作为消息发送给事件管理器,当事件被收到时,事件管理器为每个安装的事件处理器按安装次序调用handle_event(Event, State),这个函数期待返回{ok, State1},State1是事件处理器的新状态。

在terminal_logger中

handle_event(ErrorMsg, State) ->    io:format("***Error*** ~p~n", [ErrorMsg]),    {ok, State}.

在file_logger中

handle_event(ErrorMsg, Fd) ->    io:format(Fd, "***Error*** ~p~n", [ErrorMsg]),    {ok, Fd}.

6. 删除一个事件处理器

gen_event:delete_handler(error_man, terminal_logger, []),这个函数向事件管理器error_man发送了一个消息,告诉他删除terminal_logger这个事件处理器,事件管理器将调用terminal_logger:terminate([], State),参数[]是delete_handler的第三个参数,terminate以init相反的方向调用,以完成清理工作,返回值被忽略。

在terminal_logger中,没有清理动作

terminate(_Args, _State) ->    ok.

在file_logger中,文件描述符被关掉

terminate(_Args, Fd) ->    file:close(Fd).

7. 停止

当事件管理器被停止,它给每个注册的事件处理器调用terminate/2的机会,就好像事件处理器被删除一样。如果事件管理器是监控树的一部分,不需要显示的停止事件管理器。当事件管理器作为单独进程使用时,则调用gen_event:stop(error_man).

erlang四大behaviour之四-supervisor(转载)

1. 监督规则

一个监督者负责启动、停止、监控他的子进程。监督者的一个基本概念就是当必要的时候重启子进程保证它们的存活

哪个子进程要重启和被监控是由一个子规程列表决定的,子进程按照列表中指定的顺序启动,并按相反的顺序终止

2. 实例

监督者的回调模块

-module(ch_sup).
 -behaviour(supervisor).
 -export([start_link/0]).
 -export([init/1]).
 start_link() ->    
supervisor:start_link(ch_sup, []).
 init(_Args) ->   
   {ok, {{one_for_one, 1, 60},      
    [{ch3, {ch3, start_link, []},      
      permanent, brutal_kill, worker, [ch3]}]}}.
 

one_for_one是重启策略
1和60定义了最大重启频率
{ch3, …}是子规程

3. 重启策略

one_for_one

假如一个进程终止了,仅仅这个进程会被重启

one_for_all

假如一个进程停止了,所有其他子进程也要被停止,然后所有子进程,包括这个引发停止的子进程都被重启

rest_for_one

假如一个进程停止了,它后面的子进程,也就是以启动顺序来说这个被终止的进程后面的子进程都将被停止,然后他们又被启动。

4. 最大启动频率

监督者有一个内建机制限制在给定的时间间隔里的重启次数,这由子进程启动规程中的两个参数值决定,MaxR和MaxT,它们定义在回调函数init中

init(...) ->   
 {ok, {{RestartStrategy, MaxR, MaxT},       
   [ChildSpec, ...]}}.

如果在时间MaxT里重启次数大于MaxR ,监督者进程就停止它所有子进程,然后再终止自己。

当监督者进程终止了,那么更高级别的监督者要采取些动作,它或者重启被终止的监督者或者停止自己

这个重启机制的目的是预防一个进程因某种原因频繁的终止,然后简单的重启。

5. 子规范

下面的是类型定义

{Id, StartFunc, Restart, Shutdown, Type, Modules}   
 Id = term()   
 StartFunc = {M, F, A}  
      M = F = atom()   
     A = [term()] 
   Restart = permanent | transient | temporary  
  Shutdown = brutal_kill | integer() &gt;=0 | infinity   
 Type = worker | supervisor    
Modules = [Module] | dynamic      
  Module = atom()
  • Id用来内部标识子规范
  • StartFunc是启动子进程时调用的函数,它将成为对supervisor:start_link, gen_server:start_link, gen_fsm:start_link or gen_event:start_link的调用
  • Restart标识一个进程终止后将怎样重启,一个permanent 进程总会被重启;一个temporary 进程从不会被重启;一个transient 进程仅仅当是不正常的被终止后才重启,例如非normal得退出原因
  • Shutdown 定义一个进程将怎样被终止,brutal_kill意味着子进程被exit(Child, kill)无条件的终止;一个整数值的超时时间意味着监督者告诉子进程通过调用exit(Child, shutdown)而被终止,然后等待一个返回的退出信号,假如在指定的时间里没有收到退出信号,那么子进程用exit(Child, kill)被无条件终止。
  • Type指定子进程是supervisor还是worker
  • Modules 是有一个元素的列表[Module],假如子进程是supervisor、gen_server 或 gen_fsm,那么Module 是回调模块的名称;假如子进程是gen_event,那么Modules 应该是dynamic

例如:子规范用于启动一个服务器ch3

{ch3, {ch3, start_link, []}, permanent, brutal_kill, worker, [ch3]}

子规范用于启动一个事件管理器

{error_man, {gen_event, start_link, [{local, error_man}]}, permanent, 5000, worker, dynamic}

监督者然后根据子规程启动所有子进程,这个例子中是一个子进程ch3

6. 启动supervisor

像这样

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

启动

监督者进程调用init

init(_Args) ->    
{ok, {{one_for_one, 1, 60},       
   [{ch3, {ch3, start_link, []},      
      permanent, brutal_kill, worker, [ch3]}]}}.

并期待init返回{ok, StartSpec}

注意supervisor:start_link是同步的,它一直等到所有子进程都启动了才返回

7. 添加子进程

除静态监控树外,我们也可以通过supervisor:start_child(Sup, ChildSpec)向监督者动态添加子进程,Sup 是监督者的pid或名称,ChildSpec 是一个子规范。子进程用start_child/2来添加。注意:假如监督者死掉后重启,那么所有动态添加的子进程都不复存在

8. 停止子进程

任何静态动态添加的子进程都可以用supervisor:terminate_child(Sup, Id)来停止。一个停止子进程规范可以用supervisor:delete_child(Sup, Id)来删除。Sup是监督者的pid或名称,Id是子规范的id

9. Simple-One-For-One

监督者的simple_one_for_one启动策略是one_for_one的简版,所有子进程都是同一进程实例而被动态添加,下面是一个simple_one_for_one监督者的实例

-module(simple_sup).
-behaviour(supervisor).
-export([start_link/0]).
-export([init/1]).
start_link() ->    supervisor:start_link(simple_sup, []).
init(_Args) -> 
   {ok, {{simple_one_for_one, 0, 1},   
       [{call, {call, start_link, []}, 
           temporary, brutal_kill, worker, [call]}]}}.

当启动时,监督者不启动任何子进程,取而代之的是所有子进程都通过调用supervisor:start_child(Sup, List)来动态添加,Sup 是监督者的pid或名称,List 是添加给子规范中指定参数列表term列表,如果启动函数是{M, F, A}这种形式,那么子进程通过调用apply(M, F, A++List)而被启动

例如,给上面的例子添加一个子进程

supervisor:start_child(Pid, [id1])

那么子进程通过调用apply(call, start_link, []++[id1])而被启动,实际上就是call:start_link(id1)

10. 停止

因为监控者是监控树的一部分,它自动被他的监督者停止,根据相应规范,它反序停止它的所有子进程,然后终止自己

至此,四种behavour已经全部翻译完了,熟练应用他们是你构建高扩展、高容错、高并发应用的基础,努力吧!

erlang四大behaviour简述相关推荐

  1. erlang四大behaviour之四-supervisor(转载)

    1. 监督规则 一个监督者负责启动.停止.监控他的子进程.监督者的一个基本概念就是当必要的时候重启子进程保证它们的存活 哪个子进程要重启和被监控是由一个子规程列表决定的,子进程按照列表中指定的顺序启动 ...

  2. erlang OTP中的四大behaviour fsm的例子

    下面是一个fsm的例子,代码如下: 1 -module(code_lock2). 2 3 -behaviour(gen_fsm). 4 -export([start_link/1]). 5 -expo ...

  3. web2.0网站的四大特性简述

    收集和学习了大家对于Web2.0特性的看法,加上这几天的思考,整理我对于Web2.0到底是什么的看法,清理自己的思路.在我看来,Web2.0之所以区别于1.0,关键在于以下四个重要的特性: 可重用的微 ...

  4. Erlang/OTP设计原则(文档翻译)

    http://erlang.org/doc/design_principles/des_princ.html 图和代码皆源自以上链接中Erlang官方文档,翻译时的版本为20.1. 这个设计原则,其实 ...

  5. Everyday-FE-Articles 8~11月前端文章日推 [持续更新]

    Everyday-FE-Articles 由来自@阿里马跃的每日文章推荐,于是顺手整理,以便学习... -- By Ale-cc /* 注:序号不准确 */ 八月份 8月7日 星期二,农历六月廿六 S ...

  6. 杭州初级Java面试总结

    五月底毕业,儿童节当天抵达杭州,经过四天的面试奔波,现在已经稳定工作~ 想写点东西总结总结,毕竟是人生第一次啊 都说万事开头难,但这个' 难 ' 它也有个前提,就是你事前有没有做充分的准备. 大致准备 ...

  7. 【MFC】CApp,CMainFrame,CDoc,CView暨相关函数介绍

    01.目录 文章目录 01.目录 02.MFC四大类简述 03.MFC四大类常用成员函数 3.1. CMainFrame 3.2. CDoc 3.3. CView 3.4. CApp 04. 总结 0 ...

  8. Erlang/OTP:基于Behaviour的回调函数

    原始链接:https://blog.zhustec.me/posts/erlang-otp-1-callback-based-on-behaviour OTP 是什么 OTP 的全称是开源电信平台 ( ...

  9. 自然语言处理︱简述四大类文本分析中的“词向量”(文本词特征提取)

    笔者在看各种NLP的论文.文献.博客之中发现在应用过程中,有种类繁多的词向量的表达.笔者举例所看到的词向量有哪些. 词向量一般被看做是文档的特征,不同词向量有不同的用法,本文介绍了四类词向量: Has ...

最新文章

  1. 半导体制冷片中的N,P半导体测试
  2. 万年历java课程设计报告_java万年历课程设计报告2010
  3. 服务器内存技术知识充电
  4. php 获取当前页面url_PHP获取URL –如何获取当前页面的完整URL
  5. arduino openmv 显示图像_OpenMV与Arduino NUO的连接方式
  6. 韩国央行行长李柱烈:加密货币的波动给金融稳定带来风险
  7. CloudStack之nfs
  8. 在Git中更改文件名的大小写
  9. 随机数相加等于固定值_excel随机函数出来的数相加等于一个想要的固定值
  10. jquery学习之1.19-小练习3-输入用户名密码时焦点触发和失去焦点
  11. 附上堆和栈的区别 (转贴)
  12. 牛学长周年庆活动:软件大促限时抢,注册码免费送!
  13. 4399小游戏 十滴水 求解器(输出路径的bfs)
  14. Warning One or more files are in a conflicted state.
  15. angular 自定义打包文件名
  16. Win10 环境下 Mingw-w64,CMake,CLion 安装及配置 C/C++ 和 OpenCV 运行环境
  17. Weisfeiler-Lehman(WL)算法
  18. 「C++游戏」双人大乱斗
  19. 使用正则表达式去除斜杆(\)
  20. 后台产品的数据可视化图表产品设计

热门文章

  1. 比较版本号大小c语言,比较两个版本字符串version1和version2
  2. Open3d之计算源点云与目标云的距离
  3. 数据库学习--DML(数据管理语言)
  4. 【计算机本科补全计划】Mysql 学习小计(3)
  5. 【编译原理笔记10】语法制导翻译:在递归预测过程中进行翻译,L属性定义的自底向上翻译
  6. windows下php不支持mysql
  7. 数字后端基本概念-合集
  8. 计算机电池功能,蓄电池检测仪的主要功能都有哪些
  9. java收集碎片_Thinking in Java 笔记碎片
  10. mysql cluster 乱码_Mysql中文乱码问题完美解决方案