View Source Hello World
The purpose of this guide is to show you how to create a very basic server implementing the commands ping
, register
, login
, whois
, whoami
, users
and clients
. We will be using ranch to provide a plain-text interface to our server though for real projects you'll typically want something more structured than just plain-text.
Create project
mix new --sup hello_world_server
Add dependencies
In mix.exs
def deps do
[
{:teiserver, path: "../teiserver"},
{:ecto_sql, "~> 3.10"},
{:postgrex, ">= 0.0.0"},
{:ranch, "~> 1.8"}
]
end
Now get the relevant dependencies
mix deps.get && mix compile
Create a migration
mix ecto.gen.migration add_teiserver_tables
And populate it like so:
defmodule HelloWorldServer.Repo.Migrations.AddTeiserverTables do
use Ecto.Migration
def up do
Teiserver.Migration.up()
end
def down do
Teiserver.Migration.down(version: 1)
end
end
Application files
lib/hello_world_server/repo.ex
defmodule HelloWorldServer.Repo do
use Ecto.Repo,
otp_app: :hello_world_server,
adapter: Ecto.Adapters.Postgres
end
config/config.exs
import Config
config :hello_world_server,
ecto_repos: [HelloWorldServer.Repo]
config :hello_world_server, HelloWorldServer.Repo,
database: "hello_world_server",
username: "postgres",
password: "postgres",
hostname: "localhost"
config :hello_world_server, Teiserver,
repo: HelloWorldServer.Repo
Edit application.ex
to start the various components on startup.
children = [
HelloWorldServer.Repo,
{Ecto.Migrator,
repos: Application.fetch_env!(:hello_world_server, :ecto_repos),
skip: System.get_env("SKIP_MIGRATIONS") == "true"},
{Teiserver, Application.get_env(:hello_world_server, Teiserver)},
%{
id: HelloWorldServer.TcpServer,
start: {HelloWorldServer.TcpServer, :start_link, [[]]}
}
]
Add our tcp server
Place in lib/hello_world_server/tcp_server.ex
defmodule HelloWorldServer.TcpServer do
use GenServer
alias HelloWorldServer.{TcpIn, TcpOut}
@behaviour :ranch_protocol
def start_link(_opts) do
:ranch.start_listener(
make_ref(),
:ranch_tcp,
[port: 8200],
__MODULE__,
[]
)
end
@impl true
def start_link(ref, socket, transport, _opts) do
pid = :proc_lib.spawn_link(__MODULE__, :init, [ref, socket, transport])
{:ok, pid}
end
def init(ref, socket, transport) do
:ranch.accept_ack(ref)
transport.setopts(socket, [{:active, true}])
state = %{
user_id: nil,
socket: socket,
transport: transport,
}
:gen_server.enter_loop(__MODULE__, [], state)
end
@impl true
def init(init_arg) do
{:ok, init_arg}
end
@impl true
def handle_info(:init_timeout, %{userid: nil} = state) do
send(self(), :terminate)
{:noreply, state}
end
def handle_info(:init_timeout, state) do
{:noreply, state}
end
# If Ctrl + C is sent through it kills the connection, makes telnet debugging easier
def handle_info({_, _socket, <<255, 244, 255, 253, 6>>}, state) do
send(self(), :terminate)
{:noreply, state}
end
def handle_info({:tcp, _socket, data}, state) do
data = data
|> to_string
|> String.trim
{new_state, response} = TcpIn.data_in(data, state)
TcpOut.data_out(response, state)
{:noreply, new_state}
end
end
Data in
Place in lib/hellow_world_server/tcp_in.ex
, this will handle all the commands coming in.
defmodule HelloWorldServer.TcpIn do
def data_in("ping" <> _data, state) do
{state, "pong"}
end
def data_in("login " <> data, state) do
[name, password] = String.split(data, " ")
case Teiserver.Account.get_user_by_name(name) do
nil ->
{state, "Login failed (no user)"}
user ->
if Teiserver.Account.verify_user_password(user, password) do
Teiserver.Connections.login_user(user)
{%{state | user_id: user.id}, "You are now logged in as '#{user.name}'"}
else
{state, "Login failed (bad password)"}
end
end
end
def data_in("register " <> data, state) do
[name, password] = String.split(data, " ")
params = %{
"name" => name,
"password" => password,
# We're not using emails right now but Teiserver expects them to be unique
# this will do for the purposes of this example
"email" => to_string(:rand.uniform())
}
case Teiserver.Account.create_user(params) do
{:ok, _user} ->
{state, "User created, you can now login with 'login name password'"}
{:error, _} ->
{state, "Error registering user"}
end
end
def data_in("whois " <> data, state) do
case Teiserver.Account.get_user_by_name(data) do
nil ->
{state, "I cannot find a user by the name of '#{data}'"}
user ->
{state, "User #{user.name} exists with an ID of #{user.id}"}
end
end
def data_in("whoami" <> _data, %{user_id: user_id} = state) do
case Teiserver.Account.get_user_by_id(user_id) do
nil ->
{state, "You are not logged in"}
user ->
{state, "You are '#{user.name}'"}
end
end
def data_in("users" <> _data, %{user_id: user_id} = state) do
names = Teiserver.Account.list_users(select: [:name])
|> Enum.map(fn %{name: name} -> name end)
|> Enum.join(", ")
{state, "User names: #{names}"}
end
def data_in("clients" <> _data, %{user_id: user_id} = state) do
client_ids = Teiserver.Connections.list_client_ids()
names = Teiserver.Account.list_users(where: [id_in: client_ids], select: [:name])
|> Enum.map(fn %{name: name} -> name end)
|> Enum.join(", ")
{state, "Client names: #{names}"}
end
end
Data out
Place in lib/hellow_world_server/tcp_out.ex
, this will handle sending data back to our users.
defmodule HelloWorldServer.TcpOut do
def data_out(msg, state) do
state.transport.send(state.socket, msg <> "\n")
end
end
Showtime!
Get deps We now need to run our application.
mix run --no-halt
In another terminal we can then do:
telnet localhost 8200
# Trying 127.0.0.1...
# Connected to localhost.
# Escape character is '^]'.
ping
# pong
whoami
# You are not logged in
whois teifion
# I cannot find a user by the name of 'Teifion'
register teifion password1
# User created, you can now login with 'login name password'
whois teifion
# User Teifion exists with an ID of 1
login teifion nopass
# Login failed (bad password)
login teifion password1
# You are now logged in as Teifion
whoami
# You are 'Teifion'
register bob password1
# User created, you can now login with 'login name password'
users
# User names: teifion, bob
clients
# Client names: teifion