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)
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:thanks.