<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>tt67wq (方源)</title>
    <link>https://ruby-china.org/tt67wq</link>
    <description></description>
    <language>en-us</language>
    <item>
      <title>[辣鸡翻译] Elixir 宏与编译时</title>
      <description>&lt;h2 id="Elixir宏与编译时"&gt;Elixir 宏与编译时&lt;/h2&gt;
&lt;hr&gt;

&lt;p&gt;宏是 Elixir 中元编程常用手段。网上有非常多的资料会解释什么是宏，以及怎样使用他们：有 Elixir 官网"Getting Start"中关于&lt;a href="https://elixir-lang.org/getting-started/meta/macros.html" rel="nofollow" target="_blank" title=""&gt;宏&lt;/a&gt;的部分，由 Saša Jurić遍写的一系列很不错的&lt;a href="https://www.theerlangelist.com/2014/06/understanding-elixir-macros-part-1.html" rel="nofollow" target="_blank" title=""&gt;文章&lt;/a&gt;，甚至是 McCord 写的&lt;a href="https://pragprog.com/book/cmelixir/metaprogramming-elixir" rel="nofollow" target="_blank" title=""&gt;一本书&lt;/a&gt;。这篇文章中，我假设你已经很熟练的使用宏编程而且 c 知道其原理，这里我着重讲一下宏的另外一种很少见的使用场景：在宏中做编译时工作。&lt;/p&gt;
&lt;h2 id="宏展开"&gt;宏展开&lt;/h2&gt;
&lt;p&gt;宏经常被用来操作抽象语法树，然后生成一个新的抽象语法树。举个例子，&lt;code&gt;if&lt;/code&gt;的宏看起来像这样：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defmacro if(condition, do: do_block, else: else_block) do
  quote do
    case unquote(condition) do
      x when x in [false, nil] -&amp;gt; unquote(else_block)
      _                        -&amp;gt; unquote(do_block)
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;if&lt;/code&gt;展开后是一个&lt;code&gt;case&lt;/code&gt;的表达式，检查&lt;code&gt;condition&lt;/code&gt;是否为真，然后执行相应的代码。&lt;/p&gt;

&lt;p&gt;这里的核心概念是&lt;em&gt;展开&lt;/em&gt;：一个宏会被转换成其他的代码。用函数&lt;code&gt;Macro.expand/2&lt;/code&gt;或&lt;code&gt;Macro.expand_once/2&lt;/code&gt;很容易就能看到这一过程。让我们用一个简单的宏试试看：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defmodule SimpleMacro do
  defmacro plus(x, y) do
    quote do: unquote(x) + unquote(y)
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;展开的过程比较琐碎：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;iex&amp;gt; import SimpleMacro
iex&amp;gt; ast = quote do: plus(x, 23)
iex&amp;gt; ast |&amp;gt; Macro.expand(__ENV__) |&amp;gt; Macro.to_string
"x + 23"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;展开一个宏的意思是指执行宏内的代码，将宏的调用替换为它返回的抽象语法树。展开的过程发生在编译阶段：一个宏是在编译阶段被执行，然后返回它递归生成的代码，这些代码在只在运行时被执行。我们刚好可以利用这一点！我们可以写一个宏，它在不会翻译它接收的抽象语法树，而是在编译阶段利用抽象语法树来执行某些操作。&lt;/p&gt;
&lt;h2 id="编译时操作"&gt;编译时操作&lt;/h2&gt;
&lt;p&gt;通常来说，宏被描述为接收代码返回代码的函数；在这个描述中，我们把宏看成一种函数。但是，我们也可以将函数函数定义为一种宏：每个函数只是在编译时啥都不做的宏而已。&lt;/p&gt;

&lt;p&gt;比如我们有以下代码：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defmodule MacroPhilosophy do
  def hello(name) do
    "Hello #{name}!"
  end
end
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;iex&amp;gt; hello "Elixir"
"Hello Elixir!"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以将&lt;code&gt;hello/1&lt;/code&gt;函数改写成一个宏，除了需要&lt;code&gt;require&lt;/code&gt;一下&lt;code&gt;MacroPhilosophy&lt;/code&gt;这个模块以外，不用修改所有依赖它的代码。我们只需要将&lt;code&gt;hello/1&lt;/code&gt;修改为代码引用而非代码执行：幸好&lt;code&gt;quote&lt;/code&gt;函数有&lt;code&gt;:bind_quoted&lt;/code&gt;选项，我们可以利用这一点。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defmodule MacroPhilosophy do
  defmacro hello(name) do
    quote bind_quoted: binding() do
      "Hello #{name}!"
    end
  end
end
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;iex&amp;gt; require MacroPhilosophy
iex&amp;gt; hello "Elixir"
"Hello Elixir"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如你所见，函数体部分在函数实现和宏实现中是一致的。&lt;/p&gt;

