Ruby TCP Chat

Share this article

Ruby TCP Chat

Today, we are going to build a little TCP ruby chat application using the ruby standard library Socket. I’m using ruby 2.0.0, and Ubuntu Linux 12.04LTS, but it should work on Mac OS too. I haven’t tried it on Windows.

First a short overview of TCP (Transmission Control Protocol):

TCP is one of the core protocols of the Internet protocol suite (IP), and is so common that the entire suite is often called TCP/IP. Web browsers use TCP when they connect to servers on the World Wide Web, and it is used to deliver email and transfer files from one location to another. For more detaled information visit TCP Wikipedia

This is how our TCP chat is going to work: TCP-Chat

Description

First, we will create a server that receives the client connections and stores them in data dictionaries. These dictionaries will keep track of what room the client is located in, receive messages, and relay the messages to other users. Each user MUST have a different username, which will be our primary key to look up our connections in the data dictionary so we can keep track of connected users. Right now, we aren’t going to store messages in a database, but it wouldn’t take much to add such a feature. Once completed, we’ll test our chat by opening different command terminals, one for each simulated user.

First we are going to create the necessary files: ‘server.rb’ and ‘client.rb’ In server.rb and client.rb we have to require the Socket library.

# in client.rb and server.rb
require "socket"

Then create the respective classes with some attributes to handle users.

The client receives a server instance so it can establish a connection with the server. We need to initialize a request and response to send and receive messages through the server. Right now our @response and @request objects are null, but later on we are going to build two threads and assign them to our objects to read and write at the same time.

# in client.rb
require "socket"
class Client
  def initialize(server)
    @server = server
    @request = nil
    @response = nil
  end
end

If you want to know more on threads, check out:

Here’s the start of our server. The server receives a port which will be our channel for establishing a connection between users. The server listens to the port for any event and sends a response to everyone who is interested. The initializer also creates three 3 hashes:

  • @connections is a pool of users connected to server.
  • @rooms is keyed on room name and holds the users in each room.
  • @clients are our connected client instances

#in server.rb
require "socket"
class Server
  def initialize(port, ip)
    @server = nil
    @connections  = {}
    @rooms = {}
    @clients = {}
  end
end

Now we can track which user is in which room. It’s important to reiterate that the client name/username must be unique. Here is what our hashes will look like with some data

# hash Connections preview
connections: {
  clients: { client_name: {attributes}, ... },
  rooms: { room_name: [clients_names], ... }
}

Then we need to create two threads on the client side so it can read/write messages at the same time. Without this functionality, our chat would be very boring. Imagine typing your message and only after finishing being able to look for an answer without the posibility of doing both at the same time. This is how most chat clients work basically.

# ( @request, @response ) objects preview and descriptions
# The request and response objects implementation may look like this

# in client.rb
def send
  @request = Thread.new do
    loop { # write as much as you want
      # read from the console
      # with the enter key, send the message to the server
    }
  end
end

def listen 
  @response = Thread.new do
    loop { # listen for ever
      # listen the server responses
      # show them in the console
    }
  end
end

Here is the client.rb file:

#!/usr/bin/env ruby -w
require "socket"
class Client
  def initialize( server )
    @server = server
    @request = nil
    @response = nil
    listen
    send
    @request.join
    @response.join
  end

  def listen
    @response = Thread.new do
      loop {
        msg = @server.gets.chomp
        puts "#{msg}"
      }
    end
  end

  def send
    puts "Enter the username:"
    @request = Thread.new do
      loop {
        msg = $stdin.gets.chomp
        @server.puts( msg )
      }
    end
  end
end

server = TCPSocket.open( "localhost", 3000 )
Client.new( server )

On the server side we need something similar, basically one thread per connected user. This way, we can handle as many users as possible without any concurrency issues.

# in server.rb
def run
  loop {
    Thread.start do |client| # each client thread
    end
  }
end

For our test, the IP ip is local. The port MUST be the same on the client and server side and, in this case. Remember, ports are virtual:

A port is not a physical device, but an abstraction to facilitate communication between a server and a client. A machine can have a maximum of 65536 port numbers (ranging from 0 to 65535). The port numbers are divided into three ranges: the Well Known Ports, the Registered Ports, and the Dynamic and/or Private Ports. – Brief Description of TCP and UDP

We’ll also clean up all of the extra characters at the end of a message, such as the end of the line, tabs, etc.

The implementation is quite simple. All we need is to finish up with the run method, and verify the uniqueness of the username provided. If the username is taken, tell the client with an error message and kill the connection. Otherwise, give the client a successfull connection message.

# server.rb ( server side )
class Server
  def  initialize(port,ip)
    @server = TCPServer.open(ip, port)
    ...
  end

  def run
    loop {
      # for each user connected and accepted by server, it will create a new thread object
      # and which pass the connected client as an instance to the block
      Thread.start(@server.accept) do | client |
        nick_name = client.gets.chomp.to_sym
        @connections[:clients].each do |other_name, other_client|
          if nick_name == other_name || client == other_client
            client.puts "This username already exist"
            Thread.kill self
          end
        end
        puts "#{nick_name} #{client}"
        @connections[:clients][nick_name] = client
        client.puts "Connection established, Thank you for joining! Happy chatting"
      end
    }
  end
