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!

Get your free chapter of Level Up Your Web Apps with Go

Get a free chapter of Level Up Your Web Apps with Go, plus updates and exclusive offers from SitePoint.


  • junk66com

    very interesting ! thx

Related books & courses
Rails Deep Dive

Available on SitePoint Premium. Check it out now!