&lt;p&gt;这让我们可以用另外一个视角来看待函数，同时也强调了宏的重要特点：它们可被用来做编译时工作。我们可以在编译时的宏内部执行任何代码，只要我们能返回合法的代码。同时，在我们返回代码之前的所有工作都会在运行时消失不见。Poof!&lt;/p&gt;
&lt;h3 id="一个没用的表达式记数宏"&gt;一个没用的表达式记数宏&lt;/h3&gt;
&lt;p&gt;为了保持例子和现实世界毫无关联的优良传统，我们编写一个宏来日志打印 Elixir 的表达式数量：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defmodule UselessExamplesAreFun do
  defmacro log_number_of_expressions(code) do
    {_, counter} = Macro.prewalk code, 0, fn(expr, counter) -&amp;gt;
      {expr, counter + 1}
    end

    IO.puts "You passed me #{counter} expressions/sub-expressions"

    code
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;让我们简单看一下这个宏，我们用&lt;code&gt;Macro.walk/3&lt;/code&gt;函数来统计表达式数量。然后我们打印出这个数字：这就是我们编译时工作。最后我们返回参数中的代码 (抽象语法树)。这个宏在运行时实际上啥也不干：它甚至不会在编译好的代码中留下痕迹。这是个性能友好的特性，因为编译时的日志代码消失不见了。&lt;/p&gt;
&lt;h3 id="一个现实世界的例子"&gt;一个现实世界的例子&lt;/h3&gt;
&lt;p&gt;当我们在编写&lt;a href="https://github.com/elixir-gettext/gettext" rel="nofollow" target="_blank" title=""&gt;gettext for elixir&lt;/a&gt;的时候，José Valim 建议我们使用这项技术，在那之后我意识到宏可以用来做编译时的工作。Gettext 提供了名叫&lt;code&gt;mix gettext.extract&lt;/code&gt;的任务用于提取源文件中的翻译写入到&lt;code&gt;。po&lt;/code&gt;文件中。翻译动作就变成了带上字符串作为参数调用 gettext 宏。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# in lib/greetings.ex
import MyApp.Gettext
gettext "Hello people of Gotham!", "fr"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行&lt;code&gt;mix gettext.extract&lt;/code&gt;的结果会写入一个&lt;code&gt;.po&lt;/code&gt;的文件中：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#: lib/greetings.ex:2
msgid "Hello people of Gotham!"
msgstr ""
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大部分其他语言 (例如 Python) 的 gettext 实现是解析代码然后寻找&lt;code&gt;gettext()&lt;/code&gt;函数调用。而在 Elixir 中，我们只需要在宏中注入这个字符串，然后重新编译展开这个宏来完成翻译工作。Awesome！&lt;/p&gt;

&lt;p&gt;这就是&lt;code&gt;gettext&lt;/code&gt;函数定义的大致模样：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defmacro gettext(msgid, locale) do
  extract(msgid)

  quote do
    translate(unquote(msgid), unquote(locale))
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们调用&lt;code&gt;extract/2&lt;/code&gt;函数时，我们在重新编译之前将&lt;code&gt;msgid&lt;/code&gt;注入到一个代理中。当编译工作完成后，我们将代理的状态导出即可。这一切在运行时不会有任何副作用：调用&lt;code&gt;gettext/2&lt;/code&gt;就如同调用&lt;code&gt;translate/2&lt;/code&gt;一样。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;深入了解宏和其工作机制是元编程，优化和理解 Elixir 代码的基础。在这篇文章中，我们实践了用宏来完成编译时工作。我们看到了一个非现实世界的例子和一个 gettext 项目中真实的例子。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;原文链接：&lt;a href="https://andrealeopardi.com/posts/compile-time-work-with-elixir-macros/" rel="nofollow" target="_blank" title=""&gt;compile-time-work-with-elixir-macros&lt;/a&gt;&lt;/p&gt;</description>
      <author>tt67wq</author>
      <pubDate>Mon, 27 Apr 2020 14:41:37 +0800</pubDate>
      <link>https://ruby-china.org/topics/39805</link>
      <guid>https://ruby-china.org/topics/39805</guid>
    </item>
    <item>
      <title>[辣基翻译] Elixir 中 TCP 连接处理技巧 -- Andrea Leopardi</title>
      <description>&lt;h2 id="[译] Elixir中TCP连接处理技巧 -- Andrea Leopardi"&gt;[译] Elixir 中 TCP 连接处理技巧 -- Andrea Leopardi&lt;/h2&gt;
&lt;p&gt;Elixir 作为一门 Erlang 虚拟机上的语言，由于 Erlang 的设计风格和 Erlang 虚拟机的特性，在网络编程中大展身手。在这个大前提下，我们经常需要在网络世界中处理外部链接。
举个例子：一个典型的 Web 应用需要连接一个关系型数据库和一个 kvdb，或者一个嵌入式的系统需要连接其他的 node。&lt;/p&gt;

