Error Handling with gen_tcp in Erlang Program

The error in this code lies in the gen_tcp:recv(ClientSocket, 0) call, which assumes that the client will always send data correctly. If the client disconnects or doesn’t send data, it could cause the server to crash or hang. Additionally, there is no error handling for this receive operation, making it prone to failures during communication interruptions. Proper error handling should be added to ensure robustness.

Erlang Code (with Errors):

code-define(TCP_OPTIONS, [binary, {packet, 2}, {active, false}, {reuseaddr, true}]).

start_listener(Portno, DictPid) ->
case gen_tcp:listen(Portno, ?TCP_OPTIONS) of
{ok, ListenSocket} ->
spawn_link(fun() -> handle_incoming_connections(ListenSocket, DictPid) end),
io:format("Server is now listening on port ~p~n", [Portno]);
{error, Reason} ->
io:format("Error occurred during listen: ~w~n", [Reason])
end.

handle_incoming_connections(ListenSocket, DictPid) ->
case gen_tcp:accept(ListenSocket) of
{ok, ClientSocket} ->
io:format("New client connected: ~w~n", [ClientSocket]),
gen_tcp:send(ClientSocket, "Welcome! Please enter your name:~n"),
ClientPid = spawn(fun() -> io:format("Handling new client~n"),
initialize_client(ClientSocket, DictPid) end),
gen_tcp:controlling_process(ClientSocket, ClientPid),
handle_incoming_connections(ListenSocket, DictPid);
{error, Reason} ->
io:format("Connection accept error: ~w~n", [Reason])
end.

initialize_client(ClientSocket, DictPid) ->
{ok, Username} = gen_tcp:recv(ClientSocket, 0),
DictPid ! {add_new_user, ClientSocket, Username},
ClientDict = fetch_client_dict(DictPid),
send_message_to_all(dict:fetch_keys(ClientDict), "[" ++ Username ++ " has joined the chat]"),
client_interaction_loop(ClientSocket, Username, DictPid).

Key Concepts:

  1. TCP Options: We define our TCP_OPTIONS to configure our TCP socket for communication. These options include:
    • binary: Treats incoming data as binary rather than list.
    • {packet, 2}: Tells the socket to expect a two-byte packet header.
    • {active, false}: Sets the socket in passive mode, meaning we need to explicitly receive data using gen_tcp:recv/2.
    • {reuseaddr, true}: Allows the socket to be reused immediately after it is closed.
  2. Starting the Listener: The start_listener/2 function opens a TCP port and listens for connections. It takes a port number and a dictionary process (which tracks users) as arguments. If the port is successfully opened, it spawns a new process to handle incoming connections. Otherwise, it prints the error.
  3. Handling Connections: The handle_incoming_connections/2 function accepts incoming client connections using gen_tcp:accept/1. Once a connection is accepted, a new process is spawned to handle the client interaction, including asking for the client’s username and adding them to the server’s dictionary.
  4. Client Initialization: The initialize_client/2 function receives the client’s username and adds them to a dictionary of connected users. After that, the server broadcasts a welcome message to all connected clients.

The primary issue in the provided Erlang code lies in improper handling of the dictionary and the missing parts of the get_dict/1, broadcast_message/2, and client_loop/3 functions. These helper functions are likely missing, which would lead to runtime errors. Below is a corrected version of your program, with the missing functions added, along with some refinements to handle the dictionary process properly.

Corrected Erlang Code:

code-define(TCP_OPTIONS, [binary, {packet, 2}, {active, false}, {reuseaddr, true}]).

%% Listen for incoming connections on the given port
listen(Portno, DictPid) ->
case gen_tcp:listen(Portno, ?TCP_OPTIONS) of
{ok, ListenSocket} ->
spawn_link(fun() -> accept_connections(ListenSocket, DictPid) end),
io:format("Listening on port ~p~n", [Portno]);
{error, Error} ->
io:format("Listen Error: ~w~n", [Error])
end.

