TCP chat app with ruby
Tags: ruby, rails
Published: 2020-01-04 08:48:01

**NOTE: This article is intended for developers with a year or more experience with ruby**

Link to [source code](https://github.com/sushant12/ruby-socket-chat/tree/1) on github

Slides on my talk [`Socket programming with ruby`] (https://rubynepal.org/assets/meetup_dec_2019_socket_programming_sushant12.pdf)
 
To create our chat app we need to require `socket` which is a part of ruby's standard library.

Our chat app will need

- Server

This will be a TCPServer which will *bind* to a specific port, *listen* and *accept* connections on that port

- Client

This will be us connecting to a TCPServer and sending messages to it.


So, let's create a server first

``` ruby
require 'socket'

class Server
  def initialize(port)
    @server = TCPServer.new(port)
    puts "Listening on port #{port}"
  end
end
```

`TCPServer.new` creates a tcp server that will bind to a port and listen on it.

 It could have also be written as

``` ruby
require 'socket'

socket = Socket.new(:INET, :STREAM)
socket.bind(Socket.pack_sockaddr_in(3000, '127.0.0.1'))
socket.listen(Socket::SOMAXCONN)
```

`Socket.new(:INET, :STREAM)` takes two parameters where :INET means internet and :STREAM means the socket will be of type TCP. If we want to create a UDP socket, we could have passed :DGRAM.


`Socket.pack_sockaddr_in(3000, '127.0.0.1')` this method create a C struct that holds the port and ip.

`socket.bind` binds to that port 

`Socket::SOMAXCONN` is a constant that gives how many connections the listen queue can accept. Listen queue is the total pending connection that a socket can tolerate.


`socket.listen` will listen for connection on the port.

As you can see, we could achieve all this in just one line with `TCPServer.new`. We, ruby programmers, love one-liners.


Now the server needs to accept connections

`Socket.accept_loop()` will pop a connection from the listen queue, process it and then exit. 

`Socket.accept_loop()` can also be written as

``` ruby
# old code
require 'socket'

socket = Socket.new(:INET, :STREAM)
socket.bind(Socket.pack_sockaddr_in(3000, '127.0.0.1'))
socket.listen(Socket::SOMAXCONN)
# new code
loop do
  connection, _ = socket.accept
end
```

**Fun fact: To create a server you need a forever loop. In our case `loop`**


socket.accept is a blocking call.

When the connection is accepted and processed, the connection is closed itself. But, we personally want to close it ourselves too because
- we want the garbage collector to collect unused references to the socket 
- there is a limit to how much a process can have open files at a given time.

`socket.close` will close the connection.


We save how to create a tcp socket, bind it to a port, listen on that port and accept connections.

## The Server

So, for our server, we need to do something similar.

``` ruby
require 'socket'

class Server
  def initialize(port)
    @server = TCPServer.new(port)
    @connections = []
    puts "Listening on port #{port}"
  end

  def start
    Socket.accept_loop(@server) do |connection|
      @connections << connection
      puts @connections
      Thread.new do
        loop do
          handle(connection)
        end
      end
    end
  end

  private

  def handle(connection)
    request = connection.gets
    connection.close if request.nil?
    @connections.each do |client|
      next if client.closed?
      client.puts(request) if client != connection && !client.closed?
    end
  end
end

server = Server.new(4002)
server.start
```

Our server needs to keep the state of connected clients. It needs to relay messages to all connected clients and close a connection if a client exits the chat.

To maintain the state of the connected client, we can set up an instance variable in our constructor `@connections`

Inside of `accept_loop` , which listens for new connections, we will push the connection inside `@connections`.

Our private `handle()` method, takes a connection and reads its message and then relay it to every connection. It will close those connections of the user who exited the chat.

Since we will support more than one active connection we will run them inside a separate thread.




Phew, this was too much.


Let's move to the client

To create a server we used `TCPServer.new` but we need to create a client so we create a `TCPSocket.new`. It will accept two-parameter. A host and a port.


`TCPSocket.new` is a ruby wrapper for a much more line of code.

``` ruby
require 'socket'
socket = Socket.new(:INET, :STREAM)
remote_addr = Socket.pack_sockaddr_in(3000, '127.0.0.1')
socket.connect(remote_addr)
```

This is similar to creating a server except for we don't bind and listen on a port.

## Our Client

``` ruby
require 'socket'

class Client
  class << self
    attr_accessor :host, :port
  end

  def self.request
    @client = TCPSocket.new(host, port)
    listen
    send
  end

  def self.listen
    Thread.new do
      loop do
        puts "====#{@client.gets}"
      end
    end
  end

  def self.send
    Thread.new do
      loop do
        msg = $stdin.gets.chomp
        @client.puts(msg)
      end
    end.join
  end
end


Client.host = '127.0.0.1'
Client.port = 4002
Client.request
```

The listen class method will print for any new messages send by other clients connected to the socket.

The send class method will send the server message.

They run in their own separate thread so that we can listen and send messages without getting blocked.