&lt;p&gt;大多数情况下，这些网络连接对程序员来说是无需关心的，因为已经有很多已经封装好的网络驱动 (例如数据库驱动)，但是我认为了解这些连接如何手动编写是一件很有趣的事情。如果某些特殊的网络服务没有外部的驱动代码可用，又或者你想了解这些驱动是怎样工作的，这些知识就会很有用。&lt;/p&gt;

&lt;p&gt;这篇文章中我们只会讨论 TCP 协议的连接，因为 TCP 协议可能是网络世界中最基础和使用最多的协议了。但是我们所使用的方法和原理，在其他协议面前也是通用的，例如 UDP 协议。&lt;/p&gt;
&lt;h3 id="一个很现实的例子"&gt;一个很现实的例子&lt;/h3&gt;
&lt;p&gt;作为这篇文章的目标，我们想编写一个差不多能 work 的 redis 驱动。Redis 服务是能够收发 message 的 TCP 服务。Redis 在 TCP 之上使用了一个自定义的应用层协议，并没有使用通用的 HTTP 协议。而我们并不关心这些，我们只关心怎样处理我们的 Elixir 应用和 Redis Server 之间的 TCP 连接。&lt;/p&gt;

&lt;p&gt;一点题外话：显然，社区里已经有很多的 Erlang 和 Elixir 的 Redis 驱动，不过，懒惰的我懒得再去想一个聪明的名字，我们就叫他 Redis 好了。&lt;/p&gt;

&lt;p&gt;这就开始吧。&lt;/p&gt;
&lt;h3 id="Erlang/Elixir中TCP简述"&gt;Erlang/Elixir中TCP简述&lt;/h3&gt;
&lt;p&gt;在 Erlang/Elixir 中，tcp 连接是用&lt;code&gt;:gen_tcp&lt;/code&gt;模块来处理的。这篇文章中我们只编写客户端部分来与 Redis 服务交互，实际上&lt;code&gt;:gen_tcp&lt;/code&gt;也可以用来编写 TCP 服务端。&lt;/p&gt;

&lt;p&gt;所有的发向 Server 的消息都用&lt;code&gt;:gen_tcp.send/2&lt;/code&gt;函数来发送。而从服务端发送至客户端的消息我们总是倾向于把它们当作 Erlang Message 来处理，因为这样处理起来比较直观。后面我们会看到，我们将通过设置 TCP socket 的&lt;code&gt;:active&lt;/code&gt;option 选项来控制发送至客户端的消息。&lt;/p&gt;

&lt;p&gt;我们通过传递 host、port 等参数至&lt;code&gt;:gen_tcp.connect/3&lt;/code&gt;来建立与服务端的连接。默认情况下，调用 connect 函数的进程会被认为是这个 tcp 连接的“controlling process”，意思就是这个进程将会处理所有发到这个 socket 的 tcp 消息。&lt;/p&gt;

