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.
start_link() function
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.
Check out the GitHub repository on Killer Elixir Tips
Glad if you can contribute with a ★