Last active
August 29, 2015 14:20
-
-
Save maarek/37bad17c20e9f9311e5b to your computer and use it in GitHub Desktop.
Elixir Chat Server
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
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Just a quick and simple elixir chat server using Tasks, Agents and Sockets.