&lt;p&gt;以上是我们对 tcp 连接所需要了解的知识，我们继续。&lt;/p&gt;
&lt;h3 id="第一个版本"&gt;第一个版本&lt;/h3&gt;
&lt;p&gt;我们将使用&lt;code&gt;GenServer&lt;/code&gt;作为我们 TCP 连接的接口。我们需要一个 GenServer 以便于我们在 state 中保持 socket 的状态和在所有消息通信中复用这个 socket。&lt;/p&gt;
&lt;h4 id="建立连接"&gt;建立连接&lt;/h4&gt;
&lt;p&gt;因为我们使用 GenServer 作为 TCP 连接的接口，所以我们一次只能在 state 的 socket 中维护单个连接的状态，我们希望它总是和 Server 保持连接的状态。最优的策略实在 GenServer 启动的时候来做连接的工作，具体是在 init 的回调函数中实现。&lt;code&gt;init/1&lt;/code&gt;是在&lt;code&gt;GenServer.start_link/2&lt;/code&gt;被调用的时候触发的回调，GenServer 在 init 被调用前不会做多余的工作，所有是我们建立连接的理想场所。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defmodule Redis do
  use GenServer

  @initial_state %{socket: nil}

  def start_link do
    GenServer.start_link(__MODULE__, @initial_state)
  end

  def init(state) do
    opts = [:binary, active: false]
    {:ok, socket} = :gen_tcp.connect('localhost', 6379, opts)
    {:ok, %{state | socket: socket}}
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们给&lt;code&gt;:gen_tcp.connect/3&lt;/code&gt;设定的参数非常直观。&lt;code&gt;:binary&lt;/code&gt;要求 socket 从 TCP server 中接收的消息以 binary 的格式接收而不是 Erlang 默认的 charlist 格式：在 Elixir 中这可能是我们想要的，而且可能是最高效的选择。&lt;code&gt;active: false&lt;/code&gt;告诉 socket 永远不要把 TCP message 转换成发送给 GenServer 的 Erlang message；我们将用&lt;code&gt;:gen_tcp.recv/2&lt;/code&gt;函数来显式的接收 tcp 消息。我们这样做是为了我们的 GenServer 不被汹涌而来的 tcp 消息淹没：我们只在我们想要的时候去接收并处理它们。&lt;/p&gt;
&lt;h4 id="发送消息"&gt;发送消息&lt;/h4&gt;
&lt;p&gt;现在我们已经有了一个连接上 Redis 服务的 GenServer 了，现在让我们给 Redis 发送一些指令。&lt;/p&gt;
&lt;h5 id="RESP PROTOCL"&gt;RESP PROTOCL&lt;/h5&gt;
&lt;p&gt;这里需要简单提一下 Redis 的二进制协议，RESP：这是 Redis 用于编解码它的 Requst/Reply 的协议，&lt;a href="https://redis.io/topics/protocol" rel="nofollow" target="_blank" title=""&gt;协议的细节&lt;/a&gt;简单明了，如果你想了解更多，我建议你看看。为了这篇文章的中心目标，我们假设我们有了 RESP 的完全实现：它提供了&lt;code&gt;encode/decode&lt;/code&gt;两个函数：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Redis.RESP.encode/1&lt;/code&gt;: 将 list 编码成 redis command，例如:
&lt;code&gt;
Redis.RESP.encode(["GET", "mykey"])
#=&amp;gt; &amp;lt;&amp;lt;...&amp;gt;&amp;gt;
&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Redis.RESP.decode/1: 将一个 binary 解码成一个 Elixir 对象，例如:
&lt;code&gt;
resp_to_get_command = &amp;lt;&amp;lt;...&amp;gt;&amp;gt;
Redis.RESP.decode(resp_to_get_command)
#=&amp;gt; 1
&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=":gen_tcp.send/2"&gt;&lt;code&gt;:gen_tcp.send/2&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;我们在文章开头提到过，我们利用&lt;code&gt;:gen_tcp.send/2&lt;/code&gt;来向 tcp 连接发送消息。我们的 Redis 模块将提供单独一个函数来向 Redis Server 发送命令：&lt;code&gt;Redis.command/2&lt;/code&gt;。具体实现也很直观：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defmodule Redis do
  # ...as before...

  def command(pid, cmd) do
    GenServer.call(pid, {:command, cmd})
  end

  def handle_call({:command, cmd}, from, %{socket: socket} = state) do
    :ok = :gen_tcp.send(socket, Redis.RESP.encode(cmd))

    # `0` means receive all available bytes on the socket.
    {:ok, msg} = :gen_tcp.recv(socket, 0)
    {:reply, Redis.RESP.decode(msg), state}
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码没啥瑕疵。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{:ok, pid} = Redis.start_link()
Redis.command(pid, ["SET", "mykey", 1])
Redis.command(pid, ["GET", "mykey"])
#=&amp;gt; 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;... 但这里有个问题。&lt;/p&gt;
&lt;h4 id="哪里有问题呢？"&gt;哪里有问题呢？&lt;/h4&gt;
&lt;p&gt;长话短说：&lt;code&gt;:gen_tcp.recv/2&lt;/code&gt;函数是阻塞的。&lt;/p&gt;

&lt;p&gt;这段代码能顺利工作的前提是这个 GenServer 只被单个 Elixir 进程调用。当一个进程想发送命令给 Redis Server 的时候会发生如下事件：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Elixir 进程调用 GenServer 的&lt;code&gt;command/2&lt;/code&gt;命令，然后进程阻塞的等待结果&lt;/li&gt;
&lt;li&gt;GenServer 向 Redis Server 发送指令然后阻塞在&lt;code&gt;:gen_tcp.recv/2&lt;/code&gt;上&lt;/li&gt;
&lt;li&gt;Redis Server 回复结果&lt;/li&gt;
&lt;li&gt;GenServer 回应调用进程&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;你能看出问题出在哪里了吗？GenServer 在等待 Redis Server 回复的过程中是阻塞的。当然在单个进程的情况下这样是没问题的，但当多个进程同时想通过 GenServer 跟 Redis Server 做交互的时候情况就会变得很糟糕。幸好，我们可以做一个更好的实现。&lt;/p&gt;
&lt;h4 id="使用队列"&gt;使用队列&lt;/h4&gt;
&lt;p&gt;你可能知道这样一个事实，GenServer 的&lt;code&gt;handle_call/3&lt;/code&gt;函数可以不用立即返回结果，它可以先返回一个&lt;code&gt;{:noreply, state}&lt;/code&gt;作为应答，然后通过&lt;code&gt;GenServer.reply/2&lt;/code&gt;函数返回真实的结果给请求进程。&lt;/p&gt;

