Table of contents
In this blog, we'll go through the process of building a chat system using Elixir servers and connecting to it with a simple Java client. We'll dive into the architecture and functionality. Through this, you can better your knowledge of Elixir processes and TCP communication with Erlang's provided :gen_tcp
module.
You can clone this application from my GitHub and follow the instructions in the README.md on how to use it. https://github.com/claudeomosa/NatterNet
Requirements
We are tasked with developing an Elixir server capable of accepting external TCP connections from clients. The server allows users to specify the port for incoming connections, with a default of 6666. Additionally, we are creating a simple Java client that can establish connections with the server.
At the heart of our project are four essential commands:
/LIST
requires no arguments and any words passed after the command are ignored. This command lists all Online users./NICK [nickname]
requires at least one argument and words after the first argument are ignored. Sets the current user's nickname and saves it to anets
table./BC [message]
accepts one or more words after the command. Send the message to all connected clients./MSG [nickname] [message]
requires at least two words after the command, the first one is the recipient's nickname which is checked if it exists in theets
table and second is the message.
Elixir Server
Our Elixir server is structured with two major modules: Chat.BroadcastServer
and Chat.ProxyServer
. Here's an explanation of their roles and how they work in tandem:
Chat.BroadcastServer:
The
Chat.BroadcastServer
is a GenServer module dedicated to managing user nicknames within the chat application using ETS (Erlang Term Storage) tables. It facilitates interactions related to setting and validating nicknames.Server Initialization: To begin using the
Chat.BroadcastServer
, you can start it by callingChat.BroadcastServer.start_link([])
. This action initializes the server and makes it ready to manage user nicknames. This is handled by our supervision tree.Nickname Management:
Setting Nicknames: The server allows users to set their nicknames using the
Chat.BroadcastServer.set_nickname/2
function. This function requires a nickname and the user's process identifier (PID). It ensures that the chosen nickname adheres to certain criteria before setting it.Removing Nicknames: Invoking
Chat.BroadcastServer.remove_nickname/1
removes the passed nickname from theets
table.
Nickname Format: User nicknames must adhere to the following format:
Start with a letter
Be up to 12 characters in length
Consist of only letters, numbers, and underscores
Nicknames Storage: The server employs an ETS table named
:nicknames
to maintain the state of user nicknames. This table is configured as a set with a named table.Querying Nicknames: The server offers various functions for querying and managing nicknames, such as
get_nicknames_with_pids/0
, which retrieves a list of nicknames with associated PIDs, andget_pid_with_nickname/1
, which finds the PID corresponding to a given nickname.
Chat.ProxyServer:
The
Chat.ProxyServer
module serves as a bridge between external clients and theChat.BroadcastServer
. It uses:gen_tcp
module and is responsible for accepting incoming TCP connections from clients and acting as a proxy to the broadcast server. Here's how it works:accept/1
: Theaccept/1
function initializes the proxy server, listening for incoming connections on a specified port (defaulting to 6666). It sets up the server and creates an initial state. This is initiated by our Application Supervisor.Connection Handling: The proxy server manages incoming client connections, setting up client sockets and responding to client requests.
Parsing and Validation: It parses and validates commands from clients, ensuring they adhere to the defined format.
User Interaction: The proxy server handles client interaction, it allows users to broadcast messages and send private messages to other users.
Chat.ClientsStateAgent:
The
Chat.ClientsStateAgent
module in our chat application serves as an Agent responsible for managing the state of connected clients. It maintains a state that includes client Process Identifiers (PIDs) and their associated sockets, facilitating efficient communication between users. Users can be added or removed using functions likeadd_client/2
andremove_client/1
. Additionally, the module offers functions for retrieving client information, includingget_clients/0
andget_client/1
. It ensures concurrent access to client data is handled safely.Chat.TaskSupervisor:
The
Chat.TaskSupervisor
module serves as a supervisor in the chat application, responsible for managing child processes, which in this context represent individual client connections. Each time a client establishes a connection with the chat server, a new child process is created to handle that specific client. The supervisor ensures that these child processes are well-maintained and automatically restarted in the event of any issues or failures, making sure that the chat application remains robust and responsive. The supervisor utilizes the:one_for_one
strategy to monitor and control these individual client connections, enhancing the overall reliability of the system.Chat.Application:
The
Chat.Application
module is the backbone of our chat server application. It configures and manages supervisors, child processes, and agents. Notably, it allows dynamic port configuration, initializes key components, and sets up a supervision tree to ensure the reliable and fault-tolerant operation of the chat server.
Java Client
The Java client serves as an interface to interact with the Elixir chat server. Here's an overview of its functionality:
Command-Line Arguments: The client can accept command-line arguments to specify the host and port of the Elixir chat server. If provided, it uses these arguments; otherwise, it defaults to connecting to the local host (
localhost
) on port6666
.Socket Communication: It establishes a socket connection to the specified or default host and port, enabling bi-directional communication with the chat server.
User Interaction: The client reads input from the user using the standard input (
System.in
) and sends it to the chat server via the socket connection. This allows users to send chat messages and commands to the server.Server Responses: The client listens for responses from the server, displaying them to the user. When the server sends messages or feedback, it prints them to the console, making the communication with the server interactive.
In essence, this Java client provides a straightforward and convenient way for users to connect to the Elixir chat server, send messages, and interact with the server's responses. You can also access the server using netcat(nc)
or telnet