Create a chat in the command line with Ruby
Tags: ruby, beginners
Published: 2021-01-12 17:08:47

Last weekend I learned how to create a chat application in the command line. The idea was to open at least 3 tabs in the terminal: one for the server that will handle the connexion between the users, and X number of tabs for the users to communicate, at least 2.

This project made me discover two built-in classes in Ruby that we don't really use when we do a full stack application with Rails:
* Socket
* Thread
* ARGV

I will talk about them later.

## Let's talk about the server

So what do we need exactly to create this chat? We talked about a server, so we will need a file for this that will be called server.rb
What do we need to do here? We need to establish the connexion through the port of our choice. For this we will need the class Socket, and we have to require it at the beginning of the file.
To understand what this method offers, make sure to understand what a socket is. Here is a definition provided by Oracle docs:
> "A socket is one endpoint of a two-way communication link between two programs running on the network. A socket is bound to a port number so that the TCP layer can identify the application that data is destined to be sent to."

Requiring Socket will give us access to other classes and methods to create our server ([see the Ruby documentation for Socket](https://ruby-doc.org/stdlib-2.7.0/libdoc/socket/rdoc/Socket.html)).
Our first line of command will be to create a TCP server on the port we want, and to make sure it's launched I will print a confirmation text:
```ruby
require 'socket'

server = TCPServer.new 2000 # Server bound to port 2000
puts "I'm ready to accept new clients on port 2000"
```
Our server is working, now we need it to listen to whatever connexion is coming up from a client, and we need to do it constantly. So we need to create a loop. We also want the server to handle multiple clients, and give information to users. So we will create an array to store them, and append every new client to the array.

```ruby
clients = []

loop do
  # we wait for a client to connect, and assign it to client
  client = server.accept
  clients << client
end
```

It is now time to introduce Thread. We use a thread to split a program and have a tasks that will run simultaneously (or pseudo-simultaneously). Creating a thread will allow our program to run two processes in the same time ([see documentation for Thread](https://ruby-doc.org/core-2.7.0/Thread.html))
We need it because while the server is waiting for a client to connect in the loop, we also need to constantly handle the messaging between the clients already connected. 
We do that by writing `Thread.new {}`. The block will be used to tell the program what to do with this thread. 
So what do we need from it? We need to know who connects to it, to receive and display text to others. We also need to detect when a client disconnect and remove it from the `clients` array.

It's a lot, so let's create a method for this.
```ruby
def handle_client(clients, client)
# for this method I need the new client, and the list of existing clients.

  # client_name will take whatever name the client put when it will connect to the server. We will see later how it's sent from the client perspective.
  client_name = client.gets.chomp

  # here we will display a welcome message and show how many clients are already connected
  client.puts "Hello #{client_name}! Clients connected: #{clients.count}"

  # this method is described below. It announces to all clients who is the new client.
  announce_to_everyone(clients, "#{client_name} joined!")

  # this is another loop. gets will take any text coming from the client...
  while line = client.gets
    incoming_data_from_client = line.chomp
    #... and this text will be shared to all the clients. A little bit of formatting to indicate who said what.
    announce_to_everyone clients, "#{client_name}: #{incoming_data_from_client}"
  end

  # it will close the client connexion and remove it from the clients array. And other clients will receive a notification.
  client.close
  clients.delete(client)
  announce_to_everyone(clients, "#{client_name} left!")
end

# this method takes the text sent by a client, and the clients connected. For each client from clients, the text will be displayed
def announce_to_everyone(clients, text)
  clients.each { |client| client.puts text }
end
```

We are done with `server.rb`. The server is running, and waiting to get clients connexions. It will receive messages and display them to all connected clients, and give them cool info for a better user experience (well, from the terminal... for what it's worth).


## Let's talk about the client

The idea here is to open let say 3 terminal tabs to simulate 3 clients / users, we will launch the client's program from each tab.
For that we need to create a file that we will call `client.rb`.

First we need to make sure our client will create a connexion on the server. Previously, on server side, we used `TCPServer.new`. This time, we will create an instance of `TCPSocket`.

```ruby
require 'socket'
socket = TCPSocket.new 'localhost', 2000  
# bound to port 2000, like the server
```
When we connect to the chat, we want a way to enter the user name. There is a simple way to do that. 
From the terminal, we can write `$ruby client.rb Aurelie`, where `Aurelie` is an argument passed to the script.
In our file, first thing to do is to grab this argument and assign it to a variable. For that we need to use the ARGV built-in class.

```ruby
name = ARGV.shift
```

ARGV takes all the arguments you pass to a script, and put them into an array in order of apparition. And #shift will use the first one of the array.

Do you remember how we assigned the client's name in `server.rb` above? 
Here we will use `name`, and do a `puts` applied to the socket. The first connexion to the server through the socket will be to send the name to the server, and that's how the server will pick-up `name` with its `gets` and assign it to `client_name`
```ruby
socket.puts "#{name}"
```

Now we need a way to take what the user types in the client and send it to the server, but we also need to keep receiving whatever messages other clients send to us. Two processes at a time, it's time to use Thread again!

We will create one thread for the local typing, and one for what the client receives from the server.
```ruby
local_typing_thread = Thread.new { local_typing(socket) }
receive_from_server_thread = Thread.new { receive_from_server(socket) }
```
Two things here, we assign each thread to a variable for a later use, and we pass in each thread a method to handle our processes.

Let's talk about the `local_typing` method.
This method needs to know which socket to use to give information to the server. It also needs to constantly check for what to send, so we need a loop.
```ruby
def local_typing(socket)
  loop do
    # when a user sends a message, this message will appear in the user client preceded by a little string that shows it's the user's message. For example: (me)> Hey it's me!
    print "(me)> "
    text_to_send = gets.chomp
    socket.puts text_to_send
  end
end
```

Then we need to tackle the `receive_from_server` method.
Again, it needs to know about the socket. And again, we want it to constantly capture all the messages sent to it:
```ruby
def receive_from_server(socket)
  # read the lines coming from the socket, and write them in the terminal
  while line = socket.gets
    puts line
  end
end
```

Finally, since we created two separate threads, we need to make them join, and we need to close the socket when we are done
```ruby
local_typing_thread.join
receive_from_server_thread.join

socket.close
```

Tadaa! We created a little chat in the terminal, on localhost.
See what it looks like:
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/jjhm48b7otnpdbdx77zu.png)

You can find the files in my repo [here](https://github.com/aurelieverrot/ruby-chat)