热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

elixir官方教程Mix与OTP(三)通用服务器

为什么80%的码农都做不了架构师?#通用服务器我们的第一个通用服务器测试一个通用服务器监控的需要call,cast还是info?监控器还是链接?之前的章节里我们

为什么80%的码农都做不了架构师?>>>   hot3.png

#通用服务器

  1. 我们的第一个通用服务器
  2. 测试一个通用服务器
  3. 监控的需要
  4. call,cast还是info?
  5. 监控器还是链接?

之前的章节里我们用代理来代表桶.在第一章,我们曾指出想要给每个桶命名,之后我们可以这样做:

CREATE shopping
OKPUT shopping milk 1
OKGET shopping milk
1
OK

因为代理也是进程,所以每个桶有一个进程辨识符(pid)却没有名字.我们已经在进程章节中学习过名称注册,你可以在这里使用它.例如,我们可以这样创造一个桶:

iex> Agent.start_link(fn -> %{} end, name: :shopping)
{:ok, #PID<0.43.0>}
iex> KV.Bucket.put(:shopping, "milk", 1)
:ok
iex> KV.Bucket.get(:shopping, "milk")
1

然而,这是一个糟糕的主意!在Elixir中进程名必须是原子,这意味着我们需要将桶名(通常接收自外部客户端)转换成原子,而我们永远不应该将用户输入转换成原子.这是因为原子不支持垃圾回收.原子一旦创造出来,就永远不会被回收.从用户输入中生成原子意味着用户可以注入足够多的不同名字来耗尽我们的系统内存!

在实际中你会在耗尽内存之前达到Erlang虚拟机的原子上限,然后系统会被强制关闭.

没有滥用名称注册功能,我们创造了自己的_注册过程_.就是将桶名与桶进程的联系放到了一个映射中.

注册表需要保证词典总是最新的.例如,如果一个桶进程因为一个bug而崩溃了,注册表必须将词典清理干净以避免提供陈旧的条目.在Elixir中,我们将此描述为注册表需要_监控_每个桶.

我们将使用通用服务器来创建一个能监控桶进程的注册表进程.GenServers是Elixir和OTP中用于构建通用服务器的go-to抽象.

#我们的第一个通用服务器

一个通用服务器由两部分实现:客户端API和服务器回调,要么是在一个模块中,要么是在两个不同的模块中实现的它们.客户端与服务器运行在不同的进程中,当函数被调用时,客户端将会传递信息给服务器.在此我们使用一个模块来定义服务器回调和客户端API.新建lib/kv/registry.ex文件:

defmodule KV.Registry douse GenServer## Client API&#64;doc """Starts the registry."""def start_link doGenServer.start_link(__MODULE__, :ok, [])end&#64;doc """Looks up the bucket pid for &#96;name&#96; stored in &#96;server&#96;.Returns &#96;{:ok, pid}&#96; if the bucket exists, &#96;:error&#96; otherwise."""def lookup(server, name) doGenServer.call(server, {:lookup, name})end&#64;doc """Ensures there is a bucket associated to the given &#96;name&#96; in &#96;server&#96;."""def create(server, name) doGenServer.cast(server, {:create, name})end## Server Callbacksdef init(:ok) do{:ok, %{}}enddef handle_call({:lookup, name}, _from, names) do{:reply, Map.fetch(names, name), names}enddef handle_cast({:create, name}, names) doif Map.has_key?(names, name) do{:noreply, names}else{:ok, bucket} &#61; KV.Bucket.start_link{:noreply, Map.put(names, name, bucket)}endend
end

第一个函数是start_link/3,它开启了一个新的GenServer并传送了三个参数:

  1. 服务器回调将会在哪一个模块中实现,__MODULE__意为当前模块

  2. 初始参数,此时是原子:ok

  3. 选项列表,它有保存服务器名称等功能.现在我们传送一个空列表

你可以向GenServer发送两种请求:呼叫与投掷.呼叫是同步的,服务器必须回应.投掷是异步的,服务器不会回应.

接下来的两个函数,lookup/2create/2负责发送这些请求到服务器.这些请求会出现在handle_call/3handle_cast/2的第一个参数.本例中,我们分别使用{:lookup, name}{:create, name}.请求通常以元组形式表达,这是为了在第一个参数的位置上提供不止一个"参数".通常元组的第一个元素是请求的动作,余下的元素是该动作的参数.

在服务器端,我们可以实现多种回调来保证服务器的初始化,终止和请求处理.这些回调是可选的,现在我们只实现了我们在乎的那些.

第一个回调是init/1,它得到了提供给GenServer.start_link/3的参数,并返回{:ok, state},这里的状态是一个新映射.我们已经可以注意到GenServerAPI是如何使客户端/服务器隔离得更明显的.start_link/3发生在客户端,而init/1是运行在服务器上的回调.

我们必须实现一个handle_call/3回调来接收call/2request,我们是从哪个进程收到请求的(_from),和当前服务器状态(names).handle_call/3回调返回了一个格式为{:reply, reply, new_state}的元组,reply是将要发送到客户端的内容,而new_state是新的服务器状态.

我们必须实现一个handle_cast/2回调来接收cast/2request,和当前服务器状态(names).handle_cast/2回调返回了一个格式为{:noreply, new_state}的元组.

这里还有一些handle_call/3handle_cast/2回调都可能返回的元组格式.我们还可以实现其它的回调,例如terminate/2code_change/3.欢迎到full GenServer文档中学习更多相关内容.

现在,让我们来写一些测试来保证我们的GenServer运作正常.

#测试通用服务器

测试GenServer与测试代理差不多.我们将生成一个设置回调上的服务器,并在测试中使用它.创建一个名为test/kv/registry_test.exs的文件:

defmodule KV.RegistryTest douse ExUnit.Case, async: truesetup do{:ok, registry} &#61; KV.Registry.start_link{:ok, registry: registry}endtest "spawns buckets", %{registry: registry} doassert KV.Registry.lookup(registry, "shopping") &#61;&#61; :errorKV.Registry.create(registry, "shopping")assert {:ok, bucket} &#61; KV.Registry.lookup(registry, "shopping")KV.Bucket.put(bucket, "milk", 1)assert KV.Bucket.get(bucket, "milk") &#61;&#61; 1end
end

我们的测试已经完成!

我们不需要明确地关闭注册,因为它将在测试结束时接收到一个:shutdown信号.尽管对于测试来说这种方法很正常,但如果需要将停止GenServer作为应用逻辑中的一部分,我们可以使用GenServer.stop/1函数:

## Client API&#64;doc """
Stops the registry.
"""
def stop(server) doGenServer.stop(server)
end

#监控的需要

我们的注册表快要完成了.唯一的问题就是注册表在桶停止或崩溃时有可能过时.让我们添加一个测试到KV.Registry_Test来引发这个bug:

test "removes buckets on exit", %{registry: registry} doKV.Registry.create(registry, "shopping"){:ok, bucket} &#61; KV.Registry.lookup(registry, "shopping")Agent.stop(bucket)assert KV.Registry.lookup(registry, "shopping") &#61;&#61; :error
end

这个测试会在最后一个断言失败,因为桶名仍然存在于注册表中,即使桶进程已经停止了.

为了修复这个bug,我们需要注册表监控每个由它生成的桶.设置好监视器之后,每当有桶退出,注册表就会收到一个通知,让我们清理词典.

让我们与监视器开始初次接触,通过运行iex -S mix来开启一个新的控制台:

iex> {:ok, pid} &#61; KV.Bucket.start_link
{:ok, #PID<0.66.0>}
iex> Process.monitor(pid)
#Reference<0.0.0.551>
iex> Agent.stop(pid)
:ok
iex> flush
{:DOWN, #Reference<0.0.0.551>, :process, #PID<0.66.0>, :normal}

注意Process.monitor(pid)返回了一个独特的参照,我们能够用它来匹配即将到来的信息.在我们停止了代理之后,我们可以flush/0所有消息,发现收到了一个:DOWN信息,带有由监视器返回的确切的参照,提醒我们桶进程因为:normal原因退出了.

让我们重新实现服务器回调来修复bug并使测试通过.首先,我们会将GenServer状态修改成两个词典:一个包含name -> pid,另一个包含ref -> name.然后我们需要在handle_cast/2上监控桶,还需要实现handle_info/2回调来处理监控信息.完整的服务器回调实现如下所示:

## Server callbacksdef init(:ok) donames &#61; %{}refs &#61; %{}{:ok, {names, refs}}
enddef handle_call({:lookup, name}, _from, {names, _} &#61; state) do{:reply, Map.fetch(names, name), state}
enddef handle_cast({:create, name}, {names, refs}) doif Map.has_key?(names, name) do{:noreply, {names, refs}}else{:ok, pid} &#61; KV.Bucket.start_linkref &#61; Process.monitor(pid)refs &#61; Map.put(refs, ref, name)names &#61; Map.put(names, name, pid){:noreply, {names, refs}}end
enddef handle_info({:DOWN, ref, :process, _pid, _reason}, {names, refs}) do{name, refs} &#61; Map.pop(refs, ref)names &#61; Map.delete(names, name){:noreply, {names, refs}}
enddef handle_info(_msg, state) do{:noreply, state}
end

观察到,我们能大大改变服务器的实现,而不改变任何客户端接口.这是将服务器与客户端明确分离的好处之一.

最后,与其他回调不同,我们为handle_info/2定义了一个"全部捕获"从句,它抛弃了所有未知信息.想知道为什么,请看下一部分.

#call,cast还是info?

目前我们已经使用了三种回调:handle_call/3,handle_cast/2以及handle_info/2.它们适用于不同场景:

  1. handle_call/3必须用于同步任务.这应当是默认的选择,因为等待服务器回应是一个很有用的背压机制.

  2. handle_cast/2必须用于异步请求,当你不关心回复时.投掷甚至不保证服务器接收到信息,所以必须谨慎使用.例如,我们在本章中定义的create/2函数就应当使用call/2.出于教学目的我们使用了 cast/2.

  3. handle_info/2必须用于服务器可接收的所有不由GenServer.call/2GenServer.cast/2发送的信息,包括由send/2发送的定期信息.监控:DOWN信息就是一个完美的例子.

因为任何信息,包括由send/2发送的,都传给了handle_info/2,服务器就有可能受到不想要的信息.因此,如果我们不定义捕获全部的从句,这些信息可能会使我们的注册表崩溃,因为没有从句可以匹配.

我们不需要为handle_call/3handle_cast/2担心这个,因为这些请求只由GenServer来完成,所以未知信息很有可能是因为开发者的错误.

#监控器还是链接?

我们已经在进程那一章中学过链接了.现在,注册表完成了,你可能想知道:我们什么时候应该使用监控器,什么时候该使用链接?

链接是双向的.如果你将两个进程链接在一起,那么如果其中一个崩溃了,另一个也会崩溃(除非它捕获退出).监控器是单向的:只有监控进程会收到关于被监控者的消息.简而言之,当你想传递崩溃时就用链接,当你只想得到崩溃,退出等等的提示时就用监控器.

回到我们的handle_cast/2实现,你会看到注册表即链接又监控了桶:

{:ok, pid} &#61; KV.Bucket.start_link
ref &#61; Process.monitor(pid)

这是一个坏主意,因为我们不想桶的崩溃导致注册表崩溃!我们避免直接创建新的进程,而是将此责任交于监督者.如下一章我们将看到的,监督者依赖于链接,这解释了为什么在Elixir和OTP中基于链接的API(spawn_link,start_link等等)如此流行.


转:https://my.oschina.net/ljzn/blog/730665



推荐阅读
  • Web开发框架概览:Java与JavaScript技术及框架综述
    Web开发涉及服务器端和客户端的协同工作。在服务器端,Java是一种优秀的编程语言,适用于构建各种功能模块,如通过Servlet实现特定服务。客户端则主要依赖HTML进行内容展示,同时借助JavaScript增强交互性和动态效果。此外,现代Web开发还广泛使用各种框架和库,如Spring Boot、React和Vue.js,以提高开发效率和应用性能。 ... [详细]
  • 如何撰写适应变化的高效代码:策略与实践
    编写高质量且适应变化的代码是每位程序员的追求。优质代码的关键在于其可维护性和可扩展性。本文将从面向对象编程的角度出发,探讨实现这一目标的具体策略与实践方法,帮助开发者提升代码效率和灵活性。 ... [详细]
  • 用阿里云的免费 SSL 证书让网站从 HTTP 换成 HTTPS
    HTTP协议是不加密传输数据的,也就是用户跟你的网站之间传递数据有可能在途中被截获,破解传递的真实内容,所以使用不加密的HTTP的网站是不 ... [详细]
  • 本文详细介绍了在 CentOS 7 系统中配置 fstab 文件以实现开机自动挂载 NFS 共享目录的方法,并解决了常见的配置失败问题。 ... [详细]
  • Python 数据可视化实战指南
    本文详细介绍如何使用 Python 进行数据可视化,涵盖从环境搭建到具体实例的全过程。 ... [详细]
  • 本文详细介绍了 PHP 中对象的生命周期、内存管理和魔术方法的使用,包括对象的自动销毁、析构函数的作用以及各种魔术方法的具体应用场景。 ... [详细]
  • 使用虚拟机配置服务器
    本文详细介绍了如何使用虚拟机配置服务器,包括购买云服务器的操作步骤、系统默认配置以及相关注意事项。通过这些步骤,您可以高效地配置和管理您的服务器。 ... [详细]
  • 浏览器作为我们日常不可或缺的软件工具,其背后的运作机制却鲜为人知。本文将深入探讨浏览器内核及其版本的演变历程,帮助读者更好地理解这一关键技术组件,揭示其内部运作的奥秘。 ... [详细]
  • 深入解析Struts、Spring与Hibernate三大框架的面试要点与技巧 ... [详细]
  • 为什么多数程序员难以成为架构师?
    探讨80%的程序员为何难以晋升为架构师,涉及技术深度、经验积累和综合能力等方面。本文将详细解析Tomcat的配置和服务组件,帮助读者理解其内部机制。 ... [详细]
  • 如何将TS文件转换为M3U8直播流:HLS与M3U8格式详解
    在视频传输领域,MP4虽然常见,但在直播场景中直接使用MP4格式存在诸多问题。例如,MP4文件的头部信息(如ftyp、moov)较大,导致初始加载时间较长,影响用户体验。相比之下,HLS(HTTP Live Streaming)协议及其M3U8格式更具优势。HLS通过将视频切分成多个小片段,并生成一个M3U8播放列表文件,实现低延迟和高稳定性。本文详细介绍了如何将TS文件转换为M3U8直播流,包括技术原理和具体操作步骤,帮助读者更好地理解和应用这一技术。 ... [详细]
  • Python错误重试让多少开发者头疼?高效解决方案出炉
    ### 优化后的摘要在处理 Python 开发中的错误重试问题时,许多开发者常常感到困扰。为了应对这一挑战,`tenacity` 库提供了一种高效的解决方案。首先,通过 `pip install tenacity` 安装该库。使用时,可以通过简单的规则配置重试策略。例如,可以设置多个重试条件,使用 `|`(或)和 `&`(与)操作符组合不同的参数,从而实现灵活的错误重试机制。此外,`tenacity` 还支持自定义等待时间、重试次数和异常处理,为开发者提供了强大的工具来提高代码的健壮性和可靠性。 ... [详细]
  • 本文详细探讨了几种常用的Java后端开发框架组合及其具体应用场景。通过对比分析Spring Boot、MyBatis、Hibernate等框架的特点和优势,结合实际项目需求,为开发者提供了选择合适框架组合的参考依据。同时,文章还介绍了这些框架在微服务架构中的应用,帮助读者更好地理解和运用这些技术。 ... [详细]
  • 在 Axublog 1.1.0 版本的 `c_login.php` 文件中发现了一个严重的 SQL 注入漏洞。该漏洞允许攻击者通过操纵登录请求中的参数,注入恶意 SQL 代码,从而可能获取敏感信息或对数据库进行未授权操作。建议用户尽快更新到最新版本并采取相应的安全措施以防止潜在的风险。 ... [详细]
  • 在使用SSH框架进行项目开发时,经常会遇到一些常见的问题。例如,在Spring配置文件中配置AOP事务声明后,进行单元测试时可能会出现“No Hibernate Session bound to thread”的错误。本文将详细探讨这一问题的原因,并提供有效的解决方案,帮助开发者顺利解决此类问题。 ... [详细]
author-avatar
平凡屋之换
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有