&lt;p&gt;在客户端请求然后阻塞的等待结果的同时 GenServer 继续工作直到它有了对这个客户端的回复，这样一种方法正式我们所需要的。&lt;/p&gt;

&lt;p&gt;为了执行我们这一策略，我们需要摆脱&lt;code&gt;:gen_tcp.recv/2&lt;/code&gt;函数，转而用 Erlang Message 的形式来接收 TCP message。我们可以在连接 Redis 服务的时候将 socket 参数中的&lt;code&gt;active: false&lt;/code&gt;转换成&lt;code&gt;active: true&lt;/code&gt;，当 active 被设置为 true 的时候，所有 tcp socket 接收的消息都会转换成&lt;code&gt;{:tcp, socket, message}&lt;/code&gt;形式的 Erlang Message 发送给 GenServer。&lt;/p&gt;

&lt;p&gt;这些事情将会发生：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Elixir 进程在 GenServer 中调用&lt;code&gt;command/2&lt;/code&gt;，然后阻塞自己等待结果&lt;/li&gt;
&lt;li&gt;GenServer 将命令发向 Redis Server 然后返回&lt;code&gt;{:noreply, state}&lt;/code&gt;，所以它自身不会被阻塞&lt;/li&gt;
&lt;li&gt;Redis Server 回复一条 tcp message 给 GenServer，GenServer 以&lt;code&gt;{:tcp, socket, message}&lt;/code&gt;的形式接收到&lt;/li&gt;
&lt;li&gt;GenServer 在&lt;code&gt;handle_info/2&lt;/code&gt;函数中处理这条消息，并回应调用的 Elixir 进程&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;不难看出，从 GenServer 发出命令给 Redis Server 到它接收到 Redis Server 的回应这段时间内，GenServer 是非阻塞的，它还能继续发送其他的命令给 GenServer，Nice！&lt;/p&gt;

&lt;p&gt;剩下需要解决的问题就是，GenServer 怎样回执给正确的调用进程：当 GenServer 接收到一条&lt;code&gt;{:tcp, ....}&lt;/code&gt;的消息时，它怎么知道&lt;code&gt;GenServer.reply/2&lt;/code&gt;函数该发给谁呢？我们知道 Redis 是严格按照 fifo 的顺序来应答的，我们可以利用一个简单的队列来把请求的进程存储起来。我们将在 GenServer 的 state 中维护一个队列，当进程请求的时候入队，当有应答到来的时候出队。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defmodule Redis do
  @initial_state %{socket: nil, queue: :queue.new()}
  # ...as before...

  def handle_call({:command, cmd}, from, %{queue: queue} = state) do
    # We send the command...
    :ok = :gen_tcp.send(state.socket, Redis.RESP.encode(cmd))

    # ...enqueue the client...
    state = %{state | queue: :queue.in(from, queue)}

    # ...and we don't reply right away.
    {:noreply, state}
  end

  def handle_info({:tcp, socket, msg}, %{socket: socket} = state) do
    # We dequeue the next client:
    {{:value, client}, new_queue} = :queue.out(state.queue)

    # We can finally reply to the right client.
    GenServer.reply(client, Redis.RESP.decode(msg))

    {:noreply, %{state | queue: new_queue}}}
  end
end
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="按需求收取消息"&gt;按需求收取消息&lt;/h4&gt;
&lt;p&gt;在上面的篇幅中，为了能够以 Erlang Message 的形式接收 TCP 消息，我们从一个&lt;code&gt;active: false&lt;/code&gt;的 socket 转移到了&lt;code&gt;active: true&lt;/code&gt;的 socket。它能正常运行，但在一种情况下会出现问题：当 TCP 服务发送大量数据给 GenServer 的时候，因为 Erlang 本身并没有对消息接收的队列大小做限制，这样很容易造成 GenServer 的消息雪崩；这也是我们最开始选择&lt;code&gt;active: false&lt;/code&gt;的原因。为了解决这个问题，我们可以将&lt;code&gt;active: true&lt;/code&gt;改成更保守的&lt;code&gt;active: once&lt;/code&gt;：这样每次只会有一个 tcp 消息被转换成 Erlang Message，然后 socket 又回到了&lt;code&gt;active: false&lt;/code&gt;的状态。我们可以重新设置&lt;code&gt;active: once&lt;/code&gt;来接收下一条消息，如此循环。我们每次只转换一条 TCP 消息为 Erlang Message，这样可以保证我们能够处理它们。&lt;/p&gt;

&lt;p&gt;我们只要记得在接收一条&lt;code&gt;{:tcp, ...}&lt;/code&gt;的消息的时候重新激活 Socket 即可，我们可以利用&lt;code&gt;:inet:setopt/2&lt;/code&gt;函数来实现。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defmodule Redis do
  # ...as before...

  def handle_info({:tcp, socket, msg}, %{socket: socket} = state) do
    # Allow the socket to send us the next message.
    :inet.setopts(socket, active: :once)

    # exactly as before
  end
