There’s nothing mysterious about a node. It is simply a running Erlang VM. Throughout this book we’ve been running our code on a node.
The Erlang VM, called Beam, is like its own little operating system running on top of your host operating system. It handles its own events, process scheduling, memory, naming services, and interprocess communication.
A node can connect to other nodes—in the same computer, across a LAN, or across the Internet—and provide many of the same services across these connections that it provides to the processes it hosts locally.
If we ask Elixir what the current node is called,
it’ll give us a made-up name:
iex> Node.self
:nonode@nohost
# we can set the name
$ iex --name wibble@light-boy.local
iex(wibble@light-boy.local)> Node.self
:"wibble@light-boy.local"
# the name returned is an atom
$ iex --sname wobble
iex(wobble@light-boy)> Node.self
:"wobble@light-boy"
Notice the name is an atom.
Let's get two nodes running:
# Window 1
$ iex --sname node_one
iex(node_one@light-boy)>
# Window 2
$ iex --sname node_two
iex(node_two@light-boy)> Node.list
[]
# notice the atom node name
iex(node_two@light-boy)> Node.connect :"node_one@light-boy"
true
iex(node_two@light-boy)> Node.list
[:"node_one@light-boy"]
We now have two nodes connected
using the connect command.
iex(node_one@light-boy)> func = fn -> IO.inspect Node.self end
#Function<erl_eval.20.82930912>
# run using spawn
iex(node_one@light-boy)> spawn(func)
#PID<0.59.0>
node_one@light-boy
# specify node to spawn
iex(node_one@light-boy)> Node.spawn(:"node_one@light-boy", func)
#PID<0.57.0>
node_one@light-boy
# notice the 0 in the first field of the pid, that means local
iex(node_one@light-boy)> Node.spawn(:"node_two@light-boy", func)
#PID<7393.48.0>
node_two@light-boy
# notice the output from the other node appears here
# even though it was run on the other node
# you can specify cookie from the command line
$ iex --sname one --cookie chocolate-chip
iex(one@light-boy)> Node.get_cookie
:"chocolate-chip”
Can you run arbitrary code on any connected node?
No. Every remote node uses a cookie to determine if the code should be executed.
When Erlang starts, it looks for an .erlang.cookie file in your home directory. If that file doesn’t exist, Erlang creates it and stores a random string in it.
Be careful when connecting nodes over a public network—the cookie is transmitted in plain text.
Although a PID is displayed as three numbers, it contains just two fields; the first number is the node ID and the next two numbers are the low and high bits of the process ID. Local processes will have a node ID of zero.
But how can the callback find the generator in the first place? One way is for the generator to register its PID, giving it a name. The callback on the other node can look up the generator by name, using the PID that comes back to send messages to it.
#PID<0.59.0>
#PID<7393.48.0>
defmodule Ticker do
@interval 2000 # 2 seconds
@name :ticker
def start do
pid = spawn(__MODULE__, :generator, [[]])
:global.register_name(@name, pid)
end
def register(client_pid) do
send :global.whereis_name(@name), { :register, client_pid }
end
def generator(clients) do
receive do
{ :register, pid } ->
IO.puts "registering #{inspect pid}"
generator([pid | clients])
after
@interval ->
IO.puts "tick"
Enum.each clients, fn client ->
send client, { :tick }
end
generator(clients)
end
end
end
We define a start function that spawns the server process. It then uses :global.register_name to register the PID of this server under the name :ticker.
Clients who want to register to receive ticks call the register function.
This function sends a message to the Ticker server, asking it to add those clients to its list.
generator is the spawned process and it waits for two events. When it gets a tuple containing :register and a PID, it adds the PID to the list of clients and recurses.
It may time out after 2 seconds, in which case it sends a {:tick} message to all registered clients.
It spawns a receiver to handle the incoming ticks, and passes the receiver’s PID to the server as an argument to the register function.
defmodule Client do
def start do
pid = spawn(__MODULE__, :receiver, [])
Ticker.register(pid)
end
def receiver do
receive do
{ :tick } ->
IO.puts "tock in client"
receiver
end
end
end
# Window 1
nodes % iex --sname one
iex(one@light-boy)> c("ticker.ex")
[Client,Ticker]
iex(one@light-boy)>
Node.connect :"two@light-boy"
true
iex(one@light-boy)> Ticker.start
:yes
tick
tick
iex(one@light-boy)> Client.start
registering #PID<0.59.0>
{:register,#PID<0.59.0>}
tick
tock in client
tick
tock in client
tick
tock in client
tick
tock in client
: : :
# Window 2
nodes % iex --sname two
iex(two@light-boy)> c("ticker.ex")
[Client,Ticker]
iex(two@light-boy)> Client.start
{:register,#PID<0.53.0>}
tock in client
tock in client
tock in client
: : :
”
defmodule Ticker do
@interval 2000 # 2 seconds
@name :ticker
def start do
pid = spawn(__MODULE__, :generator, [[]])
:global.register_name(@name, pid)
end
def register(client_pid) do
send :global.whereis_name(@name), { :register, client_pid }
end
def generator(clients) do
receive do
{ :register, pid } ->
IO.puts "registering #{inspect pid}"
generator([pid | clients])
after
@interval ->
IO.puts "tick"
Enum.each clients, fn client ->
send client, { :tick }
end
generator(clients)
end
end
end
# elixir IO.puts function
def puts(device \\ group_leader(), item) do
erl_dev = map_dev(device)
:io.put_chars erl_dev, [to_iodata(item), ?\n]
end
Input and output in the Erlang VM are performed using I/O servers. These are simply Erlang processes that implement a low-level message interface.
In Elixir you identify an open file or device by the PID of its I/O server. And these PIDs behave just like all other.
The default device it uses is returned by the function :erlang.group_leader.
This will be the PID of an I/O server.
# Window 1
$ iex --sname one
iex(one@light-boy) >
Lets connect to node one from node two, and register the PID returned by group_leader under a global name (we use :two).
The default device it uses is returned by the function :erlang.group_leader.
This will be the PID of an I/O server.
# Window 2
$ iex --sname two
iex(two@light-boy) >
Node.connect(:"one@light-boy")
true
iex(two@light-boy) >
:global.register_name(:two, :erlang.group_leader)
:yes
# Window 1
iex(one@light-boy) > two = :global.whereis_name :two
#PID<7419.30.0>
iex(one@light-boy) > IO.puts(two, "Hello")
:ok
iex(one@light-boy) > IO.puts(two, "World!")
:ok
Now that we’ve registered the PID, we can access it from the other node. And once we’ve done that, we can pass it to IO.puts; the output appears in the other terminal window.
# Window 2
Hello
World
iex(two@light-boy) >
We’ve seen how we can create and interlink a number of Erlang virtual machines, potentially communicating across a network.