Last minute changes
This commit is contained in:
parent
0d1068226a
commit
ab0ede644a
4 changed files with 222 additions and 54 deletions
26
Dockerfile
Normal file
26
Dockerfile
Normal file
|
@ -0,0 +1,26 @@
|
|||
FROM elixir:1.13.2-slim
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get -y --no-install-recommends install git curl ca-certificates && \
|
||||
apt-get autoremove -y && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists
|
||||
|
||||
# install livebook
|
||||
RUN mix local.hex --force && mix local.rebar --force
|
||||
RUN mix escript.install --force github livebook-dev/livebook
|
||||
|
||||
ENV LIVEBOOK_IP 0.0.0.0
|
||||
ENV LIVEBOOK_HOME /app
|
||||
ENV LIVEBOOK_PORT 5588
|
||||
ENV LIVEBOOK_IFRAME_PORT 5589
|
||||
|
||||
EXPOSE 5588/tcp
|
||||
EXPOSE 5589/tcp
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ADD . /app
|
||||
|
||||
RUN mix do deps.get, compile
|
||||
CMD ["/root/.mix/escripts/livebook", "server", "--default-runtime", "mix", "--no-token"]
|
101
lib/ranch_talk/socket_message_displayer.ex
Normal file
101
lib/ranch_talk/socket_message_displayer.ex
Normal file
|
@ -0,0 +1,101 @@
|
|||
defmodule RanchTalk.SocketMessageDisplayer do
|
||||
use Kino.JS
|
||||
use Kino.JS.Live
|
||||
|
||||
@spec new() :: Kino.JS.Live.t()
|
||||
def new() do
|
||||
Kino.JS.Live.new(__MODULE__, [])
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(messages, ctx) do
|
||||
{:ok, assign(ctx, messages: messages)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:form_message, %{data: data}}, ctx) do
|
||||
data = Map.put(data, :date, DateTime.utc_now())
|
||||
broadcast_event(ctx, "add_message", data)
|
||||
new_messages = [data | ctx.assigns.messages]
|
||||
{:noreply, assign(ctx, messages: new_messages)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:subscribe_to_form, form}, ctx) do
|
||||
subscribe_to_form(form)
|
||||
{:noreply, ctx}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_connect(ctx) do
|
||||
IO.inspect(ctx)
|
||||
# subscribe_to_form(ctx.assigns.form)
|
||||
{:ok, ctx.assigns, ctx}
|
||||
end
|
||||
|
||||
defp subscribe_to_form(form) do
|
||||
Kino.Control.subscribe(form, :form_message)
|
||||
end
|
||||
|
||||
asset "main.js" do
|
||||
"""
|
||||
var cache = [
|
||||
'',
|
||||
' ',
|
||||
' ',
|
||||
' ',
|
||||
' ',
|
||||
' ',
|
||||
' ',
|
||||
' ',
|
||||
' ',
|
||||
' '
|
||||
]
|
||||
|
||||
function leftPad(str, len, ch) {
|
||||
str = str + ''
|
||||
len = len - str.length
|
||||
if (len <= 0) return str
|
||||
if (!ch && ch !== 0) ch = ' '
|
||||
ch = ch + ''
|
||||
if (ch === ' ' && len < 10) return cache[len] + str
|
||||
var pad = ''
|
||||
while (true) {
|
||||
if (len & 1) pad += ch
|
||||
len >>= 1
|
||||
if (len) ch += ch
|
||||
else break
|
||||
}
|
||||
return pad + str
|
||||
}
|
||||
|
||||
function dateStr(date) {
|
||||
console.log("before str", date)
|
||||
const hours = leftPad(date.getHours().toString(), 2, '0')
|
||||
const minutes = leftPad(date.getMinutes().toString(), 2, '0')
|
||||
const seconds = leftPad(date.getSeconds().toString(), 2, '0')
|
||||
return `${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
export function init(ctx, state) {
|
||||
const header = document.createElement("h1")
|
||||
header.textContent = "Received Messages:"
|
||||
ctx.root.appendChild(header)
|
||||
|
||||
const messages = document.createElement("ul")
|
||||
messages.style.fontFamily = 'IosevkaLyte, monospace'
|
||||
messages.style.fontSize = '16px'
|
||||
ctx.root.appendChild(messages)
|
||||
|
||||
ctx.handleEvent("add_message", ({name, message, date}) => {
|
||||
console.log("Raw Date", date)
|
||||
const messageDate = new Date(date)
|
||||
console.log("Message Date", messageDate)
|
||||
const messageEl = document.createElement("li")
|
||||
messageEl.textContent = `${dateStr(messageDate)} ${name}: ${message}`
|
||||
messages.appendChild(messageEl)
|
||||
})
|
||||
}
|
||||
"""
|
||||
end
|
||||
end
|
|
@ -27,7 +27,7 @@ alias RanchTalk.TalkTimer
|
|||
## Ranch Introduction
|
||||
|
||||
> Special thanks to Cody Poll for the excuse to waste a ton of time playing
|
||||
> around with Livebook!
|
||||
> around with Livebook! Source for this talk is available [here](https://git.lyte.dev/lytedev/ranch-talk)
|
||||
|
||||
From https://ninenines.eu/docs/en/ranch/2.1/guide/introduction/:
|
||||
|
||||
|
@ -71,7 +71,7 @@ loop look _very loosely_ like this now:
|
|||
1. Open a socket
|
||||
2. Check if any pending connections exist on the socket
|
||||
3. If there are any pending connections, add them to our list of connections
|
||||
- This is that "acceptor" part that Ranch takes care of for us
|
||||
* This is that "acceptor" part that Ranch takes care of for us
|
||||
4. For each active connection, check if the connection has any messages
|
||||
5. If the connection has any messages, handle them
|
||||
6. For each active connection, check if the connection is still active
|
||||
|
@ -85,6 +85,8 @@ synchronously reaching out to a cache, then querying a database, or calling
|
|||
another service. It would be stuck waiting for all of these operations while
|
||||
new messages pour into your socket!
|
||||
|
||||
Let's look at a visual example... in Factorio!
|
||||
|
||||
### How does Ranch solve this?
|
||||
|
||||
Well, let's imagine _you_ are the poor, single-threaded program taking care of
|
||||
|
@ -92,7 +94,7 @@ all this stuff. You're running around like mad from the OS socket, to the
|
|||
connection list, shuffling messages all over the place, **and** you're
|
||||
responsible for reading every single one, processing it, and responding.
|
||||
|
||||
Obviously, so modern web framework or socket library works this way for obvious
|
||||
Obviously, no modern web framework or socket library works this way for obvious
|
||||
reasons. You (or your machine) would be completely overwhelmed!
|
||||
|
||||
But this is where Ranch (and Erlang/OTP and Elixir) really shine.
|
||||
|
@ -140,35 +142,39 @@ documentation to see how it works so we know better what to look for. Don't
|
|||
worry, I'm not really going to make you read documentation yourself during
|
||||
a talk, so I've summarized the important stuff we'll look at below:
|
||||
|
||||
- https://ninenines.eu/docs/en/ranch/2.1/guide/introduction/
|
||||
- Just the stuff we've already talked about (minus all the boring socket
|
||||
<!-- livebook:{"break_markdown":true} -->
|
||||
|
||||
* https://ninenines.eu/docs/en/ranch/2.1/guide/introduction/
|
||||
* Just the stuff we've already talked about (minus all the boring socket
|
||||
detail stuff)
|
||||
- https://ninenines.eu/docs/en/ranch/2.1/guide/listeners/
|
||||
- We start Ranch by adding the dependency and running
|
||||
* https://ninenines.eu/docs/en/ranch/2.1/guide/listeners/
|
||||
* We start Ranch by adding the dependency and running
|
||||
[`:application.ensure_all_started(:ranch)`](https://ninenines.eu/docs/en/ranch/2.1/manual/)
|
||||
- We can start a listener with
|
||||
* We can start a listener with
|
||||
[`:ranch.start_listener/5`](https://ninenines.eu/docs/en/ranch/2.1/manual/ranch.start_listener/)
|
||||
- https://ninenines.eu/docs/en/ranch/2.1/guide/internals/
|
||||
- Ranch is an OTP `Application` (named `:ranch`)
|
||||
- It has a "top `Supervisor`" which supervises the `:ranch_server` process
|
||||
* https://ninenines.eu/docs/en/ranch/2.1/guide/internals/
|
||||
* Ranch is an OTP `Application` (named `:ranch`)
|
||||
* It has a "top `Supervisor`" which supervises the `:ranch_server` process
|
||||
_and_ any listeners
|
||||
- Ranch uses a "custom `Supervisor`" for managing connections
|
||||
- Listeners are grouped into the `:ranch_listener_sup` `Supervisor`
|
||||
- Listeners consist of three kinds of processes:
|
||||
- The listener `GenServer`
|
||||
- A `Supervisor` that watches the acceptor processes
|
||||
- The second argument to `:ranch/start_listener/5` indicates the number
|
||||
* Ranch uses a "custom `Supervisor`" for managing connections
|
||||
* Listeners are grouped into the `:ranch_listener_sup` `Supervisor`
|
||||
* Listeners consist of three kinds of processes:
|
||||
* The listener `GenServer`
|
||||
* A `Supervisor` that watches the acceptor processes
|
||||
* The second argument to `:ranch/start_listener/5` indicates the number
|
||||
of processes that will be accepting new connections and we should be
|
||||
careful choosing this number
|
||||
- It defaults to `100`
|
||||
- A `Supervisor` that watches the connection processes
|
||||
- Each listener is registered with the `:ranch_server` `GenServer`
|
||||
- All socket operations go through "transport handlers"
|
||||
- These are simple callback modules (`@behaviour`s) for performing
|
||||
* It defaults to `100`
|
||||
* A `Supervisor` that watches the connection processes
|
||||
* Each listener is registered with the `:ranch_server` `GenServer`
|
||||
* All socket operations go through "transport handlers"
|
||||
* These are simple callback modules (`@behaviour`s) for performing
|
||||
operations on sockets
|
||||
- Accepted connections are given to "the protocol handler" (just TCP for our
|
||||
* Accepted connections are given to "the protocol handler" (just TCP for our
|
||||
use case)
|
||||
|
||||
<!-- livebook:{"break_markdown":true} -->
|
||||
|
||||
Sweet! Armed with this knowledge, we should be able to find evidence of these
|
||||
facts in our system _right now_. Let's do it!
|
||||
|
||||
|
@ -198,10 +204,10 @@ Now, if you selected the Livebook node and NOT the empty shell-of-a-node that
|
|||
is the attached Mix project (you should switch to the correct one now!), you
|
||||
will see that `:ranch_server` monitors a couple of `Supervisor`s:
|
||||
|
||||
- `:ranch_conns_sup`
|
||||
- `:ranch_listener_sup`
|
||||
- `:ranch_conns_sup`
|
||||
- `:ranch_listener_sup`
|
||||
* `:ranch_conns_sup`
|
||||
* `:ranch_listener_sup`
|
||||
* `:ranch_conns_sup`
|
||||
* `:ranch_listener_sup`
|
||||
|
||||
Awesome! We can see the connection `Supervisor` and listener `Supervisor` for
|
||||
port `5588` and likewise for port `5589`. The former for serving the page
|
||||
|
@ -216,14 +222,11 @@ processes under `Monitors` hanging out waiting for connections. What gives?
|
|||
Yeah, I dunno. Maybe somebody in the audience knows why they aren't monitored
|
||||
(or at least why they don't show up here).
|
||||
|
||||
But if you go to [the Processes page](/dashboard/processes) and `Ctrl-F ":ranch_acceptor.loop"` you will see exactly 200 results.
|
||||
But if you go to [the Processes page](/dashboard/processes) and search for `:ranch_acceptor.loop`, you will see exactly 200 results. Proof!
|
||||
|
||||
Ok, this is cool and all, and if we had time, we could look at this in the
|
||||
Observer from pretty much any `iex` session like so:
|
||||
Let's see how this plays out visually, yes, again, in Factorio! Then I'll show you how to use Ranch in the code below and we'll see if we can explore what it does.
|
||||
|
||||
```elixir
|
||||
:observer.start()
|
||||
```
|
||||
<!-- livebook:{"break_markdown":true} -->
|
||||
|
||||
But we're all getting impatient to build our own Ranch. It won't have horses on
|
||||
it, but it'll have something even better. TCP sockets!
|
||||
|
@ -231,7 +234,7 @@ it, but it'll have something even better. TCP sockets!
|
|||
## Building Your Own Ranch in 30 Seconds
|
||||
|
||||
My apologies to all the folks that built real ranches over much longer periods
|
||||
of time and with far fewer TCP sockets to show for it.
|
||||
of time and with far fewer TCP sockets to show for it. I hope this isn't too upsetting!
|
||||
|
||||
```elixir
|
||||
Application.ensure_all_started(:ranch)
|
||||
|
@ -241,23 +244,30 @@ Man, being able to take advantage of Open Source contributors' work is really
|
|||
hard work. That was so easy! Now let's start accepting some TCP connections!
|
||||
|
||||
```elixir
|
||||
socket_message_displayer = RanchTalk.SocketMessageDisplayer.new()
|
||||
|
||||
defmodule EchoHandler do
|
||||
def start_link(ref, transport, opts) do
|
||||
pid = spawn_link(__MODULE__, :init, [ref, transport, opts])
|
||||
{:ok, pid}
|
||||
end
|
||||
|
||||
def init(ref, transport, _opts \\ []) do
|
||||
def init(ref, transport, opts) do
|
||||
{:ok, socket} = :ranch.handshake(ref)
|
||||
loop(socket, transport)
|
||||
loop(socket, transport, ref, opts)
|
||||
end
|
||||
|
||||
defp loop(socket, transport) do
|
||||
defp loop(socket, transport, ref, opts) do
|
||||
case transport.recv(socket, 0, 5000) do
|
||||
{:ok, data} ->
|
||||
IO.inspect(data)
|
||||
displayer = Keyword.get(opts, :displayer)
|
||||
transport.send(socket, data)
|
||||
loop(socket, transport)
|
||||
|
||||
if displayer do
|
||||
RanchTalk.SocketMessageDisplayer.add_message(displayer, data)
|
||||
end
|
||||
|
||||
loop(socket, transport, ref, opts)
|
||||
|
||||
_ ->
|
||||
:ok = transport.close(socket)
|
||||
|
@ -265,9 +275,11 @@ defmodule EchoHandler do
|
|||
end
|
||||
end
|
||||
|
||||
:ranch.start_listener(:tcp_echo, :ranch_tcp, %{socket_opts: [port: 5555]}, EchoHandler, [])
|
||||
:ranch.start_listener(:tcp_echo, :ranch_tcp, %{socket_opts: [port: 5555]}, EchoHandler,
|
||||
displayer: socket_message_displayer
|
||||
)
|
||||
|
||||
# Ranch Complete
|
||||
socket_message_displayer
|
||||
```
|
||||
|
||||
Ooh, _now_ if we look in our dashboard (at the non-Livebook node) we can see
|
||||
|
@ -279,17 +291,35 @@ to it and see if it really does echo back to us! You can use `nc` (netcat),
|
|||
`telnet`, or we can use Erlang's `:gen_tcp` like so:
|
||||
|
||||
```elixir
|
||||
{:ok, socket} = :gen_tcp.connect({127, 0, 0, 1}, 5555, [:binary, active: true])
|
||||
```
|
||||
defmodule MyRanchClient do
|
||||
def send_message(message) do
|
||||
{:ok, socket} = :gen_tcp.connect({127, 0, 0, 1}, 5555, [:binary, active: true])
|
||||
|
||||
See how `:gen_tcp` returns `{:ok, #Port<...>}`? A `Port` is a special
|
||||
Erlang/OTP-ism we can learn about another time. For now, should have got us
|
||||
a connection! Let's send something.
|
||||
:gen_tcp.send(socket, [
|
||||
to_string(DateTime.utc_now()),
|
||||
" ",
|
||||
inspect(socket),
|
||||
": ",
|
||||
message
|
||||
])
|
||||
|
||||
**NOTE**: If you don't hurry and send the message, the TCP socket will be closed due to your inactivity. Better act fast!
|
||||
:gen_tcp.close(socket)
|
||||
end
|
||||
end
|
||||
|
||||
```elixir
|
||||
:gen_tcp.send(socket, "Hello, socket! " <> to_string(DateTime.utc_now()))
|
||||
form =
|
||||
Kino.Control.form(
|
||||
[
|
||||
name: Kino.Input.text("Name", default: "Anonymous"),
|
||||
message: Kino.Input.text("Message", default: "")
|
||||
],
|
||||
submit: "Send Message",
|
||||
reset_on_submit: [:message]
|
||||
)
|
||||
|
||||
Kino.JS.Live.cast(socket_message_displayer, {:subscribe_to_form, form})
|
||||
|
||||
form
|
||||
```
|
||||
|
||||
And if we got `:ok`, this `Process` should have a message in its
|
||||
|
|
21
readme.md
21
readme.md
|
@ -11,18 +11,29 @@ Thanks to [Divvy][divvy] for inviting me to give this talk.
|
|||
|
||||
# Usage
|
||||
|
||||
Install and run a local Livebook in `attached` mode and automatically grab my
|
||||
code:
|
||||
|
||||
```bash
|
||||
asdf install
|
||||
mix escript.install github livebook-dev/livebook
|
||||
git clone https://git.lyte.dev/lytedev/ranch-talk.git
|
||||
cd ranch-talk
|
||||
mix do deps.get, compile
|
||||
```
|
||||
|
||||
Install and run a local Livebook in `attached` mode and automatically grab my
|
||||
code:
|
||||
|
||||
```fish
|
||||
env LIVEBOOK_PORT=5588 LIVEBOOK_IFRAME_PORT=5589 \
|
||||
livebook server --default-runtime mix \
|
||||
"$(pwd)/ranch-talk.livemd"
|
||||
LIVEBOOK_HOME=(pwd) LIVEBOOK_IP=0.0.0.0 \
|
||||
livebook server --default-runtime mix --no-token
|
||||
```
|
||||
|
||||
Or if you're gonna share this with everybody, it's probably safer to run it
|
||||
inside of Docker:
|
||||
|
||||
```bash
|
||||
docker build . --tag ranch-talk
|
||||
docker run -it --rm -p 5588:5588 -p 5589:5589 ranch-talk
|
||||
```
|
||||
|
||||
Enjoy!
|
||||
|
|
Loading…
Reference in a new issue