0

I have 2 elixir applications.

In one of them I create a TCP server which listens on the port 8080 for packets. I know that it is defined correctly, because I connected to it with telnet and everything was working fine.

The problem appears when I try to connect to this TCP server from my other Elixir application.

That's how I connect to it

host = 'localhost'
{:ok, socket} = :gen_tcp.connect(host, 8080, [])

Not sure about the options that I should specify tho.

When trying to connect to it I get in the logs of the application with TCP server:

00:21:11.235 [error] Task #PID<0.226.0> started from MessageBroker.Controller terminating
** (MatchError) no match of right hand side value: {:error, :closed}
    (message_broker 0.1.0) lib/message_broker/network/controller.ex:110: MessageBroker.Controller.read_line/1
    (message_broker 0.1.0) lib/message_broker/network/controller.ex:101: MessageBroker.Controller.serve/1
    (elixir 1.12.3) lib/task/supervised.ex:90: Task.Supervised.invoke_mfa/2
    (stdlib 3.12) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
Function: #Function<0.126110026/0 in MessageBroker.Controller.loop_acceptor/1>
    Args: []

At the line with

{:ok, data} = :gen_tcp.recv(socket, 0)

Any ideas, suggestions?

Radu Aramă
  • 195
  • 1
  • 1
  • 10
  • 1
    What do you do in the client after the connect? You wait? Because, if the client terminates immediately after, the server, trying to read with recv, will indeed get a {:error, :closed}. As a general rule, an Internet server must check for everything and be prepared to a lot of different results for recv. You may get inspiration from this echo (RFC 862) TCP server https://www.bortzmeyer.org/files/echo.ex – bortzmeyer May 26 '22 at 04:59

1 Answers1

3

(MatchError) no match of right hand side value: {:error, :closed}

At the line with

{:ok, data} = :gen_tcp.recv(socket, 0)

The error message is saying that the function call :gen_tcp.recv(socket, 0) returned {:error, :closed} and that {:ok, data} does not match {:error, :closed}. If you call recv() on a socket that was closed by the other side, then recv() returns {:error, :closed}, and the other side will not be sending anymore data to that socket.

Not sure about the options that I should specify tho.

Yeah, those are pretty important. The basic tenet is that when you send data to a socket, you have no idea how many chunks the data will be split into. But, the client and server have to be able to know when to stop trying to read data because the end of the data has been reached. To solve the "indeterminate number of chunks problem", the client and server have to agree on some signal to mark the end of the data:

  1. A newline?
  2. The sender closes the socket?
  3. Use an agreed upon number of bytes at the start of the data to specify the length of the data?

For #3, send() and recv() will automatically handle packaging and unpackaging the data for you if you simply tell gen_tcp how many bytes are necessary to specify the length of a message, for instance you can specify the option {:packet, 2}. If you do that, send() will automatically calculate the length of the data, then add 2 bytes to the front of the data containing the length of the data. Likewise, recv() will automatically read the first 2 bytes from the socket, then read the integer contained in those 2 bytes, say 64,999, then recv() will wait until it has read an additional 64,999 bytes from the socket, then it will return the whole 64,999 bytes of data.

How to send a message through TCP protocol to a TCP server using gen_tcp in Elixir?

Here is an example of #1, where a newline is used to mark the end of the data:

TCP Server:

defmodule App1 do

  def start_tcp_server do
    port = 8080

    {:ok, listen_socket} = :gen_tcp.listen(
                    port,
                    [
                      {:mode, :binary},   #Received data is delivered as a string (v. a list)

                      {:active, :false},  #Data sent to the socket will not be
                                          #placed in the process mailbox, so you can't 
                                          #use a receive block to read it.  Instead 
                                          #you must call recv() to read data directly
                                          #from the socket.

                      {:packet, :line},   #recv() will read from the socket until 
                                          #a newline is encountered, then return.
                                          #If a newline is not read from the socket,
                                          #recv() will hang until it reads a newline
                                          #from the socket.

                      {:reuseaddr, true}  #Allows you to immediately restart the server
                                          #with the same port, rather than waiting
                                          #for the system to clean up and free up
                                          #the port.
                    ]
    )

    IO.puts "Listening on port #{port}...."
    listen_loop(listen_socket)
  end

  defp listen_loop(listen_socket) do
    {:ok, client_socket} = :gen_tcp.accept(listen_socket) #client_socket is created with the same options as listen_socket
    handle_client(client_socket)
    listen_loop(listen_socket)
  end

  defp handle_client(client_socket) do
    case :gen_tcp.recv(client_socket, 0) do    #Do not specify the number
                                               #of bytes to read, instead write 0
                                               #to indicate that the :packet option
                                               #will take care of how many bytes to read.
      {:ok, line}  ->
        #Echo back what was received:
        IO.write("Server echoing back: #{line}")
        :gen_tcp.send(client_socket, "Server received: #{line}") #line has a "\n" on the end

      {:error, :closed} ->
        {:ok, :client_disconnected}
    end
  end
                

end

TCP client:

defmodule App2 do

  def send_data do
    host = :localhost
    port = 8080

    {:ok, socket} = :gen_tcp.connect(host, port,
                    [
                      {:active, :false}, #Data sent to the socket will not be put in the process mailbox.
                      {:mode, :binary},  
                      {:packet, :line}   #Must be same as server.
                    ]
    )

    :ok = :gen_tcp.send(socket, "Hi server!\n")

    case :gen_tcp.recv(socket, 0) do
      {:ok, line} -> 
        IO.puts ~s(Client got: "#{String.trim line}")
        :ok = :gen_tcp.close(socket)

      {:error, :closed} -> IO.puts("Server closed socket.")
    end

  end

end

Output in server window:

iex(1)> App1.start_tcp_server
Listening on port 8080....
Server echoing back: Hi server!

Output in client window:

ex(1)> App2.send_data
Client got: "Server received: Hi server!"
:ok

If you want to use {:error, :closed} to mark the end of the data, then you need to:

  1. Specify the option {:packet, :raw} for both the client and the server socket.

  2. After sending the data, close the socket by calling :gen_tcp.shutdown/2, which will ensure that all the data has been sent before closing the socket.

  3. Loop over the recv(), saving each chunk returned by recv(), until the recv() returns {:error, :closed}, marking the end of the data, then your loop can return all the chunks it read. For instance:

       defp get_data(socket, chunks) do
         case :gen_tcp.recv(socket, 0) do  #reads a chunk of indeterminate length from the socket
           {:ok, chunk} -> 
             get_data(socket, [chunk | chunks])
           {:error, :closed} -> 
             {:ok, Enum.reverse(chunks) }
         end
       end
    

You would call that function like this:

{:ok, data} = get_data(client_socket, [])

For more details on the gen_tcp options, see the following answer

Erlang client-server example using gen_tcp is not receiving anything

7stud
  • 46,922
  • 14
  • 101
  • 127