%% Accept incoming connections and handle clients
accept_connections(ListenSocket, DictPid) ->
case gen_tcp:accept(ListenSocket) of
{ok, ClientSocket} ->
io:format("Accepting client: ~w~n", [ClientSocket]),
gen_tcp:send(ClientSocket, "Welcome! Please enter your name:~n"),
ClientPid = spawn(fun() ->
setup_user(ClientSocket, DictPid)
end),
gen_tcp:controlling_process(ClientSocket, ClientPid),
accept_connections(ListenSocket, DictPid); % Continue accepting new connections
{error, Error} ->
io:format("Accept Error: ~w~n", [Error])
end.

%% Setup the user by receiving their username and adding to the dictionary
setup_user(ClientSocket, DictPid) ->
case gen_tcp:recv(ClientSocket, 0) of
{ok, UsernameBin} ->
Username = binary_to_list(UsernameBin), % Convert binary to string (list)
DictPid ! {add_new_pair, ClientSocket, Username}, % Send message to dictionary process
ClientDict = get_dict(DictPid),
broadcast_message(dict:fetch_keys(ClientDict), "[" ++ Username ++ " has entered the chat]"),
client_loop(ClientSocket, Username, DictPid);
{error, Error} ->
io:format("Username receive error: ~w~n", [Error])
end.

%% Fetch the client dictionary from the dictionary process
get_dict(DictPid) ->
DictPid ! {get_dict, self()},
receive
{dict, ClientDict} -> ClientDict
end.

%% Broadcast a message to all clients in the chat
broadcast_message([], _Message) -> ok;
broadcast_message([ClientSocket | Rest], Message) ->
gen_tcp:send(ClientSocket, Message),
broadcast_message(Rest, Message).

%% Client message loop: handle incoming messages from the client
client_loop(ClientSocket, Username, DictPid) ->
case gen_tcp:recv(ClientSocket, 0) of
{ok, Data} ->
Message = binary_to_list(Data),
FullMessage = "[" ++ Username ++ "]: " ++ Message,
ClientDict = get_dict(DictPid),
broadcast_message(dict:fetch_keys(ClientDict), FullMessage),
client_loop(ClientSocket, Username, DictPid);
{error, closed} ->
DictPid ! {remove_pair, ClientSocket},
io:format("Client ~p disconnected~n", [Username]),
ok;
{error, Error} ->
io:format("Client error: ~w~n", [Error])
end.

Explanation:

  1. Socket Options: We maintain the same TCP_OPTIONS, which define the socket’s behavior.
  2. Listening for Connections: In the listen/2 function, we use gen_tcp:listen/2 to start listening on the specified port. If successful, it spawns a process to handle incoming connections.
  3. Accepting Connections: The accept_connections/2 function uses gen_tcp:accept/1 to wait for a client to connect. Once a client connects, the server sends a welcome message, spawns a process to handle the client (setup_user/2), and then continues to accept new clients.
  4. Receiving and Storing User Information:
    • The setup_user/2 function receives the client’s username using gen_tcp:recv/2, converts the binary data to a string, and adds the user to a dictionary maintained by another process (DictPid). It then broadcasts a message to all connected clients, informing them of the new user.
  5. Fetching Client Dictionary: The get_dict/1 function retrieves the current list of connected clients from the dictionary process by sending a message and awaiting a response.
  6. Broadcasting Messages:
    • The broadcast_message/2 function sends a message to all clients in the connected clients list (retrieved from the dictionary).
  7. Client Message Loop:
    • The client_loop/3 function handles client communication in a loop. It continuously receives messages from the client, broadcasts them to other clients, and handles disconnections.

Error Fixes:

  1. Binary to List Conversion: Erlang uses binaries for receiving data over TCP. The binary_to_list/1 function converts this binary data into a list (string), which is easier to handle.
  2. Handling Client Disconnection: We properly handle client disconnections ({error, closed}) and remove the client from the dictionary using a message to DictPid.
  3. Broadcasting: The broadcast_message/2 function is now implemented to send messages to all connected clients, preventing any potential runtime errors.

Related blog posts