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.