end
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="剧情转折"&gt;剧情转折&lt;/h3&gt;
&lt;p&gt;上文描述的模式并不是我想出来的，很意外对吧？我所形容的模式在一票 Erlang 和 Elixir 应用中非常常见。这个模式在任何需要连接 tcp 服务的场合 (或者类似的场合) 都表现的十分良好，它经常被用在数据库驱动，这也是我为啥选 Redis 来做例子的理由。&lt;/p&gt;

&lt;p&gt;很多现实世界中的库都使用着我所描述的模式：举个例子，&lt;a href="https://github.com/wooga/eredis" rel="nofollow" target="_blank" title=""&gt;eredis&lt;/a&gt;(Erlang 最常用的 Redis 驱动) 就跟我们的例子很类似：看看这部分&lt;a href="https://github.com/wooga/eredis/blob/770f828918db710d0c0958c6df63e90a4d341ed7/src/eredis_client.erl#L1-L21" rel="nofollow" target="_blank" title=""&gt;代码注释&lt;/a&gt;，基本上就是这篇文章的总结。另外一个跟我们的模式大致相似的例子就是&lt;a href="https://github.com/ericmj/postgrex" rel="nofollow" target="_blank" title=""&gt;PostgreSQL&lt;/a&gt;和&lt;a href="https://github.com/ankhers/mongodb" rel="nofollow" target="_blank" title=""&gt;MongoDB&lt;/a&gt;的 Elixir 驱动。目前我正在为&lt;a href="https://orientdb.com/orientdb/" rel="nofollow" target="_blank" title=""&gt;OrientDB&lt;/a&gt;编写 Elixir 驱动，也使用的是这个模式。所以这个肯定是可行的。&lt;/p&gt;
&lt;h3 id="TCP连接处理的更优解"&gt;TCP 连接处理的更优解&lt;/h3&gt;
&lt;p&gt;上文中我们愉快的忽略了一个令人烦躁的问题 -- 错误处理！&lt;/p&gt;

&lt;p&gt;我们将继续愉快的忽略一系列可能发生的错误，例如，消息到来的时候遇到空队列 (它会报一个&lt;code&gt;{{:value, val}, new_queue}&lt;/code&gt;的模式匹配错误)，或是接收到不完整的 TCP 消息。但是在 TCP 连接中可能发生的一系列问题例如断线和超时这些我们是可以尝试解决的。&lt;/p&gt;

&lt;p&gt;我们可以自己手动的来处理这些异常，幸运的是，Elixir 的核心开发者&lt;em&gt;James Fish&lt;/em&gt;已经在他的库&lt;a href="https://github.com/fishcakez/connection" rel="nofollow" target="_blank" title=""&gt;connection&lt;/a&gt;中做完了大部分工作。这个类库十分年轻，它已经被用在上文提到的&lt;a href="https://github.com/ankhers/mongodb" rel="nofollow" target="_blank" title=""&gt;MongoDB 驱动&lt;/a&gt;和&lt;a href="https://orientdb.com/orientdb/" rel="nofollow" target="_blank" title=""&gt;OrientDB 驱动&lt;/a&gt;之中了。&lt;/p&gt;
&lt;h4 id="使用Connection来处理连接"&gt;使用 Connection 来处理连接&lt;/h4&gt;
&lt;p&gt;这个库协议定义了一个名为&lt;code&gt;connection&lt;/code&gt;的协议：这个协议所规定的 API 是 GenServer 协议的一个超集，所以它易于理解也容易整合进现有的项目。&lt;/p&gt;

&lt;p&gt;这篇&lt;a href="https://hexdocs.pm/connection/Connection.html" rel="nofollow" target="_blank" title=""&gt;文档&lt;/a&gt;详细的解释了&lt;code&gt;Connection&lt;/code&gt;协议，这个库的主旨是实现一个连接着另一端且能做断线处理的进程。为了实现这一目标，&lt;code&gt;Connection&lt;/code&gt;协议定义了两个附加函数并且修改了部分 GenServer 的返回值。&lt;/p&gt;

