文章
Elixir in Action - 4
Runtime considerations
Erlang 提供了一个用于实现服务器进程的辅助模块——它是开放电信平台 (OTP) 框架的一部分。尽管该OTP这个框架的名称具有误导性,但它与电信无关;相反,它为创建组件、构建版本、开发服务器进程、处理和恢复运行时错误、日志记录、事件处理和升级代码等任务提供了模式和抽象。
您将在后续章节中了解 OTP 的各个部分,但在本章中,我们将重点介绍其最重要的部分之一:GenServer,该模块可简化服务器进程的实现。不过,在了解 GenServer 之前,您将基于前几章中看到的消息传递原语实现它的一个简化版本。
# 1、Building a generic server process
在前几个章节中看到了一些服务器进程的示例。尽管这些进程服务于不同的目的,但它们的实现有一些共同点。特别是,所有实现服务器进程的代码都需要执行以下操作:
- 生成单独的进程
- 在进程中运行无限循环
- 维护进程状态
- 对消息做出反应
- 将响应发送回调用者
无论您运行哪种**Server Process**,您总是需要执行这些任务,因此值得将这些代码移动到一个地方。然后,具体实现可以重用此代码并专注于其特定需求。让我们看看如何实现这样的通用代码。
## 1、Plugging in with modules
通用代码将执行服务器进程常见的各种任务,将具体决策留给具体实现。例如,通用代码将生成一个进程,但具体实现必须确定初始状态。类似地,通用代码将运行循环;接收消息;并且(可选)发送响应,但具体实现必须决定如何处理每条消息以及响应是什么。
换句话说,通用代码驱动整个流程,而具体实现必须填补缺失的部分。因此,你需要一个**插件机制**,当需要做出特定决策时,让通用代码调用具体实现。 最简单的方法是使用modules。请记住,modules的名称是一个atom。你可以将该atom存储在变量中,然后稍后使用该变量来调用模块上的函数:  你可以通过在这个特性从通用代码中提供**回调钩子**(callback hooks)。具体来说,您可以采用以下方法: - 让通用代码接受插件module作为参数。该模块称为**回调模块**(callback module) - 在进程状态中维护一个module atom - 在有需要时调用callback-module函数 显然,为了使其工作,回调模块必须实现并导出一组定义明确的函数,我将在我们实现通用代码时逐步介绍这些函数。 ## 2、Implementing the generic code 开始构建我们的通用服务器进程,首先,你需要启动一个进程然后初始化它的状态。 ```elixir defmodule ServerProcess do def start(callback_module) do spawn(fn -> initial_state = callback_module.init() # 调用回调模块的 init初始化状态 loop(callback_module, initial_state) end) end #.... end ``` ServerProcess.start/1 接受一个module atom作为参数,然后创建进程。在创建的进程中,回调函数 init/0 会被调用来创建初始状态。显然,为了实现这一点,回调模块必须导出 init/0 函数。最后,进入循环,为服务器进程提供动力并维持此状态。ServerProcess.start/1 的返回值是一个 pid,可用于向请求进程发送消息。 接下来,您需要实现等待并处理消息的循环代码。在此示例中,您将实现同步发送和响应通信模式。服务器进程必须接收消息、处理消息、将响应消息发送回调用者并更改进程状态。 通用代码负责接收和发送消息,而在具体的实现中必须处理消息并返回响应和新状态。看下面的代码: ```elixir defp loop(callback_module, state) do receive do {request, caller} -> {response, new_state} = callback_module.handle_call(request, state) # 调用回调模块的 handle_call处理请求 send(caller, {:response, response}) # 发送响应给调用者 loop(callback_module, new_state) # 继续循环,使用新的状态 end end ``` 在这里,您需要以 \{request, caller\} tuple的形式接收消息。request是标识请求的数据,对特定实现有意义。回调函数 handle_call/2 接收请求payload和当前state,并且必须返回 \{response, new_state\} 元组。然后,通用代码可以将响应发送回调用者,并继续使用新状态进行循环。最后剩下一件事要做:提供一个向服务器进程发出请求的函数。
换句话说,通用代码驱动整个流程,而具体实现必须填补缺失的部分。因此,你需要一个**插件机制**,当需要做出特定决策时,让通用代码调用具体实现。 最简单的方法是使用modules。请记住,modules的名称是一个atom。你可以将该atom存储在变量中,然后稍后使用该变量来调用模块上的函数:  你可以通过在这个特性从通用代码中提供**回调钩子**(callback hooks)。具体来说,您可以采用以下方法: - 让通用代码接受插件module作为参数。该模块称为**回调模块**(callback module) - 在进程状态中维护一个module atom - 在有需要时调用callback-module函数 显然,为了使其工作,回调模块必须实现并导出一组定义明确的函数,我将在我们实现通用代码时逐步介绍这些函数。 ## 2、Implementing the generic code 开始构建我们的通用服务器进程,首先,你需要启动一个进程然后初始化它的状态。 ```elixir defmodule ServerProcess do def start(callback_module) do spawn(fn -> initial_state = callback_module.init() # 调用回调模块的 init初始化状态 loop(callback_module, initial_state) end) end #.... end ``` ServerProcess.start/1 接受一个module atom作为参数,然后创建进程。在创建的进程中,回调函数 init/0 会被调用来创建初始状态。显然,为了实现这一点,回调模块必须导出 init/0 函数。最后,进入循环,为服务器进程提供动力并维持此状态。ServerProcess.start/1 的返回值是一个 pid,可用于向请求进程发送消息。 接下来,您需要实现等待并处理消息的循环代码。在此示例中,您将实现同步发送和响应通信模式。服务器进程必须接收消息、处理消息、将响应消息发送回调用者并更改进程状态。 通用代码负责接收和发送消息,而在具体的实现中必须处理消息并返回响应和新状态。看下面的代码: ```elixir defp loop(callback_module, state) do receive do {request, caller} -> {response, new_state} = callback_module.handle_call(request, state) # 调用回调模块的 handle_call处理请求 send(caller, {:response, response}) # 发送响应给调用者 loop(callback_module, new_state) # 继续循环,使用新的状态 end end ``` 在这里,您需要以 \{request, caller\} tuple的形式接收消息。request是标识请求的数据,对特定实现有意义。回调函数 handle_call/2 接收请求payload和当前state,并且必须返回 \{response, new_state\} 元组。然后,通用代码可以将响应发送回调用者,并继续使用新状态进行循环。最后剩下一件事要做:提供一个向服务器进程发出请求的函数。