Skip to content

Instantly share code, notes, and snippets.

@joshnuss
Last active October 9, 2023 09:11
Show Gist options
  • Star 35 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save joshnuss/08603e11615ee0de65724be4d6335475 to your computer and use it in GitHub Desktop.
Save joshnuss/08603e11615ee0de65724be4d6335475 to your computer and use it in GitHub Desktop.
Fault tolerant UDP Server in Elixir
# to run:
# > elixir --no-halt udp_server.exs
# to test:
# > echo "hello world" | nc -u -w0 localhost 2052
# > echo "quit" | nc -u -w0 localhost 2052
# Let's call our module "UDPServer"
defmodule UDPServer do
# Our module is going to use the DSL (Domain Specific Language) for Gen(eric) Servers
use GenServer
# We need a factory method to create our server process
# it takes a single parameter `port` which defaults to `2052`
# This runs in the caller's context
def start_link(port \\ 2052) do
GenServer.start_link(__MODULE__, port) # Start 'er up
end
# Initialization that runs in the server context (inside the server process right after it boots)
def init(port) do
# Use erlang's `gen_udp` module to open a socket
# With options:
# - binary: request that data be returned as a `String`
# - active: gen_udp will handle data reception, and send us a message `{:udp, socket, address, port, data}` when new data arrives on the socket
# Returns: {:ok, socket}
:gen_udp.open(port, [:binary, active: true])
end
# define a callback handler for when gen_udp sends us a UDP packet
def handle_info({:udp, _socket, _address, _port, data}, socket) do
# punt the data to a new function that will do pattern matching
handle_packet(data, socket)
end
# pattern match the "quit" message
defp handle_packet("quit\n", socket) do
IO.puts("Received: quit")
# close the socket
:gen_udp.close(socket)
# GenServer will understand this to mean we want to stop the server
# action: :stop
# reason: :normal
# new_state: nil, it doesn't matter since we're shutting down :(
{:stop, :normal, nil}
end
# fallback pattern match to handle all other (non-"quit") messages
defp handle_packet(data, socket) do
# print the message
IO.puts("Received: #{String.trim data}")
# IRL: do something more interesting...
# GenServer will understand this to mean "continue waiting for the next message"
# parameters:
# :noreply - no reply is needed
# new_state: keep the socket as the current state
{:noreply, socket}
end
end
# For extra protection, start a supervisor that will start the UDPServer
# The supervisor's job is to monitor the UDPServer
# If it crashes it will auto restart, fault tolerance in 1 line of code!!!
{:ok, _pid} = Supervisor.start_link([{UDPServer, 2052}], strategy: :one_for_one)
@dmarko484
Copy link

Hi,
thanks for sharing this gist. I really like the clear implementation but strugling running this with suggested elixir --no-halt udp_server.exs . It doesnt show up anything when I sent UDP packet to it. I added :observer.start() on init() and can see that supervisor with worker process exits after cca 5 seconds. I'm running Elixir 1.13.1 ... is there any update how to run this or have yoou observed this issue?

thanks
David

@joshnuss
Copy link
Author

@dmarko484 I just tested it with 1.13.1 and it works for me.
Can you try pasting the gist into iex and see if you have any errors?
Could be something to do with the accessing the port? Maybe another process owns that port or it requires permission?

@dmarko484
Copy link

Pasted in iex, and its working that way. It has probably some issue working as exs on my side.

@Exadra37
Copy link

I created a repo with a working example for this gist: https://github.com/Exadra37/elixir-udp-server

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment