返回文章列表

文章

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=ASIAZI2LB4663ZDUKBTQ%2F20260827%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20260827T173403Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEHEaCXVzLXdlc3QtMiJIMEYCIQDRxAU3BP5RL6bJ4DqTTuhbV51vlWrfaytOXbhH2KCgZgIhAPsDzFL25mKA52KptMmFayDJ6nDTxVb0VM6OrzzlGubBKv8DCDoQABoMNjM3NDIzMTgzODA1IgzNoaq4kBMNf26pRt0q3APOvtJqG88xvvlqB6z1xqjJVn6GxiDUVgrcmRty77VnJLAgfXmGXQDuO1rzrPyCoBPUcP%2BjjI3jXkKO2xFc0gdA3NYxLTMXkwFTlhrOG6ukh5KFg3%2Bja3WoKbla84jfvdNezMv0%2F2%2FdHm%2BQ0joJA1Cym%2F1W51r4XQEl%2B%2BnaoVS19%2BMSC0%2BAODYLz39Twnh47x4TyRH04mUbcsGoyhwAMI0%2BLue68DOm3qOBw%2FnwhwAxi8F7JD0lcXwhpaHvN5ckCr%2FMCM0jO1fR2w22hLaJJJt5oWUNkRicG2OmwgPNg%2FZgd%2FoFcftum7d%2B45OdalDOxMu1LR6B%2FpJSM8JFfxCf%2FKdWafyITSu9LH0QJqG5i%2Fl0NeqhXtizlA5C%2FGmkwpb%2B5WyuCuuAoW%2BBBgPOb8CP2FTrRTtSB29XNTthccEhePGHaJ7B2IKxx7Lkl1NaWNbaYr5O7x6fUFT101xwWK6ssse8WjgXZG7S2vYZxb7BN2hiYXheZbB31H%2B%2FjCqiLzxmhR%2B7tFsaq%2FAABT0zAOh0haFvt0FzFzhwVeyvIueZdLtX3S49qrmODABKsBa%2BtftCadlcipme4DVa6DDqRWOiTrM5F6uwCJ0k1E4CQHF26E0D8RtP%2FZsGLXr%2Bb9vwfjDFzcHUBjqkAQpD%2FtDLS3msY6TPcTGinGsEDH8mSaYht%2FHp7PTWmPL3Olh3dbmR6NRxQuIUtFGcJivbVZfjZzlStcRT91uP3W9iWwMliGgOsC1J4W9VWqkU%2BVdsVhVX0wCvz8CUy6cFBnHEepnTtIW4iXzjFIklLl5SBMY6vAv4ircxajkyU5hIc4hvnrnWZ8pxy4TWDUEqwv%2F5qx%2BI3kAOyAi9fmvdDqZjC6OP&X-Amz-Signature=6808027ff43bbb5fa2ef4e478cde8ee1dc36973a41b08fe0581931be0a0be209&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject) 你可以通过在这个特性从通用代码中提供**回调钩子**(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\} 元组。然后,通用代码可以将响应发送回调用者,并继续使用新状态进行循环。最后剩下一件事要做:提供一个向服务器进程发出请求的函数。