&lt;p&gt;我们这里只研究部分&lt;code&gt;Connection&lt;/code&gt;的函数，如果你想了解更多细节，请阅读文档。&lt;/p&gt;
&lt;h4 id="初始化连接"&gt;初始化连接&lt;/h4&gt;
&lt;p&gt;我们的&lt;code&gt;Redis.init/1&lt;/code&gt;回调函数实现了连接 Redis 服务的行为，阻塞了调用&lt;code&gt;Redis.start_link/0&lt;/code&gt;函数的进程直到回调函数返回。如果我们不希望 GenServer 在连接上 Redis 服务之前做其他事情的话是没太大问题的。但是我们的&lt;code&gt;start_link/0&lt;/code&gt;函数可能是被监控树所调用，或者是被专门来启动 GenServer 的进程所调用：在这种情况下，我们希望&lt;code&gt;start_link/0&lt;/code&gt;函数尽快的返回&lt;code&gt;{:ok, pid}&lt;/code&gt;的结果，然后在后台来完成连接的动作。我们也希望 GenServer 能用队列缓存住建立连接期间的请求。这个协议能够使进程非阻塞的启动 GenServer，但是会阻塞后续的请求直到 GenServer 连接上 Redis。&lt;/p&gt;

&lt;p&gt;有了&lt;code&gt;Connection&lt;/code&gt;我们可以完全做到这一点。&lt;code&gt;init/1&lt;/code&gt;回调函数返回&lt;code&gt;{:connect, info, state}&lt;/code&gt;而非&lt;code&gt;{:ok, state}&lt;/code&gt;迫使&lt;code&gt;start_link/0&lt;/code&gt;立即返回&lt;code&gt;{:ok, pid}&lt;/code&gt;，同时调用了&lt;code&gt;connect/2&lt;/code&gt;的 GenServer 回调阻塞 GenServer 接收其他的请求直到连接完成。&lt;code&gt;{:connect, info, state}&lt;/code&gt;中的&lt;code&gt;info&lt;/code&gt;应该包含我们建立连接的所有信息，这些信息我们并不想放在 GenServer 的 state 中保存。&lt;/p&gt;

&lt;p&gt;我们把代码做点改进：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;defmodule Redis do
  use Connection
  @initial_state %{socket: nil}

  def start_link do
    # We need Connection.start_link/2 now,
    # not GenServer.start_link/2
    Connection.start_link(__MODULE__, @initial_state)
  end

  def init(state) do
    # We use `nil` as we don't need any additional info
    # to connect
    {:connect, nil, state}
  end

  def connect(_info, state) do
    opts = [:binary, active: :once]
    {:ok, socket} = :gen_tcp.connect('localhost', 6379, opts)
    {:ok, %{state | socket: socket}}
  end
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这对我们之前的实现来说是个巨大改进，但是&lt;code&gt;Connection&lt;/code&gt;库还可以做的更好。&lt;/p&gt;
&lt;h4 id="回退"&gt;回退&lt;/h4&gt;
&lt;p&gt;我们在使用&lt;code&gt;:gen_tcp.connect/3&lt;/code&gt;连接 Redis 服务的地方直接使用&lt;code&gt;{:ok, socket} = ...&lt;/code&gt;模式匹配非常不妥，这个地方有个很大隐患。如果连接意外中断，此处的模式匹配失败，那么整个 GenServer 都会挂掉。最明显的处理方法就是用 case 语句来匹配&lt;code&gt;:gen_tcp.connect/3&lt;/code&gt;函数的返回值：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;case :gen_tcp.connect('localhost', 6379, opts) do
  {:ok, socket} -&amp;gt;
    {:ok, %{state | socket: socket}}

  {:error, reason} -&amp;gt;
    # now what?
end
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们便能够决定在有错误发生的情况下该如何处理。挂起 GenServer 或是返回 error 都很平常，现实世界中，我们通常会做重连的操作。我们可以令&lt;code&gt;connect/2&lt;/code&gt;返回一个&lt;code&gt;{:backoff, timeout, state}&lt;/code&gt;元组，这样&lt;code&gt;connect/2&lt;/code&gt;会在&lt;code&gt;timeout&lt;/code&gt;时间后被再次调用，尝试重连。我们的&lt;code&gt;connect/2&lt;/code&gt;看起来是这样：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def connect(_info, state) do
  opts = [:binary, active: :once]

  case :gen_tcp.connect('localhost', 6379, opts) do
    {:ok, socket} -&amp;gt;
      {:ok, %{state | socket: socket}}

    {:error, reason} -&amp;gt;
      IO.puts("TCP connection error: #{inspect reason}")
      # Try again in one second:
      {:backoff, 1000, state}
  end
end

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Connection&lt;/code&gt;的好处在于你可以在几乎任意一个回调函数中返回&lt;code&gt;{:backoff, timeout, state}&lt;/code&gt;，这样断线的错误处理就变得很直观。&lt;/p&gt;

