Event Handling¶
estun provides an event system to monitor binding lifecycle and state changes.
Event Types¶
| Event | Description |
|---|---|
{binding_created, Addr} |
New binding established |
{binding_refreshed, Addr} |
Binding successfully refreshed |
{binding_expiring, RemainingMs} |
Binding about to expire |
{binding_expired} |
Binding has expired |
{binding_changed, OldAddr, NewAddr} |
Mapped address changed |
{error, Reason} |
Error occurred |
Setting Event Handlers¶
Option 1: Process Messages¶
Receive events as messages to a process:
%% Set handler to receive messages
{ok, SocketRef} = estun:open_socket(),
ok = estun:set_event_handler(SocketRef, self()),
{ok, _Addr} = estun:bind_socket(SocketRef, default),
%% Handle events
receive
{estun_event, SocketRef, {binding_created, Addr}} ->
io:format("Binding created: ~p~n", [Addr]);
{estun_event, SocketRef, {binding_refreshed, Addr}} ->
io:format("Binding refreshed: ~p~n", [Addr]);
{estun_event, SocketRef, {binding_expiring, Remaining}} ->
io:format("Binding expiring in ~p ms~n", [Remaining]);
{estun_event, SocketRef, {binding_expired}} ->
io:format("Binding expired!~n");
{estun_event, SocketRef, {binding_changed, Old, New}} ->
io:format("Address changed: ~p -> ~p~n", [Old, New]);
{estun_event, SocketRef, {error, Reason}} ->
io:format("Error: ~p~n", [Reason])
end.
Option 2: Callback Function¶
Use a function to handle events:
Handler = fun(Event) ->
io:format("STUN Event: ~p~n", [Event])
end,
ok = estun:set_event_handler(SocketRef, Handler).
Option 3: Module Callback¶
Use a module:function callback:
ok = estun:set_event_handler(SocketRef, {my_handler, on_event}).
%% In my_handler.erl
-module(my_handler).
-export([on_event/1]).
on_event({binding_created, Addr}) ->
logger:info("Binding created: ~p", [Addr]);
on_event({binding_refreshed, Addr}) ->
logger:debug("Binding refreshed: ~p", [Addr]);
on_event({binding_expiring, Remaining}) ->
logger:warning("Binding expiring in ~p ms", [Remaining]);
on_event({binding_expired}) ->
logger:error("Binding expired!");
on_event({binding_changed, Old, New}) ->
logger:warning("Address changed: ~p -> ~p", [Old, New]);
on_event({error, Reason}) ->
logger:error("STUN error: ~p", [Reason]).
Practical Examples¶
Monitoring Connection Health¶
-module(connection_monitor).
-export([start/1]).
-include_lib("estun/include/estun.hrl").
start(SocketRef) ->
%% Set ourselves as handler
ok = estun:set_event_handler(SocketRef, self()),
%% Start monitoring loop
monitor_loop(SocketRef, #{
healthy => true,
last_refresh => erlang:monotonic_time(millisecond),
address => undefined
}).
monitor_loop(SocketRef, State) ->
receive
{estun_event, SocketRef, Event} ->
NewState = handle_event(Event, State),
monitor_loop(SocketRef, NewState);
{get_status, From} ->
From ! {status, State},
monitor_loop(SocketRef, State);
stop ->
ok
end.
handle_event({binding_created, Addr}, State) ->
io:format("[MONITOR] Connection established~n"),
State#{
healthy := true,
address := Addr,
last_refresh := erlang:monotonic_time(millisecond)
};
handle_event({binding_refreshed, _Addr}, State) ->
State#{
healthy := true,
last_refresh := erlang:monotonic_time(millisecond)
};
handle_event({binding_expiring, Remaining}, State) ->
io:format("[MONITOR] Warning: binding expiring in ~p ms~n", [Remaining]),
State;
handle_event({binding_expired}, State) ->
io:format("[MONITOR] ALERT: binding expired!~n"),
State#{healthy := false};
handle_event({binding_changed, Old, New}, State) ->
io:format("[MONITOR] Address changed!~n"),
io:format(" Old: ~p:~p~n", [Old#stun_addr.address, Old#stun_addr.port]),
io:format(" New: ~p:~p~n", [New#stun_addr.address, New#stun_addr.port]),
%% Notify application of address change
State#{address := New};
handle_event({error, Reason}, State) ->
io:format("[MONITOR] Error: ~p~n", [Reason]),
State#{healthy := false}.
Auto-Reconnect on Expiry¶
-module(auto_reconnect).
-export([start/0]).
-include_lib("estun/include/estun.hrl").
start() ->
%% Initial setup
{ok, SocketRef} = estun:open_socket(),
ok = estun:set_event_handler(SocketRef, self()),
{ok, Addr} = estun:bind_socket(SocketRef, default),
ok = estun:start_keepalive(SocketRef, 25),
io:format("Connected: ~p:~p~n", [
Addr#stun_addr.address, Addr#stun_addr.port
]),
event_loop(SocketRef).
event_loop(SocketRef) ->
receive
{estun_event, SocketRef, {binding_expired}} ->
io:format("Binding expired, reconnecting...~n"),
%% Close old socket
estun:close_socket(SocketRef),
%% Reconnect
start();
{estun_event, SocketRef, {binding_changed, _Old, New}} ->
io:format("Address changed to ~p:~p~n", [
New#stun_addr.address, New#stun_addr.port
]),
%% Notify application of new address
event_loop(SocketRef);
{estun_event, SocketRef, {error, Reason}} ->
io:format("Error: ~p, attempting recovery...~n", [Reason]),
%% Attempt recovery
case estun:bind_socket(SocketRef, default) of
{ok, _} ->
io:format("Recovered~n"),
event_loop(SocketRef);
{error, _} ->
io:format("Recovery failed, restarting...~n"),
estun:close_socket(SocketRef),
start()
end;
{estun_event, SocketRef, _Event} ->
%% Ignore other events
event_loop(SocketRef)
end.
Logging All Events¶
-module(event_logger).
-export([attach/1]).
attach(SocketRef) ->
Handler = fun(Event) ->
Timestamp = calendar:system_time_to_rfc3339(
erlang:system_time(second)
),
logger:info("[~s] STUN: ~p", [Timestamp, Event])
end,
estun:set_event_handler(SocketRef, Handler).
Best Practices¶
- Always set a handler when using keepalive to detect address changes
- React to
binding_changed- your public address may change - Handle
binding_expiring- opportunity to extend or notify - Log
errorevents - helps diagnose network issues - Use module callbacks for complex event handling logic