lecture 06
This commit is contained in:
28 files changed
+825
No files matched your search
@@ -0,0 +1,13 @@
|
|||||||
|
defmodule Counter.DynamicSupervisor do
|
||||||
|
@name {:global, __MODULE__}
|
||||||
|
|
||||||
|
def start_link(_) do
|
||||||
|
DynamicSupervisor.start_link(__MODULE__, nil, name: @name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def start_worker(name) do
|
||||||
|
DynamicSupervisor.start_child(@name, %{
|
||||||
|
id: Counter.Worker,
|
||||||
|
start: {Counter.Worker, :start_link, [name]}})
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
defmodule Counter.Worker do
|
||||||
|
# client
|
||||||
|
def start_link(name) do
|
||||||
|
GenServer.start_link(__MODULE__, name, name: via(name))
|
||||||
|
end
|
||||||
|
|
||||||
|
def value(name) do
|
||||||
|
GenServer.call(via(name), :value)
|
||||||
|
end
|
||||||
|
|
||||||
|
def inc(name, amt \\ 1) do
|
||||||
|
GenServer.cast(via(name), {:inc, amt})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp via(name) do
|
||||||
|
{:global, name}
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Used by "mix format"
|
||||||
|
[
|
||||||
|
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||||
|
]
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# The directory Mix will write compiled artifacts to.
|
||||||
|
/_build/
|
||||||
|
|
||||||
|
# If you run "mix test --cover", coverage assets end up here.
|
||||||
|
/cover/
|
||||||
|
|
||||||
|
# The directory Mix downloads your dependencies sources to.
|
||||||
|
/deps/
|
||||||
|
|
||||||
|
# Where third-party dependencies like ExDoc output generated docs.
|
||||||
|
/doc/
|
||||||
|
|
||||||
|
# Temporary files, for example, from tests.
|
||||||
|
/tmp/
|
||||||
|
|
||||||
|
# If the VM crashes, it generates a dump, let's ignore it too.
|
||||||
|
erl_crash.dump
|
||||||
|
|
||||||
|
# Also ignore archive artifacts (built via "mix archive.build").
|
||||||
|
*.ez
|
||||||
|
|
||||||
|
# Ignore package tarball (built via "mix hex.build").
|
||||||
|
dynamic_counters-*.tar
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Counter
|
||||||
|
|
||||||
|
**TODO: Add description**
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
|
||||||
|
by adding `dynamic_counters` to your list of dependencies in `mix.exs`:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
def deps do
|
||||||
|
[
|
||||||
|
{:dynamic_counters, "~> 0.1.0"}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
|
||||||
|
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
|
||||||
|
be found at <https://hexdocs.pm/dynamic_counters>.
|
||||||
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
defmodule Counter do
|
||||||
|
@moduledoc """
|
||||||
|
Documentation for `Counter`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Hello world.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> Counter.hello()
|
||||||
|
:world
|
||||||
|
|
||||||
|
"""
|
||||||
|
def hello do
|
||||||
|
:world
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
defmodule Counter.Application do
|
||||||
|
# See https://hexdocs.pm/elixir/Application.html
|
||||||
|
# for more information on OTP Applications
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
@registry Counter.Registry
|
||||||
|
@table Counter.Table
|
||||||
|
|
||||||
|
use Application
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def start(_type, _args) do
|
||||||
|
children = [
|
||||||
|
{Registry, [name: @registry, keys: :unique]},
|
||||||
|
# Starts a worker by calling: Counter.Worker.start_link(arg)
|
||||||
|
# {DynamicSupervisor, name: Counter.DynamicSupervisor, strategy: :one_for_one}
|
||||||
|
Counter.DynamicSupervisor
|
||||||
|
]
|
||||||
|
|
||||||
|
:ets.new(@table, [:named_table, :public])
|
||||||
|
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||||
|
# for other strategies and supported options
|
||||||
|
opts = [strategy: :one_for_one, name: Counter.Supervisor]
|
||||||
|
Supervisor.start_link(children, opts)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
defmodule Counter.DynamicSupervisor do
|
||||||
|
use DynamicSupervisor
|
||||||
|
@name {:global, __MODULE__}
|
||||||
|
|
||||||
|
def start_link(_) do
|
||||||
|
DynamicSupervisor.start_link(__MODULE__, nil, name: @name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def start_worker(name) do
|
||||||
|
DynamicSupervisor.start_child(@name, {Counter.Worker, name})
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init(_) do
|
||||||
|
DynamicSupervisor.init(strategy: :one_for_one)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
defmodule Counter.Worker do
|
||||||
|
use GenServer
|
||||||
|
@table Counter.Table
|
||||||
|
|
||||||
|
# client
|
||||||
|
def start_link(name) do
|
||||||
|
GenServer.start_link(__MODULE__, name, name: via(name))
|
||||||
|
end
|
||||||
|
|
||||||
|
def value(name) do
|
||||||
|
GenServer.call(via(name), :value)
|
||||||
|
end
|
||||||
|
|
||||||
|
def inc(name, amt \\ 1) do
|
||||||
|
GenServer.cast(via(name), {:inc, amt})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp via(name) do
|
||||||
|
{:global, name}
|
||||||
|
end
|
||||||
|
|
||||||
|
# server
|
||||||
|
@impl true
|
||||||
|
def init(arg) do
|
||||||
|
name = {__MODULE__, arg}
|
||||||
|
value = case :ets.lookup(@table, name) do
|
||||||
|
[] -> 0
|
||||||
|
[{_, v}] -> v
|
||||||
|
end
|
||||||
|
{:ok, {name, value}}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call(:value, _from, {_, value} = state) do
|
||||||
|
{:reply, value, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_cast({:inc, amt}, {name, value}) do
|
||||||
|
{:noreply, {name, value + amt}}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def terminate(_reason, state) do
|
||||||
|
:ets.insert(@table, state)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
defmodule Counter.MixProject do
|
||||||
|
use Mix.Project
|
||||||
|
|
||||||
|
def project do
|
||||||
|
[
|
||||||
|
app: :dynamic_counters,
|
||||||
|
version: "0.1.0",
|
||||||
|
elixir: "~> 1.20",
|
||||||
|
start_permanent: Mix.env() == :prod,
|
||||||
|
deps: deps()
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run "mix help compile.app" to learn about applications.
|
||||||
|
def application do
|
||||||
|
[
|
||||||
|
extra_applications: [:logger],
|
||||||
|
mod: {Counter.Application, []}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run "mix help deps" to learn about dependencies.
|
||||||
|
defp deps do
|
||||||
|
[
|
||||||
|
# {:dep_from_hexpm, "~> 0.3.0"},
|
||||||
|
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
defmodule CounterTest do
|
||||||
|
use ExUnit.Case
|
||||||
|
doctest Counter
|
||||||
|
|
||||||
|
test "greets the world" do
|
||||||
|
assert Counter.hello() == :world
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ExUnit.start()
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Used by "mix format"
|
||||||
|
[
|
||||||
|
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||||
|
]
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# The directory Mix will write compiled artifacts to.
|
||||||
|
/_build/
|
||||||
|
|
||||||
|
# If you run "mix test --cover", coverage assets end up here.
|
||||||
|
/cover/
|
||||||
|
|
||||||
|
# The directory Mix downloads your dependencies sources to.
|
||||||
|
/deps/
|
||||||
|
|
||||||
|
# Where third-party dependencies like ExDoc output generated docs.
|
||||||
|
/doc/
|
||||||
|
|
||||||
|
# Temporary files, for example, from tests.
|
||||||
|
/tmp/
|
||||||
|
|
||||||
|
# If the VM crashes, it generates a dump, let's ignore it too.
|
||||||
|
erl_crash.dump
|
||||||
|
|
||||||
|
# Also ignore archive artifacts (built via "mix archive.build").
|
||||||
|
*.ez
|
||||||
|
|
||||||
|
# Ignore package tarball (built via "mix hex.build").
|
||||||
|
counter-*.tar
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
defmodule Counter.Worker do
|
||||||
|
@name {:global, __MODULE__}
|
||||||
|
|
||||||
|
# client
|
||||||
|
def start_link(n \\ 0) do
|
||||||
|
GenServer.start_link(__MODULE__, n, name: @name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def inc(amt \\ 1) do
|
||||||
|
GenServer.cast(@name, {:inc, amt})
|
||||||
|
end
|
||||||
|
|
||||||
|
def value() do
|
||||||
|
GenServer.call(@name, :value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Counter
|
||||||
|
|
||||||
|
**TODO: Add description**
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
|
||||||
|
by adding `counter` to your list of dependencies in `mix.exs`:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
def deps do
|
||||||
|
[
|
||||||
|
{:counter, "~> 0.1.0"}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
|
||||||
|
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
|
||||||
|
be found at <https://hexdocs.pm/counter>.
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ƒa
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
defmodule Counter do
|
||||||
|
@moduledoc """
|
||||||
|
Documentation for `Counter`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Hello world.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> Counter.hello()
|
||||||
|
:world
|
||||||
|
|
||||||
|
"""
|
||||||
|
def hello do
|
||||||
|
:world
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
defmodule Counter.Application do
|
||||||
|
# See https://hexdocs.pm/elixir/Application.html
|
||||||
|
# for more information on OTP Applications
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
use Application
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def start(_type, _args) do
|
||||||
|
children = [
|
||||||
|
# Starts a worker by calling: Counter.Worker.start_link(arg)
|
||||||
|
{Counter.Store, "counter.db"},
|
||||||
|
{Counter.Worker, 0}
|
||||||
|
]
|
||||||
|
|
||||||
|
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||||
|
# for other strategies and supported options
|
||||||
|
opts = [strategy: :one_for_one, name: Counter.Supervisor]
|
||||||
|
Supervisor.start_link(children, opts)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
defmodule Counter.Store do
|
||||||
|
use GenServer
|
||||||
|
|
||||||
|
# client
|
||||||
|
def start_link(file) do
|
||||||
|
GenServer.start_link(__MODULE__, file, name: __MODULE__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def put(value) do
|
||||||
|
GenServer.cast(__MODULE__, {:put, value})
|
||||||
|
end
|
||||||
|
|
||||||
|
def get() do
|
||||||
|
GenServer.call(__MODULE__, :get)
|
||||||
|
end
|
||||||
|
|
||||||
|
# server
|
||||||
|
@impl true
|
||||||
|
def init(file) do
|
||||||
|
{:ok, file}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call(:get, _from, file) do
|
||||||
|
reply =
|
||||||
|
case File.read(file) do
|
||||||
|
{:ok, content} -> :erlang.binary_to_term(content)
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
{:reply, reply, file}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_cast({:put, value}, file) do
|
||||||
|
File.write(file, :erlang.term_to_binary(value))
|
||||||
|
{:noreply, file}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
defmodule Counter.Worker do
|
||||||
|
use GenServer
|
||||||
|
|
||||||
|
@store Counter.Store
|
||||||
|
@name {:global, __MODULE__}
|
||||||
|
|
||||||
|
# client
|
||||||
|
def start_link(n \\ 0) do
|
||||||
|
GenServer.start_link(__MODULE__, n, name: @name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def inc(amt \\ 1) do
|
||||||
|
GenServer.cast(@name, {:inc, amt})
|
||||||
|
end
|
||||||
|
|
||||||
|
def value() do
|
||||||
|
GenServer.call(@name, :value)
|
||||||
|
end
|
||||||
|
|
||||||
|
# server
|
||||||
|
@impl true
|
||||||
|
def init(arg) do
|
||||||
|
{:ok, @store.get() || arg}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call(:value, _from, state) do
|
||||||
|
{:reply, state, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_cast({:inc, amt}, state) do
|
||||||
|
{:noreply, state + amt}
|
||||||
|
end
|
||||||
|
|
||||||
|
# for demonstration only
|
||||||
|
@impl true
|
||||||
|
def handle_info(:reset, _state) do
|
||||||
|
{:noreply, 0}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def terminate(_reason, state) do
|
||||||
|
@store.put(state)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
defmodule Counter.MixProject do
|
||||||
|
use Mix.Project
|
||||||
|
|
||||||
|
def project do
|
||||||
|
[
|
||||||
|
app: :counter,
|
||||||
|
version: "0.1.0",
|
||||||
|
elixir: "~> 1.20",
|
||||||
|
start_permanent: Mix.env() == :prod,
|
||||||
|
deps: deps()
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run "mix help compile.app" to learn about applications.
|
||||||
|
def application do
|
||||||
|
[
|
||||||
|
extra_applications: [:logger],
|
||||||
|
mod: {Counter.Application, []}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run "mix help deps" to learn about dependencies.
|
||||||
|
defp deps do
|
||||||
|
[
|
||||||
|
# {:dep_from_hexpm, "~> 0.3.0"},
|
||||||
|
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
defmodule CounterTest do
|
||||||
|
use ExUnit.Case
|
||||||
|
doctest Counter
|
||||||
|
|
||||||
|
test "greets the world" do
|
||||||
|
assert Counter.hello() == :world
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ExUnit.start()
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
defmodule Echo.Active do
|
||||||
|
def start(port \\ 6666) do
|
||||||
|
opts = [:binary, active: true, packet: :line, reuseaddr: true]
|
||||||
|
{:ok, listen} = :gen_tcp.listen(port, opts)
|
||||||
|
accept(listen)
|
||||||
|
end
|
||||||
|
|
||||||
|
def accept(listen) do
|
||||||
|
{:ok, socket} = :gen_tcp.accept(listen) # data delivered as messages in the mailbox of the
|
||||||
|
# process that calls accept
|
||||||
|
spawn(fn -> accept(listen) end)
|
||||||
|
loop(socket)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp loop(socket) do
|
||||||
|
receive do
|
||||||
|
{:tcp, ^socket, data} ->
|
||||||
|
:gen_tcp.send(socket, data)
|
||||||
|
loop(socket)
|
||||||
|
{:tcp_closed, ^socket} ->
|
||||||
|
:gen_tcp.close(socket)
|
||||||
|
{:tcp_error, ^socket, _reason} ->
|
||||||
|
:gen_tcp.close(socket)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule Echo.Passive do
|
||||||
|
def start(port \\ 6666) do
|
||||||
|
opts = [:binary, active: false, packet: :line, reuseaddr: true] # note: active: false
|
||||||
|
{:ok, listen} = :gen_tcp.listen(port, opts)
|
||||||
|
accept(listen)
|
||||||
|
end
|
||||||
|
|
||||||
|
def accept(listen) do
|
||||||
|
{:ok, socket} = :gen_tcp.accept(listen) # data delivered as messages in the mailbox of the
|
||||||
|
# process that calls accept
|
||||||
|
spawn(fn -> accept(listen) end)
|
||||||
|
loop(socket)
|
||||||
|
end
|
||||||
|
|
||||||
|
def loop(socket) do
|
||||||
|
case :gen_tcp.recv(socket, 0) do
|
||||||
|
{:ok, data} ->
|
||||||
|
:gen_tcp.send(socket, data)
|
||||||
|
loop(socket)
|
||||||
|
{:error, _reason} ->
|
||||||
|
:gen_tcp.close(socket)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule Echo.Hybrid do
|
||||||
|
def start(port \\ 6666) do
|
||||||
|
opts = [:binary, active: :once, packet: :line, reuseaddr: true]
|
||||||
|
{:ok, listen} = :gen_tcp.listen(port, opts)
|
||||||
|
accept(listen)
|
||||||
|
end
|
||||||
|
|
||||||
|
def accept(listen) do
|
||||||
|
{:ok, socket} = :gen_tcp.accept(listen) # data delivered as messages in the mailbox of the
|
||||||
|
# process that calls accept (the controlling process)
|
||||||
|
spawn(fn -> accept(listen) end)
|
||||||
|
loop(socket)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp loop(socket) do
|
||||||
|
receive do
|
||||||
|
{:tcp, ^socket, data} ->
|
||||||
|
:inet.setopts(socket, active: :once)
|
||||||
|
:gen_tcp.send(socket, data)
|
||||||
|
loop(socket)
|
||||||
|
{:tcp_closed, ^socket} ->
|
||||||
|
:gen_tcp.close(socket)
|
||||||
|
{:tcp_error, ^socket, _reason} ->
|
||||||
|
:gen_tcp.close(socket)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule Echo do
|
||||||
|
use GenServer
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
def start(port \\ 6666) do
|
||||||
|
GenServer.start(__MODULE__, port)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init(port) do
|
||||||
|
opts = [:binary, active: true, packet: :line, reuseaddr: true]
|
||||||
|
{:ok, listen} = :gen_tcp.listen(port, opts)
|
||||||
|
send(self(), :accept)
|
||||||
|
{:ok, listen}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info(:accept, listen) do
|
||||||
|
{:ok, socket} = :gen_tcp.accept(listen)
|
||||||
|
log(socket, "accepted connection")
|
||||||
|
{:ok, pid} = Echo.Worker.start(socket)
|
||||||
|
:gen_tcp.controlling_process(socket, pid)
|
||||||
|
send(self(), :accept)
|
||||||
|
{:noreply, listen}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp log(socket, msg) do
|
||||||
|
Logger.info("#{inspect self()}: #{msg} #{inspect socket}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule Echo.Worker do
|
||||||
|
use GenServer
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
def start(socket) do
|
||||||
|
GenServer.start(__MODULE__, socket)
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init(socket) do
|
||||||
|
log(socket, "started worker")
|
||||||
|
{:ok, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:tcp, socket, data}, socket) do
|
||||||
|
:inet.setopts(socket, active: :once)
|
||||||
|
:gen_tcp.send(socket, data)
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:tcp_closed, socket}, socket) do
|
||||||
|
:gen_tcp.close(socket)
|
||||||
|
log(socket, "closed connection")
|
||||||
|
{:stop, :normal, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp log(socket, msg) do
|
||||||
|
Logger.info("#{inspect self()}: #{msg} #{inspect socket}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
defmodule Echo.Client do
|
||||||
|
def connect(host, port \\ 6666) do
|
||||||
|
opts = [active: false, packet: :line]
|
||||||
|
{:ok, socket} = :gen_tcp.connect(host, port, opts)
|
||||||
|
loop(socket)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp loop(socket) do
|
||||||
|
case IO.gets("> ") do
|
||||||
|
:eof ->
|
||||||
|
:gen_tcp.close(socket)
|
||||||
|
line ->
|
||||||
|
:gen_tcp.send(socket, line)
|
||||||
|
case :gen_tcp.recv(socket, 0) do
|
||||||
|
{:ok, data} ->
|
||||||
|
IO.write(data)
|
||||||
|
loop(socket)
|
||||||
|
{:error, _} ->
|
||||||
|
:gen_tcp.close(socket)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
with [host, port | _] <- System.argv(),
|
||||||
|
{port, _} <- Integer.parse(port)
|
||||||
|
do
|
||||||
|
Echo.Client.connect(String.to_atom(host), port)
|
||||||
|
else
|
||||||
|
_ -> Echo.Client.connect(:"127.0.0.1", 6666)
|
||||||
|
end
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import java.io.*;
|
||||||
|
import java.net.*;
|
||||||
|
|
||||||
|
class EchoClient {
|
||||||
|
public static void main(String... args) throws IOException {
|
||||||
|
var host = "127.0.0.1";
|
||||||
|
var port = 6666;
|
||||||
|
try (
|
||||||
|
var s = new Socket(host, port);
|
||||||
|
var in =
|
||||||
|
new BufferedReader(new InputStreamReader(s.getInputStream()));
|
||||||
|
var out =
|
||||||
|
new PrintWriter(new OutputStreamWriter(s.getOutputStream()), true);
|
||||||
|
var stdin =
|
||||||
|
new BufferedReader(new InputStreamReader(System.in));
|
||||||
|
) {
|
||||||
|
String line, reply;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
System.out.print("> ");
|
||||||
|
if ((line = stdin.readLine()) == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
out.println(line);
|
||||||
|
if ((reply = in.readLine()) == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
System.out.println(reply);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
# Lecture 06 notes: raw sockets, then splitting a client/server across nodes
|
||||||
|
# This file is not meant to be compiled/run as a whole - paste lines into
|
||||||
|
# iex (or a shell, where noted) one at a time.
|
||||||
|
|
||||||
|
|
||||||
|
### ---------------------------------------------------------------------- ###
|
||||||
|
### Part 1: echo/ - plain TCP, no distributed Elixir involved yet
|
||||||
|
### ---------------------------------------------------------------------- ###
|
||||||
|
|
||||||
|
# load the module definitions into an iex session (run from lectures/06/echo/)
|
||||||
|
# iex echo.ex
|
||||||
|
|
||||||
|
# Echo.Active / Echo.Passive / Echo.Hybrid all block the calling process
|
||||||
|
# forever inside accept/loop, so start them in a spawned process if you
|
||||||
|
# want your iex shell back:
|
||||||
|
spawn(fn -> Echo.Active.start(6666) end)
|
||||||
|
spawn(fn -> Echo.Passive.start(6667) end)
|
||||||
|
spawn(fn -> Echo.Hybrid.start(6668) end)
|
||||||
|
|
||||||
|
# the GenServer version returns immediately because init/1 just schedules
|
||||||
|
# :accept via send(self(), :accept) and returns - no spawn needed
|
||||||
|
Echo.start(6666)
|
||||||
|
# -> {:ok, #PID<...>}
|
||||||
|
|
||||||
|
# quick manual client, no Elixir needed - good for sanity-checking a server
|
||||||
|
# nc localhost 6666
|
||||||
|
# (type a line, press enter, see it echoed back; Ctrl-D or Ctrl-C to quit)
|
||||||
|
|
||||||
|
# or use the provided Elixir client (run from a second terminal):
|
||||||
|
# elixir echo_client.ex 127.0.0.1 6666
|
||||||
|
# (defaults to 127.0.0.1:6666 if no args given)
|
||||||
|
|
||||||
|
# or the Java client (compiles/runs independently - proves the server
|
||||||
|
# doesn't care what language/process connects, it's just bytes on a socket)
|
||||||
|
# cd ../java && javac EchoClient.java && java EchoClient
|
||||||
|
|
||||||
|
|
||||||
|
### ---------------------------------------------------------------------- ###
|
||||||
|
### Part 2: distributed Elixir basics - the mechanism the counter/ examples
|
||||||
|
### rely on instead of hand-rolled sockets
|
||||||
|
### ---------------------------------------------------------------------- ###
|
||||||
|
|
||||||
|
# Start two connected BEAM nodes (same machine is fine for testing).
|
||||||
|
# --sname gives each node a short name; both MUST share a cookie or they
|
||||||
|
# refuse to connect to each other.
|
||||||
|
|
||||||
|
# terminal 1 (this will become the "server" node):
|
||||||
|
# iex --sname server --cookie lecture06
|
||||||
|
|
||||||
|
# terminal 2 (this will become the "client" node):
|
||||||
|
# iex --sname client --cookie lecture06
|
||||||
|
|
||||||
|
# from either node, connect them:
|
||||||
|
Node.connect(:"server@your-hostname")
|
||||||
|
# -> true
|
||||||
|
# find the exact name to use with: Node.self() (run that on the server node)
|
||||||
|
|
||||||
|
Node.list() # -> [:"client@your-hostname"] (from the server's side)
|
||||||
|
Node.self() # -> the current node's own full name
|
||||||
|
|
||||||
|
# :global is what replaces Registry here - Registry is local-to-a-node only,
|
||||||
|
# :global registers names cluster-wide once nodes are connected
|
||||||
|
:global.registered_names() # -> [] until something registers
|
||||||
|
:global.whereis_name(Counter.Worker) # -> PID or :undefined
|
||||||
|
|
||||||
|
|
||||||
|
### ---------------------------------------------------------------------- ###
|
||||||
|
### Part 3: counter/static - one globally-registered counter
|
||||||
|
### ---------------------------------------------------------------------- ###
|
||||||
|
|
||||||
|
# -- server node --
|
||||||
|
# cd counter/static/server
|
||||||
|
# mix deps.get # first time only
|
||||||
|
# iex --sname server --cookie lecture06 -S mix
|
||||||
|
# this boots Counter.Application, which starts Counter.Store (file-backed,
|
||||||
|
# "counter.db") and Counter.Worker (registered as {:global, Counter.Worker})
|
||||||
|
|
||||||
|
Counter.Worker.value() # works locally on the server node too
|
||||||
|
Counter.Worker.inc(5)
|
||||||
|
|
||||||
|
# -- client node --
|
||||||
|
# cd counter/static/client (NOT a mix project - just loose .ex files)
|
||||||
|
# iex --sname client --cookie lecture06
|
||||||
|
Node.connect(:"server@your-hostname")
|
||||||
|
c "worker.ex" # compiles/loads Counter.Worker (the thin stub)
|
||||||
|
|
||||||
|
# these now run against the REAL process living on the server node -
|
||||||
|
# no socket code written by us, distributed Erlang routes the GenServer
|
||||||
|
# call/cast over the network
|
||||||
|
Counter.Worker.inc()
|
||||||
|
Counter.Worker.value()
|
||||||
|
# -> whatever the server node's Counter.Worker currently holds
|
||||||
|
|
||||||
|
# don't call Counter.Worker.start_link/1 from the client - the server's
|
||||||
|
# Application already started (and globally registered) the one that
|
||||||
|
# matters; starting another just races/duplicates the name
|
||||||
|
|
||||||
|
|
||||||
|
### ---------------------------------------------------------------------- ###
|
||||||
|
### Part 4: counter/dynamic - many named counters, started on demand,
|
||||||
|
### reachable from a remote node
|
||||||
|
### ---------------------------------------------------------------------- ###
|
||||||
|
|
||||||
|
# -- server node --
|
||||||
|
# cd counter/dynamic/server
|
||||||
|
# mix deps.get
|
||||||
|
# iex --sname server --cookie lecture06 -S mix
|
||||||
|
# boots: Registry (local, unused for cross-node lookup), an ETS table for
|
||||||
|
# crash-durable state, and Counter.DynamicSupervisor registered as
|
||||||
|
# {:global, Counter.DynamicSupervisor}
|
||||||
|
|
||||||
|
# -- client node --
|
||||||
|
# cd counter/dynamic/client
|
||||||
|
# iex --sname client --cookie lecture06
|
||||||
|
Node.connect(:"server@your-hostname")
|
||||||
|
c "dynamic_supervisor.ex"
|
||||||
|
c "worker.ex"
|
||||||
|
|
||||||
|
# ask the REMOTE supervisor (on the server node) to spawn a new worker -
|
||||||
|
# the child process is started under the server's supervision tree, not
|
||||||
|
# the client's
|
||||||
|
Counter.DynamicSupervisor.start_worker("c1")
|
||||||
|
# -> {:ok, #PID<...>} (a server-node PID)
|
||||||
|
|
||||||
|
Counter.Worker.inc("c1")
|
||||||
|
Counter.Worker.inc("c1", 5)
|
||||||
|
Counter.Worker.value("c1")
|
||||||
|
# -> 6
|
||||||
|
|
||||||
|
# start as many independent counters as you like, all living on the
|
||||||
|
# server node, all addressable from the client by name:
|
||||||
|
Enum.each(1..5, fn n -> Counter.DynamicSupervisor.start_worker("c#{n}") end)
|
||||||
|
Enum.each(1..5, fn n -> Counter.Worker.inc("c#{n}", n) end)
|
||||||
|
Enum.map(1..5, fn n -> Counter.Worker.value("c#{n}") end)
|
||||||
|
# -> [1, 2, 3, 4, 5]
|
||||||
|
|
||||||
|
# kill a worker on the server node and watch it come back with its value
|
||||||
|
# intact (DynamicSupervisor restarts it, ETS on the server supplies the
|
||||||
|
# last known value in init/1):
|
||||||
|
# on the SERVER node:
|
||||||
|
Process.exit(:global.whereis_name("c1"), :kill)
|
||||||
|
# back on the CLIENT node:
|
||||||
|
Counter.Worker.value("c1")
|
||||||
|
# -> still 1 (reloaded from the server's ETS table in init/1, same pattern
|
||||||
|
# as lecture 05's local DynamicSupervisor/Registry/ETS combo - just now the
|
||||||
|
# "local" side is a different machine)
|
||||||
|
|
||||||
|
|
||||||
|
### ---------------------------------------------------------------------- ###
|
||||||
|
### Misc / troubleshooting
|
||||||
|
### ---------------------------------------------------------------------- ###
|
||||||
|
|
||||||
|
# cookie mismatch or wrong hostname are the two most common reasons
|
||||||
|
# Node.connect/1 silently returns false - check `iex --sname` output for
|
||||||
|
# the exact full node name, and make sure both nodes used the same --cookie
|
||||||
|
|
||||||
|
# run a mix app's tests / compile without a full iex session:
|
||||||
|
# mix test
|
||||||
|
# mix compile
|
||||||
|
# mix run --no-halt # boots the Application and just hangs there running
|
||||||
Reference in new issue
Block a user