Last active
June 17, 2024 11:45
-
-
Save joshnuss/08603e11615ee0de65724be4d6335475 to your computer and use it in GitHub Desktop.
Fault tolerant UDP Server in Elixir
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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) |
Pasted in iex, and its working that way. It has probably some issue working as exs on my side.
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
@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?