&lt;p&gt;当&lt;code&gt;{:backoff, timeout, state}&lt;/code&gt;被返回时，&lt;code&gt;connect/2&lt;/code&gt;被调用且用&lt;code&gt;:backoff&lt;/code&gt;作它的第一个参数：这让我们很容易的区分这是初始连接还是重连的动作，方便我们做区别对待。比如说，我们想实现一个指数重连，即初次 1 秒后重试，第二次 2 秒后重试，第三次 4 秒后重试，如此直到达到最大重试次数。&lt;/p&gt;
&lt;h3 id="池化"&gt;池化&lt;/h3&gt;
&lt;p&gt;最后一个小技巧，我们的 GenServer 在&lt;a href="https://github.com/devinus/poolboy" rel="nofollow" target="_blank" title=""&gt;poolboy&lt;/a&gt;库的帮助下可以更平滑的使用。网上有许许多多关于&lt;code&gt;poolboy&lt;/code&gt;的文档，所以我并不准备去解释它是怎么工作的。我只是展示下一个例子。&lt;/p&gt;

&lt;p&gt;首先，我们用&lt;code&gt;:poolboy.start_link/2&lt;/code&gt;函数为 GenServer 创建一个固定大小的池。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;poolboy_opts = [worker_module: Redis, size: 50]
redis_opts = []
{:ok, pool} = :poolboy.start_link(poolboy_opts, redis_opts)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，我们从池中拿出一个资源（即一个 GenServer），做完 Redis 操作之后再归还至池中。&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;worker = :poolboy.checkout(pool)
Redis.command(worker, ["SET", "mykey", 1])
:ok = :poolboy.checkin(pool, worker)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没啥比这更舒服了！&lt;/p&gt;
&lt;h3 id="结论"&gt;结论&lt;/h3&gt;
&lt;p&gt;我们见识到了如何利用 GenServer 来实现一个 tcp 服务。我们构建了一个非阻塞的，能够在等待返回值的同时并发的发送请求。我们使用了&lt;a href="https://github.com/fishcakez/connection" rel="nofollow" target="_blank" title=""&gt;connection&lt;/a&gt;库的回退策略来处理 TCP 错误。最后我们简单看了看&lt;a href="https://github.com/devinus/poolboy" rel="nofollow" target="_blank" title=""&gt;poolboy&lt;/a&gt;库是怎样池化我们多个 GenServer 进程的。&lt;/p&gt;

&lt;p&gt;感谢您的阅读！&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written on June 19, 2015&lt;/em&gt;&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;原文链接：&lt;a href="https://andrealeopardi.com/posts/handling-tcp-connections-in-elixir/" rel="nofollow" target="_blank" title=""&gt;Handling TCP connections in Elixir&lt;/a&gt;&lt;/p&gt;</description>
      <author>tt67wq</author>
      <pubDate>Fri, 20 Mar 2020 08:52:19 +0800</pubDate>
      <link>https://ruby-china.org/topics/39630</link>
      <guid>https://ruby-china.org/topics/39630</guid>
    </item>
    <item>
      <title>350 行实现一个简单酸酸</title>
      <description>&lt;p&gt;&lt;img title=":sunglasses:" alt="😎" src="https://twemoji.ruby-china.com/2/svg/1f60e.svg" class="twemoji"&gt; &lt;img title=":sunglasses:" alt="😎" src="https://twemoji.ruby-china.com/2/svg/1f60e.svg" class="twemoji"&gt; 
最近闲的蛋疼，手搓了一个酸酸，包括 client 和 server，总共加起来才 349 行。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;github.com/AlDanial/cloc v 1.84  T=0.01 s (809.3 files/s, 62642.6 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Elixir                           5             76             85            226
-------------------------------------------------------------------------------
SUM:                             5             76             85            226
-------------------------------------------------------------------------------

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Client&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;github.com/AlDanial/cloc v 1.84  T=0.01 s (702.5 files/s, 31435.8 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Elixir                           4             36             20            123
-------------------------------------------------------------------------------
SUM:                             4             36             20            123
-------------------------------------------------------------------------------

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;目前加密算法现在写死了一个 aes_256_gcm。 &lt;/p&gt;

&lt;p&gt;我做了个性能测试，单个线程的连接的速度大概只能到 v2ray 的 60% ~ 70%，但是我观察了 v2ray 是请求来了再建立连接，而这个酸酸里面用 poolboy 弄了个连接池，所以在浏览网页的时候竟然意外的比 v2 还要丝滑一些，不知道是不是我的错觉&lt;img title=":walking:" alt="🚶" src="https://twemoji.ruby-china.com/2/svg/1f6b6.svg" class="twemoji"&gt; &lt;img title=":dancer:" alt="💃" src="https://twemoji.ruby-china.com/2/svg/1f483.svg" class="twemoji"&gt; &lt;/p&gt;

&lt;p&gt;代码地址： &lt;a href="https://github.com/tt67wq/ex_socks" rel="nofollow" target="_blank" title=""&gt;gayhub&lt;/a&gt;&lt;/p&gt;</description>
      <author>tt67wq</author>
      <pubDate>Tue, 17 Mar 2020 08:43:36 +0800</pubDate>
      <link>https://ruby-china.org/topics/39620</link>
      <guid>https://ruby-china.org/topics/39620</guid>
    </item>
  </channel>
</rss>
