Quick & Easy TCP GenServer with Elixir & Erlang

Jul 02, 2017 . 5 mins read

This article is all about writing your own TCP server from the scratch. You will see the implementation of GenServer receiving and sending packets inside the TCP server. We go from a very basic step, creating a mix project .

Let’s dive into action.

For your confirmation this is not about TCP basics. If you are expecting about the TCP then it is not for you and I guess you already knew how to create a mix project in elixir, if not then look down else skip the para.

Mix Project Creation

mix new gen_tcp --module TcpServer

The above command will generate a mix project with a file gen_tcp.ex where you can see the module name will be TcpServer instead of GenTcp . If you want you can also change the filename gen_tcp.ex to tcp_server.ex . It is much sensible to keep the module name and file name like/same.

Now, I am renaming the file /lib/gen_tcp.ex to /lib/tcp_server.ex as my module is TcpServer

We are using the Eralng module called gen_tcp here. This module comprises of functions for TCP/IP protocol communication with sockets.

GenServer Behavior Implementation

Now open the file lib/tcp_server.ex and you will see the default code. Remove everything inside the module and add the following line

use GenServer

With that line, we made our module to behave like the GenServer. You can read more about the GenServer Here

Configuration

Here, we go with small configuration for setting up the IP and Port numbers.

Open your config/config.ex and add the following line

config :gen_tcp, ip: {127,0,0,1}, port: 6666

Here, you have to set the value of the ip to a tuple with four integers as I did in above line and port number is also an Integer. You can give your own values. We are going to use these values inside our tcp_server.ex for creating a socket. Now, we are done with configurations.

This will act like an entry point for initiating the GenServer.

def start_link() do
    ip = Application.get_env :gen_tcp, :ip, {127,0,0,1}
    port = Application.get_env :gen_tcp, :port, 6666
    GenServer.start_link(__MODULE__, [ip,port],[])
end

Here, Application.gen_env :gen_tcp,:ip,{127,0,0,1} will fetch the values from the configuration file config/config.ex

For safe side, when the configuration is not found, we are sending the default parameters here as well.

Suppose, if you override the configuration files with config/dev.ex or config/prod.ex then, the values from the respective environment will be loaded.

Here, we are not going to do that as it has nothing to do with the concept we are talking.

The line GenServer.start_link.... will callback the server init function with a list of parameters [ip,port] here.

init()

def init [ip,port] do
    {:ok,listen_socket} = :gen_tcp.listen(port,[:binary,{:packet, 0},{:active,true},{:ip,ip}])
    {:ok,socket } = :gen_tcp.accept listen_socket
    {:ok, %{ip: ip, port: port, socket: socket}}
  end

Brief Explanation of CODE

The line :gen_tcp.listen(port, pptions) ::{ok, ListenSocket} | {error, reason} will set the socket on port

  • :binary — The received packet will be delivered as a binary . We also have a list option as an alternative for receiving the packets. Here, I prefer :binary the demo purpose.
  • {ip, address} — If the host has many network interfaces, this option specifies which one to listen on.

{:active, true}

Here, I am just using fewer options which serve our needs. The another important option is {:active, true}

If the value is true, which is the default, everything received from the socket is sent as messages to the receiving process.

If the value is false (passive mode), the process must explicitly receive incoming data by calling gen_tcp:recv/2,3,

:gen_tcp.accept() will accept the connection on listening socket.

Receiving Packets

To receive the packets inside the GenServer, we have to define a handle_info/2 function. We receive the messages from the external processes. So, we are implementing the handle_info()

def handle_info({:tcp,socket,packet},state) do
    IO.inspect packet, label: "incoming packet"
    :gen_tcp.send socket,"Hi Blackode \n"
    {:noreply,state}
  end   

  def handle_info({:tcp_closed,socket},state) do
    IO.inspect "Socket has been closed"
    {:noreply,state}
  end   

  def handle_info({:tcp_error,socket,reason},state) do
    IO.inspect socket,label: "connection closed due to #{reason}"    
    {:noreply,state}
  end

Here, we are defining three handle_info/2 functions and differentiating them with pattern matching the actual message.

The first one is for receiving the actual messages with packets. Using :gen_tcp.send we are sending the return message to the client

The second one is for catching the socket close

The last one is to catch the errors in sockets.

If everything is good, the overall file should like the following one…

defmodule TcpServer do
  use GenServer

  def start_link() do
    ip = Application.get_env :tcp_server, :ip, {127,0,0,1}
    port = Application.get_env :tcp_server, :port, 6666
    GenServer.start_link(__MODULE__,[ip,port],[])
  end

  def init [ip,port] do
    {:ok,listen_socket}= :gen_tcp.listen(port,[:binary,{:packet, 0},{:active,true},{:ip,ip}])
    {:ok,socket } = :gen_tcp.accept listen_socket
    {:ok, %{ip: ip,port: port,socket: socket}}
  end

  def handle_info({:tcp,socket,packet},state) do
    IO.inspect packet, label: "incoming packet"
    :gen_tcp.send socket,"Hi Blackode \n"
    {:noreply,state}
  end

  def handle_info({:tcp_closed,socket},state) do
    IO.inspect "Socket has been closed"
    {:noreply,state}
  end

  def handle_info({:tcp_error,socket,reason},state) do
    IO.inspect socket,label: "connection closed dut to #{reason}"
    {:noreply,state}
  end

end
tcp_server.ex hosted with ❤ by GitHub

Running and Live Execution

iex -S mix

This compiles the project and provides you with command line interface where you can interact with project. As soon as you type that, you are now inside iex interactive elixir shell and It also loads the module TcpServer

iex> TcpServer.start_link

with the above line, we are starting the server and now you are blocked inside the shell for accepting the connection. Once the connection is accepted its pid is returned as a response.

Now open another terminal or press Ctrl+Shift+t to open the another terminal in a new tab. Now run the following command

$ telnet 127.0.0.1 6666

You will be notified with connection information… If the connection is success you will see success message and it will let you enter some message…

No sooner you send the message from telnet, it returns the message with ‘hi blackode’. You can inspect the incoming packet in another terminal.

Hope this helps you apart in understanding about GenServer concepts and to level up your elixir skills.

Thanks for reading.