end
server = Server.new("localhost", 3000) # (ip, port) in each machine "localhost" = 127.0.0.1
server.run

Right now our chat is almost finished, but there is one method left for handling all the messages between all connected users. Without it, our users won’t be able to send messages to each other.

# in server.rb
def listen_user_messages(username, client)
  loop {
    # get client messages
    msg = client.gets.chomp
    # send a broadcast message, a message for all connected users, but not to its self
    @connections[:clients].each do |other_name, other_client|
      unless other_name == username
        other_client.puts "#{username.to_s}: #{msg}"
      end
    end
  }
end

All the listen_user_messages method does is listen to the user messages and send them to all the other users. Now, call this method inside the run method in the server instance and that’s it.

# in server.rb
def run
  loop {
    Thread.start(@server.accept) do | client |
      ...
      listen_user_messages(nick_name, client)
    end
  }
end

Here is the entire server.rb file:

#!/usr/bin/env ruby -w
require "socket"
class Server
  def initialize( port, ip )
    @server = TCPServer.open( ip, port )
    @connections = Hash.new
    @rooms = Hash.new
    @clients = Hash.new
    @connections[:server] = @server
    @connections[:rooms] = @rooms
    @connections[:clients] = @clients
    run
  end

  def run
    loop {
      Thread.start(@server.accept) do | client |
        nick_name = client.gets.chomp.to_sym
        @connections[:clients].each do |other_name, other_client|
          if nick_name == other_name || client == other_client
            client.puts "This username already exist"
            Thread.kill self
          end
        end
        puts "#{nick_name} #{client}"
        @connections[:clients][nick_name] = client
        client.puts "Connection established, Thank you for joining! Happy chatting"
        listen_user_messages( nick_name, client )
      end
    }.join
  end

  def listen_user_messages( username, client )
    loop {
      msg = client.gets.chomp
      @connections[:clients].each do |other_name, other_client|
        unless other_name == username
          other_client.puts "#{username.to_s}: #{msg}"
        end
      end
    }
  end
end

Server.new( 3000, "localhost" )

This is our chat working on my terminal. In future articles, we will build the chat rooms, send private messages, and more cool stuff to customize our chat.

Lets see our little chat

Initialize server

RubyChat: ./server.rb

Initialize client Simon

RubyChat: ./client.rb
Enter the username:
Simon
Connection established, Thank you for joining! Happy chatting

Initialize client Foo

RubyChat: ./client.rb
Enter the username:
Foo
Connection established, Thank you for joining! Happy chatting

Message sent From Simon

RubyChat: ./client.rb
Enter the username:
Foo
Connection established, Thank you for joining! Happy chatting
Hi! It's Simon!

Message received from Simon on Foo’s terminal

RubyChat: ./client.rbEnter the username:
Foo
Connection established, Thank you for joining! Happy chatting
Simon: Hi! It's Simon

Message sent from Foo

RubyChat: ./client.rbEnter the username:
Foo
Connection established, Thank you for joining! Happy chatting
Simon: Hi! It's Simon
Hey Simon! Foo here.

Message received from Foo on Simon’s terminal

RubyChat: ./client.rb
Enter the username:
Simon
Connection established, Thank you for joining! Happy chatting
Hi! It's Simon!
Foo: Hey Simon! Foo here!

Server-Client Connections

RubyChat: ./server.rb
Simon #<TCPSocket:0x007fbc94836820>
Foo #<TCPSocket:0x007fbc94834a98>

Next steps

Happy Coding!

Frequently Asked Questions (FAQs) about Ruby TCP Chat

How can I handle multiple clients in Ruby TCP Chat?

Handling multiple clients in Ruby TCP Chat can be achieved by using threads. When a client connects to the server, a new thread is created. This thread will handle all the communication with that client, allowing the server to continue accepting connections from other clients. Here’s a simple example:

require 'socket'
server = TCPServer.new 2000

loop do
Thread.start(server.accept) do |client|
# handle client
client.close
end
end
In this code, the server accepts incoming connections in an infinite loop. For each connection, it starts a new thread where it can interact with the client.

How can I broadcast a message to all connected clients?

Broadcasting a message to all connected clients can be done by keeping track of all the connected clients. When a message is received, iterate over all the clients and send the message. Here’s an example:

require 'socket'
server = TCPServer.new 2000
clients = []

loop do
Thread.start(server.accept) do |client|
clients << client
while message = client.gets
clients.each { |c| c.puts message }
end
end
end
In this code, each connected client is added to the clients array. When a message is received from a client, it is sent to all clients in the clients array.

How can I handle disconnections in Ruby TCP Chat?

Handling disconnections can be done by using exception handling. When a client disconnects, an IOError will be raised when trying to read from or write to the socket. You can catch this exception and remove the client from the list of connected clients. Here’s an example:

require 'socket'
server = TCPServer.new 2000
clients = []

