Skip to content

Instantly share code, notes, and snippets.

@maarek
Last active August 29, 2015 14:20
Show Gist options
  • Save maarek/37bad17c20e9f9311e5b to your computer and use it in GitHub Desktop.
Save maarek/37bad17c20e9f9311e5b to your computer and use it in GitHub Desktop.
Elixir Chat Server
defmodule Chat.Server do
use Application
# Create a Room structure that has a list of clients
defmodule Room do
defstruct users: []
end
defmodule User do
defstruct name: "", socket: Socket
end
@doc false
def start(_type, _args) do
import Supervisor.Spec
children = [
supervisor(Task.Supervisor, [[name: Chat.Server.TaskSupervisor]]),
worker(Task, [Chat.Server, :accept, [4040]])
]
opts = [strategy: :one_for_one, name: Chat.Server.Supervisor]
Supervisor.start_link(children, opts)
end
@doc false
def stop() do
end
@doc """
Starts accepting connections on the given port.
"""
def accept(port) do
{:ok, socket} = :gen_tcp.listen(port, [:binary, packet: :line, active: false])
{:ok, agent} = Agent.start_link fn -> [] end
IO.puts "Creating a new room"
Agent.update(agent, fn room -> %Room{} end)
IO.puts "Accepting connections on port #{port}"
loop_acceptor(socket, agent)
end
# Starts a thread to handle the connection managed by the Task supervisor.
defp loop_acceptor(socket, agent) do
{:ok, client} = :gen_tcp.accept(socket)
# Notice to the user
write_line(client, "Thank you for connecting to JeremyNet. To join the room type /join <name>\n")
Task.Supervisor.start_child(Chat.Server.TaskSupervisor, fn -> serve(client, agent) end)
loop_acceptor(socket, agent)
end
# Handles the run loop actor for the connection.
# Success: Send response and listen further
# Failure: Display exit output
defp serve(socket, agent) do
case read_line(socket) do
{:ok, socket, :join, nick} ->
join_room(socket, agent, nick)
serve(socket, agent)
{:ok, socket, :leave} ->
leave_room(socket, agent)
serve(socket, agent)
{:ok, socket, :nick, nick} ->
set_nick(socket, agent, nick)
serve(socket, agent)
{:ok, socket, :say, message} ->
say(socket, agent, message)
serve(socket, agent)
{:error, cause} -> IO.puts "Exited: #{cause}"
_ -> IO.puts "Exited: Something bad happened"
end
end
# Waits for a message from the client and parses
# Returns tuple of parsed message
defp read_line(socket) do
case :gen_tcp.recv(socket, 0) do
{:ok, data} ->
data = String.strip(data)
data_list = String.split(data)
command = hd(data_list)
IO.puts "data: #{data}\ncommand: #{command}"
case command do
"/join" -> {:ok, socket, :join, tl(data_list)}
"/leave" -> {:ok, socket, :leave}
"/nick" -> {:ok, socket, :nick, tl(data_list)}
_ -> {:ok, socket, :say, data}
end
{:error, _} -> {:error, "Socket Closed"}
_ -> {:error, "No clue..."}
end
end
# Send a message to the connected client
defp write_line(socket, message) do
:gen_tcp.send(socket, message <> "\n")
end
# Gets the room from the agent
defp get_room(agent) do
Agent.get(agent, fn room -> room end)
end
# Sets the room stored in the agent
defp set_room(agent, new_room) do
Agent.update(agent, fn room -> new_room end)
end
# Finds a user by socket
defp find_user(users, socket) do
Enum.find(users, fn(user) -> user.socket == socket end)
end
# Replace a user
defp replace_user(users, user) do
index = Enum.find_index(users, fn(u) -> u.socket == user.socket end)
List.replace_at(users, index, user)
end
# Adds the client to the room list
defp join_room(client, agent, nick \\ "Anonymous") do
room = get_room(agent)
user = %User{name: to_string(nick), socket: client}
new_room = %{room | users: room.users ++ [user]}
IO.puts "User #{nick} joined"
notify_all(new_room, client, "User #{nick} joined")
set_room(agent, new_room)
end
# Removes the client from the room list
defp leave_room(client, agent) do
room = get_room(agent)
user = find_user(room.users, client)
new_room = %{room | users: room.users -- [user]}
IO.puts "User #{user.name} left"
notify_all(new_room, client, "User #{user.name} left")
set_room(agent, new_room)
end
# Sets the nickname of the client
defp set_nick(client, agent, nick) do
room = get_room(agent)
user = find_user(room.users, client)
new_user = %{user | name: to_string(nick)}
new_room = %{room | users: replace_user(room.users, new_user)}
set_room(agent, new_room)
notify_all(new_room, client, "#{user.name} changed their name to #{to_string(nick)}")
end
# Notifies clients of the users message
defp say(client_socket, agent, message) do
room = get_room(agent)
user = find_user(room.users, client_socket)
notify_all(room, client_socket, "#{user.name}: #{message}")
end
# Send 'message' to all clients except 'sender'
defp notify_all(room, sender, message) do
Enum.each room.users, fn(client) ->
if client.socket != sender do
write_line(client.socket, message)
end
end
end
end
@maarek
Copy link
Author

maarek commented Apr 29, 2015

Just a quick and simple elixir chat server using Tasks, Agents and Sockets.

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