There might be not many tutorials, but there are quite a few real web servers in erlang which you can study.
I think elli is conceptually the simplest (and is very short ~2k loc with comments). It creates a process for each connection and does all the work inside that process (no message copying). If that’s something your game allows you to do, then its probably the simplest way to go.
It’s most definitely not what you need, but here you go anyway. (copy-pasted elli)
# server.ex
defmodule Server do
use GenServer
def start_link(opts) do
case Keyword.get(opts, :name) do
nil -> GenServer.start_link(__MODULE__, opts)
name -> GenServer.start_link(__MODULE__, opts, name: name)
end
end
def stop(server) do
GenServer.call(server, :stop)
end
def init(opts) do
# Use the exit signal from the acceptor processes to know when they exit
Process.flag(:trap_exit, true)
callback = Keyword.get(opts, :callback) # where the game logic might be
ip_address = Keyword.get(opts, :ip, {0, 0, 0, 0}) # can also be ipv6
port = Keyword.get(opts, :port, 4000)
min_acceptors = Keyword.get(opts, :min_acceptors, 20)
accept_timeout = Keyword.get(opts, :accept_timeout, 10_000)
request_timeout = Keyword.get(opts, :request_timeout, 60_000)
options = [
accept_timeout: accept_timeout,
request_timeout: request_timeout
]
{:ok, listen_socket} = Server.TCP.listen(port, [
:binary,
{:ip, ip_address},
{:reuseaddr, true},
{:packet, :raw},
{:active, false}
])
acceptors = :ets.new(:acceptors, [:private, :set])
for _ <- 1..min_acceptors do
add_acceptor(acceptors, listen_socket, options, callback)
end
{:ok, %{
socket: listen_socket,
acceptors: acceptors,
open_conns: 0,
options: options,
callback: callback
}}
end
def handle_call(:stop, _from, state) do
{:stop, :normal, :ok, state}
end
def handle_cast(:accepted, state) do
{:noreply, add_acceptor(state)}
end
def handle_cast(_msg, state) do
{:noreply, state}
end
def handle_info({:EXIT, pid, _reason}, state) do
{:noreply, remove_acceptor(state, pid)}
end
def terminate(_reason, _state), do: :ok
def code_change(_old_vsn, state, _extra), do: {:ok, state}
defp remove_acceptor(%{acceptors: acceptors, open_reqs: open_reqs} = state, pid) do
:ets.delete(acceptors, pid)
%{state | open_reqs: open_reqs - 1}
end
defp add_acceptor(%{acceptors: acceptors, open_reqs: open_reqs,
socket: listen_socket, options: options, callback: callback} = state) do
add_acceptor(acceptors, listen_socket, options, callback)
%{state | open_reqs: open_reqs + 1}
end
defp add_acceptor(acceptors, listen_socket, options, callback) do
pid = Server.Connection.start_link(self(), listen_socket, options, callback)
:ets.insert(acceptors, {pid})
end
end
# server/tcp.ex
defmodule Server.TCP do
def listen(port, opts), do: :gen_tcp.listen(port, opts)
def accept(listen_socket, server, timeout) do
case :gen_tcp.accept(listen_socket, timeout) do
{:ok, accept_socket} ->
GenServer.cast(server, :accepted)
{:ok, accept_socket}
{:error, reason} ->
{:error, reason}
end
end
def recv(socket, size, timeout), do: :gen_tcp.recv(socket, size, timeout)
def send(socket, data), do: :gen_tcp.send(socket, data)
def close(socket), do: :gen_tcp.close(socket)
def setopts(socket, opts), do: :inet.setopts(socket, opts)
def peername(socket), do: :inet.peername(socket)
end
# server/connection.ex
defmodule Server.Connection do
def start_link(server, listen_socket, options, callback) do
:proc_lib.spawn_link(__MODULE__, :accept, [server, listen_socket, options, callback])
end
@doc """
From elli:
Accept on the socket until a client connects. Handles the
request, then loops if we're using keep alive or chunked
transfer. If accept doesn't give us a socket within a configurable
timeout, we loop to allow code upgrades of this module.
"""
def accept(server, listen_socket, options, callback) do
case Server.TCP.accept(listen_socket, server, options[:accept_timeout]) do
{:ok, accept_socket} -> keepalive_loop(accept_socket, options, callback)
{:error, :timeout} -> accept(server, listen_socket, options, callback)
{:error, :econnaborted} -> accept(server, listen_socket, options, callback)
{:error, :closed} -> :ok
{:error, other} -> exit({:error, other})
end
end
@doc "Handle multiple requests on the same connection, ie. keep alive"
def keepalive_loop(accept_socket, req_count \\ 0, buffer \\ <<>>, options, callback) do
case handle_request(accept_socket, buffer, options, callback) do
{:keepalive, buffer, conn} ->
keepalive_loop(accept_socket, req_count + 1, buffer, options, callback)
{:close, _, _} ->
Server.TCP.close(accept_socket)
:ok
end
end
@doc """
Handle a request that will possibly come on the
socket. Returns the appropriate connection token (depending on your game protocol),
:keepalive or :close, the socket, and any buffer containing (parts of) the next request.
"""
def handle_request(socket, buffer, opts, callback) do
{request, buffer} = get_request(socket, buffer, opts) # depends on the protocol you use
response = execute_callback(callback, request)
handle_response(response, socket, buffer)
end
def handle_response(response, socket, buffer) do
send_response(response, socket)
{close_or_keepalive(response), buffer}
end
def send_response(response, socket) do
response = ["my protocol ", "\r\n", response]
case Server.TCP.send(socket, response) do
:ok -> :ok
{:error, closed} when closed in [:closed, :enotconn] -> :ok
end
end
@doc "Retrieves the request line"
def get_request(socket, buffer, options) do
case Server.Packet.decode_packet(buffer) do
{:more, _} ->
case Server.TCP.recv(socket, 0, options[:request_timeout]) do
{:ok, data} ->
get_request(socket, buffer <> data, options)
{:error, _} ->
Server.TCP.close(socket)
exit(:normal)
end
{:ok, request, rest} -> {request, rest}
_ ->
send_bad_request(socket)
Server.TCP.close(socket)
exit(:normal)
end
end
end
You might also want to read http://dbeck.github.io/Wrapping-up-my-Elixir-TCP-experiments/, if you haven’t already.