A TCP Chat Application with a pure Elixir server and a simple Java Client

ยท

5 min read

A TCP Chat Application with a pure Elixir server  and a simple Java Client

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 an ets 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 the ets 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:

  1. 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 calling Chat.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 the ets 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, and get_pid_with_nickname/1, which finds the PID corresponding to a given nickname.

  2. Chat.ProxyServer:

    The Chat.ProxyServer module serves as a bridge between external clients and the Chat.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: The accept/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.

  3. 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 like add_client/2 and remove_client/1. Additionally, the module offers functions for retrieving client information, including get_clients/0 and get_client/1. It ensures concurrent access to client data is handled safely.

  4. 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.

  5. 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:

  1. 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 port 6666.

  2. Socket Communication: It establishes a socket connection to the specified or default host and port, enabling bi-directional communication with the chat server.

  3. 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.

  4. 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

ย