Initial
This commit is contained in:
		
							
								
								
									
										4
									
								
								.formatter.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.formatter.exs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					# Used by "mix format"
 | 
				
			||||||
 | 
					[
 | 
				
			||||||
 | 
					  inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
							
								
								
									
										23
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					# The directory Mix will write compiled artifacts to.
 | 
				
			||||||
 | 
					/_build/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# If you run "mix test --cover", coverage assets end up here.
 | 
				
			||||||
 | 
					/cover/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# The directory Mix downloads your dependencies sources to.
 | 
				
			||||||
 | 
					/deps/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Where third-party dependencies like ExDoc output generated docs.
 | 
				
			||||||
 | 
					/doc/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# If the VM crashes, it generates a dump, let's ignore it too.
 | 
				
			||||||
 | 
					erl_crash.dump
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Also ignore archive artifacts (built via "mix archive.build").
 | 
				
			||||||
 | 
					*.ez
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Ignore package tarball (built via "mix hex.build").
 | 
				
			||||||
 | 
					server_try-*.tar
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Temporary files, for example, from tests.
 | 
				
			||||||
 | 
					/tmp/
 | 
				
			||||||
							
								
								
									
										19
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					# Broadcast
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Just a very simple implementation of a [broadcast program](https://roadmap.sh/projects/broadcast-server)
 | 
				
			||||||
 | 
					I made this program to learn about elixir, and its Actor model.
 | 
				
			||||||
 | 
					To be honest I think it's quite nice, even though a program like
 | 
				
			||||||
 | 
					this usually comes with quite a few unexpected difficulties Elixir
 | 
				
			||||||
 | 
					was able to handle it incredibly gracefully with basically no resistance.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					From my first impressions I think Elixir is actually a quite capable
 | 
				
			||||||
 | 
					language for the purposes of server software - even if it doesn't have
 | 
				
			||||||
 | 
					the sheer computational speed offered by lower level languages.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					As far as first impressions go for a programming language, I think that's
 | 
				
			||||||
 | 
					excellent.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## running/building
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Install elixir and its build tool `mix` through your operating system's package
 | 
				
			||||||
 | 
					manager, then run `mix run`.
 | 
				
			||||||
							
								
								
									
										19
									
								
								lib/client.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								lib/client.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					defmodule Broadcast.ClientHandler do
 | 
				
			||||||
 | 
					  use Task
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def start(sock) do
 | 
				
			||||||
 | 
					    Task.start_link(__MODULE__, :handle, [sock])
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def handle(sock) do
 | 
				
			||||||
 | 
					    case :gen_tcp.recv(sock, 0) do
 | 
				
			||||||
 | 
					      {:ok, line} ->
 | 
				
			||||||
 | 
					        Broadcast.Server.broadcast("Someone said: " <> line)
 | 
				
			||||||
 | 
					        handle(sock)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {:error, _} ->
 | 
				
			||||||
 | 
					        :gen_tcp.close(sock)
 | 
				
			||||||
 | 
					        Broadcast.Server.remove_client()
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										25
									
								
								lib/main.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								lib/main.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					defmodule Broadcast do
 | 
				
			||||||
 | 
					  @moduledoc """
 | 
				
			||||||
 | 
					  A simple broadcast server
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  use Application
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def console_loop() do
 | 
				
			||||||
 | 
					    case IO.read(:stdio, :line) do
 | 
				
			||||||
 | 
					      "mem\n" ->
 | 
				
			||||||
 | 
					        IO.inspect :erlang.memory()
 | 
				
			||||||
 | 
					      _ -> IO.puts "unknown command entered"
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					    console_loop()
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def main(_args \\ []) do
 | 
				
			||||||
 | 
					    IO.puts "started"
 | 
				
			||||||
 | 
					    {:ok, _} = Broadcast.Server.start(8080)
 | 
				
			||||||
 | 
					    console_loop()
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def start(_type, _args) do
 | 
				
			||||||
 | 
					    main()
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										65
									
								
								lib/server.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								lib/server.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,65 @@
 | 
				
			|||||||
 | 
					defmodule Broadcast.Server do
 | 
				
			||||||
 | 
					  @moduledoc """
 | 
				
			||||||
 | 
					    Implements the server. The server spawns a new process
 | 
				
			||||||
 | 
					    for every client - the processes usually don't really talk to
 | 
				
			||||||
 | 
					    each other, so this is trivial.
 | 
				
			||||||
 | 
					  """
 | 
				
			||||||
 | 
					  use GenServer
 | 
				
			||||||
 | 
					  use Agent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def init(port) do
 | 
				
			||||||
 | 
					    {:ok, sock} = :gen_tcp.listen(port, [:binary, packet: :line, active: false, reuseaddr: true])
 | 
				
			||||||
 | 
					    spawn_link(fn -> accept_loop(sock) end)
 | 
				
			||||||
 | 
					    {:ok, {sock, %{}}}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  defp accept_loop(sock) do
 | 
				
			||||||
 | 
					    case :gen_tcp.accept(sock) do
 | 
				
			||||||
 | 
					      {:ok, client_socket} ->
 | 
				
			||||||
 | 
					        {:ok, pid} = Broadcast.ClientHandler.start(client_socket)
 | 
				
			||||||
 | 
					        GenServer.cast(__MODULE__, {:new_client, pid, client_socket})
 | 
				
			||||||
 | 
					        accept_loop(sock)
 | 
				
			||||||
 | 
					      {:error, _} ->
 | 
				
			||||||
 | 
					        accept_loop(sock)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def handle_cast({:new_client, pid, client_sock}, {sock, clients}) do
 | 
				
			||||||
 | 
					    {:noreply, {sock, clients |> Map.put(pid, client_sock)}}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def handle_cast({:remove, client}, {sock, clients}) do
 | 
				
			||||||
 | 
					    new_clients = Map.delete(clients, client)
 | 
				
			||||||
 | 
					    {:noreply, {sock, new_clients}}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def handle_cast({:broadcast, msg, sender}, {sock, clients}) do
 | 
				
			||||||
 | 
					    clients |> Enum.map(fn {p, s} ->
 | 
				
			||||||
 | 
					      if sender != p do
 | 
				
			||||||
 | 
					        :gen_tcp.send(s, msg)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end)
 | 
				
			||||||
 | 
					    {:noreply, {sock, clients}}
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Print out the memory usage for the client processes specifically
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # From this point onwards is the actual API for this module,
 | 
				
			||||||
 | 
					  # that should be called by ClientHandlers and main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @spec start(integer()) :: pid()
 | 
				
			||||||
 | 
					  def start(port) when is_integer(port) do
 | 
				
			||||||
 | 
					    GenServer.start_link(__MODULE__, port, name: __MODULE__)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def broadcast(msg) do
 | 
				
			||||||
 | 
					    GenServer.cast(__MODULE__, {:broadcast, msg, self()})
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def remove_client() do
 | 
				
			||||||
 | 
					    GenServer.cast(__MODULE__, {:remove, self()})
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										30
									
								
								mix.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								mix.exs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					defmodule Broadcast.MixProject do
 | 
				
			||||||
 | 
					  use Mix.Project
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def project do
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					      app: :broadcast,
 | 
				
			||||||
 | 
					      version: "0.1.0",
 | 
				
			||||||
 | 
					      elixir: "~> 1.18",
 | 
				
			||||||
 | 
					      start_permanent: Mix.env() == :prod,
 | 
				
			||||||
 | 
					      deps: deps(),
 | 
				
			||||||
 | 
					      escript: [main_module: Broadcast]
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Run "mix help compile.app" to learn about applications.
 | 
				
			||||||
 | 
					  def application do
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					      extra_applications: [:logger],
 | 
				
			||||||
 | 
					      mod: {Broadcast, []}
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  # Run "mix help deps" to learn about dependencies.
 | 
				
			||||||
 | 
					  defp deps do
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					      # {:dep_from_hexpm, "~> 0.3.0"},
 | 
				
			||||||
 | 
					      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										8
									
								
								test/server_try_test.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								test/server_try_test.exs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					defmodule ServerTryTest do
 | 
				
			||||||
 | 
					  use ExUnit.Case
 | 
				
			||||||
 | 
					  doctest ServerTry
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  test "greets the world" do
 | 
				
			||||||
 | 
					    assert ServerTry.hello() == :world
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										1
									
								
								test/test_helper.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								test/test_helper.exs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					ExUnit.start()
 | 
				
			||||||
		Reference in New Issue
	
	Block a user