返回文章列表

文章

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存储在变量中,然后稍后使用该变量来调用模块上的函数: ![](https://prod-files-secure.s3.us-west-2.amazonaws.com/4b1b167f-4b52-4031-8233-f4f2689af574/c2f0df05-e1b1-4a55-bd98-ae5579cd752e/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466U7ERZ5DY%2F20261016%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20261016T214027Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECMaCXVzLXdlc3QtMiJIMEYCIQDqobHwqvPr3A679%2BjDipr%2FkIRJTtqvZ54r1Pg0dGCcYwIhALQc%2BMRV06zJkSZ9b071DzevZyo7RygA0PZJOjVo4fEJKogECOz%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1IgxayxHiGerECDa%2BmCQq3AOz9ioRwu3%2BQZCHoa%2BrYBqfjow7oi2OOgXJvt16isMeZHsHX0BBs%2Ftxpb39I6J16lN%2B7chqwgn1JbKrhJaWzJmjzCaKBDoBxnGpYzPeuWcWQSYbfXqV%2BsvZ34Q%2FWqOkX5kzuxSBG0wXHbzs4BbOa3NhEoAss8Bjka%2B1%2Bp5oovN3eYLgZzJ%2BmlBC6v6sDS6ZwEjH%2BlyGw4pR7Zb1q%2FeIHU6%2FdgUBYi5ypoQiniBsQAU8xHFO06Mdi1XhYo5VNznUPWjQvc2pMEdg5%2FDP4HhWD30Izu%2FllPZbxrO6NvGRV3nAGMTYcF7%2BPYqTHNy7zb0fOBg5krajyJ4ohD%2F7JeaSiOAedq6HzxwftznmIsR58ivOMIMEopcwglUULG4v90BnaXAasUB2WRpfVpRTHODgCRlzfgp03ZTgNZs0%2F1vt1kEVL7JRN6CaGJBdKt%2F6hN1kNo0HBlvVirLHkeLy5TFEo1VdMcOFreYyFhj9y2noj%2FjJcUVspj8%2Blpr5cOh2Cw%2B6RGEqyESCsxdJ0mJZYUOTNe49Yl4pL0Y9f0Cb%2BjBA8IqhC14sdGqUZQK0yW74OqmlRSdz9XlHu1zcrtdHESAMIzQDjGgxIRdS5jomtKdJsK5gWU6bs3VOoD%2FSvzkuIjC%2B68nWBjqkAWNMj1mPh9MlVNEwY7mpa%2Fp5VdYld4G4sdpYLsm3xNjPDLBTep4%2BM2GNwxlrXzqeP2e%2FV%2BOfues0w9tkal9h7fZE4jQaAruxMFLYrMJDEzGetELHzacY8WQprD918BqY7OJyJv8lwxuDpuE%2Fxz36rr3KKGFi9EbLwcnFxC0rIPfPRBttH0oNVI5K%2F%2FgOquCoA5UPVxhml6uAGQ3F0nHjfTO70%2FZh&X-Amz-Signature=5cda6a3cdbd17100e603f8d2b565096d834cb20c06a5fc80a2bd5cfe49b406c2&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject#notion_record=block.1d05c571-bb7d-80fc-8646-f3be6b23e7f3.4b1b167f-4b52-4031-8233-f4f2689af574) 你可以通过在这个特性从通用代码中提供**回调钩子**(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\} 元组。然后,通用代码可以将响应发送回调用者,并继续使用新状态进行循环。最后剩下一件事要做:提供一个向服务器进程发出请求的函数。