DEV Community

Sushant Bajracharya
Sushant Bajracharya

Posted on

TCP chat app with ruby

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

Link to source code on github

Slides on my talk Socket programming with ruby

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

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

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

# 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.

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.

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

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.

Top comments (2)

Collapse
 
piotrmurach profile image
Piotr Murach • Edited

This is a great intro to sockets in Ruby! May I suggest a small improvement to make it easier to read code examples. In markdown, you can add a language to highlight syntax in code blocks after the 3 marks say ruby, for example:

TCPServer.new
Collapse
 
sushant12 profile image
Sushant Bajracharya

thanks.