TCP is a cornerstone of building servers. Erlang/OTP provides the gen_tcp module for building TCP servers. We can take advantage of that module to build a TCP server in Elixir. Let's see what that looks like.

Project

We'll start off by building a new project.

mix new tcp_server
cd tcp_server

Now, we're not going to be test-driving this since it relies on inbound traffic to function. Instead, we're just going to write the TcpServer and then interact with it from telnet on the command line.

Open up lib/tcp_server/server.ex and just make a quick module that's going to house the TCP server.

defmodule TcpServer.Server do
  def listen(port) do
    IO.puts "listening on port #{port}"
  end
end

Now, obviously this isn't actually listening yet :) Looking at the gen_tcp documentation, it houses a function called listen that takes two arguments: the port on which to listen, and the TCP options. The TCP options is a list of options. We want to say that the data received over the socket will be interpreted as a binary. Open up the tcp_server again and let's make it actually listen:

defmodule TcpServer.Server do
  def listen(port) do
    IO.puts "listening on port #{port}"
    tcp_options = [:binary, {:packet, 0}, {:active, false}]
    {:ok, listening_socket} = :gen_tcp.listen(port, tcp_options)
  end
end

This starts a server listening, but it then immediately exits because the function literally does nothing elise. Let's accept incoming connections on that port next:

defmodule TcpServer.Server do
  def listen(port) do
    IO.puts "listening on port #{port}"
    tcp_options = [:binary, {:packet, 0}, {:active, false}]
    {:ok, listening_socket} = :gen_tcp.listen(port, tcp_options)
    do_accept(listening_socket)
  end

  def do_accept(listening_socket) do
    {:ok, socket} = :gen_tcp.accept(listening_socket)
  end
end

This plucks a connection request off of the queue of pending connections for the listening socket, and returns a connected socket that we can use to communicate with the endpoint. It blocks until a connected socket is returned, so let's go ahead and run it and see what that looks like. We run TcpServer.Server.listen(8082) in an iex, and you can see that the process blocks waiting for the call to accept to return. Open up another terminal and telnet localhost 8082. We get a connection on the client end, but if you check out iex the server process has stopped blocking. This is because we didn't do anything with our connected socket. Let's go ahead and receive on it:

defmodule TcpServer.Server do
  def listen(port) do
    IO.puts "listening on port #{port}"
    tcp_options = [:binary, {:packet, 0}, {:active, false}]
    {:ok, listening_socket} = :gen_tcp.listen(port, tcp_options)
    do_accept(listening_socket)
  end

  def do_accept(listening_socket) do
    {:ok, socket} = :gen_tcp.accept(listening_socket)
    do_listen(socket)
  end

  def do_listen(socket) do
    case :gen_tcp.recv(socket, 0) do
      {:ok, data} ->
        IO.puts "Got some data! #{data}"
        :gen_tcp.send(socket, "Roger wilco\n")
        do_listen(socket)
      {:error, :closed} ->
        IO.puts "The client closed the connection..."
    end
  end
end

You just call :gen_tcp.recv/2 on the socket. It takes a connected socket and a length. If the length is 0, it will receive all available bytes on the socket. That's what we want. If successful, it returns a tuple containing the atom :ok and the data received. We output the data we received, then we send a "Roger wilco" to the connected client. Then we listen again. We continue this until the connection is closed. Let's go ahead and try that out. Run it in an iex session with TcpServer.Server.listen(8082) and connect to it via telnet localhost 8082. Now send it a message. You'll see your message echoed out on the process hosting the server, and your telnet client will receive the "Roger wilco" we sent.

Summary

That's it! We've successfully written a TCP server with terrible performance, because it blocks the entire server for any connected client until they close the connection. This was just an overview of the :gen_tcp module. If you'd like to see a more interesting example of a TCP server in Elixir that can actually handle simultaneous connections, check out the link in the episode's Resources. See you soon!

Resources