loop do
Thread.start(server.accept) do |client|
clients << client
begin
while message = client.gets
clients.each { |c| c.puts message }
end
rescue IOError
clients.delete(client)
end
end
end
In this code, when a client disconnects, an IOError is raised. This exception is caught, and the client is removed from the clients array.

How can I implement private messaging in Ruby TCP Chat?

Implementing private messaging can be done by adding a command that allows a client to send a message to a specific client. This command could include the recipient’s name or ID. When this command is received, the server sends the message to the specified client only. Here’s an example:

require 'socket'
server = TCPServer.new 2000
clients = {}

loop do
Thread.start(server.accept) do |client|
name = client.gets.chomp
clients[name] = client
while message = client.gets
recipient_name, text = message.split(':', 2)
if recipient = clients[recipient_name]
recipient.puts "#{name}: #{text}"
end
end
end
end
In this code, each client is identified by a name. When a message is received, it is split into the recipient’s name and the text of the message. The message is then sent to the specified recipient only.

How can I secure my Ruby TCP Chat?

Securing your Ruby TCP Chat can be done by using SSL/TLS. This will encrypt the communication between the server and the clients, preventing eavesdropping and tampering. Ruby’s OpenSSL library can be used to add SSL/TLS support to your chat. Here’s an example:

require 'socket'
require 'openssl'

tcp_server = TCPServer.new 2000
ssl_context = OpenSSL::SSL::SSLContext.new
ssl_context.cert = OpenSSL::X509::Certificate.new(File.read('cert.pem'))
ssl_context.key = OpenSSL::PKey::RSA.new(File.read('key.pem'))

ssl_server = OpenSSL::SSL::SSLServer.new(tcp_server, ssl_context)

loop do
Thread.start(ssl_server.accept) do |client|
# handle client
client.close
end
end
In this code, an SSLServer is created with the given TCPServer and SSLContext. The SSLContext is configured with a certificate and a private key, which are used to establish the SSL/TLS connection. The server then accepts SSL/TLS connections in the same way as a regular TCPServer.

How can I add authentication to my Ruby TCP Chat?

Adding authentication to your Ruby TCP Chat can be done by requiring clients to provide a username and password when they connect. The server can then verify these credentials before allowing the client to send or receive messages. Here’s an example:

require 'socket'
server = TCPServer.new 2000
users = { 'alice' => 'password1', 'bob' => 'password2' }

loop do
Thread.start(server.accept) do |client|
username = client.gets.chomp
password = client.gets.chomp
if users[username] == password
# handle client
else
client.puts 'Invalid credentials'
client.close
end
end
end
In this code, the server has a list of valid users and their passwords. When a client connects, it sends its username and password. The server checks if these match the stored credentials, and if they do, it allows the client to send and receive messages. If they don’t, it sends an error message to the client and closes the connection.

How can I handle large messages in Ruby TCP Chat?

Handling large messages in Ruby TCP Chat can be done by reading from the socket in chunks. Instead of using gets, which reads until it encounters a newline character, you can use readpartial, which reads a specified number of bytes. Here’s an example:

require 'socket'
server = TCPServer.new 2000

loop do
Thread.start(server.accept) do |client|
while true
begin
message = client.readpartial(4096)
# handle message
rescue EOFError
break
end
end
client.close
end
end
In this code, the server reads up to 4096 bytes from the client at a time. If the client sends a message that is larger than this, it will be read in multiple chunks.

How can I add a GUI to my Ruby TCP Chat?

Adding a GUI to your Ruby TCP Chat can be done by using a library such as Tk, Gtk, or Qt. These libraries allow you to create windows, buttons, text boxes, and other GUI elements. You can then use these elements to display messages and get input from the user. Here’s an example using Tk:

require 'socket'
require 'tk'

server = TCPServer.new 2000
root = TkRoot.new { title 'Chat' }
text = TkText.new(root) { pack }
entry = TkEntry.new(root) { pack }

Thread.new do
loop do
client = server.accept
Thread.start(client) do |c|
while message = c.gets
text.insert 'end', message
end
end
end
end

entry.bind('Return') do
message = entry.get
# send message
entry.delete 0, 'end'
end

Tk.mainloop
In this code, a Tk window is created with a text box for displaying messages and an entry box for typing messages. When the user presses Return in the entry box, the text in the box is sent as a message.

How can I test my Ruby TCP Chat?

Testing your Ruby TCP Chat can be done by using a tool such as telnet or netcat. These tools allow you to connect to a TCP server and send and receive data. Here’s an example:

$ telnet localhost 2000
In this command, telnet connects to the server running on localhost at port 2000. You can then type messages and press Enter to send them, and any messages received from the server will be displayed.

How can I deploy my Ruby TCP Chat?

Deploying your Ruby TCP Chat can be done by running it on a server that is accessible over the internet. This could be a virtual private server (VPS), a dedicated server, or a cloud server. You will need to open the port that your chat is running on in the server’s firewall, and you may also need to configure port forwarding on your router if the server is behind a NAT. Once the server is running and accessible, clients can connect to it by using its IP address or domain name and the port number.

Simon BenitezSimon Benitez
View Author
ruby
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week